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 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
- if (readPid()) {
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(dirname(file), { recursive: true });
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
- if (!existsSync(file)) {
712
- pos = 0;
713
- buf = "";
714
- return;
715
- }
716
- const size = statSync(file).size;
717
- if (size < pos) {
718
- pos = 0; // file trimmed/rewritten: re-follow from the start of what remains
719
- buf = "";
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(dirname(file), { recursive: true });
737
- watch(dirname(file), (_evt, filename) => {
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: ${dirname(file)}`);
743
- process.exit(1);
774
+ console.error(`cannot watch event dir: ${dir} (falling back to polling only)`);
744
775
  }
745
- // fs.watch is unreliable for freshly-created files on Windows poll as a
746
- // safety net so events are never missed.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
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" };