mslxdff 0.1.76 → 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.76",
3
+ "version": "0.1.78",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/autostart.js CHANGED
@@ -156,21 +156,73 @@ async function linuxEnable() {
156
156
  lingerRes = lr.err ? (lr.stderr || lr.stdout || lr.err.message || "").slice(0, 200) : "ok";
157
157
  }
158
158
  } catch {}
159
- // stop bare detached daemon that may hold the port, let systemd take over (best-effort)
159
+ // stop bare detached daemon that may hold the port, let systemd take over (best-effort, wait for port free)
160
160
  try {
161
- const { readPid, isPidAlive } = await import("../daemon.js");
162
- const { existsSync: exists2, readFileSync: read2 } = await import("node:fs");
163
- // Only stop if pid exists and is alive and not already managed by systemd
161
+ const { isPidAlive, stopDaemon } = await import("../daemon.js");
162
+ const { resolvePort } = await import("../server.js");
163
+ const port = resolvePort();
164
164
  const pidFile = join(homedir(), ".config", "mslxdff", "daemon.pid");
165
- if (exists2(pidFile)) {
166
- const raw = read2(pidFile, "utf8").trim().split("\n")[0];
167
- const pid = Number(raw);
168
- if (Number.isInteger(pid) && pid > 0) {
169
- try { if (isPidAlive(pid)) { const { stopDaemon } = await import("../daemon.js"); stopDaemon(); await new Promise((r2) => setTimeout(r2, 600)); } } catch {}
165
+ const { existsSync: exists2, readFileSync: read2 } = await import("node:fs");
166
+ let pidToWait = null;
167
+ try {
168
+ if (exists2(pidFile)) {
169
+ const raw = read2(pidFile, "utf8").trim().split("\n")[0];
170
+ const n = Number(raw);
171
+ if (Number.isInteger(n) && n > 0 && isPidAlive(n)) pidToWait = n;
170
172
  }
171
- } else {
172
- // No pid file, try generic stop (no-op if none)
173
- try { const { stopDaemon } = await import("../daemon.js"); stopDaemon(); } catch {}
173
+ } catch {}
174
+ try { stopDaemon(); } catch {}
175
+ // ensure bare daemon really exits (SIGTERM -> SIGKILL)
176
+ for (let i = 0; i < 15; i++) {
177
+ if (pidToWait && isPidAlive(pidToWait)) {
178
+ if (i === 7) try { process.kill(pidToWait, "SIGKILL"); } catch {}
179
+ await new Promise((r2) => setTimeout(r2, 200));
180
+ } else break;
181
+ }
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)
219
+ try { await execAsync("systemctl", ["--user", "stop", SERVICE_NAME]); } catch {}
220
+ // wait for :port to be free (ss probe)
221
+ for (let i = 0; i < 20; i++) {
222
+ const chk = await execAsync("ss", ["-ltn"]);
223
+ if (!chk.stdout.includes(`:${port}`)) break;
224
+ if (i === 6 || i === 12) await killHolders();
225
+ await new Promise((r2) => setTimeout(r2, 250));
174
226
  }
175
227
  } catch {}
176
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