mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 18:15:23 +00:00
fix(docker): delegate restart/install to supervisorctl in Docker deployments
The dashboard restart button and install service used local-mode process management (stopProxy + ensureCliproxyService) which conflicts with supervisord in Docker deployments. Killing the CLIProxy binary caused the bootstrap wrapper to exit, supervisord to attempt a restart, but ensureCliproxyService already spawned an orphaned process on :8317 resulting in EADDRINUSE and supervisord entering FATAL state. Detect supervisord via /var/run/supervisor.sock and delegate to supervisorctl restart, matching the pattern already used by ccs docker update. Closes #964
This commit is contained in:
1 parent
8e413e49f6
commit
7776715eba
3 files changed
+66
-1
No files matched your search
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Supervisord lifecycle helpers for Docker deployments (`ccs docker up`).
|
||||||
|
*
|
||||||
|
* In Docker, supervisord owns the CLIProxy process lifecycle. Direct
|
||||||
|
* stop+start via session-tracker / service-manager creates orphaned
|
||||||
|
* processes and causes supervisord to enter FATAL state (EADDRINUSE).
|
||||||
|
* All restart operations must delegate to `supervisorctl` instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
||||||
|
|
||||||
|
const SUPERVISOR_SOCK = '/var/run/supervisor.sock';
|
||||||
|
const SUPERVISOR_CONF = '/etc/supervisord.conf';
|
||||||
|
|
||||||
|
/** True when running inside a supervisord-managed container. */
|
||||||
|
export function isRunningUnderSupervisord(): boolean {
|
||||||
|
return fs.existsSync(SUPERVISOR_SOCK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restart the cliproxy program via supervisorctl. Returns port on success. */
|
||||||
|
export function restartCliproxyViaSupervisord(): {
|
||||||
|
success: boolean;
|
||||||
|
port?: number;
|
||||||
|
error?: string;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
execSync(`supervisorctl -c ${SUPERVISOR_CONF} restart cliproxy`, { timeout: 15_000 });
|
||||||
|
return { success: true, port: CLIPROXY_DEFAULT_PORT };
|
||||||
|
} catch (err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[cliproxy] supervisorctl restart failed: ${detail}`);
|
||||||
|
return { success: false, error: 'supervisorctl restart failed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import {
|
||||||
|
isRunningUnderSupervisord,
|
||||||
|
restartCliproxyViaSupervisord,
|
||||||
|
} from '../../docker/supervisord-lifecycle';
|
||||||
import {
|
import {
|
||||||
fetchCliproxyStats,
|
fetchCliproxyStats,
|
||||||
fetchCliproxyModels,
|
fetchCliproxyModels,
|
||||||
@@ -1014,7 +1018,14 @@ router.post('/install', async (req: Request, res: Response): Promise<void> => {
|
|||||||
*/
|
*/
|
||||||
router.post('/restart', async (_req: Request, res: Response): Promise<void> => {
|
router.post('/restart', async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Stop proxy first
|
if (isRunningUnderSupervisord()) {
|
||||||
|
// Docker mode: delegate to supervisord which owns the process lifecycle
|
||||||
|
const result = restartCliproxyViaSupervisord();
|
||||||
|
res.json(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local mode: direct process management
|
||||||
await stopProxy();
|
await stopProxy();
|
||||||
|
|
||||||
// Small delay to ensure port is released
|
// Small delay to ensure port is released
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { ensureCliproxyService, type ServiceStartResult } from '../../cliproxy/s
|
|||||||
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
|
import { getProxyStatus as getProxyProcessStatus } from '../../cliproxy/session-tracker';
|
||||||
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
|
import { isCliproxyRunning } from '../../cliproxy/stats-fetcher';
|
||||||
import type { CLIProxyBackend } from '../../cliproxy/types';
|
import type { CLIProxyBackend } from '../../cliproxy/types';
|
||||||
|
import {
|
||||||
|
isRunningUnderSupervisord,
|
||||||
|
restartCliproxyViaSupervisord,
|
||||||
|
} from '../../docker/supervisord-lifecycle';
|
||||||
|
|
||||||
interface ProxyStatusLike {
|
interface ProxyStatusLike {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@@ -63,6 +67,20 @@ export async function installDashboardCliproxyVersion(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In Docker, supervisord owns process lifecycle — delegate restart to it
|
||||||
|
if (isRunningUnderSupervisord()) {
|
||||||
|
const result = restartCliproxyViaSupervisord();
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
restarted: result.success,
|
||||||
|
port: result.port,
|
||||||
|
error: result.error,
|
||||||
|
message: result.success
|
||||||
|
? `Successfully installed ${backendLabel} v${version} and restarted it on port ${result.port}`
|
||||||
|
: `Installed ${backendLabel} v${version}, but restart failed`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const startResult = await deps.ensureCliproxyService();
|
const startResult = await deps.ensureCliproxyService();
|
||||||
if (!startResult.started && !startResult.alreadyRunning) {
|
if (!startResult.started && !startResult.alreadyRunning) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in new issue
Block a user