fix: improve Windows managed OpenCode shutdown and launch behavior (#844)

* fix: windows shutdown and restart orphaned cleanup

* fix: launch managed OpenCode directly on Windows
Unwrap OpenCode wrappers to launch directly on Windows, improving shutdown reliability and avoid orphans.

* fix: restore desktopNotifyEnabled in health snapshot

---------

Signed-off-by: Dr. Zed <142888684+DocterZed@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Dr. Zed
2026-04-11 22:29:09 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent a3d73a8b67
commit 636dcd5314
8 changed files with 512 additions and 100 deletions
+112 -31
View File
@@ -1784,6 +1784,99 @@ function isProcessRunning(pid) {
}
}
function waitForProcessExit(pid, timeoutMs) {
if (!Number.isFinite(pid) || pid <= 0) {
return Promise.resolve(true);
}
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const check = () => {
if (!isProcessRunning(pid)) {
resolve(true);
return;
}
if (Date.now() >= deadline) {
resolve(false);
return;
}
setTimeout(check, 150);
};
check();
});
}
async function terminateProcessTree(pid, options = {}) {
if (!Number.isFinite(pid) || pid <= 0) {
return true;
}
const gracefulTimeoutMs = Number.isFinite(options.gracefulTimeoutMs) && options.gracefulTimeoutMs >= 0
? Math.trunc(options.gracefulTimeoutMs)
: 2500;
const forceTimeoutMs = Number.isFinite(options.forceTimeoutMs) && options.forceTimeoutMs >= 0
? Math.trunc(options.forceTimeoutMs)
: 3000;
if (process.platform === 'win32') {
try {
spawnSync('taskkill', ['/pid', String(pid), '/t'], {
stdio: 'ignore',
timeout: 3000,
windowsHide: true,
});
} catch {
}
if (await waitForProcessExit(pid, gracefulTimeoutMs)) {
return true;
}
try {
spawnSync('taskkill', ['/pid', String(pid), '/f', '/t'], {
stdio: 'ignore',
timeout: 5000,
windowsHide: true,
});
} catch {
}
return waitForProcessExit(pid, forceTimeoutMs);
}
try {
process.kill(pid, 'SIGTERM');
} catch {
}
if (await waitForProcessExit(pid, gracefulTimeoutMs)) {
return true;
}
try {
process.kill(pid, 'SIGKILL');
} catch {
}
return waitForProcessExit(pid, forceTimeoutMs);
}
async function stopInstanceProcess(pid, options = {}) {
if (!Number.isFinite(pid) || pid <= 0) {
return true;
}
const shutdownWaitMs = Number.isFinite(options.shutdownWaitMs) && options.shutdownWaitMs >= 0
? Math.trunc(options.shutdownWaitMs)
: 5000;
if (await waitForProcessExit(pid, shutdownWaitMs)) {
return true;
}
return terminateProcessTree(pid, options);
}
async function requestServerShutdown(port) {
if (!Number.isFinite(port) || port <= 0) return false;
const controller = new AbortController();
@@ -3060,18 +3153,11 @@ const commands = {
const requested = await requestServerShutdown(options.port);
if (Number.isFinite(systemInfo.pid) && isProcessRunning(systemInfo.pid)) {
try {
process.kill(systemInfo.pid, 'SIGTERM');
let attempts = 0;
while (isProcessRunning(systemInfo.pid) && attempts < 20) {
await new Promise((resolve) => setTimeout(resolve, 250));
attempts++;
}
if (isProcessRunning(systemInfo.pid)) {
process.kill(systemInfo.pid, 'SIGKILL');
}
} catch {
}
await stopInstanceProcess(systemInfo.pid, {
shutdownWaitMs: requested ? 5000 : 0,
gracefulTimeoutMs: 2500,
forceTimeoutMs: 3000,
}).catch(() => false);
}
const stopped = await isPortAvailable(options.port);
@@ -3142,15 +3228,14 @@ const commands = {
}
stopSpin?.start(`Stopping OpenChamber on port ${instance.port}...`);
try {
await requestServerShutdown(instance.port);
process.kill(instance.pid, 'SIGTERM');
let attempts = 0;
while (isProcessRunning(instance.pid) && attempts < 20) {
await new Promise((resolve) => setTimeout(resolve, 250));
attempts++;
}
if (isProcessRunning(instance.pid)) {
process.kill(instance.pid, 'SIGKILL');
const requested = await requestServerShutdown(instance.port);
const stopped = await stopInstanceProcess(instance.pid, {
shutdownWaitMs: requested ? 5000 : 0,
gracefulTimeoutMs: 2500,
forceTimeoutMs: 3000,
});
if (!stopped && isProcessRunning(instance.pid)) {
throw new Error(`Timed out stopping pid ${instance.pid}`);
}
removePidFile(instance.pidFilePath);
removeInstanceFile(instance.instanceFilePath);
@@ -4667,16 +4752,12 @@ const commands = {
updateSpin?.message(`Stopping ${runningInstances.length} running instance(s)...`);
for (const instance of runningInstances) {
try {
await requestServerShutdown(instance.port);
process.kill(instance.pid, 'SIGTERM');
let attempts = 0;
while (isProcessRunning(instance.pid) && attempts < 20) {
await new Promise((resolve) => setTimeout(resolve, 250));
attempts++;
}
if (isProcessRunning(instance.pid)) {
process.kill(instance.pid, 'SIGKILL');
}
const requested = await requestServerShutdown(instance.port);
await stopInstanceProcess(instance.pid, {
shutdownWaitMs: requested ? 5000 : 0,
gracefulTimeoutMs: 2500,
forceTimeoutMs: 3000,
});
removePidFile(instance.pidFilePath);
} catch {
}