fullcourtdefense-cli 1.15.3 → 1.15.4
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/dist/commands/daemon.js +144 -36
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -77,6 +77,55 @@ function daemonDir() {
|
|
|
77
77
|
function pidFile() {
|
|
78
78
|
return path.join(daemonDir(), 'daemon.pid');
|
|
79
79
|
}
|
|
80
|
+
/** Sidecar next to the pid lock recording WHICH build owns the daemon. */
|
|
81
|
+
function metaFile() {
|
|
82
|
+
return path.join(daemonDir(), 'daemon.meta.json');
|
|
83
|
+
}
|
|
84
|
+
function readDaemonMeta() {
|
|
85
|
+
try {
|
|
86
|
+
const meta = JSON.parse(fs.readFileSync(metaFile(), 'utf8'));
|
|
87
|
+
return meta && typeof meta.pid === 'number' ? meta : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeDaemonMeta() {
|
|
94
|
+
try {
|
|
95
|
+
const meta = {
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
version: cliVersion(),
|
|
98
|
+
entry: cliEntry(),
|
|
99
|
+
startedAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(metaFile(), JSON.stringify(meta, null, 2), 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
/** Kill a process and wait (up to ~5s) for it to actually exit. */
|
|
106
|
+
function stopPid(pid) {
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid);
|
|
109
|
+
}
|
|
110
|
+
catch { /* may already be gone */ }
|
|
111
|
+
const deadline = Date.now() + 5_000;
|
|
112
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
if (!isPidAlive(pid))
|
|
115
|
+
return true;
|
|
116
|
+
Atomics.wait(sleeper, 0, 0, 250);
|
|
117
|
+
}
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
(0, child_process_1.spawnSync)('taskkill', ['/PID', String(pid), '/F'], { stdio: 'ignore', windowsHide: true, timeout: 5_000 });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 'SIGKILL');
|
|
124
|
+
}
|
|
125
|
+
catch { /* ignore */ }
|
|
126
|
+
}
|
|
127
|
+
return !isPidAlive(pid);
|
|
128
|
+
}
|
|
80
129
|
function logFile() {
|
|
81
130
|
return path.join(daemonDir(), 'daemon.log');
|
|
82
131
|
}
|
|
@@ -117,24 +166,56 @@ function isPidAlive(pid) {
|
|
|
117
166
|
return error.code === 'EPERM';
|
|
118
167
|
}
|
|
119
168
|
}
|
|
120
|
-
/**
|
|
169
|
+
/** Lexicographic-free semver compare; missing/unparseable sorts oldest. */
|
|
170
|
+
function compareVersions(a, b) {
|
|
171
|
+
const parse = (value) => String(value || '0').split('.').map(part => parseInt(part, 10) || 0);
|
|
172
|
+
const [pa, pb] = [parse(a), parse(b)];
|
|
173
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
174
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
175
|
+
if (diff !== 0)
|
|
176
|
+
return diff;
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Take the single-instance lock. A NEWER build supersedes a running older
|
|
182
|
+
* daemon (e.g. the MSI-bundled copy kept running after an npm update): the old
|
|
183
|
+
* process is stopped and this one takes over, so the fleet never keeps running
|
|
184
|
+
* stale enforcement code silently. Same-or-newer versions keep the lock.
|
|
185
|
+
*/
|
|
121
186
|
function acquirePidLock() {
|
|
122
187
|
fs.mkdirSync(daemonDir(), { recursive: true });
|
|
123
188
|
try {
|
|
124
189
|
const existing = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
125
190
|
if (Number.isFinite(existing) && existing > 0 && existing !== process.pid && isPidAlive(existing)) {
|
|
126
|
-
|
|
191
|
+
const meta = readDaemonMeta();
|
|
192
|
+
const runningVersion = meta && meta.pid === existing ? meta.version : undefined;
|
|
193
|
+
// No meta = pre-1.15.4 build (never wrote one) → treated as older.
|
|
194
|
+
if (compareVersions(cliVersion(), runningVersion) > 0) {
|
|
195
|
+
log(`Superseding older daemon (pid ${existing}, version ${runningVersion || 'unknown'}) with ${cliVersion() || 'this build'}.`);
|
|
196
|
+
if (!stopPid(existing))
|
|
197
|
+
return false; // could not stop it — yield rather than double-run
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
127
202
|
}
|
|
128
203
|
}
|
|
129
204
|
catch { /* no pidfile — free to start */ }
|
|
130
205
|
fs.writeFileSync(pidFile(), String(process.pid), 'utf8');
|
|
206
|
+
writeDaemonMeta();
|
|
131
207
|
return true;
|
|
132
208
|
}
|
|
133
209
|
function releasePidLock() {
|
|
134
210
|
try {
|
|
135
211
|
const recorded = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
136
|
-
if (recorded === process.pid)
|
|
212
|
+
if (recorded === process.pid) {
|
|
137
213
|
fs.unlinkSync(pidFile());
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(metaFile());
|
|
216
|
+
}
|
|
217
|
+
catch { /* best-effort */ }
|
|
218
|
+
}
|
|
138
219
|
}
|
|
139
220
|
catch { /* best-effort */ }
|
|
140
221
|
}
|
|
@@ -467,8 +548,15 @@ function isWindowsRunKeyInstalled() {
|
|
|
467
548
|
function startDaemonNowWindows(vbs) {
|
|
468
549
|
try {
|
|
469
550
|
const existing = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
470
|
-
if (Number.isFinite(existing) && existing > 0 && isPidAlive(existing))
|
|
471
|
-
|
|
551
|
+
if (Number.isFinite(existing) && existing > 0 && isPidAlive(existing)) {
|
|
552
|
+
// Skip only when the running daemon is the same version or newer; an
|
|
553
|
+
// older one (e.g. stale MSI copy) gets superseded by the spawned daemon,
|
|
554
|
+
// whose acquirePidLock() stops it and takes over.
|
|
555
|
+
const meta = readDaemonMeta();
|
|
556
|
+
const runningVersion = meta && meta.pid === existing ? meta.version : undefined;
|
|
557
|
+
if (compareVersions(cliVersion(), runningVersion) <= 0)
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
472
560
|
}
|
|
473
561
|
catch { /* not running */ }
|
|
474
562
|
const escaped = vbs.replace(/'/g, "''");
|
|
@@ -513,23 +601,23 @@ function launchdPlistPath() {
|
|
|
513
601
|
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
514
602
|
}
|
|
515
603
|
function installMacos() {
|
|
516
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
517
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
518
|
-
<plist version="1.0">
|
|
519
|
-
<dict>
|
|
520
|
-
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
521
|
-
<key>ProgramArguments</key>
|
|
522
|
-
<array>
|
|
523
|
-
<string>${process.execPath}</string>
|
|
524
|
-
<string>${cliEntry()}</string>
|
|
525
|
-
<string>daemon</string>
|
|
526
|
-
</array>
|
|
527
|
-
<key>RunAtLoad</key><true/>
|
|
528
|
-
<key>KeepAlive</key><true/>
|
|
529
|
-
<key>StandardOutPath</key><string>${logFile()}</string>
|
|
530
|
-
<key>StandardErrorPath</key><string>${logFile()}</string>
|
|
531
|
-
</dict>
|
|
532
|
-
</plist>
|
|
604
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
605
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
606
|
+
<plist version="1.0">
|
|
607
|
+
<dict>
|
|
608
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
609
|
+
<key>ProgramArguments</key>
|
|
610
|
+
<array>
|
|
611
|
+
<string>${process.execPath}</string>
|
|
612
|
+
<string>${cliEntry()}</string>
|
|
613
|
+
<string>daemon</string>
|
|
614
|
+
</array>
|
|
615
|
+
<key>RunAtLoad</key><true/>
|
|
616
|
+
<key>KeepAlive</key><true/>
|
|
617
|
+
<key>StandardOutPath</key><string>${logFile()}</string>
|
|
618
|
+
<key>StandardErrorPath</key><string>${logFile()}</string>
|
|
619
|
+
</dict>
|
|
620
|
+
</plist>
|
|
533
621
|
`;
|
|
534
622
|
const file = launchdPlistPath();
|
|
535
623
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -567,16 +655,16 @@ function isDaemonAutostartInstalled() {
|
|
|
567
655
|
return fs.existsSync(systemdUnitPath());
|
|
568
656
|
}
|
|
569
657
|
function installLinux() {
|
|
570
|
-
const unit = `[Unit]
|
|
571
|
-
Description=FullCourtDefense resident daemon (config watch + heartbeat)
|
|
572
|
-
|
|
573
|
-
[Service]
|
|
574
|
-
ExecStart=${JSON.stringify(process.execPath)} ${JSON.stringify(cliEntry())} daemon
|
|
575
|
-
Restart=always
|
|
576
|
-
RestartSec=10
|
|
577
|
-
|
|
578
|
-
[Install]
|
|
579
|
-
WantedBy=default.target
|
|
658
|
+
const unit = `[Unit]
|
|
659
|
+
Description=FullCourtDefense resident daemon (config watch + heartbeat)
|
|
660
|
+
|
|
661
|
+
[Service]
|
|
662
|
+
ExecStart=${JSON.stringify(process.execPath)} ${JSON.stringify(cliEntry())} daemon
|
|
663
|
+
Restart=always
|
|
664
|
+
RestartSec=10
|
|
665
|
+
|
|
666
|
+
[Install]
|
|
667
|
+
WantedBy=default.target
|
|
580
668
|
`;
|
|
581
669
|
const file = systemdUnitPath();
|
|
582
670
|
try {
|
|
@@ -621,9 +709,20 @@ function statusCommand() {
|
|
|
621
709
|
try {
|
|
622
710
|
const pid = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
623
711
|
running = Number.isFinite(pid) && pid > 0 && isPidAlive(pid);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
712
|
+
if (running) {
|
|
713
|
+
const meta = readDaemonMeta();
|
|
714
|
+
const runningVersion = meta && meta.pid === pid ? meta.version : undefined;
|
|
715
|
+
const mine = cliVersion();
|
|
716
|
+
console.log(`${COLOR.green}Running${COLOR.reset} (pid ${pid}, version ${runningVersion || 'unknown — pre-1.15.4 build'})`);
|
|
717
|
+
if (meta?.entry)
|
|
718
|
+
console.log(`${COLOR.gray}Binary:${COLOR.reset} ${meta.entry}`);
|
|
719
|
+
if (mine && compareVersions(mine, runningVersion) > 0) {
|
|
720
|
+
console.log(`${COLOR.yellow}Outdated:${COLOR.reset} this CLI is ${mine} — run ${COLOR.bold}fullcourtdefense daemon${COLOR.reset} to supersede the old daemon.`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
console.log(`${COLOR.yellow}Not running${COLOR.reset} (stale pid file: ${pidFile()})`);
|
|
725
|
+
}
|
|
627
726
|
}
|
|
628
727
|
catch {
|
|
629
728
|
console.log(`${COLOR.yellow}Not running${COLOR.reset} (no pid file)`);
|
|
@@ -631,7 +730,16 @@ function statusCommand() {
|
|
|
631
730
|
console.log(`${COLOR.gray}Log:${COLOR.reset} ${logFile()}`);
|
|
632
731
|
console.log(`${COLOR.gray}Autostart:${COLOR.reset}`);
|
|
633
732
|
if (process.platform === 'win32') {
|
|
634
|
-
(0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], { stdio: '
|
|
733
|
+
const task = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 5_000 });
|
|
734
|
+
if (task.status === 0) {
|
|
735
|
+
console.log(` Scheduled Task "${TASK_NAME}" installed`);
|
|
736
|
+
}
|
|
737
|
+
else if (isWindowsRunKeyInstalled()) {
|
|
738
|
+
console.log(` Run key installed (HKCU\\...\\Run\\${WINDOWS_RUN_VALUE}) — starts at logon`);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.log(' not installed — run: fullcourtdefense daemon --install true');
|
|
742
|
+
}
|
|
635
743
|
}
|
|
636
744
|
else if (process.platform === 'darwin') {
|
|
637
745
|
console.log(fs.existsSync(launchdPlistPath()) ? ` launchd agent installed (${launchdPlistPath()})` : ' not installed');
|
package/dist/version.json
CHANGED