@podway/relay 0.2.4 → 0.2.6
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/FAQ.md +53 -4
- package/README.md +1 -1
- package/dist/main.js +43 -46
- package/dist/status.d.ts +45 -0
- package/dist/status.js +109 -0
- package/dist/stop.d.ts +11 -0
- package/dist/stop.js +46 -0
- package/package.json +1 -1
package/FAQ.md
CHANGED
|
@@ -33,15 +33,31 @@ normal login shell. If `npx` works after you log in but fails in a script, set
|
|
|
33
33
|
|
|
34
34
|
## How do I make the relay survive a reboot?
|
|
35
35
|
|
|
36
|
-
The relay itself does not need root.
|
|
37
|
-
|
|
36
|
+
The relay itself does not need root. For a machine that starts it at boot, **install it once** instead
|
|
37
|
+
of running it through `npx`: `npx` unpacks the package into a cache on every start, and a start that is
|
|
38
|
+
cut off (a reboot, two starts at once) can leave that cache broken so the next start fails with
|
|
39
|
+
`ENOTEMPTY`.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm i -g @podway/relay@latest # update later with the same command
|
|
43
|
+
relay status # check it runs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If `npm i -g` says "permission denied", give npm a folder in your home first:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm config set prefix ~/.npm-global
|
|
50
|
+
echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> ~/.profile && . ~/.profile
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then use whichever startup mechanism your host provides:
|
|
38
54
|
|
|
39
55
|
- **Synology DSM**: Go to Control Panel → Task Scheduler → Create →
|
|
40
56
|
Triggered Task → User-defined script, then choose **Boot-up**. Run:
|
|
41
57
|
|
|
42
58
|
```bash
|
|
43
|
-
export PATH
|
|
44
|
-
|
|
59
|
+
export PATH=$HOME/.npm-global/bin:/path/to/node/bin:$PATH # where relay and node live
|
|
60
|
+
relay start --gateway <url> --accept
|
|
45
61
|
```
|
|
46
62
|
|
|
47
63
|
- **A systemd host**: If you have root, create a service that runs the same
|
|
@@ -52,6 +68,9 @@ provides:
|
|
|
52
68
|
The scheduler only needs to run the command once. `start` moves the relay into
|
|
53
69
|
the background and exits.
|
|
54
70
|
|
|
71
|
+
It is safe to run `relay stop` first, "just in case": it returns within a few seconds even on a
|
|
72
|
+
slow NAS (it waits up to 5 s for the old relay to exit, then force-stops it).
|
|
73
|
+
|
|
55
74
|
## Do I need to pass `--code` again every time it starts?
|
|
56
75
|
|
|
57
76
|
No. `--code` is only used for the first pairing. The relay saves a reconnect
|
|
@@ -85,3 +104,33 @@ every authorized SSH user, not only Podway, and requires administrator access:
|
|
|
85
104
|
sudo sed -i 's/^AllowTcpForwarding no/AllowTcpForwarding yes/' /etc/ssh/sshd_config
|
|
86
105
|
sudo systemctl restart sshd # or: synosystemctl restart sshd, on some DSM versions
|
|
87
106
|
```
|
|
107
|
+
|
|
108
|
+
## `npm error code ENOTEMPTY` when I run `npx @podway/relay`
|
|
109
|
+
|
|
110
|
+
A previous `npx` run was cut off and left its cache folder half-written. Delete the cache (npm downloads
|
|
111
|
+
it again) and switch to the installed relay (see "How do I make the relay survive a reboot?"):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
rm -rf ~/.npm/_npx
|
|
115
|
+
npm i -g @podway/relay@latest
|
|
116
|
+
relay status
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## How do I see what the relay did without the web dashboard?
|
|
120
|
+
|
|
121
|
+
`relay status` prints it as tables — useful on a NAS where the dashboard's local page is out of reach:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
STATE UPTIME LINK REQ 24h OK% DATA
|
|
125
|
+
running 2d 4h connected 412 98% 61 MB
|
|
126
|
+
|
|
127
|
+
POD REQ OK% LAST
|
|
128
|
+
afisha crawler 301 99% 12 s ago
|
|
129
|
+
crypto sail club 88 93% 3 min ago
|
|
130
|
+
|
|
131
|
+
SITE REQ OK% LAST ERROR
|
|
132
|
+
polymarket.com 145 97% 403 refused, 26 min ago
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`--since 1h|24h|7d` changes the window, `--all` shows every row, `--json` is for scripts. DATA counts
|
|
136
|
+
tunnel traffic only (plain fetches have no size recorded).
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Once started, the relay runs in the background and handles both modes.
|
|
|
46
46
|
| `relay start --code <code> --gateway <url> --accept` | start the relay in the background |
|
|
47
47
|
| `relay login <domain>` | allow signed-in fetches for one domain |
|
|
48
48
|
| `relay dashboard` | open the local command center |
|
|
49
|
-
| `relay status` |
|
|
49
|
+
| `relay status` | the relay at a glance, then requests per pod and per site (OK %, last error); `--since 1h\|24h\|7d`, `--all`, `--json` |
|
|
50
50
|
| `relay stop` | stop it |
|
|
51
51
|
| `relay reset` | remove saved sessions, logins, and pairing |
|
|
52
52
|
|
package/dist/main.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import WebSocket from "ws";
|
|
3
2
|
import { spawn } from "node:child_process";
|
|
4
3
|
import { writeFileSync, readFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { load, save, addLoginDomain, resetProfile, profileDir, pidFile, dashboardRuntimeFile, isDomainBlocked, isPodPaused, revokeLoginDomain, } from "./config.js";
|
|
7
|
-
import { RelayClient } from "./relay-client.js";
|
|
8
|
-
import { RelayTunnel } from "./relay-tunnel.js";
|
|
9
|
-
import { keepAwake } from "./keep-awake.js";
|
|
10
|
-
import { BrowserFetcher } from "./browser-fetcher.js";
|
|
11
|
-
import { RelayEventStore } from "./audit.js";
|
|
12
|
-
import { serveDashboard } from "./dashboard.js";
|
|
13
|
-
import { RelayRuntime } from "./runtime.js";
|
|
14
|
-
import { attachHeartbeat } from "./heartbeat.js";
|
|
15
6
|
import { DISCLOSURE } from "./disclosure.js";
|
|
16
7
|
import { c, rows } from "./colors.js";
|
|
17
8
|
import { normalizeDomain } from "./domain.js";
|
|
9
|
+
import { stopPid } from "./stop.js";
|
|
10
|
+
import { renderStatus, statusData, WINDOWS } from "./status.js";
|
|
11
|
+
// Only `start` (the daemon) and `dashboard` need the transport, browser and dashboard modules.
|
|
12
|
+
// They load lazily so `relay stop` / `relay status` stay quick on a slow NAS CPU (a DS118 runs the
|
|
13
|
+
// boot task's `relay stop` before every start).
|
|
18
14
|
/**
|
|
19
15
|
* `relay` — one command starts a background relay; login/dashboard/stop modify it
|
|
20
16
|
* through disk state (config, audit log, pidfile). No per-request approval: once
|
|
@@ -126,6 +122,10 @@ async function cmdStart(a) {
|
|
|
126
122
|
log("");
|
|
127
123
|
}
|
|
128
124
|
async function runDaemon(a) {
|
|
125
|
+
const [{ default: WebSocket }, { RelayClient }, { RelayTunnel }, { keepAwake }, { BrowserFetcher }, { RelayEventStore }, { serveDashboard }, { RelayRuntime }, { attachHeartbeat }] = await Promise.all([
|
|
126
|
+
import("ws"), import("./relay-client.js"), import("./relay-tunnel.js"), import("./keep-awake.js"), import("./browser-fetcher.js"),
|
|
127
|
+
import("./audit.js"), import("./dashboard.js"), import("./runtime.js"), import("./heartbeat.js"),
|
|
128
|
+
]);
|
|
129
129
|
const gateway = arg(a, "--gateway");
|
|
130
130
|
const code = arg(a, "--code") ?? "";
|
|
131
131
|
const browser = new BrowserFetcher({
|
|
@@ -145,10 +145,12 @@ async function runDaemon(a) {
|
|
|
145
145
|
if (stopping)
|
|
146
146
|
return;
|
|
147
147
|
stopping = true;
|
|
148
|
+
// Never hang on the way out: whatever the cleanup does, the process is gone in 4 s.
|
|
149
|
+
setTimeout(() => process.exit(0), 4000).unref();
|
|
148
150
|
releaseKeepAwake();
|
|
149
151
|
runtime.clearActive();
|
|
150
152
|
dashboardClose?.();
|
|
151
|
-
await browser.close();
|
|
153
|
+
await Promise.race([browser.close().catch(() => undefined), new Promise((r) => setTimeout(r, 3000))]);
|
|
152
154
|
try {
|
|
153
155
|
unlinkSync(pidFile());
|
|
154
156
|
}
|
|
@@ -352,6 +354,9 @@ async function cmdDashboard(a) {
|
|
|
352
354
|
return;
|
|
353
355
|
}
|
|
354
356
|
}
|
|
357
|
+
const [{ RelayEventStore }, { RelayRuntime }, { serveDashboard }] = await Promise.all([
|
|
358
|
+
import("./audit.js"), import("./runtime.js"), import("./dashboard.js"),
|
|
359
|
+
]);
|
|
355
360
|
const cfg = load();
|
|
356
361
|
const store = preview ? undefined : new RelayEventStore({ retentionDays: cfg.retentionDays ?? 30 }).init();
|
|
357
362
|
const runtime = preview ? undefined : new RelayRuntime({ daemon: "stopped" });
|
|
@@ -363,22 +368,27 @@ async function cmdDashboard(a) {
|
|
|
363
368
|
openBrowser(url);
|
|
364
369
|
await new Promise(() => { }); // stay up until Ctrl-C
|
|
365
370
|
}
|
|
366
|
-
async function cmdStatus() {
|
|
371
|
+
async function cmdStatus(a = []) {
|
|
372
|
+
const flag = (n) => a.includes(n);
|
|
373
|
+
const sinceArg = a[a.indexOf("--since") + 1];
|
|
374
|
+
const window = a.includes("--since") && sinceArg && sinceArg in WINDOWS ? sinceArg : "24h";
|
|
367
375
|
const pid = isRunning();
|
|
368
376
|
const cfg = load();
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
377
|
+
let startedAt;
|
|
378
|
+
try {
|
|
379
|
+
startedAt = JSON.parse(readFileSync(dashboardRuntimeFile(), "utf8")).startedAt;
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
// audit.js is fs-only; reload() reads the saved history without pruning or writing.
|
|
383
|
+
const { RelayEventStore } = await import("./audit.js");
|
|
384
|
+
const store = new RelayEventStore({ retentionDays: 30 });
|
|
385
|
+
store.reload();
|
|
386
|
+
const data = statusData(store.query({ limit: 10_000 }), { state: pid ? "running" : "stopped", startedAt, link: pid ? await gatewayLinkState() : undefined }, window);
|
|
387
|
+
if (flag("--json"))
|
|
388
|
+
return log(JSON.stringify({ ...data, pid, gateway: cfg.gatewayUrl ?? null, asYou: cfg.loginDomains }, null, 2));
|
|
389
|
+
log(renderStatus(data, { limit: flag("--all") ? Infinity : 10 }));
|
|
372
390
|
log("");
|
|
373
|
-
log(
|
|
374
|
-
// The PROCESS being alive is not the same as the gateway LINK being up — a slept host
|
|
375
|
-
// leaves a dead link the process can't see. Ask the daemon's own dashboard for the
|
|
376
|
-
// live state so status stops reporting "running" over a dead link.
|
|
377
|
-
if (pid)
|
|
378
|
-
log(`${label("link")}${await gatewayLinkState()}`);
|
|
379
|
-
log(`${label("gateway")}${cfg.gatewayUrl ? c.cyan(cfg.gatewayUrl) : c.dim("(unset)")}`);
|
|
380
|
-
log(`${label("dashboard")}${dashboardUrl() ? c.cyan(dashboardUrl()) : c.dim("relay dashboard opens saved history")}`);
|
|
381
|
-
log(`${label("as-you")}${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : c.dim("no sites signed in — all fetches are clean")}`);
|
|
391
|
+
log(c.dim(`gateway ${cfg.gatewayUrl ?? "(unset)"} · as-you ${cfg.loginDomains.length ? cfg.loginDomains.join(", ") : "none"} · --since 1h|24h|7d · --all · --json`));
|
|
382
392
|
log("");
|
|
383
393
|
}
|
|
384
394
|
/** Live gateway-link state, read from the running daemon's local dashboard (`/data`).
|
|
@@ -388,22 +398,16 @@ async function cmdStatus() {
|
|
|
388
398
|
async function gatewayLinkState() {
|
|
389
399
|
const base = dashboardUrl();
|
|
390
400
|
if (!base)
|
|
391
|
-
return
|
|
401
|
+
return "starting";
|
|
392
402
|
try {
|
|
393
403
|
const ac = new AbortController();
|
|
394
404
|
const timer = setTimeout(() => ac.abort(), 1500);
|
|
395
405
|
const res = await fetch(`${base}/data`, { signal: ac.signal }).finally(() => clearTimeout(timer));
|
|
396
406
|
const data = (await res.json());
|
|
397
|
-
|
|
398
|
-
return c.green("connected");
|
|
399
|
-
if (data.state === "reconnecting")
|
|
400
|
-
return c.yellow("reconnecting… (host asleep or offline)");
|
|
401
|
-
if (data.state === "stopped")
|
|
402
|
-
return c.yellow("stopped");
|
|
403
|
-
return c.dim("unknown");
|
|
407
|
+
return data.state === "connected" || data.state === "reconnecting" || data.state === "stopped" ? data.state : "unknown";
|
|
404
408
|
}
|
|
405
409
|
catch {
|
|
406
|
-
return
|
|
410
|
+
return "unreachable"; // the daemon isn't answering — relay restart
|
|
407
411
|
}
|
|
408
412
|
}
|
|
409
413
|
/** Stop-then-start, reusing the stored gateway + reconnect token. The escape hatch for a
|
|
@@ -411,12 +415,7 @@ async function gatewayLinkState() {
|
|
|
411
415
|
async function cmdRestart(a) {
|
|
412
416
|
const pid = isRunning();
|
|
413
417
|
if (pid) {
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
unlinkSync(pidFile());
|
|
417
|
-
}
|
|
418
|
-
catch { }
|
|
419
|
-
await new Promise((r) => setTimeout(r, 600)); // let it release the pid + dashboard port
|
|
418
|
+
await stopPid(pid, { files: [pidFile(), dashboardRuntimeFile()] }); // waits until it is gone (≤ 5 s)
|
|
420
419
|
log(c.dim("restarting the relay…"));
|
|
421
420
|
}
|
|
422
421
|
else {
|
|
@@ -424,23 +423,21 @@ async function cmdRestart(a) {
|
|
|
424
423
|
}
|
|
425
424
|
return cmdStart(a);
|
|
426
425
|
}
|
|
427
|
-
function cmdStop() {
|
|
426
|
+
async function cmdStop() {
|
|
428
427
|
const pid = isRunning();
|
|
429
428
|
if (!pid)
|
|
430
429
|
return log(c.dim("no relay running."));
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
catch { }
|
|
436
|
-
log(`${c.green("✓")} relay stopped.`);
|
|
430
|
+
const how = await stopPid(pid, { files: [pidFile(), dashboardRuntimeFile()] });
|
|
431
|
+
log(how === "killed"
|
|
432
|
+
? `${c.green("✓")} relay stopped ${c.dim("(it did not exit within 5 s, so it was killed)")}.`
|
|
433
|
+
: `${c.green("✓")} relay stopped.`);
|
|
437
434
|
}
|
|
438
435
|
async function dispatch(a) {
|
|
439
436
|
switch (a[0]) {
|
|
440
437
|
case "start": return cmdStart(a.slice(1));
|
|
441
438
|
case "login": return cmdLogin(a.slice(1));
|
|
442
439
|
case "dashboard": return cmdDashboard(a.slice(1));
|
|
443
|
-
case "status": return cmdStatus();
|
|
440
|
+
case "status": return cmdStatus(a.slice(1));
|
|
444
441
|
case "stop": return cmdStop();
|
|
445
442
|
case "restart": return cmdRestart(a.slice(1));
|
|
446
443
|
case "reset":
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `relay status` stats as docker-ps style tables (owner, 2026-11-08: the web dashboard is not reachable on
|
|
3
|
+
* a headless NAS, so the terminal has to show what the relay did). Pure: events in, text/JSON out. It reads
|
|
4
|
+
* only the saved event history — no transport, browser or dashboard module — so status stays fast.
|
|
5
|
+
*/
|
|
6
|
+
import type { RelayEventV2 } from "./audit.js";
|
|
7
|
+
export declare const WINDOWS: {
|
|
8
|
+
readonly "1h": 3600000;
|
|
9
|
+
readonly "24h": 86400000;
|
|
10
|
+
readonly "7d": number;
|
|
11
|
+
};
|
|
12
|
+
export type StatusWindow = keyof typeof WINDOWS;
|
|
13
|
+
export interface StatusHead {
|
|
14
|
+
state: "running" | "stopped";
|
|
15
|
+
startedAt?: string;
|
|
16
|
+
link?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface StatusData {
|
|
19
|
+
window: StatusWindow;
|
|
20
|
+
state: StatusHead["state"];
|
|
21
|
+
uptimeMs: number | null;
|
|
22
|
+
link: string | null;
|
|
23
|
+
requests: number;
|
|
24
|
+
okPct: number | null;
|
|
25
|
+
bytes: number | null;
|
|
26
|
+
pods: {
|
|
27
|
+
name: string;
|
|
28
|
+
requests: number;
|
|
29
|
+
okPct: number;
|
|
30
|
+
lastAt: string;
|
|
31
|
+
}[];
|
|
32
|
+
sites: {
|
|
33
|
+
host: string;
|
|
34
|
+
requests: number;
|
|
35
|
+
okPct: number;
|
|
36
|
+
lastError: string | null;
|
|
37
|
+
lastErrorAt: string | null;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
export declare function statusData(events: RelayEventV2[], head: StatusHead, window: StatusWindow, now?: number): StatusData;
|
|
41
|
+
export declare function ago(ms: number): string;
|
|
42
|
+
export declare function renderStatus(d: StatusData, opts?: {
|
|
43
|
+
limit: number;
|
|
44
|
+
now?: number;
|
|
45
|
+
}): string;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { c } from "./colors.js";
|
|
2
|
+
export const WINDOWS = { "1h": 3_600_000, "24h": 86_400_000, "7d": 7 * 86_400_000 };
|
|
3
|
+
const ERROR_WORD = {
|
|
4
|
+
"site-refused": "refused",
|
|
5
|
+
"owner-blocked": "blocked by you",
|
|
6
|
+
"safety-blocked": "blocked (safety)",
|
|
7
|
+
"rate-limited": "rate limited",
|
|
8
|
+
"network-error": "network error",
|
|
9
|
+
};
|
|
10
|
+
function errorText(e) {
|
|
11
|
+
return [e.httpStatus, ERROR_WORD[e.outcome] ?? e.outcome].filter(Boolean).join(" ");
|
|
12
|
+
}
|
|
13
|
+
function podName(e) {
|
|
14
|
+
if (!e.source)
|
|
15
|
+
return "(unknown)";
|
|
16
|
+
if ("system" in e.source)
|
|
17
|
+
return null; // the relay's own health checks are not a pod
|
|
18
|
+
return e.source.podName || e.source.podId;
|
|
19
|
+
}
|
|
20
|
+
function group(events, key) {
|
|
21
|
+
const by = new Map();
|
|
22
|
+
for (const e of events) {
|
|
23
|
+
const k = key(e);
|
|
24
|
+
if (!k)
|
|
25
|
+
continue;
|
|
26
|
+
const t = Date.parse(e.startedAt);
|
|
27
|
+
const r = by.get(k) ?? { name: k, req: 0, ok: 0, last: 0 };
|
|
28
|
+
r.req++;
|
|
29
|
+
if (e.outcome === "ok")
|
|
30
|
+
r.ok++;
|
|
31
|
+
else if (!r.lastError || t > Date.parse(r.lastError.startedAt))
|
|
32
|
+
r.lastError = e;
|
|
33
|
+
r.last = Math.max(r.last, t);
|
|
34
|
+
by.set(k, r);
|
|
35
|
+
}
|
|
36
|
+
return [...by.values()].sort((a, b) => b.req - a.req || b.last - a.last);
|
|
37
|
+
}
|
|
38
|
+
const pct = (ok, n) => (n ? Math.round((ok / n) * 100) : 0);
|
|
39
|
+
export function statusData(events, head, window, now = Date.now()) {
|
|
40
|
+
const inWindow = events.filter((e) => Date.parse(e.startedAt) >= now - WINDOWS[window]);
|
|
41
|
+
const counted = inWindow.filter((e) => podName(e) !== null);
|
|
42
|
+
const ok = counted.filter((e) => e.outcome === "ok").length;
|
|
43
|
+
return {
|
|
44
|
+
window,
|
|
45
|
+
state: head.state,
|
|
46
|
+
uptimeMs: head.state === "running" && head.startedAt ? Math.max(0, now - Date.parse(head.startedAt)) : null,
|
|
47
|
+
link: head.state === "running" ? (head.link ?? "unknown") : null,
|
|
48
|
+
requests: counted.length,
|
|
49
|
+
okPct: counted.length ? pct(ok, counted.length) : null,
|
|
50
|
+
bytes: counted.some((e) => e.mode === "tunnel") ? counted.filter((e) => e.mode === "tunnel").reduce((s, e) => s + (e.bytesUp ?? 0) + (e.bytesDown ?? 0), 0) : null,
|
|
51
|
+
pods: group(counted, podName).map((r) => ({ name: r.name, requests: r.req, okPct: pct(r.ok, r.req), lastAt: new Date(r.last).toISOString() })),
|
|
52
|
+
sites: group(counted, (e) => e.host).map((r) => ({
|
|
53
|
+
host: r.name, requests: r.req, okPct: pct(r.ok, r.req),
|
|
54
|
+
lastError: r.lastError ? errorText(r.lastError) : null,
|
|
55
|
+
lastErrorAt: r.lastError ? r.lastError.startedAt : null,
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function ago(ms) {
|
|
60
|
+
const s = Math.max(0, Math.round(ms / 1000));
|
|
61
|
+
if (s < 60)
|
|
62
|
+
return `${s} s ago`;
|
|
63
|
+
if (s < 3600)
|
|
64
|
+
return `${Math.round(s / 60)} min ago`;
|
|
65
|
+
if (s < 86_400)
|
|
66
|
+
return `${Math.round(s / 3600)} h ago`;
|
|
67
|
+
return `${Math.round(s / 86_400)} d ago`;
|
|
68
|
+
}
|
|
69
|
+
function duration(ms) {
|
|
70
|
+
const m = Math.floor(ms / 60_000), h = Math.floor(m / 60), d = Math.floor(h / 24);
|
|
71
|
+
return d ? `${d}d ${h % 24}h` : h ? `${h}h ${m % 60}m` : `${m}m`;
|
|
72
|
+
}
|
|
73
|
+
function size(b) {
|
|
74
|
+
if (b < 1024)
|
|
75
|
+
return `${b} B`;
|
|
76
|
+
if (b < 1024 ** 2)
|
|
77
|
+
return `${Math.round(b / 1024)} KB`;
|
|
78
|
+
if (b < 1024 ** 3)
|
|
79
|
+
return `${Math.round(b / 1024 ** 2)} MB`;
|
|
80
|
+
return `${(b / 1024 ** 3).toFixed(1)} GB`;
|
|
81
|
+
}
|
|
82
|
+
const cut = (s, w) => (s.length > w ? s.slice(0, w - 1) + "…" : s);
|
|
83
|
+
/** Columns padded to fixed widths that fit 80 columns; numbers right-aligned. */
|
|
84
|
+
function table(head, widths, right, body) {
|
|
85
|
+
const line = (cells) => cells.map((v, i) => (i === cells.length - 1 ? v : right[i] ? v.padStart(widths[i]) : cut(v, widths[i]).padEnd(widths[i]))).join(" ").trimEnd();
|
|
86
|
+
return [c.dim(line(head)), ...body.map(line)];
|
|
87
|
+
}
|
|
88
|
+
export function renderStatus(d, opts = { limit: 10 }) {
|
|
89
|
+
const now = opts.now ?? Date.now();
|
|
90
|
+
const out = [""];
|
|
91
|
+
const stateWord = d.state === "running" ? c.green("running") : c.yellow("stopped");
|
|
92
|
+
const head = table(["STATE", "UPTIME", "LINK", `REQ ${d.window}`, "OK%", "DATA"], [8, 8, 12, 8, 4, 0], [false, false, false, true, true, false], [[d.state, d.uptimeMs === null ? "—" : duration(d.uptimeMs), d.link ?? "—", String(d.requests), d.okPct === null ? "—" : `${d.okPct}%`, d.bytes === null ? "—" : size(d.bytes)]]);
|
|
93
|
+
head[1] = head[1].replace(/^(running|stopped)/, stateWord);
|
|
94
|
+
out.push(...head);
|
|
95
|
+
if (!d.requests) {
|
|
96
|
+
out.push("", c.dim(`no pod requests in the last ${d.window}`));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out.push("");
|
|
100
|
+
out.push(...table(["POD", "REQ", "OK%", "LAST"], [24, 6, 4, 0], [false, true, true, false], d.pods.slice(0, opts.limit).map((p) => [p.name, String(p.requests), `${p.okPct}%`, ago(now - Date.parse(p.lastAt))])));
|
|
101
|
+
out.push("");
|
|
102
|
+
out.push(...table(["SITE", "REQ", "OK%", "LAST ERROR"], [24, 6, 4, 0], [false, true, true, false], d.sites.slice(0, opts.limit).map((s) => [s.host, String(s.requests), `${s.okPct}%`,
|
|
103
|
+
s.lastError ? `${s.lastError}, ${ago(now - Date.parse(s.lastErrorAt))}` : "—"])));
|
|
104
|
+
const hidden = Math.max(0, d.pods.length - opts.limit) + Math.max(0, d.sites.length - opts.limit);
|
|
105
|
+
if (hidden)
|
|
106
|
+
out.push("", c.dim(`${hidden} more rows — relay status --all`));
|
|
107
|
+
}
|
|
108
|
+
return out.join("\n");
|
|
109
|
+
}
|
package/dist/stop.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop a relay daemon and WAIT until it is gone, but never for long: SIGTERM, poll for up to
|
|
3
|
+
* `graceMs`, then SIGKILL. A boot script runs `relay stop` before `relay start`, so returning
|
|
4
|
+
* while the old daemon still holds the dashboard port or the browser profile would race the new
|
|
5
|
+
* one — and a daemon stuck in a slow shutdown (a swapping NAS) must not hang the script.
|
|
6
|
+
*/
|
|
7
|
+
export declare function stopPid(pid: number, opts?: {
|
|
8
|
+
graceMs?: number;
|
|
9
|
+
pollMs?: number;
|
|
10
|
+
files?: string[];
|
|
11
|
+
}): Promise<"not-running" | "stopped" | "killed">;
|
package/dist/stop.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
const alive = (pid) => {
|
|
3
|
+
try {
|
|
4
|
+
process.kill(pid, 0);
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Stop a relay daemon and WAIT until it is gone, but never for long: SIGTERM, poll for up to
|
|
13
|
+
* `graceMs`, then SIGKILL. A boot script runs `relay stop` before `relay start`, so returning
|
|
14
|
+
* while the old daemon still holds the dashboard port or the browser profile would race the new
|
|
15
|
+
* one — and a daemon stuck in a slow shutdown (a swapping NAS) must not hang the script.
|
|
16
|
+
*/
|
|
17
|
+
export async function stopPid(pid, opts = {}) {
|
|
18
|
+
const { graceMs = 5000, pollMs = 100, files = [] } = opts;
|
|
19
|
+
const clean = () => { for (const f of files)
|
|
20
|
+
try {
|
|
21
|
+
unlinkSync(f);
|
|
22
|
+
}
|
|
23
|
+
catch { /* gone */ } };
|
|
24
|
+
if (!alive(pid)) {
|
|
25
|
+
clean();
|
|
26
|
+
return "not-running";
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, "SIGTERM");
|
|
30
|
+
}
|
|
31
|
+
catch { /* exited meanwhile */ }
|
|
32
|
+
const deadline = Date.now() + graceMs;
|
|
33
|
+
while (Date.now() < deadline) {
|
|
34
|
+
if (!alive(pid)) {
|
|
35
|
+
clean();
|
|
36
|
+
return "stopped";
|
|
37
|
+
}
|
|
38
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, "SIGKILL");
|
|
42
|
+
}
|
|
43
|
+
catch { /* exited at the last moment */ }
|
|
44
|
+
clean();
|
|
45
|
+
return "killed";
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@podway/relay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Podway relay \u2014 runs a fetch relay on your machine so a pod can read pages from your network, using sessions you sign into. Install: npm i -g @podway/relay, then `relay start`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|