mslxdff 0.1.77 → 0.1.78
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/package.json +1 -1
- package/src/autostart.js +44 -8
- package/src/runtime/auto-update.js +84 -0
- package/src/runtime/bootstrap.js +62 -79
package/package.json
CHANGED
package/src/autostart.js
CHANGED
|
@@ -159,6 +159,8 @@ async function linuxEnable() {
|
|
|
159
159
|
// stop bare detached daemon that may hold the port, let systemd take over (best-effort, wait for port free)
|
|
160
160
|
try {
|
|
161
161
|
const { isPidAlive, stopDaemon } = await import("../daemon.js");
|
|
162
|
+
const { resolvePort } = await import("../server.js");
|
|
163
|
+
const port = resolvePort();
|
|
162
164
|
const pidFile = join(homedir(), ".config", "mslxdff", "daemon.pid");
|
|
163
165
|
const { existsSync: exists2, readFileSync: read2 } = await import("node:fs");
|
|
164
166
|
let pidToWait = null;
|
|
@@ -177,16 +179,50 @@ async function linuxEnable() {
|
|
|
177
179
|
await new Promise((r2) => setTimeout(r2, 200));
|
|
178
180
|
} else break;
|
|
179
181
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
// robust: scan ss for any holder of :port (covers stale pidFile, fuser not installed)
|
|
183
|
+
const killHolders = async () => {
|
|
184
|
+
let killed = 0;
|
|
185
|
+
try {
|
|
186
|
+
const r = await execAsync("ss", ["-lptn", `sport = :${port}`]);
|
|
187
|
+
const out = r.stdout || "";
|
|
188
|
+
const re = /pid=(\d+)/g;
|
|
189
|
+
let m;
|
|
190
|
+
const pids = new Set();
|
|
191
|
+
while ((m = re.exec(out))) pids.add(Number(m[1]));
|
|
192
|
+
// fallback: full ss if sport filter empty (busybox ss)
|
|
193
|
+
if (!pids.size) {
|
|
194
|
+
const r2 = await execAsync("ss", ["-lptn"]);
|
|
195
|
+
const out2 = r2.stdout || "";
|
|
196
|
+
// only consider lines containing :port
|
|
197
|
+
for (const line of out2.split("\n")) {
|
|
198
|
+
if (!line.includes(`:${port}`)) continue;
|
|
199
|
+
const re2 = /pid=(\d+)/g;
|
|
200
|
+
let m2;
|
|
201
|
+
while ((m2 = re2.exec(line))) pids.add(Number(m2[1]));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const p of pids) {
|
|
205
|
+
if (p === process.pid) continue;
|
|
206
|
+
try { process.kill(p, "SIGTERM"); killed++; } catch {}
|
|
207
|
+
}
|
|
208
|
+
if (killed) await new Promise((r2) => setTimeout(r2, 400));
|
|
209
|
+
for (const p of pids) {
|
|
210
|
+
try { if (isPidAlive(p)) process.kill(p, "SIGKILL"); } catch {}
|
|
211
|
+
}
|
|
212
|
+
} catch {}
|
|
213
|
+
// fuser as extra best-effort (may not exist)
|
|
214
|
+
try { await execAsync("fuser", ["-k", `${port}/tcp`]); } catch {}
|
|
215
|
+
return killed;
|
|
216
|
+
};
|
|
217
|
+
await killHolders();
|
|
218
|
+
// stop any leftover systemd instance before start (avoid double)
|
|
183
219
|
try { await execAsync("systemctl", ["--user", "stop", SERVICE_NAME]); } catch {}
|
|
184
|
-
// wait for :
|
|
185
|
-
for (let i = 0; i <
|
|
220
|
+
// wait for :port to be free (ss probe)
|
|
221
|
+
for (let i = 0; i < 20; i++) {
|
|
186
222
|
const chk = await execAsync("ss", ["-ltn"]);
|
|
187
|
-
if (!chk.stdout.includes(
|
|
188
|
-
|
|
189
|
-
|
|
223
|
+
if (!chk.stdout.includes(`:${port}`)) break;
|
|
224
|
+
if (i === 6 || i === 12) await killHolders();
|
|
225
|
+
await new Promise((r2) => setTimeout(r2, 250));
|
|
190
226
|
}
|
|
191
227
|
} catch {}
|
|
192
228
|
let r = await execAsync("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { autoUpdateIntervalMs } from "../cli/policy.js";
|
|
2
|
+
import { errMsg, npmCmd, run } from "../cli/util.js";
|
|
3
|
+
import { resolvePort } from "../server.js";
|
|
4
|
+
|
|
5
|
+
export function setupAutoUpdate({ VERSION, bus, logs }) {
|
|
6
|
+
const autoUpdateMs = autoUpdateIntervalMs();
|
|
7
|
+
function emitAutoUpdate(type, data = {}) {
|
|
8
|
+
const entry = { ts: Date.now(), type, ...data };
|
|
9
|
+
try { bus?.emit(entry); } catch {}
|
|
10
|
+
try { logs?.appendEvent?.(entry); } catch {}
|
|
11
|
+
const line = `[auto-update] ${type} ${JSON.stringify(data)}`;
|
|
12
|
+
console.log(line);
|
|
13
|
+
}
|
|
14
|
+
if (autoUpdateMs) {
|
|
15
|
+
console.log(`auto-update enabled: checking every ${Math.round(autoUpdateMs / 60000)}m`);
|
|
16
|
+
emitAutoUpdate("auto-update-enabled", { intervalMs: autoUpdateMs, current: VERSION });
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
emitAutoUpdate("auto-update-check", { current: VERSION });
|
|
19
|
+
checkAndAutoUpdate().catch((err) => {
|
|
20
|
+
console.log(`auto-update check failed: ${errMsg(err)}`);
|
|
21
|
+
emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
|
|
22
|
+
});
|
|
23
|
+
}, 30_000).unref?.();
|
|
24
|
+
const autoUpdateTimer = setInterval(() => {
|
|
25
|
+
emitAutoUpdate("auto-update-check", { current: VERSION });
|
|
26
|
+
checkAndAutoUpdate().catch((err) => {
|
|
27
|
+
console.log(`auto-update check failed: ${errMsg(err)}`);
|
|
28
|
+
emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
|
|
29
|
+
});
|
|
30
|
+
}, autoUpdateMs);
|
|
31
|
+
autoUpdateTimer.unref();
|
|
32
|
+
} else {
|
|
33
|
+
console.log(`auto-update disabled (set MSLXDFF_AUTO_UPDATE=1 to enable hourly)`);
|
|
34
|
+
emitAutoUpdate("auto-update-disabled", { current: VERSION });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function checkAndAutoUpdate() {
|
|
38
|
+
emitAutoUpdate("auto-update-query", { current: VERSION });
|
|
39
|
+
const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
|
|
40
|
+
if (info.err) {
|
|
41
|
+
emitAutoUpdate("auto-update-query-failed", { error: info.err.message || String(info.stderr || "").slice(0, 500) });
|
|
42
|
+
throw new Error(info.err.message || String(info.stderr || "").slice(0, 500));
|
|
43
|
+
}
|
|
44
|
+
let latest = "";
|
|
45
|
+
try {
|
|
46
|
+
latest = JSON.parse(String(info.stdout || "").trim());
|
|
47
|
+
if (Array.isArray(latest)) latest = latest[latest.length - 1];
|
|
48
|
+
latest = String(latest || "").replace(/^v/, "").trim();
|
|
49
|
+
} catch {
|
|
50
|
+
const raw = String(info.stdout || "").trim();
|
|
51
|
+
const m = raw.match(/(\d+\.\d+\.\d+[^\s'"]*)/);
|
|
52
|
+
latest = m ? m[1] : raw.split(/\s+/).pop()?.replace(/['"]/g, "") || "";
|
|
53
|
+
}
|
|
54
|
+
latest = latest.replace(/['"]/g, "").trim();
|
|
55
|
+
emitAutoUpdate("auto-update-queried", { current: VERSION, latest, stdout: String(info.stdout || "").trim().slice(0, 200) });
|
|
56
|
+
if (!latest || latest === VERSION) {
|
|
57
|
+
emitAutoUpdate("auto-update-noop", { current: VERSION, latest });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const { compareSemver } = await import("../cli/policy.js");
|
|
61
|
+
if (compareSemver(latest, VERSION) <= 0) {
|
|
62
|
+
emitAutoUpdate("auto-update-noop", { current: VERSION, latest, reason: "not newer" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
emitAutoUpdate("auto-update-found", { current: VERSION, latest });
|
|
66
|
+
console.log(`auto-update: v${VERSION} -> v${latest}, installing...`);
|
|
67
|
+
emitAutoUpdate("auto-update-installing", { current: VERSION, latest });
|
|
68
|
+
const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`]);
|
|
69
|
+
if (up.err) {
|
|
70
|
+
emitAutoUpdate("auto-update-install-failed", { current: VERSION, latest, error: up.err.message || String(up.stderr || "").slice(0, 500) });
|
|
71
|
+
throw new Error(up.err.message || String(up.stderr || "").slice(0, 500));
|
|
72
|
+
}
|
|
73
|
+
emitAutoUpdate("auto-update-installed", { current: VERSION, latest, stdout: String(up.stdout || "").slice(0, 500) });
|
|
74
|
+
console.log(`auto-update: installed v${latest}, restarting daemon...`);
|
|
75
|
+
emitAutoUpdate("auto-update-restarting", { current: VERSION, latest });
|
|
76
|
+
const { stopDaemon, startDaemon } = await import("../daemon.js");
|
|
77
|
+
try { stopDaemon(); } catch (e) { emitAutoUpdate("auto-update-stop-failed", { error: errMsg(e) }); }
|
|
78
|
+
const { waitForHealth } = await import("../cli/policy.js");
|
|
79
|
+
const newPid = startDaemon([]);
|
|
80
|
+
await waitForHealth(resolvePort(), 8000);
|
|
81
|
+
console.log(`auto-update: restarted as v${latest} (pid ${newPid})`);
|
|
82
|
+
emitAutoUpdate("auto-update-restarted", { current: VERSION, latest, newPid });
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -191,7 +191,46 @@ export async function startDaemonMain(VERSION) {
|
|
|
191
191
|
process.on("SIGTERM", restore2);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
// Robust ready: if EADDRINUSE (bare daemon still holds port), kill holders and retry once
|
|
195
|
+
try {
|
|
196
|
+
await srv.ready();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const msg = String(err?.message || err);
|
|
199
|
+
const code = err?.code || "";
|
|
200
|
+
if (code === "EADDRINUSE" || msg.includes("EADDRINUSE")) {
|
|
201
|
+
console.log(`port ${resolvePort()} in use — freeing stale holder and retrying...`);
|
|
202
|
+
try {
|
|
203
|
+
const { execFile } = await import("node:child_process");
|
|
204
|
+
const execAsync2 = (f, a) => new Promise((res) => execFile(f, a, { windowsHide: true, timeout: 4000 }, (e, so, se) => res({ e, so: String(so||""), se: String(se||"") })));
|
|
205
|
+
const port = resolvePort();
|
|
206
|
+
// kill via ss parse (same as autostart)
|
|
207
|
+
const ss1 = await execAsync2("ss", ["-lptn", `sport = :${port}`]);
|
|
208
|
+
const out = ss1.so || "";
|
|
209
|
+
const pids = new Set();
|
|
210
|
+
let m;
|
|
211
|
+
const re = /pid=(\d+)/g;
|
|
212
|
+
while ((m = re.exec(out))) pids.add(Number(m[1]));
|
|
213
|
+
if (!pids.size) {
|
|
214
|
+
const ss2 = await execAsync2("ss", ["-lptn"]);
|
|
215
|
+
for (const line of (ss2.so||"").split("\n")) {
|
|
216
|
+
if (!line.includes(`:${port}`)) continue;
|
|
217
|
+
let m2; const re2 = /pid=(\d+)/g;
|
|
218
|
+
while ((m2 = re2.exec(line))) pids.add(Number(m2[1]));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const p of pids) { if (p !== process.pid) try { process.kill(p, "SIGTERM"); } catch {} }
|
|
222
|
+
if (pids.size) await new Promise((r2) => setTimeout(r2, 600));
|
|
223
|
+
for (const p of pids) try { const { isPidAlive } = await import("../daemon.js"); if (isPidAlive(p)) process.kill(p, "SIGKILL"); } catch {}
|
|
224
|
+
try { await execAsync2("fuser", ["-k", `${port}/tcp`]); } catch {}
|
|
225
|
+
for (let i=0;i<10;i++) {
|
|
226
|
+
const chk = await execAsync2("ss", ["-ltn"]);
|
|
227
|
+
if (!chk.so.includes(`:${port}`)) break;
|
|
228
|
+
await new Promise((r2)=>setTimeout(r2,200));
|
|
229
|
+
}
|
|
230
|
+
} catch {}
|
|
231
|
+
await srv.ready();
|
|
232
|
+
} else throw err;
|
|
233
|
+
}
|
|
195
234
|
|
|
196
235
|
if (loadedPlugins.length) {
|
|
197
236
|
runHook(loadedPlugins, "server:start", { port: srv.server.address()?.port, host: listenHost, version: VERSION }).catch(() => {});
|
|
@@ -233,6 +272,26 @@ export async function startDaemonMain(VERSION) {
|
|
|
233
272
|
console.log(`hedge: ${hd ? `${hd}ms` : "off"} (MSLXDFF_HEDGE_DELAY_MS)`);
|
|
234
273
|
} catch {}
|
|
235
274
|
|
|
275
|
+
// best-effort: ensure autostart on Linux (so daemon survives reboot/SSH disconnect without manual cmd)
|
|
276
|
+
if (process.platform === "linux" && !process.env.MSLXDFF_NO_AUTOSTART) {
|
|
277
|
+
setTimeout(async () => {
|
|
278
|
+
try {
|
|
279
|
+
const { getAutostartStatus, enableAutostart } = await import("../autostart.js");
|
|
280
|
+
const st = await getAutostartStatus();
|
|
281
|
+
if (!st.enabled) {
|
|
282
|
+
const r = await enableAutostart();
|
|
283
|
+
if (r.ok) {
|
|
284
|
+
console.log(`autostart auto-enabled: ${r.method}${r.linger ? ` linger=${r.linger}` : ""}`);
|
|
285
|
+
try { bus?.emit({ ts: Date.now(), type: "autostart-auto-enabled", method: r.method }); } catch {}
|
|
286
|
+
try { appendEvent({ ts: Date.now(), type: "autostart-auto-enabled", method: r.method }); } catch {}
|
|
287
|
+
} else {
|
|
288
|
+
console.log(`autostart auto-enable failed: ${r.error || "unknown"} (run mslxdff -enable-autostart manually)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch {}
|
|
292
|
+
}, 2500).unref?.();
|
|
293
|
+
}
|
|
294
|
+
|
|
236
295
|
// group sync
|
|
237
296
|
const { syncAllJoinedGroups } = await import("../cli/group-helpers.js");
|
|
238
297
|
syncAllJoinedGroups({ peers, groups })
|
|
@@ -329,84 +388,8 @@ export async function startDaemonMain(VERSION) {
|
|
|
329
388
|
console.log(`broadband relay: heartbeat 30s + poll 1s for ${broadbandGroups().length} group(s)`);
|
|
330
389
|
}
|
|
331
390
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
const entry = { ts: Date.now(), type, ...data };
|
|
335
|
-
try { bus?.emit(entry); } catch {}
|
|
336
|
-
try { logs?.appendEvent?.(entry); } catch {}
|
|
337
|
-
const line = `[auto-update] ${type} ${JSON.stringify(data)}`;
|
|
338
|
-
console.log(line);
|
|
339
|
-
}
|
|
340
|
-
if (autoUpdateMs) {
|
|
341
|
-
console.log(`auto-update enabled: checking every ${Math.round(autoUpdateMs / 60000)}m`);
|
|
342
|
-
emitAutoUpdate("auto-update-enabled", { intervalMs: autoUpdateMs, current: VERSION });
|
|
343
|
-
setTimeout(() => {
|
|
344
|
-
emitAutoUpdate("auto-update-check", { current: VERSION });
|
|
345
|
-
checkAndAutoUpdate().catch((err) => {
|
|
346
|
-
console.log(`auto-update check failed: ${errMsg(err)}`);
|
|
347
|
-
emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
|
|
348
|
-
});
|
|
349
|
-
}, 30_000).unref?.();
|
|
350
|
-
const autoUpdateTimer = setInterval(() => {
|
|
351
|
-
emitAutoUpdate("auto-update-check", { current: VERSION });
|
|
352
|
-
checkAndAutoUpdate().catch((err) => {
|
|
353
|
-
console.log(`auto-update check failed: ${errMsg(err)}`);
|
|
354
|
-
emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
|
|
355
|
-
});
|
|
356
|
-
}, autoUpdateMs);
|
|
357
|
-
autoUpdateTimer.unref();
|
|
358
|
-
} else {
|
|
359
|
-
console.log(`auto-update disabled (set MSLXDFF_AUTO_UPDATE=1 to enable hourly)`);
|
|
360
|
-
emitAutoUpdate("auto-update-disabled", { current: VERSION });
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async function checkAndAutoUpdate() {
|
|
364
|
-
emitAutoUpdate("auto-update-query", { current: VERSION });
|
|
365
|
-
const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
|
|
366
|
-
if (info.err) {
|
|
367
|
-
emitAutoUpdate("auto-update-query-failed", { error: info.err.message || String(info.stderr || "").slice(0, 500) });
|
|
368
|
-
throw new Error(info.err.message || String(info.stderr || "").slice(0, 500));
|
|
369
|
-
}
|
|
370
|
-
let latest = "";
|
|
371
|
-
try {
|
|
372
|
-
latest = JSON.parse(String(info.stdout || "").trim());
|
|
373
|
-
if (Array.isArray(latest)) latest = latest[latest.length - 1];
|
|
374
|
-
latest = String(latest || "").replace(/^v/, "").trim();
|
|
375
|
-
} catch {
|
|
376
|
-
const raw = String(info.stdout || "").trim();
|
|
377
|
-
const m = raw.match(/(\d+\.\d+\.\d+[^\s'"]*)/);
|
|
378
|
-
latest = m ? m[1] : raw.split(/\s+/).pop()?.replace(/['"]/g, "") || "";
|
|
379
|
-
}
|
|
380
|
-
latest = latest.replace(/['"]/g, "").trim();
|
|
381
|
-
emitAutoUpdate("auto-update-queried", { current: VERSION, latest, stdout: String(info.stdout || "").trim().slice(0, 200) });
|
|
382
|
-
if (!latest || latest === VERSION) {
|
|
383
|
-
emitAutoUpdate("auto-update-noop", { current: VERSION, latest });
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
const { compareSemver } = await import("../cli/policy.js");
|
|
387
|
-
if (compareSemver(latest, VERSION) <= 0) {
|
|
388
|
-
emitAutoUpdate("auto-update-noop", { current: VERSION, latest, reason: "not newer" });
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
emitAutoUpdate("auto-update-found", { current: VERSION, latest });
|
|
392
|
-
console.log(`auto-update: v${VERSION} -> v${latest}, installing...`);
|
|
393
|
-
emitAutoUpdate("auto-update-installing", { current: VERSION, latest });
|
|
394
|
-
const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`]);
|
|
395
|
-
if (up.err) {
|
|
396
|
-
emitAutoUpdate("auto-update-install-failed", { current: VERSION, latest, error: up.err.message || String(up.stderr || "").slice(0, 500) });
|
|
397
|
-
throw new Error(up.err.message || String(up.stderr || "").slice(0, 500));
|
|
398
|
-
}
|
|
399
|
-
emitAutoUpdate("auto-update-installed", { current: VERSION, latest, stdout: String(up.stdout || "").slice(0, 500) });
|
|
400
|
-
console.log(`auto-update: installed v${latest}, restarting daemon...`);
|
|
401
|
-
emitAutoUpdate("auto-update-restarting", { current: VERSION, latest });
|
|
402
|
-
const { stopDaemon, startDaemon } = await import("../daemon.js");
|
|
403
|
-
try { stopDaemon(); } catch (e) { emitAutoUpdate("auto-update-stop-failed", { error: errMsg(e) }); }
|
|
404
|
-
const { waitForHealth } = await import("../cli/policy.js");
|
|
405
|
-
const newPid = startDaemon([]);
|
|
406
|
-
await waitForHealth(resolvePort(), 8000);
|
|
407
|
-
console.log(`auto-update: restarted as v${latest} (pid ${newPid})`);
|
|
408
|
-
emitAutoUpdate("auto-update-restarted", { current: VERSION, latest, newPid });
|
|
409
|
-
}
|
|
391
|
+
const { setupAutoUpdate } = await import("./auto-update.js");
|
|
392
|
+
setupAutoUpdate({ VERSION, bus, logs });
|
|
410
393
|
}
|
|
411
394
|
|
|
412
395
|
|