synabun 2026.4.26-beta.11 → 2026.4.26-beta.12
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 +65 -22
- package/package.json +1 -1
- package/updater.mjs +39 -3
|
@@ -1594,11 +1594,15 @@ async function gracefulShutdown(reason = 'shutdown') {
|
|
|
1594
1594
|
|
|
1595
1595
|
try { closeDb(); } catch {}
|
|
1596
1596
|
|
|
1597
|
-
// 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
|
|
1598
|
-
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
|
-
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
|
-
// the process tree and terminates every descendant.
|
|
1601
|
-
|
|
1597
|
+
// 5. Belt-and-suspenders: tree-kill THIS process's descendants on Windows.
|
|
1598
|
+
// Synabun spawns CLI links, pty terminals, cloudflare tunnel etc. that may
|
|
1599
|
+
// not be reachable through the registries above. The taskkill /T flag walks
|
|
1600
|
+
// the process tree and terminates every descendant.
|
|
1601
|
+
//
|
|
1602
|
+
// Do not run this for click-to-update. The updater terminal is deliberately
|
|
1603
|
+
// launched before this server exits; taskkill /T would kill that brand-new
|
|
1604
|
+
// terminal too, which looks like a brief blink and no update on Windows.
|
|
1605
|
+
if (process.platform === 'win32' && reason !== 'run-update') {
|
|
1602
1606
|
try {
|
|
1603
1607
|
// Use spawn(detached) so taskkill survives our own exit and finishes
|
|
1604
1608
|
// killing children even if we exit before it completes.
|
|
@@ -1635,9 +1639,45 @@ app.post('/api/server/shutdown', async (req, res) => {
|
|
|
1635
1639
|
}, 200);
|
|
1636
1640
|
});
|
|
1637
1641
|
|
|
1638
|
-
function shellSingleQuote(s) {
|
|
1639
|
-
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1640
|
-
}
|
|
1642
|
+
function shellSingleQuote(s) {
|
|
1643
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function collectWindowsDescendantPids(rootPid) {
|
|
1647
|
+
if (process.platform !== 'win32' || !rootPid) return [];
|
|
1648
|
+
try {
|
|
1649
|
+
const script = [
|
|
1650
|
+
`$root=${Number(rootPid)}`,
|
|
1651
|
+
'$procs=Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId',
|
|
1652
|
+
'$seen=@{}',
|
|
1653
|
+
'$front=@($root)',
|
|
1654
|
+
'$out=@()',
|
|
1655
|
+
'while ($front.Count -gt 0) {',
|
|
1656
|
+
' $next=@()',
|
|
1657
|
+
' foreach ($p in $procs) {',
|
|
1658
|
+
' if ($front -contains $p.ParentProcessId -and -not $seen.ContainsKey($p.ProcessId)) {',
|
|
1659
|
+
' $seen[$p.ProcessId]=$true',
|
|
1660
|
+
' $out += $p.ProcessId',
|
|
1661
|
+
' $next += $p.ProcessId',
|
|
1662
|
+
' }',
|
|
1663
|
+
' }',
|
|
1664
|
+
' $front=$next',
|
|
1665
|
+
'}',
|
|
1666
|
+
'$out -join ","',
|
|
1667
|
+
].join('; ');
|
|
1668
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
1669
|
+
encoding: 'utf8',
|
|
1670
|
+
windowsHide: true,
|
|
1671
|
+
timeout: 5000,
|
|
1672
|
+
});
|
|
1673
|
+
const out = result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
1674
|
+
return out
|
|
1675
|
+
? out.split(',').map(v => Number(v)).filter(pid => Number.isFinite(pid) && pid > 0 && pid !== rootPid)
|
|
1676
|
+
: [];
|
|
1677
|
+
} catch {
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1641
1681
|
|
|
1642
1682
|
function cleanupOldUpdaterStages(baseDir) {
|
|
1643
1683
|
try {
|
|
@@ -1653,7 +1693,7 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
1653
1693
|
} catch {}
|
|
1654
1694
|
}
|
|
1655
1695
|
|
|
1656
|
-
function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
1696
|
+
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [] }) {
|
|
1657
1697
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
1658
1698
|
if (!existsSync(sourceUpdater)) {
|
|
1659
1699
|
throw new Error(`updater.mjs not found at ${sourceUpdater}. SynaBun install may be corrupt - reinstall manually: ${installPlan.displayCommand}`);
|
|
@@ -1678,20 +1718,22 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart }) {
|
|
|
1678
1718
|
displayCommand: installPlan.displayCommand,
|
|
1679
1719
|
source: installPlan.source,
|
|
1680
1720
|
current: installPlan.current,
|
|
1681
|
-
target: installPlan.target,
|
|
1682
|
-
channel: installPlan.channel,
|
|
1683
|
-
|
|
1721
|
+
target: installPlan.target,
|
|
1722
|
+
channel: installPlan.channel,
|
|
1723
|
+
cleanupPids,
|
|
1724
|
+
};
|
|
1684
1725
|
writeFileSync(payloadPath, JSON.stringify(payload, null, 2) + '\n', 'utf8');
|
|
1685
1726
|
|
|
1686
|
-
if (process.platform === 'win32') {
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
'
|
|
1690
|
-
'
|
|
1691
|
-
'
|
|
1692
|
-
'
|
|
1693
|
-
'set "
|
|
1694
|
-
|
|
1727
|
+
if (process.platform === 'win32') {
|
|
1728
|
+
const nodeBin = String(process.execPath || 'node').replace(/"/g, '');
|
|
1729
|
+
writeFileSync(runnerPath, [
|
|
1730
|
+
'@echo off',
|
|
1731
|
+
'setlocal enableextensions',
|
|
1732
|
+
'title SynaBun Updater',
|
|
1733
|
+
'cd /d "%~dp0"',
|
|
1734
|
+
'set "EXIT_CODE=0"',
|
|
1735
|
+
`set "NODE_BIN=${nodeBin}"`,
|
|
1736
|
+
'if defined SYNABUN_NODE_BIN set "NODE_BIN=%SYNABUN_NODE_BIN%"',
|
|
1695
1737
|
'"%NODE_BIN%" "%~dp0updater.mjs" --payload "%~dp0payload.json" --no-hold',
|
|
1696
1738
|
'set "EXIT_CODE=%ERRORLEVEL%"',
|
|
1697
1739
|
'echo.',
|
|
@@ -1761,7 +1803,8 @@ app.post('/api/system/run-update', async (req, res) => {
|
|
|
1761
1803
|
|
|
1762
1804
|
const plat = process.platform;
|
|
1763
1805
|
const serverPid = process.pid;
|
|
1764
|
-
const
|
|
1806
|
+
const cleanupPids = collectWindowsDescendantPids(serverPid);
|
|
1807
|
+
const staged = stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids });
|
|
1765
1808
|
|
|
1766
1809
|
let spawned = null;
|
|
1767
1810
|
let launchError = null;
|
package/package.json
CHANGED
package/updater.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const legacyTag = sanitizeTag(args.tag || 'latest');
|
|
|
20
20
|
const installSpec = sanitizeInstallSpec(payload.installSpec || args['install-spec'] || `synabun@${legacyTag}`);
|
|
21
21
|
const displayCommand = payload.displayCommand || `npm i -g ${installSpec}`;
|
|
22
22
|
const serverPid = toPid(payload.serverPid ?? args.pid);
|
|
23
|
+
const cleanupPids = sanitizePidList(payload.cleanupPids).filter(pid => pid !== serverPid && pid !== process.pid);
|
|
23
24
|
const autoRestart = payload.autoRestart != null ? !!payload.autoRestart : !!args.restart;
|
|
24
25
|
const safeCwd = payload.cwd || process.cwd();
|
|
25
26
|
const noHold = !!args['no-hold'];
|
|
@@ -46,8 +47,8 @@ try {
|
|
|
46
47
|
if (exited) {
|
|
47
48
|
console.log(' Server exited.\n');
|
|
48
49
|
} else {
|
|
49
|
-
console.warn(' Server still alive after 30s - forcing kill.\n');
|
|
50
|
-
|
|
50
|
+
console.warn(' Server still alive after 30s - forcing server process kill.\n');
|
|
51
|
+
forceKillPid(serverPid);
|
|
51
52
|
await sleep(1500);
|
|
52
53
|
}
|
|
53
54
|
} else {
|
|
@@ -55,7 +56,15 @@ try {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
await sleep(1500);
|
|
58
|
-
if (
|
|
59
|
+
if (cleanupPids.length) {
|
|
60
|
+
console.log(`Cleaning up ${cleanupPids.length} pre-existing child process${cleanupPids.length === 1 ? '' : 'es'}...`);
|
|
61
|
+
for (const pid of cleanupPids) {
|
|
62
|
+
if (!isPidAlive(pid)) continue;
|
|
63
|
+
forceKillTree(pid);
|
|
64
|
+
}
|
|
65
|
+
await sleep(1000);
|
|
66
|
+
console.log(' Cleanup complete.\n');
|
|
67
|
+
}
|
|
59
68
|
|
|
60
69
|
console.log(`Running: ${displayCommand}\n`);
|
|
61
70
|
console.log(HR);
|
|
@@ -143,6 +152,19 @@ function toPid(raw) {
|
|
|
143
152
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
function sanitizePidList(raw) {
|
|
156
|
+
if (!Array.isArray(raw)) return [];
|
|
157
|
+
const out = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
for (const item of raw) {
|
|
160
|
+
const pid = toPid(item);
|
|
161
|
+
if (!pid || seen.has(pid)) continue;
|
|
162
|
+
seen.add(pid);
|
|
163
|
+
out.push(pid);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
async function waitForExit(pid, timeoutMs) {
|
|
147
169
|
const deadline = Date.now() + timeoutMs;
|
|
148
170
|
while (Date.now() < deadline) {
|
|
@@ -176,6 +198,20 @@ function forceKillTree(pid) {
|
|
|
176
198
|
} catch {}
|
|
177
199
|
}
|
|
178
200
|
|
|
201
|
+
function forceKillPid(pid) {
|
|
202
|
+
try {
|
|
203
|
+
if (isWin) {
|
|
204
|
+
spawnSync('taskkill', ['/pid', String(pid), '/F'], {
|
|
205
|
+
shell: true,
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
stdio: 'ignore',
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
try { process.kill(pid, 'SIGKILL'); } catch {}
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
}
|
|
214
|
+
|
|
179
215
|
function printFixTips(command) {
|
|
180
216
|
console.error('Common fixes:');
|
|
181
217
|
if (isWin) {
|