mslxdff 0.1.9 → 0.1.11
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/mslxdff.js +62 -32
- package/package.json +1 -1
- package/src/daemon.js +20 -3
package/bin/mslxdff.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createRouter } from "../src/routes.js";
|
|
|
8
8
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
9
9
|
import { createModelsService } from "../src/models.js";
|
|
10
10
|
import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroupsJoined, loadModelErrors } from "../src/state.js";
|
|
11
|
-
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid } from "../src/daemon.js";
|
|
11
|
+
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
|
|
12
12
|
import { createAutoSelector } from "../src/auto.js";
|
|
13
13
|
import { createPeersService } from "../src/peers.js";
|
|
14
14
|
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
|
|
@@ -303,6 +303,26 @@ async function syncAllJoinedGroups({ peers, groups }) {
|
|
|
303
303
|
return results;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// Upgrade-aware handling of a running daemon: keep it when it already runs
|
|
307
|
+
// our version, otherwise stop it so the spawn below takes over. Cleans up
|
|
308
|
+
// stale pid files (daemon died without -stop).
|
|
309
|
+
function stopDaemonIfOutdated() {
|
|
310
|
+
const pid = readPid();
|
|
311
|
+
if (!pid) return;
|
|
312
|
+
if (!isPidAlive(pid)) {
|
|
313
|
+
console.log(`daemon pid ${pid} is stale (not running) — starting fresh`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const runningVersion = readPidVersion();
|
|
317
|
+
if (runningVersion === VERSION) return;
|
|
318
|
+
console.log(
|
|
319
|
+
runningVersion
|
|
320
|
+
? `daemon running v${runningVersion} — upgrading to v${VERSION}, restarting...`
|
|
321
|
+
: `daemon version unknown — restarting with v${VERSION}...`
|
|
322
|
+
);
|
|
323
|
+
stopDaemon();
|
|
324
|
+
}
|
|
325
|
+
|
|
306
326
|
// -port N: persist the port, then restart the daemon on it if one is running.
|
|
307
327
|
// Skip when we ARE the daemon child (it already carries the port via args).
|
|
308
328
|
const portArg = argValue("-port", "--port");
|
|
@@ -330,6 +350,7 @@ if (args.includes("-d") || args.includes("--daemon")) {
|
|
|
330
350
|
if (!process.env.MSLXDFF_DAEMON) {
|
|
331
351
|
// foreground: spawn the detached background instance, then wait for health
|
|
332
352
|
const port = effectivePort();
|
|
353
|
+
stopDaemonIfOutdated();
|
|
333
354
|
const spawnedPid = startDaemon(args.filter((a) => a !== "-d" && a !== "--daemon"));
|
|
334
355
|
await waitForHealth(port, 4000);
|
|
335
356
|
console.log(`mslxdff daemon started (pid ${spawnedPid})`);
|
|
@@ -343,12 +364,14 @@ if (args.includes("-d") || args.includes("--daemon")) {
|
|
|
343
364
|
// Bare run: show status + help when the daemon is already up; otherwise spawn it
|
|
344
365
|
// as a background daemon and exit — never holds the terminal (npx-friendly).
|
|
345
366
|
if (!process.env.MSLXDFF_DAEMON) {
|
|
346
|
-
|
|
367
|
+
const pid = readPid();
|
|
368
|
+
if (pid && isPidAlive(pid) && readPidVersion() === VERSION) {
|
|
347
369
|
await printStatus();
|
|
348
370
|
printHelp();
|
|
349
371
|
process.exit(0);
|
|
350
372
|
}
|
|
351
373
|
const port = effectivePort();
|
|
374
|
+
stopDaemonIfOutdated();
|
|
352
375
|
const spawnedPid = startDaemon([]);
|
|
353
376
|
await waitForHealth(port, 4000);
|
|
354
377
|
console.log(`mslxdff v${VERSION} started as a background daemon (pid ${spawnedPid})`);
|
|
@@ -388,7 +411,7 @@ const srv = startServer({ router });
|
|
|
388
411
|
await srv.ready();
|
|
389
412
|
models.startAutoRefresh();
|
|
390
413
|
if (process.env.MSLXDFF_DAEMON) {
|
|
391
|
-
writePid(process.pid);
|
|
414
|
+
writePid(process.pid, VERSION);
|
|
392
415
|
}
|
|
393
416
|
const addr = srv.server.address();
|
|
394
417
|
const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
|
|
@@ -688,9 +711,10 @@ function fmtEvent(e) {
|
|
|
688
711
|
async function liveDebug() {
|
|
689
712
|
const file = eventsFile();
|
|
690
713
|
// ensure the event file exists so watch() doesn't fail on a fresh daemon dir
|
|
714
|
+
const dir = dirname(file);
|
|
691
715
|
if (!existsSync(file)) {
|
|
692
716
|
try {
|
|
693
|
-
mkdirSync(
|
|
717
|
+
mkdirSync(dir, { recursive: true });
|
|
694
718
|
closeSync(openSync(file, "a"));
|
|
695
719
|
} catch {
|
|
696
720
|
console.error(`cannot create event file: ${file}`);
|
|
@@ -703,48 +727,54 @@ async function liveDebug() {
|
|
|
703
727
|
for (const e of recent) console.log(fmtEvent(e));
|
|
704
728
|
}
|
|
705
729
|
console.log("--- live (Ctrl+C to exit) ---");
|
|
730
|
+
console.log(`watching: ${file}`);
|
|
706
731
|
if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
|
|
707
732
|
|
|
708
733
|
let pos = existsSync(file) ? statSync(file).size : 0;
|
|
709
734
|
let buf = "";
|
|
710
735
|
const pump = () => {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (size === pos) return;
|
|
722
|
-
buf += readFileSync(file, "utf8").slice(pos);
|
|
723
|
-
pos = size;
|
|
724
|
-
const lines = buf.split("\n");
|
|
725
|
-
buf = lines.pop() || "";
|
|
726
|
-
for (const l of lines) {
|
|
727
|
-
if (!l.trim()) continue;
|
|
728
|
-
try {
|
|
729
|
-
console.log(fmtEvent(JSON.parse(l)));
|
|
730
|
-
} catch {
|
|
731
|
-
// partial/corrupt line while trimming — skip
|
|
736
|
+
try {
|
|
737
|
+
if (!existsSync(file)) {
|
|
738
|
+
pos = 0;
|
|
739
|
+
buf = "";
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const size = statSync(file).size;
|
|
743
|
+
if (size < pos) {
|
|
744
|
+
pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
|
|
745
|
+
buf = "";
|
|
732
746
|
}
|
|
747
|
+
if (size === pos) return;
|
|
748
|
+
buf += readFileSync(file, "utf8").slice(pos);
|
|
749
|
+
pos = size;
|
|
750
|
+
const lines = buf.split("\n");
|
|
751
|
+
buf = lines.pop() || "";
|
|
752
|
+
for (const l of lines) {
|
|
753
|
+
if (!l.trim()) continue;
|
|
754
|
+
try {
|
|
755
|
+
console.log(fmtEvent(JSON.parse(l)));
|
|
756
|
+
} catch {
|
|
757
|
+
// partial/corrupt line while trimming — skip
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.error(`[debug] poll error: ${err.message}`);
|
|
733
762
|
}
|
|
734
763
|
};
|
|
764
|
+
// fs.watch is unreliable across platforms for freshly-created/rotated files,
|
|
765
|
+
// so polling is the source of truth (200ms); the directory watch just adds
|
|
766
|
+
// low-latency wake-ups and silently degrades when it misbehaves.
|
|
735
767
|
try {
|
|
736
|
-
mkdirSync(
|
|
737
|
-
watch(
|
|
768
|
+
mkdirSync(dir, { recursive: true });
|
|
769
|
+
watch(dir, (_evt, filename) => {
|
|
738
770
|
if (!filename || basename(String(filename)) !== basename(file)) return;
|
|
739
771
|
pump();
|
|
740
772
|
});
|
|
741
773
|
} catch {
|
|
742
|
-
console.error(`cannot watch event dir: ${
|
|
743
|
-
process.exit(1);
|
|
774
|
+
console.error(`cannot watch event dir: ${dir} (falling back to polling only)`);
|
|
744
775
|
}
|
|
745
|
-
//
|
|
746
|
-
|
|
747
|
-
setInterval(pump, 250);
|
|
776
|
+
// Poll is the source of truth; 200ms keeps human-perceived latency negligible.
|
|
777
|
+
setInterval(pump, 200);
|
|
748
778
|
pump();
|
|
749
779
|
await new Promise(() => {}); // run until Ctrl+C
|
|
750
780
|
}
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -33,19 +33,36 @@ export function startDaemon(args = []) {
|
|
|
33
33
|
return child.pid;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function writePid(pid) {
|
|
36
|
+
export function writePid(pid, version) {
|
|
37
37
|
const dir = daemonDir();
|
|
38
38
|
mkdirSync(dir, { recursive: true });
|
|
39
|
-
writeFileSync(pidFile(), String(pid), { mode: 0o600 });
|
|
39
|
+
writeFileSync(pidFile(), version ? `${pid}\n${version}` : String(pid), { mode: 0o600 });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function readPid() {
|
|
43
43
|
if (!existsSync(pidFile())) return null;
|
|
44
44
|
const raw = readFileSync(pidFile(), "utf8").trim();
|
|
45
|
-
const n = Number(raw);
|
|
45
|
+
const n = Number(raw.split("\n")[0]);
|
|
46
46
|
return Number.isInteger(n) && n > 0 ? n : null;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function readPidVersion() {
|
|
50
|
+
if (!existsSync(pidFile())) return null;
|
|
51
|
+
const raw = readFileSync(pidFile(), "utf8");
|
|
52
|
+
const lines = raw.split("\n");
|
|
53
|
+
return lines.length > 1 && lines[1].trim() ? lines[1].trim() : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Best-effort liveness check (signal 0); ESRCH means the process is gone.
|
|
57
|
+
export function isPidAlive(pid) {
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return err.code !== "ESRCH";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
export function stopDaemon() {
|
|
50
67
|
const pid = readPid();
|
|
51
68
|
if (!pid) return { stopped: false, reason: "no pid file" };
|