gm-plugkit 2.0.1540 → 2.0.1542
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/bootstrap.js +19 -1
- package/package.json +1 -1
- package/plugkit-wasm-wrapper.js +44 -1
- package/plugkit.version +1 -1
- package/supervisor.js +32 -3
package/bootstrap.js
CHANGED
|
@@ -606,7 +606,25 @@ function copyWasmToGmTools(wasmPath, version) {
|
|
|
606
606
|
if (cur === src) wasmFresh = true;
|
|
607
607
|
} catch (_) {}
|
|
608
608
|
}
|
|
609
|
-
if (!wasmFresh)
|
|
609
|
+
if (!wasmFresh) {
|
|
610
|
+
// copyFileSync truncates the target before streaming ~149MB, leaving a window where
|
|
611
|
+
// a crash or a concurrent watcher load sees a truncated/absent wasm (the
|
|
612
|
+
// "self-heal: wasm not installed" crash-loop during an upgrade). Copy to a
|
|
613
|
+
// pid-suffixed temp and rename over the target: same-volume rename is atomic,
|
|
614
|
+
// with the Windows EEXIST/EPERM unlink+retry.
|
|
615
|
+
const tmp = `${target}.partial-${process.pid}`;
|
|
616
|
+
fs.copyFileSync(wasmPath, tmp);
|
|
617
|
+
try { fs.renameSync(tmp, target); }
|
|
618
|
+
catch (err) {
|
|
619
|
+
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
620
|
+
try { fs.unlinkSync(target); } catch (_) {}
|
|
621
|
+
fs.renameSync(tmp, target);
|
|
622
|
+
} else {
|
|
623
|
+
try { fs.unlinkSync(tmp); } catch (_) {}
|
|
624
|
+
throw err;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
610
628
|
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
611
629
|
|
|
612
630
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1542",
|
|
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
|
@@ -2212,6 +2212,32 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2212
2212
|
detected_at: Date.now(),
|
|
2213
2213
|
});
|
|
2214
2214
|
try { console.error(`[plugkit-wasm] VERB ABORT detected: prior watcher pid=${priorVerb.pid} died inside verb=${priorVerb.verb} task=${priorVerb.task}`); } catch (_) {}
|
|
2215
|
+
// The aborted dispatch otherwise gets NO response file: the in-file was consumed,
|
|
2216
|
+
// the prior watcher died before writing out/, and the agent waits forever on a
|
|
2217
|
+
// Read that never lands (then must git-archaeology whether the verb's side effects
|
|
2218
|
+
// happened). Write a definite failure response so the agent's Read returns
|
|
2219
|
+
// immediately and it re-dispatches. Both out-name shapes are written because
|
|
2220
|
+
// .verb-active.json does not record whether the dispatch was root or nested;
|
|
2221
|
+
// the agent reads whichever its dispatch shape expects, the other is swept.
|
|
2222
|
+
if (priorVerb.verb && priorVerb.task) {
|
|
2223
|
+
try {
|
|
2224
|
+
const abortBody = JSON.stringify({
|
|
2225
|
+
ok: false,
|
|
2226
|
+
error: `verb aborted: watcher pid=${priorVerb.pid} died mid-verb; side effects may be partial -- verify state, then re-dispatch`,
|
|
2227
|
+
verb_aborted: true,
|
|
2228
|
+
verb: priorVerb.verb,
|
|
2229
|
+
task: priorVerb.task,
|
|
2230
|
+
});
|
|
2231
|
+
const abortOutDir = path.join(path.dirname(STATUS_PATH_FOR_VERB_ABORT), 'out');
|
|
2232
|
+
fs.mkdirSync(abortOutDir, { recursive: true });
|
|
2233
|
+
const nestedName = path.join(abortOutDir, `${priorVerb.verb}-${priorVerb.task}.json`);
|
|
2234
|
+
if (!fs.existsSync(nestedName)) fs.writeFileSync(nestedName, abortBody);
|
|
2235
|
+
if (priorVerb.verb !== priorVerb.task) {
|
|
2236
|
+
const rootName = path.join(abortOutDir, `${priorVerb.task}.json`);
|
|
2237
|
+
if (!fs.existsSync(rootName)) fs.writeFileSync(rootName, abortBody);
|
|
2238
|
+
}
|
|
2239
|
+
} catch (_) {}
|
|
2240
|
+
}
|
|
2215
2241
|
}
|
|
2216
2242
|
try { fs.unlinkSync(VERB_ACTIVE_PATH); } catch (_) {}
|
|
2217
2243
|
}
|
|
@@ -3628,7 +3654,24 @@ async function selfHealFromGithubReleases() {
|
|
|
3628
3654
|
}
|
|
3629
3655
|
const toolsDir = GM_TOOLS_ROOT;
|
|
3630
3656
|
fs.mkdirSync(toolsDir, { recursive: true });
|
|
3631
|
-
|
|
3657
|
+
// Replace the live wasm atomically. A direct writeFileSync truncates the target
|
|
3658
|
+
// before streaming ~149MB, so a crash mid-write or a concurrent watcher load in
|
|
3659
|
+
// that window sees a truncated or absent wasm ("self-heal: wasm not installed"
|
|
3660
|
+
// crash-loop). Write to a pid-suffixed temp and rename over the target; rename
|
|
3661
|
+
// on the same volume is atomic, with the Windows EEXIST/EPERM unlink+retry.
|
|
3662
|
+
const wasmTarget = path.join(toolsDir, 'plugkit.wasm');
|
|
3663
|
+
const wasmTmp = `${wasmTarget}.partial-${process.pid}`;
|
|
3664
|
+
fs.writeFileSync(wasmTmp, wasm);
|
|
3665
|
+
try { fs.renameSync(wasmTmp, wasmTarget); }
|
|
3666
|
+
catch (renameErr) {
|
|
3667
|
+
if (renameErr.code === 'EEXIST' || renameErr.code === 'EPERM') {
|
|
3668
|
+
try { fs.unlinkSync(wasmTarget); } catch (_) {}
|
|
3669
|
+
fs.renameSync(wasmTmp, wasmTarget);
|
|
3670
|
+
} else {
|
|
3671
|
+
try { fs.unlinkSync(wasmTmp); } catch (_) {}
|
|
3672
|
+
throw renameErr;
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3632
3675
|
fs.writeFileSync(path.join(toolsDir, 'plugkit.version'), version);
|
|
3633
3676
|
const wrapperSrc = __filename;
|
|
3634
3677
|
const wrapperDst = path.join(toolsDir, 'plugkit-wasm-wrapper.js');
|
package/plugkit.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.639
|
package/supervisor.js
CHANGED
|
@@ -85,10 +85,39 @@ function acquireSingleInstance() {
|
|
|
85
85
|
let other = NaN;
|
|
86
86
|
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
87
87
|
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// An alive holder pid is not the same as a working holder: a wedged supervisor
|
|
89
|
+
// (event loop stuck, watcher dead, neither .supervisor.json nor .status.json
|
|
90
|
+
// advancing) blocks every newcomer forever under a pidAlive-only check, forcing
|
|
91
|
+
// manual process kills to recover the spool. Discriminate by progress, not
|
|
92
|
+
// liveness: holder is wedged only when its own status heartbeat AND the spool
|
|
93
|
+
// status are both stale past the takeover window, honoring a future busy_until
|
|
94
|
+
// exactly as checkWatcherHealth does.
|
|
95
|
+
const TAKEOVER_STALE_MS = 45_000;
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
let supTs = 0;
|
|
98
|
+
try { supTs = (JSON.parse(fs.readFileSync(SUPERVISOR_PATH, 'utf-8')).ts) || 0; } catch (_) {}
|
|
99
|
+
const spool = readStatus();
|
|
100
|
+
const spoolBusy = spool && spool.busy_until && spool.busy_until > now;
|
|
101
|
+
const spoolTs = (spool && spool.ts) || 0;
|
|
102
|
+
const holderWedged = !spoolBusy
|
|
103
|
+
&& (now - supTs) > TAKEOVER_STALE_MS
|
|
104
|
+
&& (now - spoolTs) > TAKEOVER_STALE_MS;
|
|
105
|
+
if (!holderWedged) {
|
|
106
|
+
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
logEvent('supervisor.takeover-wedged', {
|
|
110
|
+
existing_pid: other,
|
|
111
|
+
supervisor_status_age_ms: now - supTs,
|
|
112
|
+
spool_status_age_ms: now - spoolTs,
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
});
|
|
115
|
+
try { process.kill(other, 'SIGTERM'); } catch (_) {}
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(other)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
118
|
+
}
|
|
90
119
|
}
|
|
91
|
-
// Holder is dead/stale: remove and retry the exclusive create once.
|
|
120
|
+
// Holder is dead/stale/wedged: remove and retry the exclusive create once.
|
|
92
121
|
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
93
122
|
continue;
|
|
94
123
|
}
|