fix: auto-detect and safely connect to existing OpenCode server on startup (#402)
Add probeExternalOpenCode() to detect a running OpenCode instance via GET /global/health before attempting to spawn a new one. When an external server is detected (or OPENCODE_SKIP_START is set), the isExternalOpenCode flag prevents gracefulShutdown() and restartOpenCode() from killing the user's independent OpenCode process. - Probe configured port, then default 4096 on startup - Use /global/health + body.healthy === true for reliable detection - Skip kill -9 on shutdown/restart when server is externally owned - Re-probe (instead of kill + respawn) on restart for external servers
This commit is contained in:
@@ -2687,6 +2687,7 @@ let openCodeApiDetectionTimer = null;
|
|||||||
let lastOpenCodeError = null;
|
let lastOpenCodeError = null;
|
||||||
let isOpenCodeReady = false;
|
let isOpenCodeReady = false;
|
||||||
let openCodeNotReadySince = 0;
|
let openCodeNotReadySince = 0;
|
||||||
|
let isExternalOpenCode = false;
|
||||||
let exitOnShutdown = true;
|
let exitOnShutdown = true;
|
||||||
let uiAuthController = null;
|
let uiAuthController = null;
|
||||||
let cloudflareTunnelController = null;
|
let cloudflareTunnelController = null;
|
||||||
@@ -2740,6 +2741,36 @@ async function isOpenCodeProcessHealthy() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probe if an external OpenCode instance is already running on the given port.
|
||||||
|
* Unlike isOpenCodeProcessHealthy(), this doesn't require openCodeProcess to be set.
|
||||||
|
* Used to auto-detect and connect to an existing OpenCode instance on startup.
|
||||||
|
*/
|
||||||
|
async function probeExternalOpenCode(port) {
|
||||||
|
if (!port || port <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||||
|
const response = await fetch(`http://127.0.0.1:${port}/global/health`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...getOpenCodeAuthHeaders(),
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (!response.ok) return false;
|
||||||
|
const body = await response.json().catch(() => null);
|
||||||
|
return body?.healthy === true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
const ENV_CONFIGURED_OPENCODE_PORT = (() => {
|
||||||
const raw =
|
const raw =
|
||||||
process.env.OPENCODE_PORT ||
|
process.env.OPENCODE_PORT ||
|
||||||
@@ -4338,6 +4369,31 @@ async function restartOpenCode() {
|
|||||||
openCodeNotReadySince = Date.now();
|
openCodeNotReadySince = Date.now();
|
||||||
console.log('Restarting OpenCode process...');
|
console.log('Restarting OpenCode process...');
|
||||||
|
|
||||||
|
// For external OpenCode servers, re-probe instead of kill + respawn
|
||||||
|
if (isExternalOpenCode) {
|
||||||
|
console.log('Re-probing external OpenCode server...');
|
||||||
|
const probePort = openCodePort || ENV_CONFIGURED_OPENCODE_PORT || 4096;
|
||||||
|
const healthy = await probeExternalOpenCode(probePort);
|
||||||
|
if (healthy) {
|
||||||
|
console.log(`External OpenCode server on port ${probePort} is healthy`);
|
||||||
|
setOpenCodePort(probePort);
|
||||||
|
isOpenCodeReady = true;
|
||||||
|
lastOpenCodeError = null;
|
||||||
|
openCodeNotReadySince = 0;
|
||||||
|
syncToHmrState();
|
||||||
|
} else {
|
||||||
|
lastOpenCodeError = `External OpenCode server on port ${probePort} is not responding`;
|
||||||
|
console.error(lastOpenCodeError);
|
||||||
|
throw new Error(lastOpenCodeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expressApp) {
|
||||||
|
setupProxy(expressApp);
|
||||||
|
ensureOpenCodeApiPrefix();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const portToKill = openCodePort;
|
const portToKill = openCodePort;
|
||||||
|
|
||||||
if (openCodeProcess) {
|
if (openCodeProcess) {
|
||||||
@@ -5018,7 +5074,7 @@ async function gracefulShutdown(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only stop OpenCode if we started it ourselves (not when using external server)
|
// Only stop OpenCode if we started it ourselves (not when using external server)
|
||||||
if (!ENV_SKIP_OPENCODE_START) {
|
if (!ENV_SKIP_OPENCODE_START && !isExternalOpenCode) {
|
||||||
const portToKill = openCodePort;
|
const portToKill = openCodePort;
|
||||||
|
|
||||||
if (openCodeProcess) {
|
if (openCodeProcess) {
|
||||||
@@ -10592,6 +10648,23 @@ Context:
|
|||||||
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
|
console.log(`Using external OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT} (skip-start mode)`);
|
||||||
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||||
isOpenCodeReady = true;
|
isOpenCodeReady = true;
|
||||||
|
isExternalOpenCode = true;
|
||||||
|
lastOpenCodeError = null;
|
||||||
|
openCodeNotReadySince = 0;
|
||||||
|
syncToHmrState();
|
||||||
|
} else if (ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(ENV_CONFIGURED_OPENCODE_PORT)) {
|
||||||
|
console.log(`Auto-detected existing OpenCode server on port ${ENV_CONFIGURED_OPENCODE_PORT}`);
|
||||||
|
setOpenCodePort(ENV_CONFIGURED_OPENCODE_PORT);
|
||||||
|
isOpenCodeReady = true;
|
||||||
|
isExternalOpenCode = true;
|
||||||
|
lastOpenCodeError = null;
|
||||||
|
openCodeNotReadySince = 0;
|
||||||
|
syncToHmrState();
|
||||||
|
} else if (!ENV_CONFIGURED_OPENCODE_PORT && await probeExternalOpenCode(4096)) {
|
||||||
|
console.log('Auto-detected existing OpenCode server on default port 4096');
|
||||||
|
setOpenCodePort(4096);
|
||||||
|
isOpenCodeReady = true;
|
||||||
|
isExternalOpenCode = true;
|
||||||
lastOpenCodeError = null;
|
lastOpenCodeError = null;
|
||||||
openCodeNotReadySince = 0;
|
openCodeNotReadySince = 0;
|
||||||
syncToHmrState();
|
syncToHmrState();
|
||||||
|
|||||||
Reference in New Issue
Block a user