@orb44/cli 0.1.5 → 0.1.7
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/README.md +4 -4
- package/bin/orb44.mjs +38 -34
- package/package.json +2 -1
- package/server/pulse.mjs +353 -20
- package/server/sat-http.mjs +65 -0
- package/server/sat-i18n.mjs +16 -0
- package/server/sat-update.mjs +19 -0
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ npx @orb44/cli login --url https://orb44.com
|
|
|
13
13
|
Open the printed link in a browser where you are already signed in, confirm the domain, then optionally install the systemd daemon (default no). After a global install the command is `orb44`.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @orb44/cli@0.1.
|
|
17
|
-
npx @orb44/cli@0.1.
|
|
18
|
-
npx @orb44/cli@0.1.
|
|
16
|
+
npx @orb44/cli@0.1.7 status
|
|
17
|
+
npx @orb44/cli@0.1.7 pulse
|
|
18
|
+
npx @orb44/cli@0.1.7 install # later: background pulse, starts after reboot
|
|
19
19
|
orb44 update # after install: replace /usr/lib/orb44-sat from npm
|
|
20
|
-
npx @orb44/cli@0.1.
|
|
20
|
+
npx @orb44/cli@0.1.7 logout
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Do not run bare `npx @orb44/cli` — it can reuse an old cache. Pin the version or use `orb44` after install.
|
package/bin/orb44.mjs
CHANGED
|
@@ -9,7 +9,8 @@ import { collectPulse, formatPulsePreview } from "../server/pulse.mjs";
|
|
|
9
9
|
import { LANGS, LANG_LABEL, detectLang, normalizeLang, t } from "../server/sat-i18n.mjs";
|
|
10
10
|
import { pickFromList } from "../server/cli-menu.mjs";
|
|
11
11
|
import { SYSTEM_DEVICE, parseDeviceJson, hasExistingInstall, pickLiveDevice, deviceSearchPaths, loadFirstDevice, stripDevicePath } from "../server/sat-local.mjs";
|
|
12
|
-
import { CLI_PACKAGE, cmpVer, readCliVersion, pickSatRoots, applyUpdateTree, fetchLatestMeta, unpackTarball } from "../server/sat-update.mjs";
|
|
12
|
+
import { CLI_PACKAGE, cmpVer, readCliVersion, pickSatRoots, staleSatRoots, applyUpdateTree, fetchLatestMeta, unpackTarball } from "../server/sat-update.mjs";
|
|
13
|
+
import { cabinetRequest, apiFailText } from "../server/sat-http.mjs";
|
|
13
14
|
|
|
14
15
|
const DEVICE_FILE = process.env.ORB44_DEVICE_FILE || path.join(os.homedir(), ".config", "orb44", "device.json");
|
|
15
16
|
const CLI_FILE = process.env.ORB44_CLI_FILE || path.join(path.dirname(DEVICE_FILE), "cli.json");
|
|
@@ -82,16 +83,12 @@ function clearDevice() {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
body: json ? JSON.stringify(json) : undefined,
|
|
92
|
-
});
|
|
93
|
-
const body = await r.json().catch(() => ({}));
|
|
94
|
-
return { ok: r.ok, status: r.status, body };
|
|
86
|
+
function api(url, pathname, opts) {
|
|
87
|
+
return cabinetRequest(url, pathname, opts);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function failLine(out, fallbackKey = "pulse_fail") {
|
|
91
|
+
return "⚠️ " + apiFailText(lang, out, t, fallbackKey);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
function dim(s) {
|
|
@@ -125,7 +122,7 @@ async function ensureLang(opts) {
|
|
|
125
122
|
return lang;
|
|
126
123
|
}
|
|
127
124
|
lang = detectLang();
|
|
128
|
-
if (opts.yes || !input.isTTY || !output.isTTY) {
|
|
125
|
+
if (opts.yes || opts.url || !input.isTTY || !output.isTTY) {
|
|
129
126
|
saveCli({ ...loadCli(), lang });
|
|
130
127
|
return lang;
|
|
131
128
|
}
|
|
@@ -245,9 +242,10 @@ async function revokeLocalDevices(devices, fallbackApi) {
|
|
|
245
242
|
}
|
|
246
243
|
|
|
247
244
|
async function cmdLogin(opts) {
|
|
245
|
+
const url = String(opts.url || process.env.ORB44_API || "http://127.0.0.1:8787").replace(/\/$/, "");
|
|
246
|
+
process.stdout.write(`orb44 login → ${url}\n`);
|
|
248
247
|
await ensureLang(opts);
|
|
249
248
|
banner();
|
|
250
|
-
const url = String(opts.url || process.env.ORB44_API || "http://127.0.0.1:8787").replace(/\/$/, "");
|
|
251
249
|
const found = existingOnBox();
|
|
252
250
|
if (hasExistingInstall(found)) {
|
|
253
251
|
const replace = await wantReplace(found, opts);
|
|
@@ -259,7 +257,7 @@ async function cmdLogin(opts) {
|
|
|
259
257
|
}
|
|
260
258
|
const { out } = await sendPulse(live);
|
|
261
259
|
if (!out.ok) {
|
|
262
|
-
console.error(
|
|
260
|
+
console.error(failLine(out));
|
|
263
261
|
process.exit(1);
|
|
264
262
|
}
|
|
265
263
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -276,7 +274,7 @@ async function cmdLogin(opts) {
|
|
|
276
274
|
json: { hostname, name },
|
|
277
275
|
});
|
|
278
276
|
if (!begin.ok || !begin.body.pollToken) {
|
|
279
|
-
console.error(
|
|
277
|
+
console.error(failLine(begin, "pair_fail"));
|
|
280
278
|
process.exit(1);
|
|
281
279
|
}
|
|
282
280
|
console.log(`🔗 API ${url}`);
|
|
@@ -320,7 +318,7 @@ async function cmdLogin(opts) {
|
|
|
320
318
|
|
|
321
319
|
const { out } = await sendPulse(device);
|
|
322
320
|
if (!out.ok) {
|
|
323
|
-
console.error(
|
|
321
|
+
console.error(failLine(out));
|
|
324
322
|
process.exit(1);
|
|
325
323
|
}
|
|
326
324
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -350,10 +348,7 @@ async function cmdPulse() {
|
|
|
350
348
|
banner();
|
|
351
349
|
const { out } = await sendPulse(device);
|
|
352
350
|
if (!out.ok) {
|
|
353
|
-
console.error(
|
|
354
|
-
"⚠️ " +
|
|
355
|
-
(out.body.error === "bad_secret" || out.body.error === "not_found" ? t(lang, "key_revoked") : out.body.error || t(lang, "pulse_fail"))
|
|
356
|
-
);
|
|
351
|
+
console.error(failLine(out));
|
|
357
352
|
process.exit(1);
|
|
358
353
|
}
|
|
359
354
|
console.log("\n📡 " + t(lang, "pulse_ok"));
|
|
@@ -370,14 +365,17 @@ async function cmdDaemon(opts) {
|
|
|
370
365
|
const sec = intervalSec(opts.interval);
|
|
371
366
|
console.log(`🛰️ ${t(lang, "daemon_run", { sec, host: device.host })}`);
|
|
372
367
|
const tick = async () => {
|
|
373
|
-
const { out, pulse } = await sendPulse(device, { preview: false });
|
|
374
368
|
const hh = new Date().toISOString().slice(11, 19);
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
369
|
+
try {
|
|
370
|
+
const { out, pulse } = await sendPulse(device, { preview: false });
|
|
371
|
+
if (!out.ok) {
|
|
372
|
+
console.error(`⚠️ ${hh} ${apiFailText(lang, out, t)}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
console.log(`📡 ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error(`⚠️ ${hh} ${String(e?.message || e).split("\n")[0].slice(0, 200)}`);
|
|
379
378
|
}
|
|
380
|
-
console.log(`📡 ${hh} ok load ${pulse.load1} ${pulse.gradeInside || "—"}`);
|
|
381
379
|
};
|
|
382
380
|
await tick();
|
|
383
381
|
const id = setInterval(tick, sec * 1000);
|
|
@@ -646,23 +644,24 @@ async function cmdUpdate() {
|
|
|
646
644
|
process.exit(1);
|
|
647
645
|
}
|
|
648
646
|
const pin = t(lang, "update_npx", { version: meta.version });
|
|
649
|
-
if (cmpVer(current, meta.version) >= 0 && !opts.force) {
|
|
650
|
-
console.log("✅ " + t(lang, "update_same", { version: current }));
|
|
651
|
-
console.log(dim(pin));
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
647
|
const roots = pickSatRoots(SCRIPT);
|
|
648
|
+
const stale = staleSatRoots(roots, meta.version, { force: Boolean(opts.force) });
|
|
655
649
|
if (!roots.length) {
|
|
656
650
|
console.error("⚠️ " + t(lang, "update_no_tree"));
|
|
657
651
|
console.log(dim(pin));
|
|
658
652
|
process.exit(1);
|
|
659
653
|
}
|
|
654
|
+
if (!stale.length) {
|
|
655
|
+
console.log("✅ " + t(lang, "update_same", { version: current || meta.version }));
|
|
656
|
+
console.log(dim(pin));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
660
659
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "orb44-upd-"));
|
|
661
660
|
let wrote = [];
|
|
662
661
|
try {
|
|
663
662
|
const pkg = await unpackTarball(meta.tarball, tmp);
|
|
664
663
|
const blocked = [];
|
|
665
|
-
for (const root of
|
|
664
|
+
for (const root of stale) {
|
|
666
665
|
try {
|
|
667
666
|
applyUpdateTree(pkg, root);
|
|
668
667
|
wrote.push(root);
|
|
@@ -713,7 +712,7 @@ async function cmdLogout() {
|
|
|
713
712
|
applyLang(opts);
|
|
714
713
|
const device = loadDevice();
|
|
715
714
|
if (device?.secret) {
|
|
716
|
-
await api(device.api, "/api/satellites/logout", { method: "POST", json: { secret: device.secret } });
|
|
715
|
+
await api(device.api, "/api/satellites/logout", { method: "POST", json: { secret: device.secret } }).catch(() => {});
|
|
717
716
|
}
|
|
718
717
|
clearDevice();
|
|
719
718
|
console.log("✅ " + t(lang, "logged_out"));
|
|
@@ -769,4 +768,9 @@ if (!fn) {
|
|
|
769
768
|
help();
|
|
770
769
|
process.exit(1);
|
|
771
770
|
}
|
|
772
|
-
|
|
771
|
+
try {
|
|
772
|
+
await fn();
|
|
773
|
+
} catch (e) {
|
|
774
|
+
console.error("⚠️ " + String(e?.message || e).split("\n")[0].slice(0, 200));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orb44/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Orb44: street snapshot of your site. This CLI is the Watch satellite — pulse from the host (load, listeners, hardening). The cabinet does not run commands on the machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"server/sat-i18n.mjs",
|
|
13
13
|
"server/cli-menu.mjs",
|
|
14
14
|
"server/sat-local.mjs",
|
|
15
|
+
"server/sat-http.mjs",
|
|
15
16
|
"server/sat-update.mjs"
|
|
16
17
|
],
|
|
17
18
|
"engines": {
|
package/server/pulse.mjs
CHANGED
|
@@ -78,17 +78,26 @@ function run(cmd, args) {
|
|
|
78
78
|
|
|
79
79
|
const SKIP_TOP = new Set(["ps", "ss", "lsof"]);
|
|
80
80
|
|
|
81
|
+
const STAT_RE = /^[DRSTZIX][A-Za-z+<]*$/;
|
|
82
|
+
|
|
81
83
|
export function parsePsTop(text) {
|
|
82
84
|
const rows = [];
|
|
83
85
|
for (const line of String(text || "").split("\n")) {
|
|
84
|
-
const m = line.trim().match(/^(\d+)\s+([\d.]+)\s+(\d+)\s+(\S+)
|
|
86
|
+
const m = line.trim().match(/^(\d+)\s+([\d.]+)\s+(\d+)\s+(\S+)(?:\s+(\S+))?$/);
|
|
85
87
|
if (!m) continue;
|
|
86
|
-
|
|
88
|
+
let stat = null;
|
|
89
|
+
let commRaw = m[4];
|
|
90
|
+
if (m[5] && STAT_RE.test(m[4]) && m[4].length <= 8) {
|
|
91
|
+
stat = m[4].slice(0, 4);
|
|
92
|
+
commRaw = m[5];
|
|
93
|
+
}
|
|
94
|
+
const comm = sanitizeComm(commRaw);
|
|
87
95
|
if (!comm || SKIP_TOP.has(comm)) continue;
|
|
88
96
|
rows.push({
|
|
89
97
|
comm,
|
|
90
98
|
cpuPct: Math.round(Number(m[2]) * 10) / 10,
|
|
91
99
|
rssMb: Math.round(Number(m[3]) / 1024),
|
|
100
|
+
stat,
|
|
92
101
|
});
|
|
93
102
|
}
|
|
94
103
|
rows.sort((a, b) => b.cpuPct - a.cpuPct || b.rssMb - a.rssMb);
|
|
@@ -168,8 +177,10 @@ function listenTable() {
|
|
|
168
177
|
}
|
|
169
178
|
|
|
170
179
|
function topTable() {
|
|
171
|
-
const
|
|
172
|
-
|
|
180
|
+
const withStat = run("ps", ["-axo", "pid=,pcpu=,rss=,stat=,comm="]);
|
|
181
|
+
const parsed = parsePsTop(withStat);
|
|
182
|
+
if (parsed.length) return parsed;
|
|
183
|
+
return parsePsTop(run("ps", ["-axo", "pid=,pcpu=,rss=,comm="]));
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
function okListenAddr(a) {
|
|
@@ -180,23 +191,29 @@ function okListenAddr(a) {
|
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
export function collapseListen(rows = []) {
|
|
183
|
-
const
|
|
194
|
+
const world = [];
|
|
195
|
+
const rest = [];
|
|
184
196
|
for (const r of rows) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
197
|
+
if (r.addr === "0.0.0.0" || r.addr === "::" || r.addr === "*") world.push(r);
|
|
198
|
+
else rest.push(r);
|
|
199
|
+
}
|
|
200
|
+
const byPort = new Map();
|
|
201
|
+
for (const r of world) {
|
|
202
|
+
const port = Number(r.port);
|
|
203
|
+
if (!byPort.has(port)) byPort.set(port, []);
|
|
204
|
+
byPort.get(port).push(r);
|
|
188
205
|
}
|
|
189
206
|
const out = [];
|
|
190
|
-
for (const list of
|
|
207
|
+
for (const list of byPort.values()) {
|
|
191
208
|
const addrs = new Set(list.map((x) => x.addr));
|
|
192
|
-
const
|
|
209
|
+
const comm = list.find((x) => x.comm)?.comm || list[0].comm;
|
|
193
210
|
if (addrs.has("*") || (addrs.has("0.0.0.0") && addrs.has("::"))) {
|
|
194
|
-
out.push({ addr: "*", port: list[0].port, comm
|
|
195
|
-
out.push(...rest);
|
|
211
|
+
out.push({ addr: "*", port: list[0].port, comm });
|
|
196
212
|
} else {
|
|
197
213
|
out.push(...list);
|
|
198
214
|
}
|
|
199
215
|
}
|
|
216
|
+
out.push(...rest);
|
|
200
217
|
return out;
|
|
201
218
|
}
|
|
202
219
|
|
|
@@ -208,6 +225,7 @@ export function sanitizePulse(raw = {}) {
|
|
|
208
225
|
comm: sanitizeComm(r.comm),
|
|
209
226
|
cpuPct: Math.max(0, Math.min(100, Number(r.cpuPct) || 0)),
|
|
210
227
|
rssMb: Math.max(0, Math.round(Number(r.rssMb) || 0)),
|
|
228
|
+
stat: STAT_RE.test(String(r.stat || "")) ? String(r.stat).slice(0, 4) : null,
|
|
211
229
|
}))
|
|
212
230
|
.filter((r) => r.comm)
|
|
213
231
|
: [];
|
|
@@ -247,6 +265,7 @@ export function sanitizePulse(raw = {}) {
|
|
|
247
265
|
};
|
|
248
266
|
const limited = Boolean(raw.limited);
|
|
249
267
|
const oom = Boolean(raw.oom) || hardening.oomKills > 0;
|
|
268
|
+
const pressure = sanitizePressure(raw.pressure || raw);
|
|
250
269
|
const base = {
|
|
251
270
|
ts: Number(raw.ts) || Date.now(),
|
|
252
271
|
hostname: sanitizeComm(raw.hostname) || os.hostname().slice(0, 40),
|
|
@@ -261,6 +280,7 @@ export function sanitizePulse(raw = {}) {
|
|
|
261
280
|
oom,
|
|
262
281
|
limited,
|
|
263
282
|
hardening,
|
|
283
|
+
pressure,
|
|
264
284
|
};
|
|
265
285
|
return { ...base, gradeInside: gradeInside(base) };
|
|
266
286
|
}
|
|
@@ -345,6 +365,172 @@ export function parseVmstatOom(text) {
|
|
|
345
365
|
return m ? Number(m[1]) : 0;
|
|
346
366
|
}
|
|
347
367
|
|
|
368
|
+
export function parseSockstat(text) {
|
|
369
|
+
const tcp = /(?:^|\n)TCP:\s+inuse\s+(\d+)(?:\s+orphan\s+(\d+))?(?:\s+tw\s+(\d+))?/i.exec(String(text || ""));
|
|
370
|
+
if (!tcp) return { tcpInuse: null, tcpOrphan: null, tcpTw: null };
|
|
371
|
+
return {
|
|
372
|
+
tcpInuse: Number(tcp[1]),
|
|
373
|
+
tcpOrphan: tcp[2] != null ? Number(tcp[2]) : null,
|
|
374
|
+
tcpTw: tcp[3] != null ? Number(tcp[3]) : null,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function parseFileNr(text) {
|
|
379
|
+
const p = String(text || "").trim().split(/[\s,]+/);
|
|
380
|
+
const used = Number(p[0]);
|
|
381
|
+
const max = Number(p[2] ?? p[1]);
|
|
382
|
+
if (!Number.isFinite(used) || !Number.isFinite(max) || max <= 0) return { fileUsed: null, fileMax: null, filePct: null };
|
|
383
|
+
return { fileUsed: used, fileMax: max, filePct: Math.max(0, Math.min(100, Math.round((used / max) * 100))) };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function parseChronyTracking(text) {
|
|
387
|
+
const m =
|
|
388
|
+
/Last offset\s*:\s*([+-]?\d+(?:\.\d+)?)\s*seconds/i.exec(String(text || "")) ||
|
|
389
|
+
/System time\s*:\s*(\d+(?:\.\d+)?)\s*seconds\s+(slow|fast)/i.exec(String(text || ""));
|
|
390
|
+
if (!m) return null;
|
|
391
|
+
let n = Number(m[1]);
|
|
392
|
+
if (!Number.isFinite(n)) return null;
|
|
393
|
+
if (m[2] === "fast") n = Math.abs(n);
|
|
394
|
+
if (m[2] === "slow") n = -Math.abs(n);
|
|
395
|
+
return Math.round(n * 1000) / 1000;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function parseCgroupEvents(text) {
|
|
399
|
+
const kill = /(?:^|\n)oom_kill\s+(\d+)/.exec(String(text || ""));
|
|
400
|
+
const oom = /(?:^|\n)oom\s+(\d+)/.exec(String(text || ""));
|
|
401
|
+
return {
|
|
402
|
+
cgroupOom: kill ? Number(kill[1]) : oom ? Number(oom[1]) : 0,
|
|
403
|
+
memFailcnt: 0,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function parseNetstatListenOverflows(text) {
|
|
408
|
+
const lines = String(text || "").split("\n");
|
|
409
|
+
let header = "";
|
|
410
|
+
let values = "";
|
|
411
|
+
for (const line of lines) {
|
|
412
|
+
if (/^TcpExt:\s+/.test(line) && /ListenOverflows/.test(line)) header = line;
|
|
413
|
+
else if (/^TcpExt:\s+/.test(line) && header && !/ListenOverflows/.test(line)) values = line;
|
|
414
|
+
}
|
|
415
|
+
if (!header || !values) return { listenOverflows: null, listenDrops: null };
|
|
416
|
+
const keys = header.replace(/^TcpExt:\s+/, "").trim().split(/\s+/);
|
|
417
|
+
const nums = values.replace(/^TcpExt:\s+/, "").trim().split(/\s+/).map(Number);
|
|
418
|
+
const idx = (name) => keys.indexOf(name);
|
|
419
|
+
const n = (i) => (i >= 0 && Number.isFinite(nums[i]) ? nums[i] : null);
|
|
420
|
+
return { listenOverflows: n(idx("ListenOverflows")), listenDrops: n(idx("ListenDrops")) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function readTrim(p) {
|
|
424
|
+
try {
|
|
425
|
+
return fs.readFileSync(p, "utf8").trim();
|
|
426
|
+
} catch {
|
|
427
|
+
return "";
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function numFile(p) {
|
|
432
|
+
const n = Number(readTrim(p));
|
|
433
|
+
return Number.isFinite(n) ? n : null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function stuckFromTop(top = []) {
|
|
437
|
+
return (top || []).filter((r) => {
|
|
438
|
+
const s = String(r.stat || "")[0];
|
|
439
|
+
return s === "D" || s === "Z" || s === "T";
|
|
440
|
+
}).slice(0, 6);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function hotFromTop(top = []) {
|
|
444
|
+
return (top || []).filter((r) => Number(r.cpuPct) >= 70 || Number(r.rssMb) >= 1024).slice(0, 6);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function cpuCount(pulse) {
|
|
448
|
+
const n = Number(pulse?.pressure?.cpus);
|
|
449
|
+
return Number.isFinite(n) && n > 0 ? n : 2;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function collectPressure() {
|
|
453
|
+
const cpus = Math.max(1, os.cpus()?.length || 1);
|
|
454
|
+
const sock = parseSockstat(readTrim("/proc/net/sockstat"));
|
|
455
|
+
const files = parseFileNr(readTrim("/proc/sys/fs/file-nr"));
|
|
456
|
+
let conntrackUsed = numFile("/proc/sys/net/netfilter/nf_conntrack_count");
|
|
457
|
+
let conntrackMax = numFile("/proc/sys/net/netfilter/nf_conntrack_max");
|
|
458
|
+
if (conntrackUsed == null) conntrackUsed = numFile("/proc/sys/net/nf_conntrack_count");
|
|
459
|
+
if (conntrackMax == null) conntrackMax = numFile("/proc/sys/net/nf_conntrack_max");
|
|
460
|
+
const conntrackPct =
|
|
461
|
+
conntrackUsed != null && conntrackMax > 0 ? Math.max(0, Math.min(100, Math.round((conntrackUsed / conntrackMax) * 100))) : null;
|
|
462
|
+
const overflows = parseNetstatListenOverflows(readTrim("/proc/net/netstat"));
|
|
463
|
+
let clockOffsetSec = parseChronyTracking(runOut("chronyc", ["tracking"]));
|
|
464
|
+
let ntpSync = null;
|
|
465
|
+
if (clockOffsetSec != null) ntpSync = true;
|
|
466
|
+
else {
|
|
467
|
+
const td = runOut("timedatectl", ["show", "-p", "NTPSynchronized", "--value"]);
|
|
468
|
+
if (/^yes$/i.test(td.trim())) ntpSync = true;
|
|
469
|
+
else if (/^no$/i.test(td.trim())) ntpSync = false;
|
|
470
|
+
}
|
|
471
|
+
let cgroupOom = 0;
|
|
472
|
+
let memFailcnt = 0;
|
|
473
|
+
const ev = parseCgroupEvents(readTrim("/sys/fs/cgroup/memory.events") || readTrim("/sys/fs/cgroup/memory/memory.events"));
|
|
474
|
+
cgroupOom = ev.cgroupOom || 0;
|
|
475
|
+
memFailcnt = numFile("/sys/fs/cgroup/memory/memory.failcnt") ?? ev.memFailcnt ?? 0;
|
|
476
|
+
return {
|
|
477
|
+
cpus,
|
|
478
|
+
conntrackUsed,
|
|
479
|
+
conntrackMax,
|
|
480
|
+
conntrackPct,
|
|
481
|
+
tcpInuse: sock.tcpInuse,
|
|
482
|
+
tcpTw: sock.tcpTw,
|
|
483
|
+
tcpOrphan: sock.tcpOrphan,
|
|
484
|
+
listenOverflows: overflows.listenOverflows,
|
|
485
|
+
listenDrops: overflows.listenDrops,
|
|
486
|
+
fileUsed: files.fileUsed,
|
|
487
|
+
fileMax: files.fileMax,
|
|
488
|
+
filePct: files.filePct,
|
|
489
|
+
clockOffsetSec,
|
|
490
|
+
ntpSync,
|
|
491
|
+
memFailcnt: Math.max(0, Math.min(999999, Number(memFailcnt) || 0)),
|
|
492
|
+
cgroupOom: Math.max(0, Math.min(999, Number(cgroupOom) || 0)),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function sanitizePressure(raw = {}) {
|
|
497
|
+
const n = (v, lo, hi) => {
|
|
498
|
+
if (v == null || v === "") return null;
|
|
499
|
+
const x = Number(v);
|
|
500
|
+
if (!Number.isFinite(x)) return null;
|
|
501
|
+
return Math.max(lo, Math.min(hi, x));
|
|
502
|
+
};
|
|
503
|
+
const cpus = n(raw.cpus, 1, 512) || null;
|
|
504
|
+
const conntrackUsed = n(raw.conntrackUsed, 0, 1e9);
|
|
505
|
+
const conntrackMax = n(raw.conntrackMax, 0, 1e9);
|
|
506
|
+
const conntrackPct =
|
|
507
|
+
raw.conntrackPct != null
|
|
508
|
+
? n(raw.conntrackPct, 0, 100)
|
|
509
|
+
: conntrackUsed != null && conntrackMax > 0
|
|
510
|
+
? Math.max(0, Math.min(100, Math.round((conntrackUsed / conntrackMax) * 100)))
|
|
511
|
+
: null;
|
|
512
|
+
const clockOffsetSec = n(raw.clockOffsetSec, -86400, 86400);
|
|
513
|
+
const ntpSync = raw.ntpSync === true || raw.ntpSync === false ? raw.ntpSync : null;
|
|
514
|
+
return {
|
|
515
|
+
cpus,
|
|
516
|
+
conntrackUsed,
|
|
517
|
+
conntrackMax,
|
|
518
|
+
conntrackPct,
|
|
519
|
+
tcpInuse: n(raw.tcpInuse, 0, 1e9),
|
|
520
|
+
tcpTw: n(raw.tcpTw, 0, 1e9),
|
|
521
|
+
tcpOrphan: n(raw.tcpOrphan, 0, 1e9),
|
|
522
|
+
listenOverflows: n(raw.listenOverflows, 0, 1e12),
|
|
523
|
+
listenDrops: n(raw.listenDrops, 0, 1e12),
|
|
524
|
+
fileUsed: n(raw.fileUsed, 0, 1e12),
|
|
525
|
+
fileMax: n(raw.fileMax, 0, 1e12),
|
|
526
|
+
filePct: n(raw.filePct, 0, 100),
|
|
527
|
+
clockOffsetSec,
|
|
528
|
+
ntpSync,
|
|
529
|
+
memFailcnt: n(raw.memFailcnt, 0, 1e9) || 0,
|
|
530
|
+
cgroupOom: n(raw.cgroupOom, 0, 999) || 0,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
348
534
|
export function insideServiceWorldPorts(pulse) {
|
|
349
535
|
return [...new Set((pulse?.listen || []).filter((r) => exposed(r.addr) && ADMIN_PORTS.has(r.port)).map((r) => r.port))];
|
|
350
536
|
}
|
|
@@ -419,19 +605,23 @@ export function parseFail2banJail(text) {
|
|
|
419
605
|
export function gradeInside(pulse = {}) {
|
|
420
606
|
const listen = pulse.listen || [];
|
|
421
607
|
const h = pulse.hardening || {};
|
|
608
|
+
const pr = pulse.pressure || {};
|
|
422
609
|
const exposedAdmin = listen.filter((r) => exposed(r.addr) && ADMIN_PORTS.has(r.port));
|
|
423
610
|
const fw = Boolean(h.firewall);
|
|
424
611
|
const ban = Boolean(h.fail2ban);
|
|
425
612
|
const updates = Boolean(h.updates);
|
|
426
613
|
const diskHigh = Number(pulse.diskUsedPct) >= 85;
|
|
427
|
-
const oom = Boolean(pulse.oom) || Number(h.oomKills) > 0;
|
|
614
|
+
const oom = Boolean(pulse.oom) || Number(h.oomKills) > 0 || Number(pr.cgroupOom) > 0;
|
|
615
|
+
const loadHigh = Number(pulse.load1) >= Math.max(2, cpuCount(pulse));
|
|
616
|
+
const connHot = Number(pr.conntrackPct) >= 90;
|
|
617
|
+
const stuck = stuckFromTop(pulse.top).length > 0;
|
|
428
618
|
if (h.dockerApi) return "D";
|
|
429
619
|
if (h.sshPassword && h.sshRoot && h.sshWorld) return "D";
|
|
430
620
|
if (!fw && exposedAdmin.length) return "D";
|
|
431
621
|
if (!fw && !ban) return "D";
|
|
432
622
|
if (exposedAdmin.length || h.sshPassword || h.sshRoot) return "C";
|
|
433
623
|
if (!fw || !ban) return "C";
|
|
434
|
-
if (diskHigh || oom || h.rebootNeeded || !h.timesync) return "C";
|
|
624
|
+
if (diskHigh || oom || h.rebootNeeded || !h.timesync || loadHigh || connHot || stuck) return "C";
|
|
435
625
|
if (fw && ban && updates && h.timesync && !h.sshPassword && !h.sshRoot && !exposedAdmin.length && !h.dockerApi) return "A";
|
|
436
626
|
if (fw && ban && !exposedAdmin.length && !h.dockerApi) return "B";
|
|
437
627
|
return "C";
|
|
@@ -442,7 +632,7 @@ function labelPort(r) {
|
|
|
442
632
|
return svc ? `${svc} :${r.port}` : `:${r.port}`;
|
|
443
633
|
}
|
|
444
634
|
|
|
445
|
-
export function compareInside(pulse, rec = {}) {
|
|
635
|
+
export function compareInside(pulse, rec = {}, prev = null) {
|
|
446
636
|
const notes = [];
|
|
447
637
|
const listen = pulse?.listen || [];
|
|
448
638
|
const streetIp = rec.ticket?.ip || rec.watch?.current?.ip || null;
|
|
@@ -482,8 +672,9 @@ export function compareInside(pulse, rec = {}) {
|
|
|
482
672
|
do: "Сверьте DNS A с тем VPS, куда поставили сателлит. Если сайт за Cloudflare — это ожидаемо, не инцидент.",
|
|
483
673
|
});
|
|
484
674
|
}
|
|
485
|
-
const loadHigh = Number(pulse?.load1) >= Math.max(2, (
|
|
486
|
-
const memHigh = pulse?.memTotal && pulse.memUsed / pulse.memTotal >= 0.
|
|
675
|
+
const loadHigh = Number(pulse?.load1) >= Math.max(2, cpuCount(pulse));
|
|
676
|
+
const memHigh = pulse?.memTotal && pulse.memUsed / pulse.memTotal >= 0.85;
|
|
677
|
+
const pr = pulse?.pressure || {};
|
|
487
678
|
if (loadHigh || memHigh) {
|
|
488
679
|
const top = pulse.top?.[0];
|
|
489
680
|
const who = top ? `${top.comm} ${top.cpuPct}%` : "процесс в топе не виден";
|
|
@@ -599,6 +790,93 @@ export function compareInside(pulse, rec = {}) {
|
|
|
599
790
|
});
|
|
600
791
|
}
|
|
601
792
|
}
|
|
793
|
+
const stuck = stuckFromTop(pulse?.top);
|
|
794
|
+
if (stuck.length) {
|
|
795
|
+
const who = stuck.map((r) => `${r.comm} (${r.stat || "?"})`).join(", ");
|
|
796
|
+
notes.push({
|
|
797
|
+
kind: "stuck-proc",
|
|
798
|
+
title: "Процессы зависли или зомби",
|
|
799
|
+
text: `Состояние D/Z/T: ${who}. Так машина стоит на диске или мёртвых воркерах, а не «просто высокая нагрузка».`,
|
|
800
|
+
do: "Не убивайте с кабинета — его нет. На сервере: ps и диск, не WAF. Orb44 процессы сам не трогает.",
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const hot = hotFromTop(pulse?.top);
|
|
804
|
+
if (hot.length && !loadHigh) {
|
|
805
|
+
const who = hot.map((r) => `${r.comm} ${r.cpuPct}%/${r.rssMb}M`).join(", ");
|
|
806
|
+
notes.push({
|
|
807
|
+
kind: "hot-proc",
|
|
808
|
+
title: "Сервис жрёт CPU или RAM",
|
|
809
|
+
text: `Топ без общей перегрузки load: ${who}. Воркер уже упёрся, витрина может ещё отвечать.`,
|
|
810
|
+
do: "Смотрите этот процесс (php-fpm, node, mysql). Orb44 его не рестартит.",
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
if (Number(pr.conntrackPct) >= 80) {
|
|
814
|
+
notes.push({
|
|
815
|
+
kind: "conntrack",
|
|
816
|
+
title: "Таблица соединений почти полная",
|
|
817
|
+
text: `conntrack ${pr.conntrackUsed}/${pr.conntrackMax} (${pr.conntrackPct}%). Новые сессии начнут отбрасываться — с улицы это 502, изнутри это очередь.`,
|
|
818
|
+
do: "Ищите кто держит кучу TCP. Не открывайте порты «чтобы помогло».",
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
const prevPr = prev?.pressure || {};
|
|
822
|
+
const overDelta =
|
|
823
|
+
pr.listenOverflows != null && prevPr.listenOverflows != null
|
|
824
|
+
? Number(pr.listenOverflows) - Number(prevPr.listenOverflows)
|
|
825
|
+
: 0;
|
|
826
|
+
const dropDelta =
|
|
827
|
+
pr.listenDrops != null && prevPr.listenDrops != null
|
|
828
|
+
? Number(pr.listenDrops) - Number(prevPr.listenDrops)
|
|
829
|
+
: 0;
|
|
830
|
+
if (overDelta > 0 || dropDelta > 0) {
|
|
831
|
+
notes.push({
|
|
832
|
+
kind: "backlog",
|
|
833
|
+
title: "Очередь accept переполняется",
|
|
834
|
+
text: `С прошлого пульса ListenOverflows +${overDelta}, ListenDrops +${dropDelta}. Сервис не успевает брать соединения.`,
|
|
835
|
+
do: "Больше воркеров или меньше входа. Orb44 лимиты сам не поднимает.",
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (Number(pr.filePct) >= 85) {
|
|
839
|
+
notes.push({
|
|
840
|
+
kind: "files",
|
|
841
|
+
title: "Заканчиваются файловые дескрипторы",
|
|
842
|
+
text: `Открыто ${pr.filePct}% лимита (${pr.fileUsed}/${pr.fileMax}). Типичный «внезапно не открывается сокет».`,
|
|
843
|
+
do: "Кто держит файлы: воркер или утечка. Orb44 ulimit сам не меняет.",
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (pr.clockOffsetSec != null && Math.abs(pr.clockOffsetSec) >= 5) {
|
|
847
|
+
notes.push({
|
|
848
|
+
kind: "clock",
|
|
849
|
+
title: "Часы машины уехали",
|
|
850
|
+
text: `Смещение NTP ${pr.clockOffsetSec} с. TLS и метки Watch начнут врать.`,
|
|
851
|
+
do: "Почините chrony/timesyncd. Orb44 время сам не ставит.",
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (Number(pr.cgroupOom) > 0 || Number(pr.memFailcnt) > 20) {
|
|
855
|
+
notes.push({
|
|
856
|
+
kind: "cgroup-oom",
|
|
857
|
+
title: "Контейнер или cgroup упирается в память",
|
|
858
|
+
text: `cgroup oom=${pr.cgroupOom || 0}, failcnt=${pr.memFailcnt || 0}. Это не journal — счётчик ядра.`,
|
|
859
|
+
do: "Лимит памяти контейнера, не WAF. Orb44 лимиты сам не поднимает.",
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
if (prev?.listen && pulse?.listen) {
|
|
863
|
+
const face = (p) =>
|
|
864
|
+
(p.listen || [])
|
|
865
|
+
.filter((r) => Number(r.port) === 80 || Number(r.port) === 443)
|
|
866
|
+
.map((r) => `${r.port}:${r.comm || "?"}`)
|
|
867
|
+
.sort()
|
|
868
|
+
.join(",");
|
|
869
|
+
const a = face(prev);
|
|
870
|
+
const b = face(pulse);
|
|
871
|
+
if (a && b && a !== b) {
|
|
872
|
+
notes.push({
|
|
873
|
+
kind: "listen-swap",
|
|
874
|
+
title: "На 80/443 сменился процесс",
|
|
875
|
+
text: `Было ${a}, стало ${b}. Бинарь витрины подменили или рядом встал другой сервер.`,
|
|
876
|
+
do: "Сверьте, кто должен слушать HTTPS. Orb44 процесс сам не откатывает.",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
602
880
|
return notes;
|
|
603
881
|
}
|
|
604
882
|
|
|
@@ -612,7 +890,7 @@ export function insideWatchReasons(pulse, prev, rec = {}) {
|
|
|
612
890
|
const notes = [];
|
|
613
891
|
const incident = String(streetIncidentCode(rec) || "");
|
|
614
892
|
const streetHot = /auth_hot|auth_open|l7_exhaustion/.test(incident);
|
|
615
|
-
const loadHigh = Number(pulse?.load1) >= 2;
|
|
893
|
+
const loadHigh = Number(pulse?.load1) >= Math.max(2, cpuCount(pulse));
|
|
616
894
|
const memHigh = pulse?.memTotal && pulse.memUsed / pulse.memTotal >= 0.85;
|
|
617
895
|
const top = pulse?.top?.[0];
|
|
618
896
|
const who = top ? `${top.comm} ${top.cpuPct}%` : "процесс в топе не виден";
|
|
@@ -651,6 +929,52 @@ export function insideWatchReasons(pulse, prev, rec = {}) {
|
|
|
651
929
|
text: `Появились ${added.join(", ")}. С прошлого пульса их не было.`,
|
|
652
930
|
});
|
|
653
931
|
}
|
|
932
|
+
const face = (p) =>
|
|
933
|
+
(p?.listen || [])
|
|
934
|
+
.filter((r) => Number(r.port) === 80 || Number(r.port) === 443)
|
|
935
|
+
.map((r) => `${r.port}:${r.comm || "?"}`)
|
|
936
|
+
.sort()
|
|
937
|
+
.join(",");
|
|
938
|
+
const a = face(prev);
|
|
939
|
+
const b = face(pulse);
|
|
940
|
+
if (a && b && a !== b) {
|
|
941
|
+
notes.push({
|
|
942
|
+
kind: "listen-swap",
|
|
943
|
+
title: "На 80/443 сменился процесс",
|
|
944
|
+
text: `Было ${a}, стало ${b}.`,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (stuckFromTop(pulse?.top).length) {
|
|
949
|
+
notes.push({
|
|
950
|
+
kind: "stuck-proc",
|
|
951
|
+
title: "Процессы зависли",
|
|
952
|
+
text: stuckFromTop(pulse.top)
|
|
953
|
+
.map((r) => `${r.comm} ${r.stat}`)
|
|
954
|
+
.join(", "),
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (Number(pulse?.pressure?.conntrackPct) >= 80) {
|
|
958
|
+
notes.push({
|
|
959
|
+
kind: "conntrack",
|
|
960
|
+
title: "conntrack почти полный",
|
|
961
|
+
text: `${pulse.pressure.conntrackPct}%`,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const overDelta =
|
|
965
|
+
pulse?.pressure?.listenOverflows != null && prev?.pressure?.listenOverflows != null
|
|
966
|
+
? Number(pulse.pressure.listenOverflows) - Number(prev.pressure.listenOverflows)
|
|
967
|
+
: 0;
|
|
968
|
+
const dropDelta =
|
|
969
|
+
pulse?.pressure?.listenDrops != null && prev?.pressure?.listenDrops != null
|
|
970
|
+
? Number(pulse.pressure.listenDrops) - Number(prev.pressure.listenDrops)
|
|
971
|
+
: 0;
|
|
972
|
+
if (overDelta > 0 || dropDelta > 0) {
|
|
973
|
+
notes.push({
|
|
974
|
+
kind: "backlog",
|
|
975
|
+
title: "Очередь accept растёт",
|
|
976
|
+
text: `+${overDelta} overflows, +${dropDelta} drops с прошлого пульса.`,
|
|
977
|
+
});
|
|
654
978
|
}
|
|
655
979
|
const streetIp = rec.ticket?.ip || rec.watch?.current?.ip || rec.watch?.last?.ip || null;
|
|
656
980
|
if (pulse?.originA?.length && streetIp && !pulse.originA.includes(streetIp)) {
|
|
@@ -678,6 +1002,7 @@ export function collectPulse() {
|
|
|
678
1002
|
const top = topTable();
|
|
679
1003
|
const hardening = collectHardening(listen);
|
|
680
1004
|
const limited = listen.length > 0 && !named;
|
|
1005
|
+
const pressure = collectPressure();
|
|
681
1006
|
return sanitizePulse({
|
|
682
1007
|
ts: Date.now(),
|
|
683
1008
|
hostname: os.hostname(),
|
|
@@ -689,9 +1014,10 @@ export function collectPulse() {
|
|
|
689
1014
|
listen,
|
|
690
1015
|
originA: originAddrs(),
|
|
691
1016
|
failedUnit: null,
|
|
692
|
-
oom: Number(hardening.oomKills) > 0,
|
|
1017
|
+
oom: Number(hardening.oomKills) > 0 || Number(pressure.cgroupOom) > 0,
|
|
693
1018
|
limited,
|
|
694
1019
|
hardening,
|
|
1020
|
+
pressure,
|
|
695
1021
|
});
|
|
696
1022
|
}
|
|
697
1023
|
|
|
@@ -760,6 +1086,13 @@ export function formatPulsePreview(pulse, lang = "en") {
|
|
|
760
1086
|
[t(lang, "preview_row_ssh"), ssh],
|
|
761
1087
|
]
|
|
762
1088
|
);
|
|
763
|
-
const
|
|
1089
|
+
const extraBits = [];
|
|
1090
|
+
if (pulse.limited) extraBits.push(t(lang, "preview_limited"));
|
|
1091
|
+
const stuck = stuckFromTop(pulse.top);
|
|
1092
|
+
if (stuck.length) extraBits.push(`D/Z/T ${stuck.map((r) => `${r.comm}:${r.stat}`).join(" ")}`);
|
|
1093
|
+
const pr = pulse.pressure || {};
|
|
1094
|
+
if (pr.conntrackPct != null) extraBits.push(`conntrack ${pr.conntrackPct}%`);
|
|
1095
|
+
if (pr.cpus) extraBits.push(`${pr.cpus} CPU`);
|
|
1096
|
+
const extra = extraBits.length ? `\n${extraBits.join(" · ")}` : "";
|
|
764
1097
|
return `${machine}\n\n${listen}\n\n${top}\n\n${guard}${extra}`;
|
|
765
1098
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const FETCH_MS = 15_000;
|
|
2
|
+
|
|
3
|
+
const NET_CODES = new Set([
|
|
4
|
+
"ECONNREFUSED",
|
|
5
|
+
"ECONNRESET",
|
|
6
|
+
"ECONNABORTED",
|
|
7
|
+
"EHOSTUNREACH",
|
|
8
|
+
"ENETUNREACH",
|
|
9
|
+
"EPIPE",
|
|
10
|
+
"EAI_AGAIN",
|
|
11
|
+
"ENOTFOUND",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function classifyNetError(err) {
|
|
15
|
+
const cause = err?.cause && typeof err.cause === "object" ? err.cause : err;
|
|
16
|
+
const code = String(cause?.code || err?.code || "");
|
|
17
|
+
const name = String(err?.name || cause?.name || "");
|
|
18
|
+
const msg = `${cause?.message || ""} ${err?.message || ""}`;
|
|
19
|
+
if (name === "TimeoutError" || name === "AbortError" || code === "ABORT_ERR" || /timeout|aborted/i.test(msg)) {
|
|
20
|
+
return "timeout";
|
|
21
|
+
}
|
|
22
|
+
if (/CERT|UNABLE_TO_VERIFY|ERR_TLS|ERR_SSL/i.test(`${code} ${msg}`)) return "tls";
|
|
23
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN" || /getaddrinfo/i.test(msg)) return "dns";
|
|
24
|
+
if (NET_CODES.has(code) || /fetch failed|socket hang up/i.test(msg)) return "unreachable";
|
|
25
|
+
return "unreachable";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cabinetFail(url, err) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
status: 0,
|
|
32
|
+
body: { error: classifyNetError(err), url: String(url || "").replace(/\/$/, "") },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function cabinetRequest(url, pathname, { method = "GET", json, secret, fetchImpl = fetch, timeoutMs = FETCH_MS } = {}) {
|
|
37
|
+
const base = String(url || "").replace(/\/$/, "");
|
|
38
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
39
|
+
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
40
|
+
const signal = timeoutMs > 0 && typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
41
|
+
try {
|
|
42
|
+
const r = await fetchImpl(`${base}${pathname}`, {
|
|
43
|
+
method,
|
|
44
|
+
headers,
|
|
45
|
+
body: json ? JSON.stringify(json) : undefined,
|
|
46
|
+
signal,
|
|
47
|
+
});
|
|
48
|
+
const body = await r.json().catch(() => ({}));
|
|
49
|
+
return { ok: Boolean(r.ok), status: r.status, body };
|
|
50
|
+
} catch (err) {
|
|
51
|
+
return cabinetFail(base, err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function apiFailText(lang, out, t, fallbackKey = "pulse_fail") {
|
|
56
|
+
const err = out?.body?.error;
|
|
57
|
+
const url = out?.body?.url || "";
|
|
58
|
+
if (err === "unreachable") return t(lang, "cabinet_down", { url });
|
|
59
|
+
if (err === "timeout") return t(lang, "cabinet_timeout", { url });
|
|
60
|
+
if (err === "tls") return t(lang, "cabinet_tls", { url });
|
|
61
|
+
if (err === "dns") return t(lang, "cabinet_dns", { url });
|
|
62
|
+
if (err === "bad_secret" || err === "not_found") return t(lang, "key_revoked");
|
|
63
|
+
if (typeof err === "string" && /^[a-z][a-z0-9_]{0,40}$/.test(err)) return err;
|
|
64
|
+
return t(lang, fallbackKey);
|
|
65
|
+
}
|
package/server/sat-i18n.mjs
CHANGED
|
@@ -35,6 +35,10 @@ const STR = {
|
|
|
35
35
|
payload: "What goes to the cabinet",
|
|
36
36
|
pulse_ok: "Pulse is in the cabinet.",
|
|
37
37
|
pulse_fail: "pulse not accepted",
|
|
38
|
+
cabinet_down: "Cabinet is not reachable ({url}). Start the cabinet or the tunnel to it.",
|
|
39
|
+
cabinet_timeout: "Cabinet did not answer ({url}). Try again later.",
|
|
40
|
+
cabinet_tls: "TLS to the cabinet failed ({url}).",
|
|
41
|
+
cabinet_dns: "Cannot resolve the cabinet host ({url}).",
|
|
38
42
|
pair_fail: "could not start pairing",
|
|
39
43
|
ask_daemon: "Install as a Watch daemon?\nRuns in the background and starts again after reboot. Pulse every 5 min.",
|
|
40
44
|
daemon_no: "No",
|
|
@@ -139,6 +143,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
139
143
|
payload: "Что уходит в кабинет",
|
|
140
144
|
pulse_ok: "Пульс в кабинете.",
|
|
141
145
|
pulse_fail: "пульс не принят",
|
|
146
|
+
cabinet_down: "Кабинет недоступен ({url}). Поднимите кабинет или туннель к нему.",
|
|
147
|
+
cabinet_timeout: "Кабинет не ответил ({url}). Повторите позже.",
|
|
148
|
+
cabinet_tls: "TLS к кабинету не сошёлся ({url}).",
|
|
149
|
+
cabinet_dns: "Не резолвится хост кабинета ({url}).",
|
|
142
150
|
pair_fail: "не удалось начать пару",
|
|
143
151
|
ask_daemon: "Поставить Watch-демоном?\nБудет работать в фоне и подниматься после перезагрузки. Пульс каждые 5 мин.",
|
|
144
152
|
daemon_no: "Нет",
|
|
@@ -243,6 +251,10 @@ Outgoing pulse. Cabinet does not execute commands; it replies with advice.`,
|
|
|
243
251
|
payload: "콘솔로 보내는 내용",
|
|
244
252
|
pulse_ok: "펄스가 콘솔에 있습니다.",
|
|
245
253
|
pulse_fail: "펄스 거부",
|
|
254
|
+
cabinet_down: "콘솔에 연결할 수 없습니다 ({url}). 콘솔 또는 터널을 올리세요.",
|
|
255
|
+
cabinet_timeout: "콘솔이 응답하지 않습니다 ({url}). 나중에 다시 시도하세요.",
|
|
256
|
+
cabinet_tls: "콘솔 TLS 실패 ({url}).",
|
|
257
|
+
cabinet_dns: "콘솔 호스트를 찾지 못했습니다 ({url}).",
|
|
246
258
|
pair_fail: "페어링을 시작하지 못했습니다",
|
|
247
259
|
ask_daemon: "Watch 데몬으로 설치할까요?\n백그라운드에서 돌고 재부팅 후에도 올라옵니다. 5분마다 펄스.",
|
|
248
260
|
daemon_no: "아니요",
|
|
@@ -347,6 +359,10 @@ login에서 언어를 묻고 저장한 뒤, 데몬은 기본값 아니오입니
|
|
|
347
359
|
payload: "Qué se envía al gabinete",
|
|
348
360
|
pulse_ok: "Pulso en el gabinete.",
|
|
349
361
|
pulse_fail: "pulso rechazado",
|
|
362
|
+
cabinet_down: "El gabinete no responde ({url}). Levante el gabinete o el túnel.",
|
|
363
|
+
cabinet_timeout: "El gabinete no contestó ({url}). Pruebe más tarde.",
|
|
364
|
+
cabinet_tls: "Falló TLS al gabinete ({url}).",
|
|
365
|
+
cabinet_dns: "No se resuelve el host del gabinete ({url}).",
|
|
350
366
|
pair_fail: "no se pudo iniciar el emparejamiento",
|
|
351
367
|
ask_daemon: "¿Instalar como demonio Watch?\nCorre en segundo plano y arranca de nuevo tras el reinicio. Pulso cada 5 min.",
|
|
352
368
|
daemon_no: "No",
|
package/server/sat-update.mjs
CHANGED
|
@@ -9,6 +9,7 @@ export const SAT_TREE_FILES = [
|
|
|
9
9
|
"server/sat-i18n.mjs",
|
|
10
10
|
"server/cli-menu.mjs",
|
|
11
11
|
"server/sat-local.mjs",
|
|
12
|
+
"server/sat-http.mjs",
|
|
12
13
|
"server/sat-update.mjs",
|
|
13
14
|
"package.json",
|
|
14
15
|
];
|
|
@@ -52,6 +53,24 @@ export function readCliVersion(scriptFile) {
|
|
|
52
53
|
return "0.0.0";
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export function treeNeedsRefresh(root, latest, { force = false } = {}) {
|
|
57
|
+
if (force) return true;
|
|
58
|
+
const ver = readCliVersion(path.join(root, "bin", "orb44.mjs"));
|
|
59
|
+
if (cmpVer(ver, latest) < 0) return true;
|
|
60
|
+
try {
|
|
61
|
+
const pulse = fs.readFileSync(path.join(root, "server", "pulse.mjs"), "utf8");
|
|
62
|
+
if (!pulse.includes("function collapseListen")) return true;
|
|
63
|
+
if (!fs.existsSync(path.join(root, "server", "sat-http.mjs"))) return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function staleSatRoots(roots, latest, opts = {}) {
|
|
71
|
+
return (roots || []).filter((r) => treeNeedsRefresh(r, latest, opts));
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
export function pickSatRoots(scriptFile, { systemLib = "/usr/lib/orb44-sat" } = {}) {
|
|
56
75
|
const here = path.resolve(path.dirname(scriptFile), "..");
|
|
57
76
|
const out = [];
|