gm-thebird 2.0.1070 → 2.0.1071
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/bin/bootstrap.js +140 -366
- package/gm.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
package/bin/bootstrap.js
CHANGED
|
@@ -4,20 +4,13 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
-
const https = require('https');
|
|
8
7
|
const crypto = require('crypto');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const ATTEMPT_TIMEOUT_MS =
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const BACKOFF_MS = [2000, 5000, 15000, 30000];
|
|
16
|
-
// Worst case: a slow link downloading 140MB at 1MB/s = ~140s. Allow 30 minutes
|
|
17
|
-
// before another bootstrap process treats this lock as abandoned. Below this,
|
|
18
|
-
// concurrent bootstrap calls would wipe an in-progress download mid-stream
|
|
19
|
-
// (see the v0.1.294 incident where a race between two wrappers blew away the
|
|
20
|
-
// .partial during a 10-minute fetch).
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
|
|
11
|
+
const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
|
+
const MAX_ATTEMPTS = 3;
|
|
13
|
+
const BACKOFF_MS = [5000, 15000];
|
|
21
14
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
22
15
|
|
|
23
16
|
function log(msg) {
|
|
@@ -73,18 +66,6 @@ function obsEvent(subsystem, event, fields) {
|
|
|
73
66
|
} catch (_) {}
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
function platformKey() {
|
|
77
|
-
const p = os.platform();
|
|
78
|
-
const a = os.arch();
|
|
79
|
-
if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
|
|
80
|
-
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
81
|
-
return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function binaryName() {
|
|
85
|
-
const key = platformKey();
|
|
86
|
-
return key.startsWith('win32') ? `plugkit-${key}.exe` : `plugkit-${key}`;
|
|
87
|
-
}
|
|
88
69
|
|
|
89
70
|
function rtkBinaryName() {
|
|
90
71
|
const key = platformKey();
|
|
@@ -117,98 +98,22 @@ function gmToolsDir() {
|
|
|
117
98
|
// through node. Self-update inside the Rust binary keeps gm-tools fresh from
|
|
118
99
|
// here on. Skipped silently on any error — the next session-start hook will
|
|
119
100
|
// retry via ensure_tools_current.
|
|
120
|
-
function killHoldersOfPath(targetPath) {
|
|
121
|
-
if (process.platform !== 'win32') return 0;
|
|
122
|
-
try {
|
|
123
|
-
const { spawnSync } = require('child_process');
|
|
124
|
-
const norm = path.resolve(targetPath).replace(/\//g, '\\');
|
|
125
|
-
const r = spawnSync('wmic', ['process', 'where', `ExecutablePath='${norm.replace(/\\/g, '\\\\')}'`, 'get', 'ProcessId', '/format:value'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
126
|
-
if (r.status !== 0 || !r.stdout) return 0;
|
|
127
|
-
const pids = [];
|
|
128
|
-
for (const line of r.stdout.split(/\r?\n/)) {
|
|
129
|
-
const m = line.match(/ProcessId=(\d+)/);
|
|
130
|
-
if (m) {
|
|
131
|
-
const pid = parseInt(m[1], 10);
|
|
132
|
-
if (Number.isFinite(pid) && pid !== process.pid) pids.push(pid);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
for (const pid of pids) {
|
|
136
|
-
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
137
|
-
}
|
|
138
|
-
return pids.length;
|
|
139
|
-
} catch (_) { return 0; }
|
|
140
|
-
}
|
|
141
101
|
|
|
142
|
-
function
|
|
143
|
-
try {
|
|
144
|
-
for (const name of fs.readdirSync(dst)) {
|
|
145
|
-
if (name === exeName + '.new') continue;
|
|
146
|
-
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
147
|
-
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch (_) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function renameWithRetry(src, dst, attempts) {
|
|
154
|
-
for (let i = 0; i < attempts; i++) {
|
|
155
|
-
try { fs.renameSync(src, dst); return true; }
|
|
156
|
-
catch (err) {
|
|
157
|
-
if (err.code !== 'EEXIST' && err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES') throw err;
|
|
158
|
-
if (i === Math.floor(attempts / 2)) killHoldersOfPath(dst);
|
|
159
|
-
try { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 200)'], { timeout: 400, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function copyToGmTools(finalPath, wrapperDir, version) {
|
|
102
|
+
function copyWasmToGmTools(wasmPath, wrapperDir, version) {
|
|
166
103
|
const dst = gmToolsDir();
|
|
167
104
|
fs.mkdirSync(dst, { recursive: true });
|
|
168
|
-
const
|
|
169
|
-
const target = path.join(dst, exeName);
|
|
170
|
-
const targetTmp = target + '.new';
|
|
171
|
-
cleanOrphanNewFiles(dst, exeName);
|
|
105
|
+
const target = path.join(dst, 'plugkit.wasm');
|
|
172
106
|
if (fs.existsSync(target)) {
|
|
173
|
-
let needsRefresh = true;
|
|
174
107
|
try {
|
|
175
108
|
const cur = sha256OfFileSync(target);
|
|
176
|
-
const src = sha256OfFileSync(
|
|
177
|
-
if (cur === src)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
try { killHoldersOfPath(target); } catch (_) {}
|
|
184
|
-
}
|
|
185
|
-
fs.copyFileSync(finalPath, targetTmp);
|
|
186
|
-
if (!renameWithRetry(targetTmp, target, 8)) {
|
|
187
|
-
try { killHoldersOfPath(target); } catch (_) {}
|
|
188
|
-
try { fs.unlinkSync(target); } catch (_) {}
|
|
189
|
-
try { fs.renameSync(targetTmp, target); }
|
|
190
|
-
catch (_) {
|
|
191
|
-
try { fs.unlinkSync(targetTmp); } catch (_) {}
|
|
192
|
-
obsEvent('bootstrap', 'gmtools.rename.failed', { target });
|
|
193
|
-
writeBootstrapError({
|
|
194
|
-
expected_version: version,
|
|
195
|
-
cached_version: null,
|
|
196
|
-
error_phase: 'gmtools-rename',
|
|
197
|
-
error_message: `cannot replace ${target} after kill+unlink retry; orphan removed`,
|
|
198
|
-
});
|
|
199
|
-
throw new Error(`gm-tools update blocked: cannot replace ${target} (held open by running plugkit and kill-retry exhausted)`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
for (const name of fs.readdirSync(dst)) {
|
|
204
|
-
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
205
|
-
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
109
|
+
const src = sha256OfFileSync(wasmPath);
|
|
110
|
+
if (cur === src) {
|
|
111
|
+
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
112
|
+
return;
|
|
206
113
|
}
|
|
207
|
-
}
|
|
208
|
-
} catch (_) {}
|
|
209
|
-
if (process.platform !== 'win32') {
|
|
210
|
-
try { fs.chmodSync(target, 0o755); } catch (_) {}
|
|
114
|
+
} catch (_) {}
|
|
211
115
|
}
|
|
116
|
+
fs.copyFileSync(wasmPath, target);
|
|
212
117
|
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
213
118
|
try {
|
|
214
119
|
const srcSha = path.join(wrapperDir, 'plugkit.sha256');
|
|
@@ -319,88 +224,54 @@ function sha256OfFile(filePath) {
|
|
|
319
224
|
});
|
|
320
225
|
}
|
|
321
226
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
res.resume();
|
|
339
|
-
return resolve(fetchToFile(res.headers.location, destPath, expectedTotal));
|
|
340
|
-
}
|
|
341
|
-
if (res.statusCode === 416) {
|
|
342
|
-
res.resume();
|
|
343
|
-
try { fs.unlinkSync(destPath); } catch (_) {}
|
|
344
|
-
return reject(new Error('range-not-satisfiable: cleared partial, retry'));
|
|
345
|
-
}
|
|
346
|
-
if (!(res.statusCode === 200 || res.statusCode === 206)) {
|
|
347
|
-
res.resume();
|
|
348
|
-
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
227
|
+
async function extractNpmPackageWasm(destPath) {
|
|
228
|
+
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
229
|
+
try {
|
|
230
|
+
ensureDir(tempDir);
|
|
231
|
+
const startMs = Date.now();
|
|
232
|
+
log(`extracting npm package ${NPM_PACKAGE}@latest to ${tempDir}`);
|
|
233
|
+
obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version: 'latest' });
|
|
234
|
+
|
|
235
|
+
const result = spawnSync(
|
|
236
|
+
process.platform === 'win32' ? 'bunx.cmd' : 'bunx',
|
|
237
|
+
['--bun', NPM_PACKAGE + '@latest', '--save-exact', '--prefix', tempDir],
|
|
238
|
+
{
|
|
239
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
240
|
+
timeout: ATTEMPT_TIMEOUT_MS,
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
windowsHide: true,
|
|
349
243
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
res.on('data', c => {
|
|
369
|
-
bytes += c.length;
|
|
370
|
-
lastByte = Date.now();
|
|
371
|
-
if (Date.now() - lastStderr > 5000) {
|
|
372
|
-
const pct = expectedTotal ? ` ${Math.floor(bytes / expectedTotal * 100)}%` : '';
|
|
373
|
-
try { process.stderr.write(`[plugkit-bootstrap] downloading: ${(bytes / 1048576).toFixed(1)} MiB${pct}\n`); } catch (_) {}
|
|
374
|
-
lastStderr = Date.now();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
res.pipe(out);
|
|
378
|
-
out.on('finish', () => {
|
|
379
|
-
clearInterval(stallTimer);
|
|
380
|
-
obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: true });
|
|
381
|
-
out.close(() => resolve(bytes));
|
|
382
|
-
});
|
|
383
|
-
out.on('error', err => { clearInterval(stallTimer); obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: false, err: String(err.message || err) }); reject(err); });
|
|
384
|
-
res.on('error', err => { clearInterval(stallTimer); reject(err); });
|
|
385
|
-
res.on('end', () => clearInterval(stallTimer));
|
|
386
|
-
});
|
|
387
|
-
req.on('timeout', () => { req.destroy(new Error(`timeout after ${ATTEMPT_TIMEOUT_MS}ms`)); });
|
|
388
|
-
req.on('error', reject);
|
|
389
|
-
req.end();
|
|
390
|
-
});
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (result.error) throw result.error;
|
|
247
|
+
if (result.status !== 0) {
|
|
248
|
+
throw new Error(`bunx extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
|
|
252
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
253
|
+
throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fs.copyFileSync(nodeModulesPath, destPath);
|
|
257
|
+
log(`extracted ${nodeModulesPath} → ${destPath}`);
|
|
258
|
+
obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
|
|
259
|
+
} finally {
|
|
260
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
|
|
261
|
+
}
|
|
391
262
|
}
|
|
392
263
|
|
|
393
|
-
async function
|
|
264
|
+
async function extractNpmPackageWithRetry(destPath) {
|
|
394
265
|
let lastErr;
|
|
395
266
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
396
267
|
try {
|
|
397
|
-
log(`
|
|
398
|
-
await
|
|
268
|
+
log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@latest`);
|
|
269
|
+
await extractNpmPackageWasm(destPath);
|
|
399
270
|
return;
|
|
400
271
|
} catch (err) {
|
|
401
272
|
lastErr = err;
|
|
402
273
|
log(`attempt ${attempt} failed: ${err.message}`);
|
|
403
|
-
obsEvent('bootstrap', '
|
|
274
|
+
obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
404
275
|
if (attempt < MAX_ATTEMPTS) {
|
|
405
276
|
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
406
277
|
log(`backing off ${wait}ms`);
|
|
@@ -447,8 +318,8 @@ async function bootstrap(opts) {
|
|
|
447
318
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
448
319
|
const version = opts.version || readVersionFile(wrapperDir);
|
|
449
320
|
const shaManifest = readShaManifest(wrapperDir);
|
|
450
|
-
const
|
|
451
|
-
const
|
|
321
|
+
const wasmName = 'plugkit.wasm';
|
|
322
|
+
const wasmExpectedSha = shaManifest ? shaManifest[wasmName] : null;
|
|
452
323
|
|
|
453
324
|
let root = cacheRoot();
|
|
454
325
|
try { ensureDir(root); }
|
|
@@ -457,154 +328,116 @@ async function bootstrap(opts) {
|
|
|
457
328
|
const verDir = path.join(root, `v${version}`);
|
|
458
329
|
ensureDir(verDir);
|
|
459
330
|
|
|
460
|
-
const finalPath = path.join(verDir, binName);
|
|
461
|
-
const okSentinel = path.join(verDir, '.ok');
|
|
462
|
-
const partialPath = `${finalPath}.partial`;
|
|
463
|
-
|
|
464
|
-
const wasmName = 'plugkit.wasm';
|
|
465
|
-
const wasmExpectedSha = shaManifest ? shaManifest[wasmName] : null;
|
|
466
331
|
const wasmFinalPath = path.join(verDir, wasmName);
|
|
332
|
+
const wasmOkSentinel = path.join(verDir, '.wasm-ok');
|
|
467
333
|
const wasmPartialPath = `${wasmFinalPath}.partial`;
|
|
468
334
|
|
|
469
|
-
if (fs.existsSync(
|
|
470
|
-
if (
|
|
471
|
-
const actualSha = sha256OfFileSync(
|
|
472
|
-
if (actualSha ===
|
|
473
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path:
|
|
474
|
-
|
|
335
|
+
if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
|
|
336
|
+
if (wasmExpectedSha) {
|
|
337
|
+
const actualSha = sha256OfFileSync(wasmFinalPath);
|
|
338
|
+
if (actualSha === wasmExpectedSha) {
|
|
339
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: wasmFinalPath });
|
|
340
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
475
341
|
clearBootstrapError();
|
|
476
|
-
return
|
|
342
|
+
return wasmFinalPath;
|
|
477
343
|
}
|
|
478
|
-
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${
|
|
344
|
+
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${wasmExpectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
|
|
479
345
|
writeBootstrapError({
|
|
480
346
|
expected_version: version,
|
|
481
347
|
cached_version: null,
|
|
482
348
|
error_phase: 'cache-hit-sha-mismatch',
|
|
483
|
-
error_message: `cached
|
|
349
|
+
error_message: `cached wasm at ${wasmFinalPath} sha=${actualSha} but manifest expects ${wasmExpectedSha}`,
|
|
484
350
|
});
|
|
485
|
-
try { fs.unlinkSync(
|
|
486
|
-
try { fs.unlinkSync(
|
|
351
|
+
try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
|
|
352
|
+
try { fs.unlinkSync(wasmOkSentinel); } catch (_) {}
|
|
487
353
|
} else {
|
|
488
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path:
|
|
489
|
-
|
|
354
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: wasmFinalPath });
|
|
355
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
490
356
|
clearBootstrapError();
|
|
491
|
-
return
|
|
357
|
+
return wasmFinalPath;
|
|
492
358
|
}
|
|
493
359
|
}
|
|
494
360
|
|
|
495
|
-
if (healIfShaMatches(
|
|
496
|
-
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path:
|
|
361
|
+
if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
|
|
362
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: wasmFinalPath });
|
|
497
363
|
spawnDetachedRtkFetch(wrapperDir);
|
|
498
|
-
|
|
364
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
499
365
|
clearBootstrapError();
|
|
500
|
-
return
|
|
366
|
+
return wasmFinalPath;
|
|
501
367
|
}
|
|
502
368
|
|
|
503
369
|
const lockPath = path.join(verDir, '.lock');
|
|
504
370
|
acquireLock(lockPath);
|
|
505
371
|
try {
|
|
506
|
-
if (fs.existsSync(
|
|
507
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path:
|
|
508
|
-
|
|
372
|
+
if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
|
|
373
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: wasmFinalPath });
|
|
374
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
509
375
|
clearBootstrapError();
|
|
510
|
-
return
|
|
376
|
+
return wasmFinalPath;
|
|
511
377
|
}
|
|
512
|
-
if (healIfShaMatches(
|
|
513
|
-
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path:
|
|
378
|
+
if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
|
|
379
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: wasmFinalPath });
|
|
514
380
|
spawnDetachedRtkFetch(wrapperDir);
|
|
515
|
-
|
|
381
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
516
382
|
clearBootstrapError();
|
|
517
|
-
return
|
|
383
|
+
return wasmFinalPath;
|
|
518
384
|
}
|
|
519
385
|
|
|
520
|
-
if (fs.existsSync(
|
|
386
|
+
if (fs.existsSync(wasmPartialPath)) {
|
|
521
387
|
try {
|
|
522
|
-
const st = fs.statSync(
|
|
388
|
+
const st = fs.statSync(wasmPartialPath);
|
|
523
389
|
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
524
|
-
fs.unlinkSync(
|
|
525
|
-
log(`cleared stale partial: ${
|
|
390
|
+
fs.unlinkSync(wasmPartialPath);
|
|
391
|
+
log(`cleared stale partial: ${wasmPartialPath}`);
|
|
526
392
|
}
|
|
527
393
|
} catch (_) {}
|
|
528
394
|
}
|
|
529
|
-
const url = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${binName}`;
|
|
530
395
|
try {
|
|
531
|
-
await
|
|
532
|
-
} catch (
|
|
396
|
+
await extractNpmPackageWithRetry(wasmPartialPath);
|
|
397
|
+
} catch (extractErr) {
|
|
533
398
|
writeBootstrapError({
|
|
534
399
|
expected_version: version,
|
|
535
400
|
cached_version: null,
|
|
536
|
-
error_phase: '
|
|
537
|
-
error_message:
|
|
401
|
+
error_phase: 'npm-extract',
|
|
402
|
+
error_message: extractErr && extractErr.message ? extractErr.message : String(extractErr),
|
|
538
403
|
});
|
|
539
|
-
throw
|
|
404
|
+
throw extractErr;
|
|
540
405
|
}
|
|
541
406
|
|
|
542
|
-
if (
|
|
543
|
-
const got = await sha256OfFile(
|
|
544
|
-
if (got !==
|
|
545
|
-
try { fs.unlinkSync(
|
|
407
|
+
if (wasmExpectedSha) {
|
|
408
|
+
const got = await sha256OfFile(wasmPartialPath);
|
|
409
|
+
if (got !== wasmExpectedSha) {
|
|
410
|
+
try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
|
|
546
411
|
writeBootstrapError({
|
|
547
412
|
expected_version: version,
|
|
548
413
|
cached_version: null,
|
|
549
414
|
error_phase: 'sha256-mismatch',
|
|
550
|
-
error_message: `sha256 mismatch for ${
|
|
415
|
+
error_message: `sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`,
|
|
551
416
|
});
|
|
552
|
-
throw new Error(`sha256 mismatch for ${
|
|
417
|
+
throw new Error(`sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`);
|
|
553
418
|
}
|
|
554
419
|
log('sha256 verified');
|
|
555
420
|
} else {
|
|
556
421
|
log('no sha256 manifest — skipping verify');
|
|
557
422
|
}
|
|
558
423
|
|
|
559
|
-
try { fs.renameSync(
|
|
424
|
+
try { fs.renameSync(wasmPartialPath, wasmFinalPath); }
|
|
560
425
|
catch (err) {
|
|
561
426
|
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
562
|
-
try { fs.unlinkSync(
|
|
563
|
-
fs.renameSync(
|
|
427
|
+
try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
|
|
428
|
+
fs.renameSync(wasmPartialPath, wasmFinalPath);
|
|
564
429
|
} else throw err;
|
|
565
430
|
}
|
|
566
431
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
fs.writeFileSync(okSentinel, new Date().toISOString());
|
|
572
|
-
log(`decision: fetch reason: install-complete (${finalPath})`);
|
|
573
|
-
obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit' });
|
|
574
|
-
proactiveKillForNewInstall(version, finalPath);
|
|
432
|
+
fs.writeFileSync(wasmOkSentinel, new Date().toISOString());
|
|
433
|
+
log(`decision: fetch reason: install-complete (${wasmFinalPath})`);
|
|
434
|
+
obsEvent('bootstrap', 'install.done', { path: wasmFinalPath, version, kind: 'wasm' });
|
|
575
435
|
pruneOldVersions(root, version, readRtkVersion(wrapperDir));
|
|
576
436
|
spawnDetachedRtkFetch(wrapperDir);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
try {
|
|
580
|
-
if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, path.join(verDir, '.wasm-ok'), wasmPartialPath, 'wasm')) {
|
|
581
|
-
obsEvent('bootstrap', 'wasm.heal', { path: wasmFinalPath });
|
|
582
|
-
} else {
|
|
583
|
-
const wasmUrl = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${wasmName}`;
|
|
584
|
-
try {
|
|
585
|
-
await downloadWithRetry(wasmUrl, wasmPartialPath);
|
|
586
|
-
if (wasmExpectedSha) {
|
|
587
|
-
const wasmGot = await sha256OfFile(wasmPartialPath);
|
|
588
|
-
if (wasmGot !== wasmExpectedSha) {
|
|
589
|
-
try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
|
|
590
|
-
log(`wasm sha256 mismatch: expected ${wasmExpectedSha}, got ${wasmGot}`);
|
|
591
|
-
} else {
|
|
592
|
-
try { fs.renameSync(wasmPartialPath, wasmFinalPath); } catch (e) {
|
|
593
|
-
if (e.code === 'EEXIST') { try { fs.unlinkSync(wasmFinalPath); } catch (_) {} fs.renameSync(wasmPartialPath, wasmFinalPath); }
|
|
594
|
-
}
|
|
595
|
-
fs.writeFileSync(path.join(verDir, '.wasm-ok'), new Date().toISOString());
|
|
596
|
-
obsEvent('bootstrap', 'wasm.install', { path: wasmFinalPath });
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
} catch (err) {
|
|
600
|
-
log(`wasm download failed (non-fatal): ${err.message}`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
try { fs.copyFileSync(wasmFinalPath, path.join(gmToolsDir(), wasmName)); } catch (_) {}
|
|
604
|
-
} catch (_) {}
|
|
437
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
605
438
|
|
|
606
439
|
clearBootstrapError();
|
|
607
|
-
return
|
|
440
|
+
return wasmFinalPath;
|
|
608
441
|
} finally {
|
|
609
442
|
releaseLock(lockPath);
|
|
610
443
|
}
|
|
@@ -649,8 +482,33 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
|
|
|
649
482
|
if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
|
|
650
483
|
return rtkPath;
|
|
651
484
|
}
|
|
652
|
-
const
|
|
653
|
-
|
|
485
|
+
const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
|
|
486
|
+
const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
|
|
487
|
+
const startMs = Date.now();
|
|
488
|
+
let lastErr;
|
|
489
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
490
|
+
try {
|
|
491
|
+
log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
|
|
492
|
+
const result = spawnSync(
|
|
493
|
+
'curl',
|
|
494
|
+
['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
|
|
495
|
+
{ stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
|
|
496
|
+
);
|
|
497
|
+
if (result.error) throw result.error;
|
|
498
|
+
if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
|
|
499
|
+
break;
|
|
500
|
+
} catch (err) {
|
|
501
|
+
lastErr = err;
|
|
502
|
+
log(`rtk attempt ${attempt} failed: ${err.message}`);
|
|
503
|
+
obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
504
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
505
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
506
|
+
log(`backing off ${wait}ms`);
|
|
507
|
+
await new Promise(r => setTimeout(r, wait));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (lastErr) throw lastErr;
|
|
654
512
|
if (expected) {
|
|
655
513
|
const got = await sha256OfFile(tmp);
|
|
656
514
|
if (got !== expected) {
|
|
@@ -668,7 +526,7 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
|
|
|
668
526
|
if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
|
|
669
527
|
fs.writeFileSync(rtkOk, new Date().toISOString());
|
|
670
528
|
log(`installed ${rtkPath}`);
|
|
671
|
-
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk' });
|
|
529
|
+
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
|
|
672
530
|
return rtkPath;
|
|
673
531
|
}
|
|
674
532
|
|
|
@@ -688,7 +546,7 @@ function resolveCachedRtk(opts) {
|
|
|
688
546
|
return null;
|
|
689
547
|
}
|
|
690
548
|
|
|
691
|
-
function
|
|
549
|
+
function getWasmPath(opts) {
|
|
692
550
|
opts = opts || {};
|
|
693
551
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
694
552
|
const version = opts.version || readVersionFile(wrapperDir);
|
|
@@ -697,9 +555,9 @@ function resolveCachedBinary(opts) {
|
|
|
697
555
|
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
698
556
|
})();
|
|
699
557
|
const verDir = path.join(root, `v${version}`);
|
|
700
|
-
const
|
|
701
|
-
const okSentinel = path.join(verDir, '.ok');
|
|
702
|
-
if (fs.existsSync(
|
|
558
|
+
const wasmPath = path.join(verDir, 'plugkit.wasm');
|
|
559
|
+
const okSentinel = path.join(verDir, '.wasm-ok');
|
|
560
|
+
if (fs.existsSync(wasmPath) && fs.existsSync(okSentinel)) return wasmPath;
|
|
703
561
|
return null;
|
|
704
562
|
}
|
|
705
563
|
|
|
@@ -762,72 +620,6 @@ function killRunningDaemons(reason) {
|
|
|
762
620
|
return killedPids;
|
|
763
621
|
}
|
|
764
622
|
|
|
765
|
-
function listRunningPlugkitImagePaths() {
|
|
766
|
-
const out = [];
|
|
767
|
-
try {
|
|
768
|
-
const { spawnSync } = require('child_process');
|
|
769
|
-
if (os.platform() === 'win32') {
|
|
770
|
-
let parsed = null;
|
|
771
|
-
try {
|
|
772
|
-
const p = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', "Get-Process plugkit* -ErrorAction SilentlyContinue | Select-Object Id,Path | ConvertTo-Json -Compress"], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
773
|
-
const text = ((p && p.stdout) || '').trim();
|
|
774
|
-
if (text) {
|
|
775
|
-
const j = JSON.parse(text);
|
|
776
|
-
parsed = Array.isArray(j) ? j : [j];
|
|
777
|
-
}
|
|
778
|
-
} catch (_) {}
|
|
779
|
-
if (parsed) {
|
|
780
|
-
for (const item of parsed) {
|
|
781
|
-
if (!item) continue;
|
|
782
|
-
const pid = parseInt(item.Id, 10);
|
|
783
|
-
if (!Number.isFinite(pid)) continue;
|
|
784
|
-
out.push({ pid, path: (item.Path || '').trim() });
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
const r = spawnSync('tasklist', ['/FI', 'IMAGENAME eq plugkit*', '/FO', 'CSV', '/NH'], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
788
|
-
const text = (r && r.stdout) || '';
|
|
789
|
-
const seen = new Set();
|
|
790
|
-
for (const line of text.split(/\r?\n/)) {
|
|
791
|
-
const m = line.match(/^"([^"]+)","(\d+)"/);
|
|
792
|
-
if (!m) continue;
|
|
793
|
-
const pid = parseInt(m[2], 10);
|
|
794
|
-
if (!Number.isFinite(pid) || seen.has(pid)) continue;
|
|
795
|
-
seen.add(pid);
|
|
796
|
-
out.push({ pid, path: '' });
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
} else if (os.platform() === 'linux') {
|
|
800
|
-
let entries = [];
|
|
801
|
-
try { entries = fs.readdirSync('/proc'); } catch (_) {}
|
|
802
|
-
for (const e of entries) {
|
|
803
|
-
if (!/^\d+$/.test(e)) continue;
|
|
804
|
-
const pid = parseInt(e, 10);
|
|
805
|
-
let comm = '';
|
|
806
|
-
try { comm = fs.readFileSync(`/proc/${pid}/comm`, 'utf8').trim(); } catch (_) { continue; }
|
|
807
|
-
if (!/^plugkit/i.test(comm)) continue;
|
|
808
|
-
let imagePath = '';
|
|
809
|
-
try { imagePath = fs.readlinkSync(`/proc/${pid}/exe`); } catch (_) {}
|
|
810
|
-
out.push({ pid, path: imagePath });
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
const r = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
814
|
-
const text = (r && r.stdout) || '';
|
|
815
|
-
for (const line of text.split(/\r?\n/)) {
|
|
816
|
-
const m = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
817
|
-
if (!m) continue;
|
|
818
|
-
if (!/plugkit/i.test(m[2])) continue;
|
|
819
|
-
const pid = parseInt(m[1], 10);
|
|
820
|
-
let imagePath = '';
|
|
821
|
-
try {
|
|
822
|
-
const p = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
823
|
-
imagePath = ((p && p.stdout) || '').trim().split(/\s+/)[0] || '';
|
|
824
|
-
} catch (_) {}
|
|
825
|
-
out.push({ pid, path: imagePath });
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
} catch (_) {}
|
|
829
|
-
return out;
|
|
830
|
-
}
|
|
831
623
|
|
|
832
624
|
function killSpoolWatcherInCwd(reason) {
|
|
833
625
|
try {
|
|
@@ -844,39 +636,21 @@ function killSpoolWatcherInCwd(reason) {
|
|
|
844
636
|
return null;
|
|
845
637
|
}
|
|
846
638
|
|
|
847
|
-
function proactiveKillForNewInstall(installedVersion
|
|
639
|
+
function proactiveKillForNewInstall(installedVersion) {
|
|
848
640
|
try {
|
|
849
641
|
const reason = `install:v${installedVersion}`;
|
|
850
|
-
const target = finalPath ? path.resolve(finalPath).toLowerCase() : null;
|
|
851
|
-
const cacheRootNorm = (() => {
|
|
852
|
-
try { return path.resolve(cacheRoot()).toLowerCase(); } catch (_) { return null; }
|
|
853
|
-
})();
|
|
854
|
-
const procs = listRunningPlugkitImagePaths();
|
|
855
|
-
for (const { pid, path: imagePath } of procs) {
|
|
856
|
-
if (!Number.isFinite(pid) || pid === process.pid) continue;
|
|
857
|
-
if (!imagePath) continue;
|
|
858
|
-
const norm = path.resolve(imagePath).toLowerCase();
|
|
859
|
-
if (target && norm === target) continue;
|
|
860
|
-
if (!cacheRootNorm || !norm.startsWith(cacheRootNorm + path.sep.toLowerCase())) continue;
|
|
861
|
-
if (killPid(pid)) {
|
|
862
|
-
try { process.stderr.write(`[bootstrap] killed stale daemon pid=${pid} path=${imagePath} (current install: v${installedVersion})\n`); } catch (_) {}
|
|
863
|
-
obsEvent('bootstrap', 'daemon.killed', { pid, oldPath: imagePath, installedVersion, mechanism: 'process-path' });
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
642
|
killRunningDaemons(reason);
|
|
867
643
|
killSpoolWatcherInCwd(reason);
|
|
868
644
|
writeDaemonVersion(installedVersion);
|
|
869
645
|
} catch (_) {}
|
|
870
646
|
}
|
|
871
647
|
|
|
872
|
-
// Compare wrapper-pinned version against last-recorded daemon version. If
|
|
873
|
-
// they differ, kill the daemon so it respawns under the new binary.
|
|
874
648
|
function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
875
649
|
let currentVersion;
|
|
876
650
|
try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
|
|
877
|
-
const cached =
|
|
651
|
+
const cached = getWasmPath({ wrapperDir, version: currentVersion });
|
|
878
652
|
if (cached) {
|
|
879
|
-
proactiveKillForNewInstall(currentVersion
|
|
653
|
+
proactiveKillForNewInstall(currentVersion);
|
|
880
654
|
return;
|
|
881
655
|
}
|
|
882
656
|
const recorded = readDaemonVersion();
|
|
@@ -885,7 +659,7 @@ function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
|
885
659
|
writeDaemonVersion(currentVersion);
|
|
886
660
|
}
|
|
887
661
|
|
|
888
|
-
module.exports = { bootstrap,
|
|
662
|
+
module.exports = { bootstrap, getWasmPath, resolveCachedRtk, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
|
|
889
663
|
|
|
890
664
|
if (require.main === module) {
|
|
891
665
|
const argv = process.argv.slice(2);
|
package/gm.json
CHANGED
package/package.json
CHANGED