mslxdff 0.1.12 → 0.1.13

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
- import { readFileSync, existsSync, statSync, watch, openSync, closeSync, mkdirSync } from "node:fs";
3
+ import { readFileSync, existsSync, statSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, join, basename } from "node:path";
6
6
  import { startServer, resolvePort } from "../src/server.js";
@@ -14,7 +14,7 @@ import { createAutoSelector } from "../src/auto.js";
14
14
  import { createPeersService } from "../src/peers.js";
15
15
  import { createEventBus } from "../src/events.js";
16
16
  import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
17
- import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, eventsFile, recentEvents } from "../src/logs.js";
17
+ import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, recentEvents } from "../src/logs.js";
18
18
 
19
19
  const logs = { appendCall, appendError, appendEvent };
20
20
 
@@ -132,11 +132,22 @@ if (args.includes("-model") || args.includes("-models")) {
132
132
  process.exit(0);
133
133
  }
134
134
 
135
- // -debug: live-follow the daemon's event stream (requests, upstream errors,
136
- // peer forwards, peer errors, results) for investigation
135
+ // -debug: stop the background daemon and run the server in THIS terminal
136
+ // (foreground), printing every event to stdout in real time via the in-memory
137
+ // event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
138
+ // the background, then exits. See the daemon body below for the stream wiring.
137
139
  if (args.includes("-debug") || args.includes("--debug")) {
138
- await liveDebug();
139
- process.exit(0);
140
+ const recent = recentEvents(100);
141
+ if (recent.length) {
142
+ console.log(`--- last ${recent.length} event(s) ---`);
143
+ for (const e of recent) console.log(fmtEvent(e));
144
+ }
145
+ console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
146
+ const { stopped, pid } = stopDaemon();
147
+ if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
148
+ process.env.MSLXDFF_DEBUG = "1";
149
+ process.env.MSLXDFF_DAEMON = "1";
150
+ // fall through to the daemon body below — no process.exit() here
140
151
  }
141
152
 
142
153
  // -creategroup <name> | -group create <name> | -group sync | -group leave <name> | -group list |
@@ -407,9 +418,34 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
407
418
  const groups = createGroupsService({});
408
419
  const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
409
420
 
421
+ const isDebug = process.env.MSLXDFF_DEBUG === "1";
410
422
  const bus = createEventBus();
411
423
  const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
412
- const srv = startServer({ router });
424
+ const srv = startServer({ router, signals: !isDebug });
425
+
426
+ // -debug: push every event straight to this terminal.
427
+ if (isDebug) {
428
+ bus.subscribe((e) => {
429
+ try {
430
+ console.log(fmtEvent(e));
431
+ } catch {
432
+ // malformed event — skip
433
+ }
434
+ });
435
+ // Ctrl+C / SIGTERM: restore the background daemon, then exit.
436
+ const restore = () => {
437
+ console.log("\n[debug] restoring background daemon...");
438
+ try {
439
+ const restoredPid = startDaemon([]);
440
+ console.log(`[debug] daemon restored (pid ${restoredPid})`);
441
+ } catch (err) {
442
+ console.error(`[debug] could not restore daemon: ${err.message}`);
443
+ }
444
+ setTimeout(() => process.exit(0), 300);
445
+ };
446
+ process.on("SIGINT", restore);
447
+ process.on("SIGTERM", restore);
448
+ }
413
449
 
414
450
  await srv.ready();
415
451
  models.startAutoRefresh();
@@ -709,135 +745,8 @@ function fmtEvent(e) {
709
745
  }
710
746
  }
711
747
 
712
- // Live-follow the daemon: replay the file backlog, then stream events over
713
- // HTTP (SSE) which the daemon pushes from memory — no filesystem watch/poll
714
- // involved on the live path. Falls back to file polling if the stream fails.
715
- async function liveDebug() {
716
- const file = eventsFile();
717
- const dir = dirname(file);
718
- const recent = recentEvents(100);
719
- if (recent.length) {
720
- console.log(`--- last ${recent.length} event(s) ---`);
721
- for (const e of recent) console.log(fmtEvent(e));
722
- }
723
- console.log("--- live (Ctrl+C to exit) ---");
724
-
725
- try {
726
- await streamEventsHttp();
727
- return; // stream kept running until Ctrl+C
728
- } catch (err) {
729
- console.error(`[debug] streaming failed (${err.message}) — falling back to file polling`);
730
- }
731
-
732
- // File-poll fallback (daemon too old to expose the SSE endpoint).
733
- if (!existsSync(file)) {
734
- try {
735
- mkdirSync(dir, { recursive: true });
736
- closeSync(openSync(file, "a"));
737
- } catch {
738
- console.error(`cannot create event file: ${file}`);
739
- process.exit(1);
740
- }
741
- }
742
- let pos = existsSync(file) ? statSync(file).size : 0;
743
- let buf = "";
744
- const pump = () => {
745
- try {
746
- if (!existsSync(file)) {
747
- pos = 0;
748
- buf = "";
749
- return;
750
- }
751
- const size = statSync(file).size;
752
- if (size < pos) {
753
- pos = 0;
754
- buf = "";
755
- }
756
- if (size === pos) return;
757
- buf += readFileSync(file, "utf8").slice(pos);
758
- pos = size;
759
- const lines = buf.split("\n");
760
- buf = lines.pop() || "";
761
- for (const l of lines) {
762
- if (!l.trim()) continue;
763
- try {
764
- console.log(fmtEvent(JSON.parse(l)));
765
- } catch {
766
- // partial/corrupt line while trimming — skip
767
- }
768
- }
769
- } catch (err) {
770
- console.error(`[debug] poll error: ${err.message}`);
771
- }
772
- };
773
- setInterval(pump, 500);
774
- pump();
775
- await new Promise(() => {}); // run until Ctrl+C
776
- }
777
-
778
- // Connect to the daemon's SSE stream (authenticated), print each event.
779
- // Resolves when the connection ends; throws on connect failure.
780
- async function streamEventsHttp() {
781
- const { token } = await loadToken();
782
- const port = getPort() || DEFAULT_PORT;
783
- const url = `http://127.0.0.1:${port}/v1/_debug/stream`;
784
- // daemon may still be starting when -debug is launched right after it —
785
- // retry a few times before giving up on the stream.
786
- let res = null;
787
- let lastErr = null;
788
- for (let attempt = 1; attempt <= 10 && !res; attempt++) {
789
- try {
790
- res = await fetch(url, {
791
- headers: { Authorization: `Bearer ${token}` },
792
- signal: AbortSignal.timeout(10_000),
793
- });
794
- } catch (err) {
795
- lastErr = err;
796
- await new Promise((r) => setTimeout(r, 500));
797
- }
798
- }
799
- if (!res) throw lastErr || new Error("cannot reach daemon");
800
- if (res.status === 401) throw new Error("unauthorized (token mismatch?)");
801
- if (res.status === 404) throw new Error("daemon too old (no stream endpoint)");
802
- if (!res.ok) throw new Error(`daemon responded ${res.status}`);
803
- const reader = res.body.getReader();
804
- const decoder = new TextDecoder();
805
- let chunk = "";
806
- const first = await Promise.race([
807
- reader.read(),
808
- new Promise((_, reject) => setTimeout(() => reject(new Error("no data from stream")), 15_000)),
809
- ]);
810
- if (first.done) return;
811
- console.log(`streaming from http://127.0.0.1:${port} (Ctrl+C to exit)`);
812
- chunk += decoder.decode(first.value, { stream: true });
813
- const lines = chunk.split("\n");
814
- chunk = lines.pop() || "";
815
- for (const l of lines) {
816
- if (!l.startsWith("data: ")) continue;
817
- try {
818
- console.log(fmtEvent(JSON.parse(l.slice(6))));
819
- } catch {
820
- // skip malformed frames
821
- }
822
- }
823
- while (true) {
824
- const { done, value } = await reader.read();
825
- if (done) break;
826
- chunk += decoder.decode(value, { stream: true });
827
- const frameLines = chunk.split("\n");
828
- chunk = frameLines.pop() || "";
829
- for (const l of frameLines) {
830
- if (!l.startsWith("data: ")) continue;
831
- try {
832
- console.log(fmtEvent(JSON.parse(l.slice(6))));
833
- } catch {
834
- // skip malformed frames
835
- }
836
- }
837
- }
838
- }
839
-
840
- function fmtTs(iso) { if (!iso) return "-";
748
+ function fmtTs(iso) {
749
+ if (!iso) return "-";
841
750
  try {
842
751
  return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
843
752
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/daemon.js CHANGED
@@ -24,10 +24,14 @@ export function startDaemon(args = []) {
24
24
  const dir = daemonDir();
25
25
  mkdirSync(dir, { recursive: true });
26
26
  const logFd = openSync(logFile(), "a", 0o600);
27
+ const env = { ...process.env, MSLXDFF_DAEMON: "1" };
28
+ // a -debug foreground session wouldn't pass MSLXDFF_DEBUG to the
29
+ // background daemon it restores (that flag means "print events to stdout")
30
+ delete env.MSLXDFF_DEBUG;
27
31
  const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
28
32
  detached: true,
29
33
  stdio: ["ignore", logFd, logFd],
30
- env: { ...process.env, MSLXDFF_DAEMON: "1" },
34
+ env,
31
35
  });
32
36
  child.unref();
33
37
  return child.pid;
package/src/routes.js CHANGED
@@ -13,16 +13,6 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
13
13
 
14
14
  const route = ROUTES.find((r) => r.method === method && r.path === path);
15
15
 
16
- if (bus && path === "/v1/_debug/stream") {
17
- // live debug stream (SSE): auth REQUIRED, then replay backlog + push
18
- if (!authorized(req, token)) {
19
- res.statusCode = 401;
20
- res.setHeader("WWW-Authenticate", "Bearer");
21
- return json(res, 401, { error: "Unauthorized" });
22
- }
23
- return streamEvents(req, res, bus);
24
- }
25
-
26
16
  if (!route) return notFound(res);
27
17
 
28
18
  if (route.requiresAuth && !authorized(req, token)) {
@@ -35,35 +25,6 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
35
25
  };
36
26
  }
37
27
 
38
- // SSE endpoint consumed by `mslxdff -debug`: replays the buffered backlog,
39
- // then pushes each new event as it happens. One second heartbeat keeps
40
- // proxies/NAT from closing the connection.
41
- function streamEvents(req, res, bus) {
42
- res.statusCode = 200;
43
- res.setHeader("Content-Type", "text/event-stream");
44
- res.setHeader("Cache-Control", "no-cache");
45
- res.setHeader("Connection", "keep-alive");
46
- res.flushHeaders?.();
47
-
48
- for (const e of bus.replayAll()) {
49
- res.write(`data: ${JSON.stringify(e)}\n\n`);
50
- }
51
- const unsubscribe = bus.subscribe((e) => {
52
- res.write(`data: ${JSON.stringify(e)}\n\n`);
53
- });
54
- const heartbeat = setInterval(() => {
55
- res.write(`: ping\n\n`);
56
- }, 15_000);
57
- res.on("close", () => {
58
- clearInterval(heartbeat);
59
- unsubscribe();
60
- });
61
- req.on("close", () => {
62
- clearInterval(heartbeat);
63
- unsubscribe();
64
- });
65
- }
66
-
67
28
  function clientIp(req) {
68
29
  const fwd = req.headers["x-forwarded-for"];
69
30
  const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
package/src/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createServer as httpCreateServer } from "node:http";
2
2
  import { DEFAULT_PORT, getPort } from "./state.js";
3
3
 
4
- export function startServer({ router }, port = resolvePort()) {
4
+ export function startServer({ router, signals = true }, port = resolvePort()) {
5
5
  const server = httpCreateServer((req, res) => {
6
6
  router(req, res).catch((err) => {
7
7
  res.statusCode = 500;
@@ -24,8 +24,10 @@ export function startServer({ router }, port = resolvePort()) {
24
24
  server.closeAllConnections?.();
25
25
  });
26
26
 
27
- process.on("SIGINT", close);
28
- process.on("SIGTERM", close);
27
+ if (signals) {
28
+ process.on("SIGINT", close);
29
+ process.on("SIGTERM", close);
30
+ }
29
31
 
30
32
  return { server, ready, close };
31
33
  }