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 +45 -136
- package/package.json +1 -1
- package/src/daemon.js +5 -1
- package/src/routes.js +0 -39
- package/src/server.js +5 -3
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
|
|
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,
|
|
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:
|
|
136
|
-
//
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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
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
|
|
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
|
-
|
|
28
|
-
|
|
27
|
+
if (signals) {
|
|
28
|
+
process.on("SIGINT", close);
|
|
29
|
+
process.on("SIGTERM", close);
|
|
30
|
+
}
|
|
29
31
|
|
|
30
32
|
return { server, ready, close };
|
|
31
33
|
}
|