synabun 2026.4.26-beta.6 → 2026.4.26-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/neural-interface/server.js +44 -5
- package/package.json +1 -1
- package/preuninstall.js +74 -0
|
@@ -4509,12 +4509,36 @@ function stopOpencodeServer() {
|
|
|
4509
4509
|
stopOpencodeSSERelay();
|
|
4510
4510
|
const managedPid = readOpencodeManagedPid();
|
|
4511
4511
|
const procPid = _ocpProc?.pid || null;
|
|
4512
|
+
const isWin = process.platform === 'win32';
|
|
4513
|
+
|
|
4514
|
+
// On Windows `process.kill(pid, 'SIGTERM')` is unreliable for non-Node
|
|
4515
|
+
// children (cmd.exe shells, opencode native binaries). Use taskkill /T /F
|
|
4516
|
+
// to walk the process tree and force-terminate every descendant.
|
|
4517
|
+
// This is the recurring source of EBUSY errors during `npm install -g
|
|
4518
|
+
// synabun@latest` — opencode-ai's `serve` process survives parent SynaBun
|
|
4519
|
+
// exit and keeps file handles inside neural-interface/ open.
|
|
4520
|
+
const winKill = (pid) => {
|
|
4521
|
+
try {
|
|
4522
|
+
spawn('taskkill', ['/pid', String(pid), '/T', '/F'], {
|
|
4523
|
+
shell: true, windowsHide: true, stdio: 'ignore',
|
|
4524
|
+
});
|
|
4525
|
+
} catch {}
|
|
4526
|
+
};
|
|
4527
|
+
|
|
4512
4528
|
if (_ocpProc && !_ocpProc.killed) {
|
|
4513
|
-
|
|
4529
|
+
if (isWin && procPid) {
|
|
4530
|
+
winKill(procPid);
|
|
4531
|
+
} else {
|
|
4532
|
+
try { _ocpProc.kill(); } catch {}
|
|
4533
|
+
}
|
|
4514
4534
|
_ocpProc = null;
|
|
4515
4535
|
}
|
|
4516
4536
|
if (managedPid && managedPid !== procPid) {
|
|
4517
|
-
|
|
4537
|
+
if (isWin) {
|
|
4538
|
+
winKill(managedPid);
|
|
4539
|
+
} else {
|
|
4540
|
+
signalPids([managedPid], 'SIGTERM');
|
|
4541
|
+
}
|
|
4518
4542
|
}
|
|
4519
4543
|
clearOpencodeManagedPid();
|
|
4520
4544
|
_ocpReady = false;
|
|
@@ -23587,6 +23611,21 @@ function saveSessionSnapshot() {
|
|
|
23587
23611
|
setInterval(saveSessionSnapshot, SESSION_SNAPSHOT_INTERVAL_MS);
|
|
23588
23612
|
|
|
23589
23613
|
// Also try to save on clean shutdown (works with Ctrl+C, not window close)
|
|
23590
|
-
|
|
23591
|
-
|
|
23592
|
-
|
|
23614
|
+
// Manual termination paths (Ctrl+C, kill, parent shell exit) all flow through
|
|
23615
|
+
// gracefulShutdown — same teardown the in-app restart/shutdown/update paths
|
|
23616
|
+
// use. Without this, hand-killed servers still leave orphaned opencode +
|
|
23617
|
+
// agent children holding file handles, which blocks `npm install -g synabun`.
|
|
23618
|
+
process.on('SIGINT', async () => {
|
|
23619
|
+
console.log('\n[server] SIGINT received.');
|
|
23620
|
+
await gracefulShutdown('sigint');
|
|
23621
|
+
process.exit(0);
|
|
23622
|
+
});
|
|
23623
|
+
process.on('SIGTERM', async () => {
|
|
23624
|
+
console.log('\n[server] SIGTERM received.');
|
|
23625
|
+
await gracefulShutdown('sigterm');
|
|
23626
|
+
process.exit(0);
|
|
23627
|
+
});
|
|
23628
|
+
// `exit` fires after process.exit() — only safe for synchronous final tasks.
|
|
23629
|
+
// gracefulShutdown is async so we cannot await here; just snapshot + ocp stop
|
|
23630
|
+
// best-effort. The async paths above are the primary teardown.
|
|
23631
|
+
process.on('exit', () => { try { stopOpencodeServer(); } catch {} try { saveSessionSnapshot(); } catch {} });
|
package/package.json
CHANGED
package/preuninstall.js
CHANGED
|
@@ -69,6 +69,80 @@ async function tryGracefulServerShutdown() {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// ── Step 1b: kill any orphaned processes holding file handles ──
|
|
73
|
+
//
|
|
74
|
+
// Even after a graceful server shutdown (or when no server is running),
|
|
75
|
+
// orphaned children can survive: opencode-ai sidecars spawned via cmd.exe,
|
|
76
|
+
// MCP servers (claude-memory-mcp) spawned by Claude Code instances rather
|
|
77
|
+
// than by SynaBun itself, agent processes, etc. On Windows these orphans
|
|
78
|
+
// keep file handles inside the install dir open, causing EBUSY when npm
|
|
79
|
+
// tries to rename it.
|
|
80
|
+
//
|
|
81
|
+
// We nuke any process whose command line references the SynaBun install
|
|
82
|
+
// path. This catches every orphan regardless of how they got that way.
|
|
83
|
+
// Skips the current PID (we are the preuninstall process itself).
|
|
84
|
+
|
|
85
|
+
killOrphansInsideInstall();
|
|
86
|
+
|
|
87
|
+
function killOrphansInsideInstall() {
|
|
88
|
+
const installPath = __dirname;
|
|
89
|
+
const selfPid = process.pid;
|
|
90
|
+
const plat = platform();
|
|
91
|
+
|
|
92
|
+
if (plat === 'win32') {
|
|
93
|
+
// PowerShell: Get-CimInstance returns every process with CommandLine.
|
|
94
|
+
// Filter for our install path (escape backslashes for the -like wildcard).
|
|
95
|
+
// Stop-Process -Force kills synchronously; -ErrorAction SilentlyContinue
|
|
96
|
+
// suppresses "process already exited" noise.
|
|
97
|
+
const escaped = installPath.replace(/\\/g, '\\\\').replace(/'/g, `''`);
|
|
98
|
+
const ps = `
|
|
99
|
+
$ErrorActionPreference = 'SilentlyContinue';
|
|
100
|
+
Get-CimInstance Win32_Process |
|
|
101
|
+
Where-Object {
|
|
102
|
+
$_.ProcessId -ne ${selfPid} -and (
|
|
103
|
+
($_.CommandLine -and $_.CommandLine -like '*${escaped}*') -or
|
|
104
|
+
($_.ExecutablePath -and $_.ExecutablePath -like '${escaped}*')
|
|
105
|
+
)
|
|
106
|
+
} |
|
|
107
|
+
ForEach-Object {
|
|
108
|
+
Write-Host (' Killing orphan PID ' + $_.ProcessId + ' (' + $_.Name + ')');
|
|
109
|
+
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
|
|
110
|
+
}
|
|
111
|
+
`.trim();
|
|
112
|
+
try {
|
|
113
|
+
execSync(`powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, {
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
timeout: 8000,
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
// PowerShell unavailable, blocked, or timed out. Fall through.
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Unix: pgrep -f matches against the full command line. -P 1 skips
|
|
122
|
+
// self/children of init we shouldn't touch. We just enumerate then
|
|
123
|
+
// SIGKILL each one that isn't us.
|
|
124
|
+
try {
|
|
125
|
+
const out = execSync(`pgrep -f "${installPath.replace(/[/.*?+^$|(){}\\\[\]]/g, '\\$&')}"`, {
|
|
126
|
+
encoding: 'utf8',
|
|
127
|
+
timeout: 5000,
|
|
128
|
+
}).trim();
|
|
129
|
+
const pids = out.split('\n')
|
|
130
|
+
.map(s => Number(s.trim()))
|
|
131
|
+
.filter(n => Number.isFinite(n) && n > 0 && n !== selfPid);
|
|
132
|
+
for (const pid of pids) {
|
|
133
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// No matches or pgrep missing — fine.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Brief grace for OS to release file handles after killing.
|
|
141
|
+
// Synchronous busy-wait so we run before the rmSync calls below.
|
|
142
|
+
const until = Date.now() + 1500;
|
|
143
|
+
while (Date.now() < until) { /* spin */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
72
146
|
// ── Step 2: remove nested node_modules ──
|
|
73
147
|
|
|
74
148
|
const dirs = [
|