@decentnetwork/beagle 0.1.90 → 0.1.91

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/cli.js CHANGED
@@ -8,7 +8,8 @@ import { existsSync, readFileSync } from "node:fs";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { dirname, join, resolve } from "node:path";
10
10
  import { homedir } from "node:os";
11
- import { startBeagleServer } from "./server.js";
11
+ import { startBeagleServer, installMode } from "./server.js";
12
+ import { isSupervised, relaunchSelf, waitForPredecessor } from "./relaunch.js";
12
13
  import { openPeerHost, defaultConfigDir, decentlanCarrierDir, makeDaemonHost } from "./peer-host.js";
13
14
  import { SwitchablePeerHost } from "./lan-handoff.js";
14
15
  import { loadNodeConfig, saveAutoAccept } from "./node-config.js";
@@ -97,6 +98,9 @@ const resolveVer = (pkg) => {
97
98
  };
98
99
  async function main() {
99
100
  const args = parseArgs(process.argv.slice(2));
101
+ // Spawned by a predecessor restarting itself after an update: let it finish
102
+ // exiting before touching the identity lock or the port. No-op otherwise.
103
+ await waitForPredecessor();
100
104
  // Bootstrap list: prefer the user's decentlan config so both stacks agree on
101
105
  // the fleet, else fall back to the shipped defaults. A wrong list here is the
102
106
  // difference between "joins the network" and "silently alone".
@@ -189,6 +193,19 @@ async function main() {
189
193
  },
190
194
  listenHost: args.host,
191
195
  listenPort: args.port,
196
+ // After an update: stop the peer cleanly, then either let the supervisor
197
+ // relaunch us or, with none, spawn our own successor. The successor waits
198
+ // for this pid to be gone (waitForPredecessor above), so the identity
199
+ // lock and the port are free by the time it needs them.
200
+ onRestart: async () => {
201
+ await Promise.race([
202
+ peerHost.stop().catch(() => undefined),
203
+ new Promise((r) => setTimeout(r, 5000)),
204
+ ]);
205
+ if (!isSupervised())
206
+ relaunchSelf(installMode());
207
+ process.exit(0);
208
+ },
192
209
  });
193
210
  const shutdown = async () => {
194
211
  await peerHost.stop().catch(() => undefined);
@@ -1,4 +1,4 @@
1
- window.__DK_UI_VERSION="0.1.90";
1
+ window.__DK_UI_VERSION="0.1.91";
2
2
  "use strict";
3
3
  (() => {
4
4
  // packages/beagle-ui/dist/beagle-ui.js
@@ -4755,7 +4755,7 @@ ${peer.address}`
4755
4755
  updRestart: "Restart",
4756
4756
  updBusy: "Updating\u2026 (about a minute)",
4757
4757
  updDone: "Updated \u2014 restart to apply.",
4758
- updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If beagle was started by hand in a terminal, run it again.)",
4758
+ updRestarting: "Restarting\u2026 this page reloads when it\u2019s back. (If it has not come back after a minute, run beagle again.)",
4759
4759
  updNpx: "Running via npx \u2014 restarting picks up the latest version automatically.",
4760
4760
  updDev: "Running from a source checkout \u2014 update with:",
4761
4761
  updPeerDaemon: "The peer version belongs to the running agentnet daemon, not to this app. Update it with:",
@@ -4937,7 +4937,7 @@ ${peer.address}`
4937
4937
  updRestart: "\u91CD\u542F",
4938
4938
  updBusy: "\u66F4\u65B0\u4E2D\u2026(\u7EA6\u4E00\u5206\u949F)",
4939
4939
  updDone: "\u66F4\u65B0\u5B8C\u6210\u2014\u2014\u91CD\u542F\u540E\u751F\u6548\u3002",
4940
- updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C beagle \u662F\u5728\u7EC8\u7AEF\u91CC\u624B\u52A8\u542F\u52A8\u7684,\u8BF7\u91CD\u65B0\u8FD0\u884C\u4E00\u6B21\u3002)",
4940
+ updRestarting: "\u6B63\u5728\u91CD\u542F\u2026\u5B8C\u6210\u540E\u672C\u9875\u4F1A\u81EA\u52A8\u5237\u65B0\u3002(\u5982\u679C\u4E00\u5206\u949F\u540E\u8FD8\u6CA1\u56DE\u6765,\u8BF7\u91CD\u65B0\u8FD0\u884C\u4E00\u6B21 beagle\u3002)",
4941
4941
  updNpx: "\u4F60\u662F\u7528 npx \u8FD0\u884C\u7684\u2014\u2014\u91CD\u542F\u5373\u81EA\u52A8\u83B7\u53D6\u6700\u65B0\u7248\u3002",
4942
4942
  updDev: "\u5F53\u524D\u4ECE\u6E90\u7801\u76EE\u5F55\u8FD0\u884C,\u8BF7\u624B\u52A8\u66F4\u65B0:",
4943
4943
  updPeerDaemon: "peer \u7684\u7248\u672C\u5C5E\u4E8E\u6B63\u5728\u8FD0\u884C\u7684 agentnet daemon,\u4E0D\u5C5E\u4E8E\u672C\u5E94\u7528\u3002\u8BF7\u7528\u8FD9\u6761\u547D\u4EE4\u66F4\u65B0:",
@@ -0,0 +1,8 @@
1
+ /** Something outside this process will restart it when it exits. */
2
+ export declare function isSupervised(): boolean;
3
+ /** Start the successor. Caller exits right after. */
4
+ export declare function relaunchSelf(mode: "global" | "npx" | "dev" | "unknown"): void;
5
+ /** In the successor: block until the process that spawned us has exited.
6
+ * No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
7
+ * than hang forever behind a predecessor that will not die. */
8
+ export declare function waitForPredecessor(): Promise<void>;
@@ -0,0 +1,85 @@
1
+ // relaunch.ts — bring Beagle back after a self-update when nobody else will.
2
+ //
3
+ // "Restart" after an update used to be process.exit(0) and a hope: pm2,
4
+ // launchd and systemd relaunch a process that exits, so under any of them
5
+ // that was a restart. A `beagle` started by hand in a terminal has no such
6
+ // minder — it exited and stayed exited, the page sat on "Restarting…"
7
+ // forever, and the person had to notice and run it again (air-wli,
8
+ // 2026-09-12). The dialog even admitted as much in parentheses.
9
+ //
10
+ // When no supervisor is detected, spawn our own successor before exiting:
11
+ // the same node, the same script path (which after `npm i -g` now holds the
12
+ // new version), the same arguments, detached so our exit does not take it
13
+ // down. The successor is told our pid and waits for us to be gone before it
14
+ // opens the peer or binds the port, so it never races us for the identity
15
+ // lock or gets EADDRINUSE from a parent still shutting down.
16
+ import { spawn } from "node:child_process";
17
+ const WAIT_ENV = "BEAGLE_RELAUNCH_WAIT_PID";
18
+ /** Set on a successor we spawned, and inherited by its successors in turn.
19
+ * A detached child is reparented to pid 1, which looks exactly like a
20
+ * launchd job — without this mark the SECOND update would have exited
21
+ * and stayed exited, which is the bug this file exists to fix. */
22
+ const SELF_ENV = "BEAGLE_SELF_RELAUNCHED";
23
+ /** Something outside this process will restart it when it exits. */
24
+ export function isSupervised() {
25
+ const env = process.env;
26
+ if (env[SELF_ENV])
27
+ return false; // we are our own minder
28
+ if (env.pm_id !== undefined)
29
+ return true; // pm2
30
+ if (env.INVOCATION_ID || env.JOURNAL_STREAM)
31
+ return true; // systemd
32
+ if (process.pid === 1)
33
+ return true; // a container with a restart policy
34
+ // launchd is the parent of every job it runs. A hand-started process in a
35
+ // terminal has the shell as its parent; only when that shell is gone does
36
+ // it get reparented to 1 — and then nobody was restarting it anyway, but a
37
+ // process that outlived its terminal is not one a person is watching, so
38
+ // exiting quietly is the lesser surprise.
39
+ if (process.platform === "darwin" && process.ppid === 1)
40
+ return true;
41
+ return false;
42
+ }
43
+ /** Start the successor. Caller exits right after. */
44
+ export function relaunchSelf(mode) {
45
+ const args = process.argv.slice(2);
46
+ let cmd;
47
+ let cmdArgs;
48
+ if (mode === "npx") {
49
+ // argv[1] points into npx's cache of the OLD version. `@latest`
50
+ // re-resolves, which is the whole update for an npx user.
51
+ cmd = process.platform === "win32" ? "npx.cmd" : "npx";
52
+ cmdArgs = ["-y", "@decentnetwork/beagle@latest", ...args];
53
+ }
54
+ else {
55
+ cmd = process.execPath;
56
+ cmdArgs = [process.argv[1], ...args];
57
+ }
58
+ const child = spawn(cmd, cmdArgs, {
59
+ detached: true,
60
+ // Keep the terminal this was started from as the log, exactly as before.
61
+ stdio: "inherit",
62
+ env: { ...process.env, [WAIT_ENV]: String(process.pid), [SELF_ENV]: "1" },
63
+ shell: process.platform === "win32" && mode === "npx",
64
+ });
65
+ child.unref();
66
+ }
67
+ /** In the successor: block until the process that spawned us has exited.
68
+ * No-op when we were not spawned by relaunchSelf. Gives up after 20s rather
69
+ * than hang forever behind a predecessor that will not die. */
70
+ export async function waitForPredecessor() {
71
+ const pid = Number(process.env[WAIT_ENV]);
72
+ delete process.env[WAIT_ENV];
73
+ if (!Number.isInteger(pid) || pid <= 0)
74
+ return;
75
+ const deadline = Date.now() + 20_000;
76
+ while (Date.now() < deadline) {
77
+ try {
78
+ process.kill(pid, 0);
79
+ }
80
+ catch {
81
+ return; // ESRCH: gone. (EPERM cannot happen — same user, our own parent.)
82
+ }
83
+ await new Promise((r) => setTimeout(r, 100));
84
+ }
85
+ }
package/dist/server.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { IpcRequest, IpcResponse } from "./ipc.js";
2
+ /** How this copy of beagle got onto disk — decides what "update" can do.
3
+ * npx cache paths contain _npx AND node_modules, so test _npx first. */
4
+ export declare function installMode(): "global" | "npx" | "dev" | "unknown";
2
5
  export interface BeagleServerOptions {
3
6
  /** Call the running daemon over IPC (injected so this module stays
4
7
  * decoupled from the CLI's ipc client). */
@@ -35,6 +38,10 @@ export interface BeagleServerOptions {
35
38
  }>;
36
39
  cancelArm: () => Promise<string>;
37
40
  };
41
+ /** Called instead of process.exit(0) on /api/update-restart, so the
42
+ * process that owns the peer can shut it down and relaunch itself when
43
+ * no supervisor will (see relaunch.ts). */
44
+ onRestart?: () => void | Promise<void>;
38
45
  listenHost?: string;
39
46
  listenPort?: number;
40
47
  log?: (msg: string) => void;
package/dist/server.js CHANGED
@@ -22,6 +22,7 @@ import yaml from "js-yaml";
22
22
  import { DEFAULT_EXITS } from "./exits.js";
23
23
  import * as peerAddr from "@decentnetwork/peer";
24
24
  import { bridgeRoster } from "./bridge-roster.js";
25
+ import { isSupervised } from "./relaunch.js";
25
26
  // Directory holding the built desktop UI bundle (index.html, app.js, vendor/).
26
27
  // scripts/build-ui.mjs emits it next to this compiled module at dist/ui/desktop/.
27
28
  const DESKTOP_DIR = join(dirname(fileURLToPath(import.meta.url)), "desktop");
@@ -268,7 +269,7 @@ async function runningVersions(call) {
268
269
  }
269
270
  /** How this copy of beagle got onto disk — decides what "update" can do.
270
271
  * npx cache paths contain _npx AND node_modules, so test _npx first. */
271
- function installMode() {
272
+ export function installMode() {
272
273
  if (BEAGLE_ROOT.includes("_npx"))
273
274
  return "npx";
274
275
  if (BEAGLE_ROOT.includes(join("node_modules", "@decentnetwork", "beagle")))
@@ -1796,10 +1797,17 @@ export function startBeagleServer(opts) {
1796
1797
  sendJson(res, 403, { ok: false, error: "restart only from this machine (localhost)" });
1797
1798
  return;
1798
1799
  }
1799
- sendJson(res, 200, { ok: true });
1800
- // Let the response flush, then exit 0: pm2/launchd/systemd restart the
1801
- // process; a bare `beagle` in a terminal just exits (the UI says so).
1802
- setTimeout(() => process.exit(0), 400);
1800
+ sendJson(res, 200, { ok: true, supervised: isSupervised() });
1801
+ // Let the response flush, then hand over. Under pm2/launchd/systemd an
1802
+ // exit IS the restart; a bare `beagle` in a terminal relaunches itself
1803
+ // (cli.ts onRestart) — it used to just exit and stay exited.
1804
+ setTimeout(() => {
1805
+ if (!opts.onRestart) {
1806
+ process.exit(0);
1807
+ return;
1808
+ }
1809
+ void Promise.resolve(opts.onRestart()).catch(() => process.exit(0));
1810
+ }, 400);
1803
1811
  return;
1804
1812
  }
1805
1813
  // Public profile card for any userid (friend or directory row): what
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/beagle",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Beagle — P2P chat, file transfer and calls for regular users, on the Decent Network. No admin privilege required.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",