fix(proxy): retry raced dynamic port binds

This commit is contained in:
Wooseong Kim
2026-04-20 14:33:44 +09:00
parent 2a80f7b1c1
commit 7cf555356a
2 changed files with 151 additions and 119 deletions
+1 -1
View File
@@ -199,7 +199,7 @@ export async function handleProxyCommand(args: string[]): Promise<number> {
case 'start': case 'start':
return handleStart(args.slice(1)); return handleStart(args.slice(1));
case 'stop': { case 'stop': {
const profileName = args[1] && !args[1]?.startsWith('-') ? args[1] : undefined; const profileName = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
const result = await stopOpenAICompatProxy(profileName); const result = await stopOpenAICompatProxy(profileName);
if (!result.success) { if (!result.success) {
console.error(fail(result.error || 'Failed to stop proxy')); console.error(fail(result.error || 'Failed to stop proxy'));
+150 -118
View File
@@ -93,12 +93,17 @@ async function isPortOccupied(port: number): Promise<boolean> {
}); });
} }
async function findOpenAICompatProxyPort(): Promise<number> { async function findOpenAICompatProxyPort(
excludedPorts: ReadonlySet<number> = new Set()
): Promise<number> {
for ( for (
let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT; let candidate = OPENAI_COMPAT_PROXY_DEFAULT_PORT;
candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10; candidate <= OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10;
candidate += 1 candidate += 1
) { ) {
if (excludedPorts.has(candidate)) {
continue;
}
if (!(await isPortOccupied(candidate))) { if (!(await isPortOccupied(candidate))) {
return candidate; return candidate;
} }
@@ -107,11 +112,14 @@ async function findOpenAICompatProxyPort(): Promise<number> {
return 0; return 0;
} }
async function findOpenAICompatProxyPortNear(preferredPort: number): Promise<number> { async function findOpenAICompatProxyPortNear(
if (!(await isPortOccupied(preferredPort))) { preferredPort: number,
excludedPorts: ReadonlySet<number> = new Set()
): Promise<number> {
if (!excludedPorts.has(preferredPort) && !(await isPortOccupied(preferredPort))) {
return preferredPort; return preferredPort;
} }
return findOpenAICompatProxyPort(); return findOpenAICompatProxyPort(excludedPorts);
} }
interface OpenAICompatProxyStateRecord { interface OpenAICompatProxyStateRecord {
@@ -350,36 +358,23 @@ export async function startOpenAICompatProxy(
? portPreference.port ? portPreference.port
: status.port || portPreference.port); : status.port || portPreference.port);
const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile'; const requiresExactPort = explicitPort !== undefined || portPreference.source === 'profile';
const port = if (status.running && status.port === preferredPort && (status.host || '127.0.0.1') === host) {
status.running && status.port && (status.host || '127.0.0.1') === host
? status.port
: requiresExactPort
? preferredPort
: await findOpenAICompatProxyPortNear(preferredPort);
if (port === 0) {
return {
success: false,
port: preferredPort,
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
};
}
if (!Number.isInteger(port) || port < 1 || port > 65535) {
return { success: false, port, error: `Invalid port: ${port}` };
}
if (status.running && status.port === port && (status.host || '127.0.0.1') === host) {
return { return {
success: true, success: true,
alreadyRunning: true, alreadyRunning: true,
pid: status.pid, pid: status.pid,
port, port: preferredPort,
authToken: status.authToken, authToken: status.authToken,
}; };
} }
if (requiresExactPort && (await isPortOccupied(port))) { if (!Number.isInteger(preferredPort) || preferredPort < 1 || preferredPort > 65535) {
return { success: false, port: preferredPort, error: `Invalid port: ${preferredPort}` };
}
if (requiresExactPort && (await isPortOccupied(preferredPort))) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: `Requested proxy port ${port} is already in use`, error: `Requested proxy port ${preferredPort} is already in use`,
}; };
} }
if (status.running) { if (status.running) {
@@ -387,7 +382,7 @@ export async function startOpenAICompatProxy(
if (!stopped.success) { if (!stopped.success) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: stopped.error || 'Failed to restart the running proxy', error: stopped.error || 'Failed to restart the running proxy',
}; };
} }
@@ -397,107 +392,144 @@ export async function startOpenAICompatProxy(
if (!daemonEntry) { if (!daemonEntry) {
return { return {
success: false, success: false,
port, port: preferredPort,
error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.', error: 'OpenAI proxy daemon entrypoint not found. Run `bun run build` and retry.',
}; };
} }
return new Promise((resolve) => { const startOnPort = (port: number): Promise<StartOpenAICompatProxyResult> =>
let resolved = false; new Promise((resolve) => {
let timeout: NodeJS.Timeout | null = null; let resolved = false;
const authToken = generateProxyAuthToken(); let timeout: NodeJS.Timeout | null = null;
const authToken = generateProxyAuthToken();
const finish = (result: StartOpenAICompatProxyResult) => { const finish = (result: StartOpenAICompatProxyResult) => {
if (resolved) return; if (resolved) return;
resolved = true; resolved = true;
if (timeout) clearTimeout(timeout); if (timeout) clearTimeout(timeout);
if (!result.success) { if (!result.success) {
removeOpenAICompatProxyPid(profile.profileName); removeOpenAICompatProxyPid(profile.profileName);
removeOpenAICompatProxySession(profile.profileName); removeOpenAICompatProxySession(profile.profileName);
}
resolve(result);
};
const proc: ChildProcess = spawn(
process.execPath,
[
daemonEntry,
'--port',
String(port),
'--host',
host,
'--profile',
profile.profileName,
'--settings-path',
profile.settingsPath,
'--auth-token',
authToken,
...(options.insecure ? ['--insecure'] : []),
'--ccs-openai-proxy-daemon',
],
{ stdio: 'ignore', detached: true }
);
proc.unref();
if (proc.pid) {
writeOpenAICompatProxyPid(profile.profileName, proc.pid);
} }
resolve(result); writeOpenAICompatProxySession({
}; profileName: profile.profileName,
settingsPath: profile.settingsPath,
const proc: ChildProcess = spawn(
process.execPath,
[
daemonEntry,
'--port',
String(port),
'--host',
host, host,
'--profile',
profile.profileName,
'--settings-path',
profile.settingsPath,
'--auth-token',
authToken,
...(options.insecure ? ['--insecure'] : []),
'--ccs-openai-proxy-daemon',
],
{ stdio: 'ignore', detached: true }
);
proc.unref();
if (proc.pid) {
writeOpenAICompatProxyPid(profile.profileName, proc.pid);
}
writeOpenAICompatProxySession({
profileName: profile.profileName,
settingsPath: profile.settingsPath,
host,
port,
baseUrl: profile.baseUrl,
authToken,
model: profile.model,
insecure: options.insecure,
});
let attempts = 0;
const poll = async () => {
attempts += 1;
if (await isOpenAICompatProxyRunning(port)) {
finish({ success: true, pid: proc.pid, port, authToken });
return;
}
if (attempts >= 30) {
finish({
success: false,
port,
error: `Proxy daemon did not start within 30 seconds on port ${port}`,
});
return;
}
timeout = setTimeout(poll, 1000);
};
timeout = setTimeout(poll, 1000);
proc.on('error', (error) => {
finish({ success: false, port, error: error.message });
});
proc.on('exit', (code, signal) => {
if (code === 0) {
finish({
success: false,
port,
error: 'Proxy daemon exited before becoming healthy',
});
return;
}
if (code !== null) {
finish({
success: false,
port,
error: `Proxy daemon exited with code ${code}`,
});
return;
}
finish({
success: false,
port, port,
error: `Proxy daemon was killed by signal ${signal}`, baseUrl: profile.baseUrl,
authToken,
model: profile.model,
insecure: options.insecure,
});
let attempts = 0;
const poll = async () => {
attempts += 1;
if (await isOpenAICompatProxyRunning(port)) {
finish({ success: true, pid: proc.pid, port, authToken });
return;
}
if (attempts >= 30) {
finish({
success: false,
port,
error: `Proxy daemon did not start within 30 seconds on port ${port}`,
});
return;
}
timeout = setTimeout(poll, 1000);
};
timeout = setTimeout(poll, 1000);
proc.on('error', (error) => {
finish({ success: false, port, error: error.message });
});
proc.on('exit', (code, signal) => {
if (code === 0) {
finish({
success: false,
port,
error: 'Proxy daemon exited before becoming healthy',
});
return;
}
if (code !== null) {
finish({
success: false,
port,
error: `Proxy daemon exited with code ${code}`,
});
return;
}
finish({
success: false,
port,
error: `Proxy daemon was killed by signal ${signal}`,
});
}); });
}); });
});
if (requiresExactPort) {
return startOnPort(preferredPort);
}
const attemptedPorts = new Set<number>();
let lastResult: StartOpenAICompatProxyResult | null = null;
for (let attempt = 0; attempt < 3; attempt += 1) {
const port = await findOpenAICompatProxyPortNear(preferredPort, attemptedPorts);
if (port === 0) {
return {
success: false,
port: preferredPort,
error: `No free proxy port found in range ${OPENAI_COMPAT_PROXY_DEFAULT_PORT}-${OPENAI_COMPAT_PROXY_DEFAULT_PORT + 10}`,
};
}
const result = await startOnPort(port);
if (result.success) {
return result;
}
lastResult = result;
attemptedPorts.add(port);
if (!(await isPortOccupied(port))) {
return result;
}
}
return (
lastResult ?? {
success: false,
port: preferredPort,
error: 'Failed to start proxy',
}
);
}); });
} }