gm-plugkit 2.0.1308 → 2.0.1309
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/package.json +1 -1
- package/plugkit-wasm-wrapper.js +131 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1309",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/plugkit-wasm-wrapper.js
CHANGED
|
@@ -813,6 +813,55 @@ function startManagedBrowser(pw, profileDir) {
|
|
|
813
813
|
return { pid, port, wsEndpoint };
|
|
814
814
|
}
|
|
815
815
|
|
|
816
|
+
function killPidQuiet(pid) {
|
|
817
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
818
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
819
|
+
if (process.platform === 'win32') {
|
|
820
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
821
|
+
}
|
|
822
|
+
return true;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function purgeProfileLockFiles(profileDir) {
|
|
826
|
+
if (!profileDir) return;
|
|
827
|
+
for (const name of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
|
|
828
|
+
try { fs.unlinkSync(path.join(profileDir, name)); } catch (_) {}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function sleepSyncMs(ms) {
|
|
833
|
+
if (ms <= 0) return;
|
|
834
|
+
spawnSync(process.execPath, ['-e', `setTimeout(()=>process.exit(0),${ms})`], { timeout: ms + 500, windowsHide: true });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function gracefulCloseBrowser(entry, reason) {
|
|
838
|
+
if (!entry) return;
|
|
839
|
+
const { pid, port, profileDir } = entry;
|
|
840
|
+
if (Number.isFinite(port) && port > 0) {
|
|
841
|
+
try {
|
|
842
|
+
const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 600);
|
|
843
|
+
if (info && info.webSocketDebuggerUrl) {
|
|
844
|
+
spawnSync(process.execPath, ['-e', `
|
|
845
|
+
const http = require('http');
|
|
846
|
+
const req = http.request({host:'127.0.0.1',port:${port},path:'/json/close/browser',method:'GET',timeout:1500},
|
|
847
|
+
res => { res.resume(); res.on('end', () => process.exit(0)); });
|
|
848
|
+
req.on('error', () => process.exit(1));
|
|
849
|
+
req.on('timeout', () => { req.destroy(); process.exit(1); });
|
|
850
|
+
req.end();
|
|
851
|
+
`], { timeout: 3000, windowsHide: true });
|
|
852
|
+
}
|
|
853
|
+
} catch (_) {}
|
|
854
|
+
}
|
|
855
|
+
if (Number.isFinite(pid) && pid > 0) {
|
|
856
|
+
const deadline = Date.now() + 1500;
|
|
857
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
858
|
+
while (Date.now() < deadline && isProcessAliveSync(pid)) sleepSyncMs(Math.min(150, deadline - Date.now()));
|
|
859
|
+
if (isProcessAliveSync(pid)) killPidQuiet(pid);
|
|
860
|
+
}
|
|
861
|
+
purgeProfileLockFiles(profileDir);
|
|
862
|
+
try { logEvent('plugkit', 'browser.closed', { reason: reason || 'closed', pid, port, profileDir }); } catch (_) {}
|
|
863
|
+
}
|
|
864
|
+
|
|
816
865
|
function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
817
866
|
migrateLegacyBrowserState(cwd);
|
|
818
867
|
const portsFile = browserPortsFile(cwd);
|
|
@@ -850,6 +899,12 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
850
899
|
want_profile: wantProfile,
|
|
851
900
|
reason,
|
|
852
901
|
});
|
|
902
|
+
if (typeof gracefulCloseBrowser === 'function') {
|
|
903
|
+
try { gracefulCloseBrowser(existing, `collision:${reason}`); } catch (_) {}
|
|
904
|
+
} else if (pidOk && Number.isFinite(existing.pid)) {
|
|
905
|
+
try { killPidQuiet(existing.pid); } catch (_) {}
|
|
906
|
+
}
|
|
907
|
+
purgeProfileLockFiles(existing.profileDir);
|
|
853
908
|
delete ports[claudeSessionId];
|
|
854
909
|
delete sessions[claudeSessionId];
|
|
855
910
|
try { writeJsonFile(portsFile, ports); } catch (_) {}
|
|
@@ -1596,6 +1651,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
1596
1651
|
});
|
|
1597
1652
|
}
|
|
1598
1653
|
const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
|
|
1654
|
+
try { lastBrowserActivityMs = Date.now(); } catch (_) {}
|
|
1599
1655
|
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
|
|
1600
1656
|
return writeWasmJson(instanceRef.value, {
|
|
1601
1657
|
ok: r.status === 0,
|
|
@@ -1899,14 +1955,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1899
1955
|
lastActivityMs = Date.now();
|
|
1900
1956
|
}
|
|
1901
1957
|
|
|
1902
|
-
|
|
1903
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
1904
|
-
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
1905
|
-
if (process.platform === 'win32') {
|
|
1906
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
1907
|
-
}
|
|
1908
|
-
return true;
|
|
1909
|
-
}
|
|
1958
|
+
/* killPidQuiet, purgeProfileLockFiles, gracefulCloseBrowser are module-scope (defined above spool()). */
|
|
1910
1959
|
|
|
1911
1960
|
function teardownAll(reason) {
|
|
1912
1961
|
try {
|
|
@@ -1921,7 +1970,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1921
1970
|
const sessionsFile = browserSessionsFile(process.cwd());
|
|
1922
1971
|
const ports = readJsonFile(portsFile, {});
|
|
1923
1972
|
for (const [sid, entry] of Object.entries(ports)) {
|
|
1924
|
-
|
|
1973
|
+
gracefulCloseBrowser(entry, `teardown:${reason}`);
|
|
1925
1974
|
}
|
|
1926
1975
|
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
1927
1976
|
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
@@ -1934,6 +1983,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1934
1983
|
pid: process.pid,
|
|
1935
1984
|
idle_ms: Date.now() - lastActivityMs,
|
|
1936
1985
|
}));
|
|
1986
|
+
__shutdownReasonWritten = true;
|
|
1937
1987
|
} catch (_) {}
|
|
1938
1988
|
|
|
1939
1989
|
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
@@ -2013,6 +2063,32 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2013
2063
|
}
|
|
2014
2064
|
}, 60_000);
|
|
2015
2065
|
|
|
2066
|
+
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 10 * 60 * 1000;
|
|
2067
|
+
let lastBrowserActivityMs = Date.now();
|
|
2068
|
+
setInterval(() => {
|
|
2069
|
+
try {
|
|
2070
|
+
const browserIdleMs = Date.now() - lastBrowserActivityMs;
|
|
2071
|
+
if (browserIdleMs < BROWSER_IDLE_LIMIT_MS) return;
|
|
2072
|
+
const portsFile = browserPortsFile(process.cwd());
|
|
2073
|
+
const sessionsFile = browserSessionsFile(process.cwd());
|
|
2074
|
+
const ports = readJsonFile(portsFile, {});
|
|
2075
|
+
let closed = 0;
|
|
2076
|
+
for (const [sid, entry] of Object.entries(ports)) {
|
|
2077
|
+
if (entry && Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
|
|
2078
|
+
try { gracefulCloseBrowser(entry, 'browser-idle'); closed++; } catch (_) {}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (closed > 0) {
|
|
2082
|
+
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
2083
|
+
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
2084
|
+
logEvent('plugkit', 'browser.idle-closed', { count: closed, idle_ms: browserIdleMs });
|
|
2085
|
+
}
|
|
2086
|
+
lastBrowserActivityMs = Date.now();
|
|
2087
|
+
} catch (e) {
|
|
2088
|
+
console.error(`[browser-idle] error: ${e.message}`);
|
|
2089
|
+
}
|
|
2090
|
+
}, 60_000);
|
|
2091
|
+
|
|
2016
2092
|
setInterval(() => {
|
|
2017
2093
|
try {
|
|
2018
2094
|
const idleMs = Date.now() - lastActivityMs;
|
|
@@ -2038,20 +2114,48 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2038
2114
|
}
|
|
2039
2115
|
}, IDLE_CHECK_MS);
|
|
2040
2116
|
|
|
2041
|
-
|
|
2117
|
+
const SHUTDOWN_REQUEST_PATH = path.join(spoolDir, '.shutdown-requested');
|
|
2118
|
+
setInterval(() => {
|
|
2042
2119
|
try {
|
|
2043
|
-
fs.
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2120
|
+
if (!fs.existsSync(SHUTDOWN_REQUEST_PATH)) return;
|
|
2121
|
+
let reqReason = 'shutdown-requested';
|
|
2122
|
+
try {
|
|
2123
|
+
const raw = fs.readFileSync(SHUTDOWN_REQUEST_PATH, 'utf-8').trim();
|
|
2124
|
+
if (raw) {
|
|
2125
|
+
try { const j = JSON.parse(raw); if (j && j.reason) reqReason = String(j.reason); }
|
|
2126
|
+
catch (_) { reqReason = raw.slice(0, 64); }
|
|
2127
|
+
}
|
|
2128
|
+
} catch (_) {}
|
|
2129
|
+
try { fs.unlinkSync(SHUTDOWN_REQUEST_PATH); } catch (_) {}
|
|
2130
|
+
handleSignalShutdown(reqReason.toUpperCase());
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
console.error(`[shutdown-request] error: ${e.message}`);
|
|
2133
|
+
}
|
|
2134
|
+
}, 2000);
|
|
2135
|
+
|
|
2136
|
+
let _signalShutdownInFlight = false;
|
|
2137
|
+
function handleSignalShutdown(sig) {
|
|
2138
|
+
if (_signalShutdownInFlight) return;
|
|
2139
|
+
_signalShutdownInFlight = true;
|
|
2140
|
+
try { teardownAll(sig.toLowerCase()); } catch (_) {
|
|
2141
|
+
try {
|
|
2142
|
+
fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
|
|
2143
|
+
reason: sig.toLowerCase(),
|
|
2144
|
+
ts: Date.now(),
|
|
2145
|
+
pid: process.pid,
|
|
2146
|
+
teardown_failed: true,
|
|
2147
|
+
}));
|
|
2148
|
+
__shutdownReasonWritten = true;
|
|
2149
|
+
} catch (__) {}
|
|
2150
|
+
try { clearBootActive(); } catch (__) {}
|
|
2151
|
+
try { releaseLock(); } catch (__) {}
|
|
2152
|
+
process.exit(0);
|
|
2153
|
+
}
|
|
2052
2154
|
}
|
|
2053
2155
|
process.on('SIGINT', () => handleSignalShutdown('SIGINT'));
|
|
2054
2156
|
process.on('SIGTERM', () => handleSignalShutdown('SIGTERM'));
|
|
2157
|
+
process.on('SIGBREAK', () => handleSignalShutdown('SIGBREAK'));
|
|
2158
|
+
process.on('SIGHUP', () => handleSignalShutdown('SIGHUP'));
|
|
2055
2159
|
process.on('exit', () => { try { clearBootActive(); } catch (_) {} releaseLock(); });
|
|
2056
2160
|
|
|
2057
2161
|
try {
|
|
@@ -2089,8 +2193,9 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2089
2193
|
prior_status: _priorStatus,
|
|
2090
2194
|
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
2091
2195
|
};
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2196
|
+
const _UNPLANNED_REASONS = new Set(['uncaughtexception', 'unhandledrejection', 'wasm-abort', 'wasm-abort-graceful']);
|
|
2197
|
+
const _normalizedShutdownReason = _priorShutdown && _priorShutdown.reason ? String(_priorShutdown.reason).toLowerCase() : null;
|
|
2198
|
+
const _isPlannedBoot = !!_normalizedShutdownReason && !_UNPLANNED_REASONS.has(_normalizedShutdownReason);
|
|
2094
2199
|
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
2095
2200
|
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
2096
2201
|
const HEARTBEAT_RECENT_MS = 60_000;
|
|
@@ -2131,7 +2236,11 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2131
2236
|
history,
|
|
2132
2237
|
}, null, 2));
|
|
2133
2238
|
} catch (_) {}
|
|
2134
|
-
|
|
2239
|
+
if (_isPlannedBoot) {
|
|
2240
|
+
console.log(`[plugkit-wasm] planned restart: prior reason="${_priorShutdown.reason}" boot_reason=${_bootReason}`);
|
|
2241
|
+
} else {
|
|
2242
|
+
console.error(`[plugkit-wasm] UNPLANNED RESTART detected — prior watcher died without writing .shutdown-reason.json. prior_status_age_ms=${restartContext.prior_status_age_ms} boot_reason=${_bootReason}`);
|
|
2243
|
+
}
|
|
2135
2244
|
}
|
|
2136
2245
|
try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
|
|
2137
2246
|
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });
|