gm-plugkit 2.0.1308 → 2.0.1310

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1308",
3
+ "version": "2.0.1310",
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": {
@@ -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 (_) {}
@@ -1560,6 +1615,15 @@ function makeHostFunctions(instanceRef) {
1560
1615
  const prefix = level >= 3 ? '[plugkit-wasm:err]' : level >= 2 ? '[plugkit-wasm:warn]' : '[plugkit-wasm]';
1561
1616
  if (level >= 2) console.error(`${prefix} ${msg}`);
1562
1617
  else console.log(`${prefix} ${msg}`);
1618
+ const evtMatch = msg.match(/^evt:\s*(\{.*\})\s*$/);
1619
+ if (evtMatch) {
1620
+ try {
1621
+ const ev = JSON.parse(evtMatch[1]);
1622
+ const eventName = ev.event || 'wasm.event';
1623
+ const { event: _e, ts: _ts, sess: _s, sub: _sub, ...fields } = ev;
1624
+ logEvent(ev.sub || 'plugkit', eventName, fields);
1625
+ } catch (_) {}
1626
+ }
1563
1627
  return 0;
1564
1628
  } catch (e) {
1565
1629
  return 0;
@@ -1596,6 +1660,7 @@ function makeHostFunctions(instanceRef) {
1596
1660
  });
1597
1661
  }
1598
1662
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1663
+ try { lastBrowserActivityMs = Date.now(); } catch (_) {}
1599
1664
  const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1600
1665
  return writeWasmJson(instanceRef.value, {
1601
1666
  ok: r.status === 0,
@@ -1899,14 +1964,7 @@ async function runSpoolWatcher(instance, spoolDir) {
1899
1964
  lastActivityMs = Date.now();
1900
1965
  }
1901
1966
 
1902
- function killPidQuiet(pid) {
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
- }
1967
+ /* killPidQuiet, purgeProfileLockFiles, gracefulCloseBrowser are module-scope (defined above spool()). */
1910
1968
 
1911
1969
  function teardownAll(reason) {
1912
1970
  try {
@@ -1921,7 +1979,7 @@ async function runSpoolWatcher(instance, spoolDir) {
1921
1979
  const sessionsFile = browserSessionsFile(process.cwd());
1922
1980
  const ports = readJsonFile(portsFile, {});
1923
1981
  for (const [sid, entry] of Object.entries(ports)) {
1924
- if (entry && Number.isFinite(entry.pid)) killPidQuiet(entry.pid);
1982
+ gracefulCloseBrowser(entry, `teardown:${reason}`);
1925
1983
  }
1926
1984
  try { fs.unlinkSync(portsFile); } catch (_) {}
1927
1985
  try { fs.unlinkSync(sessionsFile); } catch (_) {}
@@ -1934,6 +1992,7 @@ async function runSpoolWatcher(instance, spoolDir) {
1934
1992
  pid: process.pid,
1935
1993
  idle_ms: Date.now() - lastActivityMs,
1936
1994
  }));
1995
+ __shutdownReasonWritten = true;
1937
1996
  } catch (_) {}
1938
1997
 
1939
1998
  try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
@@ -2013,6 +2072,32 @@ async function runSpoolWatcher(instance, spoolDir) {
2013
2072
  }
2014
2073
  }, 60_000);
2015
2074
 
2075
+ const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 10 * 60 * 1000;
2076
+ let lastBrowserActivityMs = Date.now();
2077
+ setInterval(() => {
2078
+ try {
2079
+ const browserIdleMs = Date.now() - lastBrowserActivityMs;
2080
+ if (browserIdleMs < BROWSER_IDLE_LIMIT_MS) return;
2081
+ const portsFile = browserPortsFile(process.cwd());
2082
+ const sessionsFile = browserSessionsFile(process.cwd());
2083
+ const ports = readJsonFile(portsFile, {});
2084
+ let closed = 0;
2085
+ for (const [sid, entry] of Object.entries(ports)) {
2086
+ if (entry && Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) {
2087
+ try { gracefulCloseBrowser(entry, 'browser-idle'); closed++; } catch (_) {}
2088
+ }
2089
+ }
2090
+ if (closed > 0) {
2091
+ try { fs.unlinkSync(portsFile); } catch (_) {}
2092
+ try { fs.unlinkSync(sessionsFile); } catch (_) {}
2093
+ logEvent('plugkit', 'browser.idle-closed', { count: closed, idle_ms: browserIdleMs });
2094
+ }
2095
+ lastBrowserActivityMs = Date.now();
2096
+ } catch (e) {
2097
+ console.error(`[browser-idle] error: ${e.message}`);
2098
+ }
2099
+ }, 60_000);
2100
+
2016
2101
  setInterval(() => {
2017
2102
  try {
2018
2103
  const idleMs = Date.now() - lastActivityMs;
@@ -2038,20 +2123,48 @@ async function runSpoolWatcher(instance, spoolDir) {
2038
2123
  }
2039
2124
  }, IDLE_CHECK_MS);
2040
2125
 
2041
- function handleSignalShutdown(sig) {
2126
+ const SHUTDOWN_REQUEST_PATH = path.join(spoolDir, '.shutdown-requested');
2127
+ setInterval(() => {
2042
2128
  try {
2043
- fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
2044
- reason: sig.toLowerCase(),
2045
- ts: Date.now(),
2046
- pid: process.pid,
2047
- }));
2048
- } catch (_) {}
2049
- try { clearBootActive(); } catch (_) {}
2050
- try { releaseLock(); } catch (_) {}
2051
- process.exit(0);
2129
+ if (!fs.existsSync(SHUTDOWN_REQUEST_PATH)) return;
2130
+ let reqReason = 'shutdown-requested';
2131
+ try {
2132
+ const raw = fs.readFileSync(SHUTDOWN_REQUEST_PATH, 'utf-8').trim();
2133
+ if (raw) {
2134
+ try { const j = JSON.parse(raw); if (j && j.reason) reqReason = String(j.reason); }
2135
+ catch (_) { reqReason = raw.slice(0, 64); }
2136
+ }
2137
+ } catch (_) {}
2138
+ try { fs.unlinkSync(SHUTDOWN_REQUEST_PATH); } catch (_) {}
2139
+ handleSignalShutdown(reqReason.toUpperCase());
2140
+ } catch (e) {
2141
+ console.error(`[shutdown-request] error: ${e.message}`);
2142
+ }
2143
+ }, 2000);
2144
+
2145
+ let _signalShutdownInFlight = false;
2146
+ function handleSignalShutdown(sig) {
2147
+ if (_signalShutdownInFlight) return;
2148
+ _signalShutdownInFlight = true;
2149
+ try { teardownAll(sig.toLowerCase()); } catch (_) {
2150
+ try {
2151
+ fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
2152
+ reason: sig.toLowerCase(),
2153
+ ts: Date.now(),
2154
+ pid: process.pid,
2155
+ teardown_failed: true,
2156
+ }));
2157
+ __shutdownReasonWritten = true;
2158
+ } catch (__) {}
2159
+ try { clearBootActive(); } catch (__) {}
2160
+ try { releaseLock(); } catch (__) {}
2161
+ process.exit(0);
2162
+ }
2052
2163
  }
2053
2164
  process.on('SIGINT', () => handleSignalShutdown('SIGINT'));
2054
2165
  process.on('SIGTERM', () => handleSignalShutdown('SIGTERM'));
2166
+ process.on('SIGBREAK', () => handleSignalShutdown('SIGBREAK'));
2167
+ process.on('SIGHUP', () => handleSignalShutdown('SIGHUP'));
2055
2168
  process.on('exit', () => { try { clearBootActive(); } catch (_) {} releaseLock(); });
2056
2169
 
2057
2170
  try {
@@ -2089,8 +2202,9 @@ async function runSpoolWatcher(instance, spoolDir) {
2089
2202
  prior_status: _priorStatus,
2090
2203
  prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
2091
2204
  };
2092
- const _PLANNED_REASONS = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'version-drift', 'external-planned']);
2093
- const _isPlannedBoot = _priorShutdown && _PLANNED_REASONS.has(_priorShutdown.reason);
2205
+ const _UNPLANNED_REASONS = new Set(['uncaughtexception', 'unhandledrejection', 'wasm-abort', 'wasm-abort-graceful']);
2206
+ const _normalizedShutdownReason = _priorShutdown && _priorShutdown.reason ? String(_priorShutdown.reason).toLowerCase() : null;
2207
+ const _isPlannedBoot = !!_normalizedShutdownReason && !_UNPLANNED_REASONS.has(_normalizedShutdownReason);
2094
2208
  const _isFirstBoot = !_priorShutdown && !_priorStatus;
2095
2209
  const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
2096
2210
  const HEARTBEAT_RECENT_MS = 60_000;
@@ -2131,7 +2245,11 @@ async function runSpoolWatcher(instance, spoolDir) {
2131
2245
  history,
2132
2246
  }, null, 2));
2133
2247
  } catch (_) {}
2134
- 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}`);
2248
+ if (_isPlannedBoot) {
2249
+ console.log(`[plugkit-wasm] planned restart: prior reason="${_priorShutdown.reason}" boot_reason=${_bootReason}`);
2250
+ } else {
2251
+ 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}`);
2252
+ }
2135
2253
  }
2136
2254
  try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
2137
2255
  logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });