@decentnetwork/beagle 0.1.96 → 0.1.97
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/dist/desktop/app.js +1 -1
- package/dist/ipc.d.ts +7 -3
- package/dist/ipc.js +7 -3
- package/dist/lan-handoff.d.ts +10 -0
- package/dist/lan-handoff.js +43 -5
- package/dist/server.d.ts +3 -1
- package/dist/server.js +41 -5
- package/package.json +1 -1
package/dist/desktop/app.js
CHANGED
package/dist/ipc.d.ts
CHANGED
|
@@ -44,9 +44,13 @@ export declare function ipcSocketPath(dataDir: string, platform?: NodeJS.Platfor
|
|
|
44
44
|
* Windows named pipes, so on Windows we connect and let net report. */
|
|
45
45
|
export declare function ipcSocketSupportsFsExistenceCheck(platform?: NodeJS.Platform): boolean;
|
|
46
46
|
/** How long the liveness probe waits for a connect before calling it dead.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
47
|
+
* A listening local socket accepts in well under a millisecond, so this
|
|
48
|
+
* budget never measured the daemon — it measured how long OUR event loop took
|
|
49
|
+
* to run the connect callback. At 400 ms a GC pause or a big JSON body in
|
|
50
|
+
* beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
|
|
51
|
+
* pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
|
|
52
|
+
* in lan-handoff.ts. */
|
|
53
|
+
export declare const DAEMON_PROBE_TIMEOUT_MS = 2000;
|
|
50
54
|
/**
|
|
51
55
|
* Is a decentlan daemon actually LISTENING for this identity?
|
|
52
56
|
*
|
package/dist/ipc.js
CHANGED
|
@@ -28,9 +28,13 @@ export function ipcSocketSupportsFsExistenceCheck(platform = process.platform) {
|
|
|
28
28
|
return platform !== "win32";
|
|
29
29
|
}
|
|
30
30
|
/** How long the liveness probe waits for a connect before calling it dead.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
31
|
+
* A listening local socket accepts in well under a millisecond, so this
|
|
32
|
+
* budget never measured the daemon — it measured how long OUR event loop took
|
|
33
|
+
* to run the connect callback. At 400 ms a GC pause or a big JSON body in
|
|
34
|
+
* beagle read as "daemon gone": 814 false flaps on mac-dev while the daemon's
|
|
35
|
+
* pid never changed. One miss is not believed either; see DAEMON_MISS_STREAK
|
|
36
|
+
* in lan-handoff.ts. */
|
|
37
|
+
export const DAEMON_PROBE_TIMEOUT_MS = 2000;
|
|
34
38
|
/**
|
|
35
39
|
* Is a decentlan daemon actually LISTENING for this identity?
|
|
36
40
|
*
|
package/dist/lan-handoff.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export type HandoffState = "embedded" | "releasing" | "daemon" | "waiting";
|
|
|
6
6
|
* gives up on it and takes the identity back. `agentnet restart` and a
|
|
7
7
|
* launchd/systemd kickstart are both back well inside this. */
|
|
8
8
|
export declare const DAEMON_GRACE_MS: number;
|
|
9
|
+
/** One missed probe is not a verdict. The probe runs on beagle's own event
|
|
10
|
+
* loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
|
|
11
|
+
* its timer before the queued `connect` callback runs, and a healthy daemon
|
|
12
|
+
* reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
|
|
13
|
+
* Only a streak of misses that also spans real time is believed. */
|
|
14
|
+
export declare const DAEMON_MISS_STREAK = 3;
|
|
15
|
+
export declare const DAEMON_MISS_WINDOW_MS = 5000;
|
|
9
16
|
export interface SwitchableHostOptions {
|
|
10
17
|
dataDir: string;
|
|
11
18
|
/** Build a fresh embedded host. Called on first start and on every fallback,
|
|
@@ -37,6 +44,9 @@ export declare class SwitchablePeerHost implements PeerHost {
|
|
|
37
44
|
get kind(): PeerHost["kind"];
|
|
38
45
|
get hasVirtualLan(): boolean;
|
|
39
46
|
get state(): HandoffState;
|
|
47
|
+
/** The daemon's data dir (socket and pidfile), for callers that need to
|
|
48
|
+
* watch a daemon restart through. */
|
|
49
|
+
get dataDir(): string;
|
|
40
50
|
/** Seconds left before `releasing` gives up; 0 when not releasing. */
|
|
41
51
|
get releaseSecondsLeft(): number;
|
|
42
52
|
call(req: IpcRequest): Promise<IpcResponse>;
|
package/dist/lan-handoff.js
CHANGED
|
@@ -23,6 +23,13 @@ const RELEASE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
23
23
|
* gives up on it and takes the identity back. `agentnet restart` and a
|
|
24
24
|
* launchd/systemd kickstart are both back well inside this. */
|
|
25
25
|
export const DAEMON_GRACE_MS = 60 * 1000;
|
|
26
|
+
/** One missed probe is not a verdict. The probe runs on beagle's own event
|
|
27
|
+
* loop, so any stall here (GC, a large /api/desktop body, an npm child) fires
|
|
28
|
+
* its timer before the queued `connect` callback runs, and a healthy daemon
|
|
29
|
+
* reads as gone — 814 times on mac-dev while daemon pid 75338 never changed.
|
|
30
|
+
* Only a streak of misses that also spans real time is believed. */
|
|
31
|
+
export const DAEMON_MISS_STREAK = 3;
|
|
32
|
+
export const DAEMON_MISS_WINDOW_MS = 5000;
|
|
26
33
|
/**
|
|
27
34
|
* A PeerHost whose backend can change under it.
|
|
28
35
|
*
|
|
@@ -59,6 +66,11 @@ export class SwitchablePeerHost {
|
|
|
59
66
|
get state() {
|
|
60
67
|
return this.#state;
|
|
61
68
|
}
|
|
69
|
+
/** The daemon's data dir (socket and pidfile), for callers that need to
|
|
70
|
+
* watch a daemon restart through. */
|
|
71
|
+
get dataDir() {
|
|
72
|
+
return this.#opts.dataDir;
|
|
73
|
+
}
|
|
62
74
|
/** Seconds left before `releasing` gives up; 0 when not releasing. */
|
|
63
75
|
get releaseSecondsLeft() {
|
|
64
76
|
if (this.#state !== "releasing")
|
|
@@ -130,14 +142,33 @@ export class SwitchablePeerHost {
|
|
|
130
142
|
}, WATCH_INTERVAL_MS);
|
|
131
143
|
this.#timer.unref?.();
|
|
132
144
|
}
|
|
145
|
+
/** Consecutive missed probes; 0 the moment the daemon answers. */
|
|
146
|
+
#missStreak = 0;
|
|
147
|
+
/** A probe can take its full 2 s timeout against a 1.5 s watch interval.
|
|
148
|
+
* Overlapping ticks would each act on the other's half-made decision. */
|
|
149
|
+
#ticking = false;
|
|
133
150
|
async #tick() {
|
|
134
|
-
if (this.#busy)
|
|
151
|
+
if (this.#busy || this.#ticking)
|
|
135
152
|
return;
|
|
153
|
+
this.#ticking = true;
|
|
154
|
+
try {
|
|
155
|
+
await this.#step();
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
this.#ticking = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async #step() {
|
|
136
162
|
const daemonUp = await this.#probe(this.#opts.dataDir);
|
|
137
|
-
if (daemonUp)
|
|
163
|
+
if (daemonUp) {
|
|
138
164
|
this.#daemonMissingSince = 0;
|
|
139
|
-
|
|
140
|
-
|
|
165
|
+
this.#missStreak = 0;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (this.#daemonMissingSince === 0)
|
|
169
|
+
this.#daemonMissingSince = Date.now();
|
|
170
|
+
this.#missStreak++;
|
|
171
|
+
}
|
|
141
172
|
// A tick can await; make sure nothing started while we were probing.
|
|
142
173
|
if (this.#busy)
|
|
143
174
|
return;
|
|
@@ -152,7 +183,14 @@ export class SwitchablePeerHost {
|
|
|
152
183
|
// and rm the pidfile.
|
|
153
184
|
if (this.#opts.force === "daemon") {
|
|
154
185
|
if (this.#state === "daemon" && !daemonUp) {
|
|
155
|
-
|
|
186
|
+
// Keep serving until the absence is confirmed. Every call made during
|
|
187
|
+
// a false `waiting` failed with "the daemon is restarting", which the
|
|
188
|
+
// UI showed as an empty friend list and a hung /api/desktop.
|
|
189
|
+
const missedMs = Date.now() - this.#daemonMissingSince;
|
|
190
|
+
if (this.#missStreak < DAEMON_MISS_STREAK || missedMs < DAEMON_MISS_WINDOW_MS)
|
|
191
|
+
return;
|
|
192
|
+
this.#log.info(`the daemon went away — waiting for it to come back (forced daemon backend; ` +
|
|
193
|
+
`${this.#missStreak} probes missed over ${(missedMs / 1000).toFixed(1)} s)`);
|
|
156
194
|
const old = this.#inner;
|
|
157
195
|
this.#inner = null;
|
|
158
196
|
this.#setState("waiting");
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IpcRequest, type IpcResponse } from "./ipc.js";
|
|
2
2
|
/** How this copy of beagle got onto disk — decides what "update" can do.
|
|
3
3
|
* npx cache paths contain _npx AND node_modules, so test _npx first. */
|
|
4
4
|
export declare function installMode(): "global" | "npx" | "dev" | "unknown";
|
|
@@ -32,6 +32,8 @@ export interface BeagleServerOptions {
|
|
|
32
32
|
readonly hasVirtualLan: boolean;
|
|
33
33
|
readonly state: string;
|
|
34
34
|
readonly releaseSecondsLeft: number;
|
|
35
|
+
/** The daemon's socket/pidfile dir, so "Update lan" can wait for a new pid. */
|
|
36
|
+
readonly dataDir?: string;
|
|
35
37
|
armForDaemon: () => Promise<{
|
|
36
38
|
state: string;
|
|
37
39
|
command: string;
|
package/dist/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
20
|
import { INSTALL_COMMAND as LAN_INSTALL_COMMAND } from "./lan-handoff.js";
|
|
21
21
|
import yaml from "js-yaml";
|
|
22
|
+
import { probeDaemon } from "./ipc.js";
|
|
22
23
|
import { DEFAULT_EXITS } from "./exits.js";
|
|
23
24
|
import * as peerAddr from "@decentnetwork/peer";
|
|
24
25
|
import { bridgeRoster } from "./bridge-roster.js";
|
|
@@ -513,10 +514,45 @@ async function npmQuery(args) {
|
|
|
513
514
|
}
|
|
514
515
|
});
|
|
515
516
|
}
|
|
516
|
-
/** Ask the
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
|
|
517
|
+
/** Ask the agentnet daemon to restart onto the lan that was just installed,
|
|
518
|
+
* and report success only when a NEW daemon answers. `agentnet restart`
|
|
519
|
+
* exiting 0 means the request was accepted, nothing more: under launchd a
|
|
520
|
+
* lan ≥ 0.1.302 daemon exits and launchd starts the new code, while an older
|
|
521
|
+
* one re-execs outside launchd — which left mac-dev's KeepAlive job failing
|
|
522
|
+
* every ~10 s. Either way the proof is a different pid that accepts a
|
|
523
|
+
* connection. Without a data dir there is nothing to watch, so the old
|
|
524
|
+
* answer stands. */
|
|
525
|
+
async function agentnetRestart(dataDir) {
|
|
526
|
+
const pidBefore = dataDir ? readDaemonPid(dataDir) : "";
|
|
527
|
+
const r = await requestAgentnetRestart();
|
|
528
|
+
if (!r.ok || !dataDir)
|
|
529
|
+
return r;
|
|
530
|
+
return waitForNewDaemon(dataDir, pidBefore);
|
|
531
|
+
}
|
|
532
|
+
const readDaemonPid = (dataDir) => {
|
|
533
|
+
try {
|
|
534
|
+
return readFileSync(join(dataDir, "daemon.pid"), "utf-8").trim();
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return "";
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
async function waitForNewDaemon(dataDir, pidBefore) {
|
|
541
|
+
const deadline = Date.now() + 60_000;
|
|
542
|
+
while (Date.now() < deadline) {
|
|
543
|
+
const pid = readDaemonPid(dataDir);
|
|
544
|
+
if (pid && pid !== pidBefore && (await probeDaemon(dataDir)))
|
|
545
|
+
return { ok: true };
|
|
546
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
error: `agentnet accepted the restart, but no new daemon answered within 60 s (pid still ${readDaemonPid(dataDir) || "none"})`,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/** Run `agentnet restart`: the PATH binary, then the global install's CLI entry
|
|
554
|
+
* directly (pm2's PATH often lacks the nvm shims). */
|
|
555
|
+
async function requestAgentnetRestart() {
|
|
520
556
|
const { spawn } = await import("node:child_process");
|
|
521
557
|
const cliJs = join(dirname(process.execPath), "..", "lib", "node_modules", "@decentnetwork", "lan", "dist", "cli", "index.js");
|
|
522
558
|
const attempts = [
|
|
@@ -1826,7 +1862,7 @@ export function startBeagleServer(opts) {
|
|
|
1826
1862
|
sendJson(res, 502, inst);
|
|
1827
1863
|
return;
|
|
1828
1864
|
}
|
|
1829
|
-
const rr = await agentnetRestart();
|
|
1865
|
+
const rr = await agentnetRestart(opts.lanHost?.dataDir);
|
|
1830
1866
|
updateCache = null;
|
|
1831
1867
|
sendJson(res, 200, { ok: true, restarted: rr.ok, ...(rr.ok ? {} : { note: rr.error }) });
|
|
1832
1868
|
return;
|
package/package.json
CHANGED