gm-plugkit 2.0.1307 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1307",
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": {
@@ -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
- 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
- }
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
- if (entry && Number.isFinite(entry.pid)) killPidQuiet(entry.pid);
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
- function handleSignalShutdown(sig) {
2117
+ const SHUTDOWN_REQUEST_PATH = path.join(spoolDir, '.shutdown-requested');
2118
+ setInterval(() => {
2042
2119
  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);
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 _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);
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
- 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}`);
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 });
package/plugkit.sha256 CHANGED
@@ -1 +1 @@
1
- {"plugkit.wasm":"7f24800faa40a43e615a98669dedfc7be0a2650dcb37e63dafb68e1f39c500a3"}
1
+ {"plugkit.wasm":"12f231b737248dc7b2b61518da4552469d104ceef35a2913fa8792720e5d634b"}
package/plugkit.version CHANGED
@@ -1 +1 @@
1
- 0.1.485
1
+ 0.1.486