@decentnetwork/lan 0.1.234 → 0.1.235

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.
@@ -17,6 +17,7 @@ import yaml from "js-yaml";
17
17
  import { startMultiExitRouter } from "../proxy/multi-exit-router.js";
18
18
  import { resolveEgressBindIp } from "../proxy/egress.js";
19
19
  import { startFriendUi } from "../ui/server.js";
20
+ import { installLogThrottle } from "../utils/log-throttle.js";
20
21
  /**
21
22
  * Refuse to open a second Carrier peer with this identity if the
22
23
  * daemon is already running with the same keypair. Two peers sharing
@@ -442,6 +443,12 @@ export async function cmdUp(args) {
442
443
  if (args.name) {
443
444
  config.node.name = args.name;
444
445
  }
446
+ // Collapse repeated log lines before they reach stdout. A daemon that runs
447
+ // for weeks repeats the same message shape endlessly (a peer re-keying, an
448
+ // onion path failing); unthrottled that produced a 12 GB log file, and a
449
+ // full disk stops the daemon outright. Covers the SDK's [peer-debug] output
450
+ // too, since this wraps the process console rather than our Logger.
451
+ installLogThrottle();
445
452
  const daemon = new DaemonServer({
446
453
  config,
447
454
  configDir: dir,
@@ -2390,6 +2397,22 @@ WantedBy=multi-user.target
2390
2397
  const msg = err instanceof Error ? err.message : String(err);
2391
2398
  throw new Error(`Could not write ${plistPath} (need root? try 'sudo'): ${msg}`);
2392
2399
  }
2400
+ // launchd never rotates StandardOutPath — the file grows until the disk is
2401
+ // full and the daemon dies with it (a 12 GB agentnet log is how we found
2402
+ // this). systemd users get journald rotation for free; on macOS the native
2403
+ // equivalent is a newsyslog rule, so install one alongside the plist:
2404
+ // rotate at 10 MB, keep 5 compressed generations.
2405
+ try {
2406
+ const fs = await import("fs/promises");
2407
+ await fs.mkdir("/etc/newsyslog.d", { recursive: true });
2408
+ await fs.writeFile("/etc/newsyslog.d/com.decentlan.agentnet.conf", "# logfilename [owner:group] mode count size(KB) when flags\n" +
2409
+ "/var/log/agentnet.log root:wheel 644 5 10240 * GZ\n", "utf-8");
2410
+ console.log("Log rotation: /var/log/agentnet.log rotates at 10 MB, 5 compressed generations kept.");
2411
+ }
2412
+ catch (err) {
2413
+ // Not fatal — the daemon's own log throttling already bounds the volume.
2414
+ console.log(`Note: could not install the newsyslog rotation rule (${err instanceof Error ? err.message : err}). The log is still throttled in-process.`);
2415
+ }
2393
2416
  execSync(`launchctl load ${plistPath}`);
2394
2417
  console.log(`Installed ${plistPath} and started com.decentlan.agentnet.`);
2395
2418
  console.log(`Optional — watch China video (CCTV etc.): agentnet proxy trust-ca && agentnet proxy router --hls-accel (docs/CCTV-VIEWING.md)`);
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.234";
1
+ window.__DK_UI_VERSION="0.1.235";
2
2
  const ICON_PATHS = {
3
3
  // ---- tab bar (the four must feel like one set) ----
4
4
  users: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Bound the daemon's log volume by collapsing repeated lines.
3
+ *
4
+ * A long-running daemon emits the same message shape over and over — a peer
5
+ * that keeps re-keying, an onion path that keeps failing, a node blacklisted
6
+ * on a loop. Left alone that filled a 12 GB log file. The value of the 30,000th
7
+ * copy of a line is zero, but the disk cost is not, and on a full disk the
8
+ * daemon stops working entirely.
9
+ *
10
+ * This wraps the process's console so EVERY writer is covered, including the
11
+ * `[peer-debug]` output from `@decentnetwork/peer` (which logs via console and
12
+ * so can't be reached by our own Logger class). Messages are bucketed by
13
+ * "shape" — ids, addresses and numbers stripped — so a line that differs only
14
+ * by which peer or port it names still collapses into one bucket.
15
+ *
16
+ * Behaviour per shape, per window: print the first BURST copies verbatim, then
17
+ * go quiet; when the window closes, print a single "(… ×N suppressed)" line so
18
+ * the volume is still visible. Nothing is dropped silently.
19
+ */
20
+ export interface LogThrottleOptions {
21
+ /** Copies of one shape printed verbatim before suppression. */
22
+ burst?: number;
23
+ /** Window length; the counter and suppression reset after this. */
24
+ windowMs?: number;
25
+ /** Distinct shapes tracked at once (guards the map itself from a leak). */
26
+ maxShapes?: number;
27
+ }
28
+ /** Collapse a message to its shape: strip ids, IPs, numbers and durations so
29
+ * the same event about different peers shares one bucket. */
30
+ export declare function logShape(message: string): string;
31
+ /**
32
+ * Install the throttle on `console`. Returns a restore function (used by tests;
33
+ * the daemon never uninstalls).
34
+ */
35
+ export declare function installLogThrottle(opts?: LogThrottleOptions): () => void;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Bound the daemon's log volume by collapsing repeated lines.
3
+ *
4
+ * A long-running daemon emits the same message shape over and over — a peer
5
+ * that keeps re-keying, an onion path that keeps failing, a node blacklisted
6
+ * on a loop. Left alone that filled a 12 GB log file. The value of the 30,000th
7
+ * copy of a line is zero, but the disk cost is not, and on a full disk the
8
+ * daemon stops working entirely.
9
+ *
10
+ * This wraps the process's console so EVERY writer is covered, including the
11
+ * `[peer-debug]` output from `@decentnetwork/peer` (which logs via console and
12
+ * so can't be reached by our own Logger class). Messages are bucketed by
13
+ * "shape" — ids, addresses and numbers stripped — so a line that differs only
14
+ * by which peer or port it names still collapses into one bucket.
15
+ *
16
+ * Behaviour per shape, per window: print the first BURST copies verbatim, then
17
+ * go quiet; when the window closes, print a single "(… ×N suppressed)" line so
18
+ * the volume is still visible. Nothing is dropped silently.
19
+ */
20
+ /** Collapse a message to its shape: strip ids, IPs, numbers and durations so
21
+ * the same event about different peers shares one bucket. */
22
+ export function logShape(message) {
23
+ return message
24
+ .replace(/\b[0-9a-fA-F]{16,}\b/g, "<hex>")
25
+ .replace(/\b[1-9A-HJ-NP-Za-km-z]{40,}\b/g, "<id>") // base58 userid/address
26
+ .replace(/\b\d+\.\d+\.\d+\.\d+\b/g, "<ip>")
27
+ // No word boundaries: counters are routinely glued to a unit ("60s",
28
+ // "250ms", "step2"). Requiring \b left those unnormalized, so every
29
+ // distinct duration became its own bucket and nothing collapsed.
30
+ .replace(/\d+(\.\d+)?/g, "<n>")
31
+ .slice(0, 200);
32
+ }
33
+ /**
34
+ * Install the throttle on `console`. Returns a restore function (used by tests;
35
+ * the daemon never uninstalls).
36
+ */
37
+ export function installLogThrottle(opts = {}) {
38
+ const burst = opts.burst ?? 5;
39
+ const windowMs = opts.windowMs ?? 60_000;
40
+ const maxShapes = opts.maxShapes ?? 500;
41
+ const shapes = new Map();
42
+ const original = {
43
+ log: console.log.bind(console),
44
+ debug: console.debug.bind(console),
45
+ info: console.info.bind(console),
46
+ warn: console.warn.bind(console),
47
+ error: console.error.bind(console),
48
+ };
49
+ const wrap = (emit) => (message, ...args) => {
50
+ // Only throttle plain string messages; structured/objects pass through.
51
+ if (typeof message !== "string") {
52
+ emit(message, ...args);
53
+ return;
54
+ }
55
+ const now = Date.now();
56
+ const key = logShape(message);
57
+ let st = shapes.get(key);
58
+ if (!st) {
59
+ // Bound the tracking map. Evicting the oldest window is enough: a
60
+ // genuinely hot shape re-registers on its next line.
61
+ if (shapes.size >= maxShapes) {
62
+ let oldestKey;
63
+ let oldest = Infinity;
64
+ for (const [k, v] of shapes) {
65
+ if (v.windowStart < oldest) {
66
+ oldest = v.windowStart;
67
+ oldestKey = k;
68
+ }
69
+ }
70
+ if (oldestKey)
71
+ shapes.delete(oldestKey);
72
+ }
73
+ st = { count: 0, windowStart: now, suppressed: 0, sample: message };
74
+ shapes.set(key, st);
75
+ }
76
+ if (now - st.windowStart >= windowMs) {
77
+ // Window closed — report what we swallowed, then start fresh.
78
+ if (st.suppressed > 0) {
79
+ emit(`… ${st.suppressed} more like this suppressed in the last ${Math.round(windowMs / 1000)}s: ${st.sample.slice(0, 160)}`);
80
+ }
81
+ st.count = 0;
82
+ st.suppressed = 0;
83
+ st.windowStart = now;
84
+ }
85
+ st.count++;
86
+ if (st.count <= burst) {
87
+ emit(message, ...args);
88
+ return;
89
+ }
90
+ st.suppressed++;
91
+ };
92
+ console.log = wrap(original.log);
93
+ console.debug = wrap(original.debug);
94
+ console.info = wrap(original.info);
95
+ console.warn = wrap(original.warn);
96
+ // Errors are rare and each one matters — never suppress them.
97
+ console.error = original.error;
98
+ return () => {
99
+ console.log = original.log;
100
+ console.debug = original.debug;
101
+ console.info = original.info;
102
+ console.warn = original.warn;
103
+ console.error = original.error;
104
+ };
105
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.234",
3
+ "version": "0.1.235",
4
4
  "description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",