Files
iigzx 8d237baf9b fix(web): boot 前回收 detached 残留 server,修 EADDRINUSE 起不来 (#197)
- restart-web.js 把后台 server 的 pid/url 记到 \/dsh-purge/web-server-<port>.json
- killOldServer 增加按端口兜底:清掉旧版本留下、没有 pid 记录的孤儿
- 新增 lib/bin-preflight.js:把回收钩子注入宿主 bin.js 顶部,早于 Cordis 激活与 webserver 绑端口
- 重启时打印『后台运行、关窗不会停』,避免把提示符返回误判成程序已退出
- 新增 test/web-reclaim.test.js(7 例),全量 node --test 91/91 通过
2026-10-31 21:30:17 +08:00

118 lines
3.7 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import * as core from "./core.js";
export function restartNote() {
return "Restarting dsh web…";
}
export function portFromRequest(request) {
const host = String(request?.headers?.host || "");
const m = host.match(/:(\d+)$/);
if (m) return Number(m[1]);
return Number(process.env.PORT) || 3080;
}
export function hostFromRequest(request) {
const hostHeader = String(request?.headers?.host || "127.0.0.1");
return hostHeader.replace(/:\d+$/, "") || "127.0.0.1";
}
export function restartArgv({
argv = process.argv,
findBinRoot = core.findDshBinRoot,
} = {}) {
const root = findBinRoot();
const binJs = root ? path.join(root, "lib", "bin.js") : null;
if (binJs && fs.existsSync(binJs)) {
const rest = argv.slice(2).filter((arg) => arg !== "--no-open");
if (!rest.includes("web") && !rest.includes("desktop")) {
const port = Number(process.env.PORT) || 3080;
rest.unshift("web", "--port", String(port));
}
rest.push("--no-open");
return [binJs, ...rest];
}
const next = argv.slice(1).filter((arg) => arg !== "--no-open");
if (next[0] && /\.(cmd|bat)$/i.test(next[0])) {
const dir = path.dirname(next[0]);
const candidates = [
path.resolve(dir, "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
path.resolve(dir, "..", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"),
];
const hit = candidates.find((p) => fs.existsSync(p));
if (hit) next[0] = hit;
}
next.push("--no-open");
return next;
}
function spawnRestartHelper(helper, payload, exitAfter = 250) {
try {
core.silenceCmdFlash();
} catch {}
const child = spawn(process.execPath, [helper], {
detached: true,
stdio: "ignore",
windowsHide: true,
shell: false,
env: {
...core.envForDetachedSpawn(),
DSH_PURGE_RESTART: JSON.stringify(payload),
},
});
child.unref();
setTimeout(() => process.exit(0), exitAfter);
}
/**
* @param {object} [request]
* @param {unknown} [_ctx]
* @param {{ openBrowser?: boolean }} [options]
* settle / 自愈重启传 request=null → 默认不弹浏览器,避免关窗后仍被反复拉起新窗口(#59/#61)。
*/
export function scheduleRestart(request, _ctx, options = {}) {
const openBrowser = options.openBrowser ?? (request != null);
const helper = fileURLToPath(new URL("./restart-web.js", import.meta.url));
// #197:前台进程 250ms 后就会退出,终端回到提示符,但 server 在后台跑。
// 不把这句话打出来,用户会以为程序已经关掉,然后立刻再开一个 → EADDRINUSE。
try {
console.log(
"[dsh-purge] restarting dsh web in the background; closing this window will NOT stop it. " +
"The next `dsh web` reclaims it automatically.",
);
} catch {
/* ignore */
}
spawnRestartHelper(helper, {
kind: "web",
execPath: process.execPath,
argv: restartArgv(),
cwd: process.cwd(),
host: hostFromRequest(request),
port: portFromRequest(request),
pid: process.pid,
openBrowser: openBrowser !== false,
});
}
export function scheduleCleanupRestart({ request, restartArgv: argv, cleanup = [] } = {}) {
const src = fileURLToPath(new URL("./uninstall-restart.js", import.meta.url));
const dest = path.join(os.tmpdir(), `dsh-purge-uninst-${process.pid}.mjs`);
fs.copyFileSync(src, dest);
spawnRestartHelper(dest, {
kind: "web",
execPath: process.execPath,
argv: argv || restartArgv(),
cwd: process.cwd(),
host: hostFromRequest(request),
port: portFromRequest(request),
cleanup,
helper: dest,
openBrowser: request != null,
}, 400);
}