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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
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
- // best-effort: fuser kill port if still held (some pid without pid file)
181
- try { await execAsync("fuser", ["-k", "8989/tcp"]); } catch {}
182
- // stop any leftover systemd instance before start
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 :8989 to be free
185
- for (let i = 0; i < 15; 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(":8989")) break;
188
- await new Promise((r2) => setTimeout(r2, 200));
189
- if (i === 8) try { await execAsync("fuser", ["-k", "8989/tcp"]); } catch {}
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
+ }
@@ -191,7 +191,46 @@ export async function startDaemonMain(VERSION) {
191
191
  process.on("SIGTERM", restore2);
192
192
  }
193
193
 
194
- await srv.ready();
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 autoUpdateMs = autoUpdateIntervalMs();
333
- function emitAutoUpdate(type, data = {}) {
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