quilltap 4.10.0-dev.5 → 4.10.0-dev.51
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/quilltap.js +75 -48
- package/lib/native-modules.js +102 -1
- package/package.json +1 -1
package/bin/quilltap.js
CHANGED
|
@@ -484,6 +484,51 @@ function verifyPidIsNode(pid, expectedArgv0) {
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
const HEARTBEAT_FRESH_MS = 5 * 60 * 1000;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The freshness window in words, derived from the constant the check actually
|
|
491
|
+
* uses — so the sentence `--lock-clean` prints cannot drift away from the
|
|
492
|
+
* behaviour it describes.
|
|
493
|
+
*/
|
|
494
|
+
function describeFreshWindow() {
|
|
495
|
+
const seconds = Math.round(HEARTBEAT_FRESH_MS / 1000);
|
|
496
|
+
if (seconds % 60 !== 0) return `${seconds} seconds`;
|
|
497
|
+
const minutes = seconds / 60;
|
|
498
|
+
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Decide whether a lock record belongs to a live process.
|
|
503
|
+
*
|
|
504
|
+
* A hostname that differs from ours does NOT mean a different machine: macOS
|
|
505
|
+
* derives gethostname() dynamically when `scutil --get HostName` is unset, so
|
|
506
|
+
* one Mac reports e.g. "MacBook-Pro.local" and "Mac" at different times. So
|
|
507
|
+
* check PID liveness regardless of the recorded name, and fall back to
|
|
508
|
+
* heartbeat freshness for any environment rather than only for containers.
|
|
509
|
+
* See bug 126.
|
|
510
|
+
*/
|
|
511
|
+
function assessLock(lock, hostname) {
|
|
512
|
+
const sameHost = lock.hostname === hostname;
|
|
513
|
+
const alive = isPidAlive(lock.pid);
|
|
514
|
+
const isNode = alive ? verifyPidIsNode(lock.pid, lock.processArgv0 || '') : false;
|
|
515
|
+
const heartbeatAgeMs = lock.lastHeartbeat
|
|
516
|
+
? Date.now() - new Date(lock.lastHeartbeat).getTime()
|
|
517
|
+
: Infinity;
|
|
518
|
+
const heartbeatFresh = heartbeatAgeMs < HEARTBEAT_FRESH_MS;
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
sameHost,
|
|
522
|
+
alive,
|
|
523
|
+
isNode,
|
|
524
|
+
heartbeatAgeMs,
|
|
525
|
+
heartbeatFresh,
|
|
526
|
+
// Held by something running: a confirmed local process, or any holder
|
|
527
|
+
// still refreshing its heartbeat.
|
|
528
|
+
live: (alive && isNode) || heartbeatFresh,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
487
532
|
/**
|
|
488
533
|
* Handle --lock-status, --lock-clean, --lock-override commands.
|
|
489
534
|
*/
|
|
@@ -518,32 +563,20 @@ function handleLockCommand(dataDir, opts) {
|
|
|
518
563
|
return;
|
|
519
564
|
}
|
|
520
565
|
|
|
521
|
-
const sameHost
|
|
522
|
-
|
|
523
|
-
const isNode = alive ? verifyPidIsNode(lock.pid, lock.processArgv0 || '') : false;
|
|
566
|
+
const { sameHost, alive, isNode, heartbeatAgeMs, heartbeatFresh } =
|
|
567
|
+
assessLock(lock, hostname);
|
|
524
568
|
|
|
525
569
|
// Status line
|
|
526
570
|
let status;
|
|
527
571
|
if (alive && isNode) {
|
|
528
572
|
status = '\x1b[32mACTIVE\x1b[0m (process confirmed running)';
|
|
573
|
+
} else if (heartbeatFresh) {
|
|
574
|
+
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
575
|
+
status = `\x1b[32mACTIVE\x1b[0m (${lock.environment || 'unknown'}, heartbeat ${ageStr} ago)`;
|
|
529
576
|
} else if (alive && !isNode) {
|
|
530
577
|
status = '\x1b[33mSUSPECT\x1b[0m (PID alive but does not look like Quilltap — possible PID reuse)';
|
|
531
578
|
} else if (!sameHost) {
|
|
532
|
-
|
|
533
|
-
const isVMOrContainer = lock.environment === 'docker';
|
|
534
|
-
const heartbeatAgeMs = lock.lastHeartbeat
|
|
535
|
-
? Date.now() - new Date(lock.lastHeartbeat).getTime()
|
|
536
|
-
: Infinity;
|
|
537
|
-
const heartbeatFreshMs = 5 * 60 * 1000;
|
|
538
|
-
|
|
539
|
-
if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
|
|
540
|
-
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
541
|
-
status = `\x1b[32mACTIVE (${lock.environment}, heartbeat ${ageStr} ago)\x1b[0m`;
|
|
542
|
-
} else if (isVMOrContainer) {
|
|
543
|
-
status = `\x1b[33mSTALE (${lock.environment}, no recent heartbeat)\x1b[0m — will be auto-claimed on next startup`;
|
|
544
|
-
} else {
|
|
545
|
-
status = '\x1b[33mSTALE (different host)\x1b[0m — will be auto-claimed on next startup';
|
|
546
|
-
}
|
|
579
|
+
status = `\x1b[33mSTALE (${lock.environment || 'unknown'} on ${lock.hostname}, no recent heartbeat)\x1b[0m — will be auto-claimed on next startup`;
|
|
547
580
|
} else {
|
|
548
581
|
status = '\x1b[31mSTALE (process dead)\x1b[0m — will be auto-claimed on next startup';
|
|
549
582
|
}
|
|
@@ -551,7 +584,7 @@ function handleLockCommand(dataDir, opts) {
|
|
|
551
584
|
console.log(`Instance Lock Status: ${status}`);
|
|
552
585
|
console.log();
|
|
553
586
|
console.log(` PID: ${lock.pid}`);
|
|
554
|
-
console.log(` Hostname: ${lock.hostname}${sameHost ? ' (this host)' :
|
|
587
|
+
console.log(` Hostname: ${lock.hostname}${sameHost ? ' (this host)' : ` (recorded name differs from ours: ${hostname})`}`);
|
|
555
588
|
console.log(` Environment: ${lock.environment || 'unknown'}`);
|
|
556
589
|
console.log(` Process: ${lock.processTitle || 'unknown'}`);
|
|
557
590
|
console.log(` Started: ${lock.startedAt || 'unknown'}`);
|
|
@@ -599,37 +632,31 @@ function handleLockCommand(dataDir, opts) {
|
|
|
599
632
|
return;
|
|
600
633
|
}
|
|
601
634
|
|
|
602
|
-
const sameHost
|
|
603
|
-
|
|
635
|
+
const { sameHost, alive, isNode, heartbeatAgeMs, heartbeatFresh } =
|
|
636
|
+
assessLock(lock, hostname);
|
|
604
637
|
|
|
605
|
-
if (alive) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
638
|
+
if (alive && isNode) {
|
|
639
|
+
console.log(`Lock is held by a live Quilltap process (PID ${lock.pid}). Cannot clean.`);
|
|
640
|
+
console.log('Stop the running instance first, or use --lock-override to force.');
|
|
641
|
+
process.exit(1);
|
|
642
|
+
} else if (heartbeatFresh) {
|
|
643
|
+
// Reached only when `alive` is false — the arm above has already claimed
|
|
644
|
+
// every confirmed-live case — so this must not assert that anything is
|
|
645
|
+
// running. Freshness alone is the deliberate fallback (bug 126) for
|
|
646
|
+
// environments where PID checks are unreliable; the refusal is right,
|
|
647
|
+
// and what it says is simply what was tested.
|
|
648
|
+
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
649
|
+
console.log(`Lock heartbeat is still fresh (${ageStr} ago). Cannot clean.`);
|
|
650
|
+
console.log(
|
|
651
|
+
`A lock counts as held until its heartbeat is ${describeFreshWindow()} stale, even if its process has gone. ` +
|
|
652
|
+
'Wait it out, or use --lock-override to force.'
|
|
653
|
+
);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
} else if (alive && !isNode) {
|
|
656
|
+
console.log(`Lock references PID ${lock.pid} which is alive but does NOT look like a Quilltap process.`);
|
|
657
|
+
console.log('This is likely a stale lock with a reused PID. Removing.');
|
|
615
658
|
} else if (!sameHost) {
|
|
616
|
-
|
|
617
|
-
const isVMOrContainer = lock.environment === 'docker';
|
|
618
|
-
const heartbeatAgeMs = lock.lastHeartbeat
|
|
619
|
-
? Date.now() - new Date(lock.lastHeartbeat).getTime()
|
|
620
|
-
: Infinity;
|
|
621
|
-
const heartbeatFreshMs = 5 * 60 * 1000;
|
|
622
|
-
|
|
623
|
-
if (isVMOrContainer && heartbeatAgeMs < heartbeatFreshMs) {
|
|
624
|
-
const ageStr = Math.round(heartbeatAgeMs / 1000) + 's';
|
|
625
|
-
console.log(`Lock is held by a live ${lock.environment} instance (heartbeat ${ageStr} ago). Cannot clean.`);
|
|
626
|
-
console.log('Stop the other instance first, or use --lock-override to force.');
|
|
627
|
-
process.exit(1);
|
|
628
|
-
} else if (isVMOrContainer) {
|
|
629
|
-
console.log(`Lock was held by ${lock.environment} (${lock.hostname}) with no recent heartbeat. Removing stale lock.`);
|
|
630
|
-
} else {
|
|
631
|
-
console.log(`Lock was held by a different host (${lock.hostname}). Removing stale lock.`);
|
|
632
|
-
}
|
|
659
|
+
console.log(`Lock was held by ${lock.environment || 'unknown'} on ${lock.hostname} with no recent heartbeat. Removing stale lock.`);
|
|
633
660
|
} else {
|
|
634
661
|
console.log(`Lock was held by PID ${lock.pid} which is no longer running. Removing stale lock.`);
|
|
635
662
|
}
|
package/lib/native-modules.js
CHANGED
|
@@ -62,6 +62,84 @@ function betterSqlite3NeedsRebuild() {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Locate an executable installed by a dependency, given the package directory
|
|
66
|
+
// that needs it. Checks the package's own `.bin` first, then the `.bin` beside
|
|
67
|
+
// it (the hoisted case, which is the usual one). Returns null when absent.
|
|
68
|
+
function findBinFor(pkgDir, tool) {
|
|
69
|
+
const fs = require('fs');
|
|
70
|
+
const names = process.platform === 'win32' ? [`${tool}.cmd`, `${tool}.exe`, tool] : [tool];
|
|
71
|
+
const dirs = [
|
|
72
|
+
path.join(pkgDir, 'node_modules', '.bin'), // nested install
|
|
73
|
+
path.join(pkgDir, '..', '.bin'), // hoisted alongside the package
|
|
74
|
+
];
|
|
75
|
+
for (const dir of dirs) {
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
const candidate = path.join(dir, name);
|
|
78
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Rebuild ONE native package in place, addressed by its directory.
|
|
85
|
+
//
|
|
86
|
+
// Deliberately does NOT go through `npm rebuild <name>`, which fails this job
|
|
87
|
+
// two different ways:
|
|
88
|
+
//
|
|
89
|
+
// 1. npm refuses install scripts for any package not listed in the root
|
|
90
|
+
// package.json `allowScripts` map (keyed `name@version`). The root
|
|
91
|
+
// SQLCipher copy is installed under the ALIAS `better-sqlite3`, which is
|
|
92
|
+
// not a key there, so the rebuild dies with EALLOWSCRIPTS.
|
|
93
|
+
// 2. Asking for the real name (`better-sqlite3-multiple-ciphers`) at the root
|
|
94
|
+
// instead resolves a phantom directory: npm reports "rebuilt dependencies
|
|
95
|
+
// successfully" and the binding on disk is untouched. A success message is
|
|
96
|
+
// worse than an error.
|
|
97
|
+
//
|
|
98
|
+
// Running the package's own build chain (`prebuild-install || node-gyp rebuild`,
|
|
99
|
+
// exactly what its `install` script does) in its own directory sidesteps both:
|
|
100
|
+
// no name resolution, no npm script policy. `prebuild-install` downloads the
|
|
101
|
+
// prebuilt binary for the running ABI and needs network; node-gyp compiles and
|
|
102
|
+
// needs a toolchain.
|
|
103
|
+
//
|
|
104
|
+
// Returns { ok, reason }. Never throws. Verifies the result rather than trusting
|
|
105
|
+
// the exit code — see (2): the whole failure mode here is a rebuild that claims
|
|
106
|
+
// to have worked.
|
|
107
|
+
function rebuildNativePackage(pkgDir, bindingPath) {
|
|
108
|
+
const fs = require('fs');
|
|
109
|
+
if (!fs.existsSync(pkgDir)) return { ok: false, reason: `no package at ${pkgDir}` };
|
|
110
|
+
|
|
111
|
+
const before = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
|
|
112
|
+
const attempts = [];
|
|
113
|
+
|
|
114
|
+
const prebuild = findBinFor(pkgDir, 'prebuild-install');
|
|
115
|
+
if (prebuild) attempts.push({ label: 'prebuild-install', cmd: `"${prebuild}"` });
|
|
116
|
+
|
|
117
|
+
const nodeGyp = findBinFor(pkgDir, 'node-gyp');
|
|
118
|
+
if (nodeGyp) attempts.push({ label: 'node-gyp rebuild', cmd: `"${nodeGyp}" rebuild --release` });
|
|
119
|
+
|
|
120
|
+
if (attempts.length === 0) {
|
|
121
|
+
return { ok: false, reason: 'neither prebuild-install nor node-gyp is installed' };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const failures = [];
|
|
125
|
+
for (const attempt of attempts) {
|
|
126
|
+
try {
|
|
127
|
+
execSync(attempt.cmd, { cwd: pkgDir, stdio: 'inherit' });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
failures.push(`${attempt.label}: ${err.message.split('\n')[0]}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// Trust nothing but the binary. A tool can exit 0 having done nothing.
|
|
133
|
+
const after = fs.existsSync(bindingPath) ? readCompiledAbi(bindingPath) : null;
|
|
134
|
+
if (after === process.versions.modules) return { ok: true, reason: attempt.label };
|
|
135
|
+
failures.push(
|
|
136
|
+
`${attempt.label}: exited 0 but the binding is still ${after ?? 'missing'} ` +
|
|
137
|
+
`(wanted ${process.versions.modules}${before ? `, was ${before}` : ''})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { ok: false, reason: failures.join('; ') };
|
|
141
|
+
}
|
|
142
|
+
|
|
65
143
|
// Rebuild the named native modules against the current Node ABI. Prints a
|
|
66
144
|
// friendly notice rather than throwing; returns true on success, false on
|
|
67
145
|
// failure. Backfills node-pty's spawn-helper afterward.
|
|
@@ -96,7 +174,27 @@ function ensureDatabaseNativeModule() {
|
|
|
96
174
|
} catch {
|
|
97
175
|
return true; // detection hiccup — let the real load be the source of truth
|
|
98
176
|
}
|
|
99
|
-
|
|
177
|
+
|
|
178
|
+
const bindingPath = betterSqlite3BindingPath();
|
|
179
|
+
if (!bindingPath) {
|
|
180
|
+
console.error(' Warning: could not locate the SQLCipher binding to rebuild.');
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// build/Release/better_sqlite3.node → the package directory above it.
|
|
184
|
+
const pkgDir = path.resolve(path.dirname(bindingPath), '..', '..');
|
|
185
|
+
|
|
186
|
+
console.log(` Rebuilding the SQLCipher binding for Node.js ${process.version}...`);
|
|
187
|
+
const result = rebuildNativePackage(pkgDir, bindingPath);
|
|
188
|
+
if (result.ok) {
|
|
189
|
+
console.log(` Done (${result.reason}).`);
|
|
190
|
+
console.log('');
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
console.error('');
|
|
194
|
+
console.error(` Warning: failed to rebuild the SQLCipher binding — ${result.reason}`);
|
|
195
|
+
console.error(` Try running: (cd ${pkgDir} && ./node_modules/.bin/prebuild-install)`);
|
|
196
|
+
console.error('');
|
|
197
|
+
return false;
|
|
100
198
|
}
|
|
101
199
|
|
|
102
200
|
// node-pty needs a `spawn-helper` executable beside the pty.node it loads, or
|
|
@@ -194,7 +292,10 @@ function ensureNativeModules() {
|
|
|
194
292
|
module.exports = {
|
|
195
293
|
resolveModuleDir,
|
|
196
294
|
readCompiledAbi,
|
|
295
|
+
betterSqlite3BindingPath,
|
|
197
296
|
betterSqlite3NeedsRebuild,
|
|
297
|
+
findBinFor,
|
|
298
|
+
rebuildNativePackage,
|
|
198
299
|
ensureDatabaseNativeModule,
|
|
199
300
|
ensureNativeModules,
|
|
200
301
|
reconcileNodePtySpawnHelper,
|