473 lines
14 KiB
JavaScript
473 lines
14 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
|
|
const encoded = process.env.DSH_PURGE_RESTART;
|
|
if (!encoded) process.exit(1);
|
|
|
|
let info;
|
|
try {
|
|
info = JSON.parse(encoded);
|
|
} catch {
|
|
process.exit(1);
|
|
}
|
|
|
|
const cleanup = Array.isArray(info.cleanup) ? info.cleanup : [];
|
|
const logPath = path.join(
|
|
process.env.TEMP || process.env.TMPDIR || "/tmp",
|
|
"dsh-purge-restart-desktop.log",
|
|
);
|
|
|
|
function log(msg) {
|
|
try {
|
|
fs.appendFileSync(logPath, `${new Date().toISOString()} ${msg}\n`);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
function spawnEnv() {
|
|
const env = { ...process.env };
|
|
delete env.DSH_PERMISSION_MODE;
|
|
delete env.DSH_PURGE_RESTART;
|
|
for (const key of Object.keys(env)) {
|
|
if (key.toUpperCase() === "ELECTRON_RUN_AS_NODE") delete env[key];
|
|
}
|
|
// [dsh-purge-fix 2026-10-09] 从 explorer 继承的 env 常缺 DSH_PROFILE
|
|
// (WM_SETTINGCHANGE broadcast 不刷新存活很久的 explorer)。helper relaunch
|
|
// 时如果不兜底,新 Electron → host 子进程继续缺,报 "cannot determine the
|
|
// boot profile" host crash → dsh-purge 插件没 activate → inject 全断。
|
|
if (!env.DSH_PROFILE) env.DSH_PROFILE = "desktop";
|
|
return env;
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
function pidAlive(pid) {
|
|
const n = Number(pid);
|
|
if (!n) return false;
|
|
try {
|
|
process.kill(n, 0);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function waitPidGone(pid) {
|
|
if (!pid) {
|
|
await sleep(400);
|
|
return true;
|
|
}
|
|
for (let i = 0; i < 80; i++) {
|
|
if (!pidAlive(pid)) return true;
|
|
await sleep(120);
|
|
}
|
|
return !pidAlive(pid);
|
|
}
|
|
|
|
function rmTree(dir) {
|
|
if (!dir) return;
|
|
try {
|
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 8, retryDelay: 120 });
|
|
} catch {
|
|
// 锁着就留下
|
|
}
|
|
}
|
|
|
|
function pidsForImage(image) {
|
|
const out = execFileSync("tasklist", ["/FI", `IMAGENAME eq ${image}`, "/FO", "CSV", "/NH"], {
|
|
encoding: "utf8",
|
|
windowsHide: true,
|
|
timeout: 8000,
|
|
});
|
|
const pids = [];
|
|
for (const line of String(out || "").split(/\r?\n/)) {
|
|
const cols = line.split(",");
|
|
const pid = Number(String(cols[1] || "").replace(/"/g, "").trim());
|
|
if (pid) pids.push(pid);
|
|
}
|
|
return pids;
|
|
}
|
|
|
|
function darwinBundleOf(exe) {
|
|
const n = String(exe || "").replace(/\\/g, "/");
|
|
const m = n.match(/^(.*\.app)\/Contents\/MacOS\//i);
|
|
return m ? m[1] : "";
|
|
}
|
|
|
|
function darwinBundleName(bundle) {
|
|
if (!bundle) return "";
|
|
const infoPlist = path.join(bundle, "Contents", "Info.plist");
|
|
try {
|
|
return execFileSync("defaults", ["read", path.join(bundle, "Contents", "Info"), "CFBundleName"], {
|
|
encoding: "utf8",
|
|
timeout: 5000,
|
|
}).trim();
|
|
} catch {
|
|
// fall through
|
|
}
|
|
try {
|
|
return execFileSync("plutil", ["-extract", "CFBundleName", "raw", infoPlist], {
|
|
encoding: "utf8",
|
|
timeout: 5000,
|
|
}).trim();
|
|
} catch {
|
|
return path.basename(bundle, ".app");
|
|
}
|
|
}
|
|
|
|
function listDarwinPids(exe) {
|
|
const bundle = darwinBundleOf(exe);
|
|
if (!exe || !bundle) return [];
|
|
let out = "";
|
|
try {
|
|
out = execFileSync("ps", ["-Ao", "pid=,command="], {
|
|
encoding: "utf8",
|
|
timeout: 8000,
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
const frameworks = `${bundle}/Contents/Frameworks/`;
|
|
const pids = [];
|
|
for (const line of String(out || "").split(/\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const sp = trimmed.search(/\s/);
|
|
if (sp < 0) continue;
|
|
const pid = Number(trimmed.slice(0, sp).trim());
|
|
const cmd = trimmed.slice(sp + 1).trim();
|
|
if (!pid) continue;
|
|
// CLI 独立宿主(dsh web / plugin 等),不占桌面单实例锁,不能一起杀。
|
|
if (cmd.includes("/dsh-desktop-host/lib/cli.js")) continue;
|
|
if (cmd === exe || cmd.startsWith(`${exe} `) || cmd.startsWith(frameworks)) {
|
|
pids.push(pid);
|
|
}
|
|
}
|
|
return [...new Set(pids)];
|
|
}
|
|
|
|
function commandMatchesExe(cmd, exe) {
|
|
if (!cmd || !exe) return false;
|
|
if (cmd.includes("/dsh-desktop-host/lib/cli.js")) return false;
|
|
return cmd === exe || cmd.startsWith(`${exe} `) || cmd.startsWith(`${exe}\t`);
|
|
}
|
|
|
|
function listPosixPids(exe) {
|
|
if (!exe) return [];
|
|
let out = "";
|
|
try {
|
|
out = execFileSync("ps", ["-Ao", "pid=,command="], {
|
|
encoding: "utf8",
|
|
timeout: 8000,
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
const pids = [];
|
|
for (const line of String(out || "").split(/\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const sp = trimmed.search(/\s/);
|
|
if (sp < 0) continue;
|
|
const pid = Number(trimmed.slice(0, sp).trim());
|
|
const cmd = trimmed.slice(sp + 1).trim();
|
|
if (!pid || !commandMatchesExe(cmd, exe)) continue;
|
|
pids.push(pid);
|
|
}
|
|
return [...new Set(pids)];
|
|
}
|
|
|
|
function listDesktopPids(exe = info.execPath) {
|
|
if (process.platform === "darwin") return listDarwinPids(exe);
|
|
if (process.platform !== "win32") return listPosixPids(exe);
|
|
const images = ["DeepSeek Harness.exe", "DSH Desktop.exe"];
|
|
const pids = [];
|
|
for (const image of images) {
|
|
try {
|
|
pids.push(...pidsForImage(image));
|
|
} catch {
|
|
// 该进程名不存在
|
|
}
|
|
}
|
|
return pids;
|
|
}
|
|
|
|
function signalPids(pids, signal) {
|
|
for (const pid of pids) {
|
|
if (pid === process.pid) continue;
|
|
try {
|
|
process.kill(pid, signal);
|
|
} catch {
|
|
// 已经退了
|
|
}
|
|
}
|
|
}
|
|
|
|
async function waitDesktopGone(exe, timeoutMs = 8000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
const left = listDesktopPids(exe).filter((pid) => pid !== process.pid);
|
|
if (left.length === 0) return true;
|
|
await sleep(200);
|
|
}
|
|
return listDesktopPids(exe).filter((pid) => pid !== process.pid).length === 0;
|
|
}
|
|
|
|
async function quitPosixDesktop(exe) {
|
|
signalPids(listDesktopPids(exe), "SIGTERM");
|
|
if (await waitDesktopGone(exe, 8000)) {
|
|
log("quit by SIGTERM");
|
|
return "SIGTERM";
|
|
}
|
|
signalPids(listDesktopPids(exe), "SIGKILL");
|
|
await waitDesktopGone(exe, 2000);
|
|
log("quit by SIGKILL");
|
|
return "SIGKILL";
|
|
}
|
|
|
|
async function quitDarwinDesktop(exe) {
|
|
const bundle = darwinBundleOf(exe);
|
|
const name = darwinBundleName(bundle);
|
|
if (name) {
|
|
try {
|
|
execFileSync(
|
|
"osascript",
|
|
["-e", `if application "${name}" is running then tell application "${name}" to quit`],
|
|
{ timeout: 15000 },
|
|
);
|
|
log("osascript quit sent");
|
|
} catch (e) {
|
|
log(`osascript quit failed: ${e && e.message ? e.message : e}`);
|
|
}
|
|
if (await waitDesktopGone(exe, 8000)) {
|
|
log("quit by apple event");
|
|
return "apple-event";
|
|
}
|
|
}
|
|
signalPids(listDesktopPids(exe), "SIGTERM");
|
|
if (await waitDesktopGone(exe, 5000)) {
|
|
log("quit by SIGTERM");
|
|
return "SIGTERM";
|
|
}
|
|
signalPids(listDesktopPids(exe), "SIGKILL");
|
|
await waitDesktopGone(exe, 2000);
|
|
log("quit by SIGKILL");
|
|
return "SIGKILL";
|
|
}
|
|
|
|
function killOtherDesktopProcesses(keepPid, exe = info.execPath) {
|
|
for (const pid of listDesktopPids(exe)) {
|
|
if (pid === keepPid || pid === process.pid) continue;
|
|
try {
|
|
process.kill(pid, process.platform === "darwin" ? "SIGTERM" : undefined);
|
|
} catch {
|
|
// 已经退了
|
|
}
|
|
}
|
|
}
|
|
|
|
async function moveAsarAsideAsync(asar) {
|
|
if (!asar) return false;
|
|
try {
|
|
if (!fs.existsSync(asar) || !fs.statSync(asar).isFile()) return true;
|
|
} catch {
|
|
return true;
|
|
}
|
|
const bak = `${asar}.bak`;
|
|
try {
|
|
if (fs.existsSync(bak)) fs.rmSync(bak, { force: true });
|
|
} catch {
|
|
// keep going
|
|
}
|
|
for (let i = 0; i < 80; i++) {
|
|
try {
|
|
if (!fs.existsSync(asar) || !fs.statSync(asar).isFile()) {
|
|
log("asar already absent");
|
|
return true;
|
|
}
|
|
fs.renameSync(asar, bak);
|
|
log(`asar moved to ${bak}`);
|
|
return true;
|
|
} catch {
|
|
await sleep(300);
|
|
}
|
|
}
|
|
log("asar move failed after retries");
|
|
return false;
|
|
}
|
|
|
|
function asarStillSealed(asar) {
|
|
try {
|
|
return Boolean(asar) && fs.existsSync(asar) && fs.statSync(asar).isFile();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function releaseSealedAsar(asar, exe) {
|
|
if (!asar) return true;
|
|
if (await moveAsarAsideAsync(asar)) return true;
|
|
log("retry quit then move asar");
|
|
if (process.platform === "darwin") await quitDarwinDesktop(exe);
|
|
else if (process.platform === "win32") {
|
|
killOtherDesktopProcesses(process.pid, exe);
|
|
await sleep(800);
|
|
} else await quitPosixDesktop(exe);
|
|
if (await moveAsarAsideAsync(asar)) return true;
|
|
return !asarStillSealed(asar);
|
|
}
|
|
|
|
function relaunchDesktop() {
|
|
if (!info.execPath) {
|
|
process.exit(1);
|
|
}
|
|
const env = spawnEnv();
|
|
if (process.platform === "darwin") {
|
|
const bundle = darwinBundleOf(info.execPath);
|
|
if (bundle) {
|
|
const child = spawn("open", [bundle], {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
env,
|
|
shell: false,
|
|
});
|
|
child.on("error", () => process.exit(1));
|
|
child.unref();
|
|
log("relaunched via open");
|
|
return;
|
|
}
|
|
}
|
|
// [dsh-purge-fix 2026-10-09] 复用 Electron 自带的 --updated argv 分支:
|
|
// main.js:11256 process.argv.includes("--updated") → raiseAfterUpdate=true
|
|
// → enterWorkspace 时 window.moveTop() + window.focus() 内部强制前台。
|
|
// 比外部 PS SetForegroundWindow 可靠得多 (Win11 前台保护只挡外部进程)。
|
|
const baseArgv = Array.isArray(info.argv) ? info.argv.slice() : [];
|
|
if (process.platform === "win32" && !baseArgv.includes("--updated")) {
|
|
baseArgv.push("--updated");
|
|
}
|
|
const child = spawn(info.execPath, baseArgv, {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
cwd: info.cwd || process.cwd(),
|
|
env,
|
|
windowsHide: true,
|
|
shell: false,
|
|
});
|
|
child.on("error", () => process.exit(1));
|
|
child.unref();
|
|
log("relaunched via execPath (argv=" + JSON.stringify(baseArgv) + ")");
|
|
|
|
// [dsh-purge-fix 2026-10-09] Windows: 新 Electron 从 detached helper spawn,
|
|
// Windows 焦点规则让它跑在后台。helper 已经不是前台(老 Electron 被 kill 了),
|
|
// 直接 SetForegroundWindow 会被 Windows 静默 reject。
|
|
// 三重 trick 强推前台:
|
|
// 1) keybd_event 发送虚拟 Alt key — 模拟用户输入,解除 foreground lock
|
|
// 2) AttachThreadInput attach 当前前台线程的 input queue — 继承前台权限
|
|
// 3) ShowWindow(SW_RESTORE) + BringWindowToTop + SetForegroundWindow
|
|
if (process.platform === "win32") {
|
|
const exeLiteral = info.execPath.replace(/'/g, "''");
|
|
const psScript =
|
|
"$ErrorActionPreference='SilentlyContinue';" +
|
|
"$sig=@'\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr h);\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern bool ShowWindow(IntPtr h,int c);\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern bool BringWindowToTop(IntPtr h);\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern IntPtr GetForegroundWindow();\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern uint GetWindowThreadProcessId(IntPtr h,out uint pid);\n" +
|
|
"[DllImport(\"kernel32.dll\")] public static extern uint GetCurrentThreadId();\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern bool AttachThreadInput(uint a,uint b,bool f);\n" +
|
|
"[DllImport(\"user32.dll\")] public static extern void keybd_event(byte k,byte s,uint f,int e);\n" +
|
|
"'@;" +
|
|
"$t=Add-Type -MemberDefinition $sig -Name W -Namespace U -PassThru;" +
|
|
"[void]$t::keybd_event(0x12,0,0,0);" +
|
|
"[void]$t::keybd_event(0x12,0,2,0);" +
|
|
"for($i=0;$i -lt 25;$i++){" +
|
|
" Start-Sleep -Milliseconds 300;" +
|
|
" $p=Get-Process | Where-Object {$_.Path -eq '" + exeLiteral + "' -and $_.MainWindowHandle -ne 0} | Select-Object -First 1;" +
|
|
" if($p){" +
|
|
" $h=$p.MainWindowHandle;" +
|
|
" $fg=$t::GetForegroundWindow();" +
|
|
" $pid2=0;" +
|
|
" $fgTid=$t::GetWindowThreadProcessId($fg,[ref]$pid2);" +
|
|
" $curTid=$t::GetCurrentThreadId();" +
|
|
" [void]$t::AttachThreadInput($curTid,$fgTid,$true);" +
|
|
" [void]$t::ShowWindow($h,9);" +
|
|
" [void]$t::BringWindowToTop($h);" +
|
|
" [void]$t::SetForegroundWindow($h);" +
|
|
" [void]$t::AttachThreadInput($curTid,$fgTid,$false);" +
|
|
" break;" +
|
|
" }" +
|
|
"}";
|
|
try {
|
|
const ps = spawn("powershell.exe", [
|
|
"-NoProfile", "-WindowStyle", "Hidden", "-ExecutionPolicy", "Bypass",
|
|
"-Command", psScript,
|
|
], { detached: true, stdio: "ignore", windowsHide: true, shell: false });
|
|
// detached + unref 后 ENOENT 等异步错走 'error' 事件,外层 try/catch 捕不到 → uncaught 崩进程。
|
|
ps.on("error", (err) => log("ps bring-to-front spawn error: " + String(err?.message || err)));
|
|
ps.unref();
|
|
log("spawned ps bring-to-front helper (keybd+attach+setfg)");
|
|
} catch (err) {
|
|
log("ps bring-to-front spawn failed: " + String(err));
|
|
}
|
|
}
|
|
}
|
|
|
|
let asarReleased = true;
|
|
if (info.killApp) {
|
|
// #85: 先杀 Electron,再 sleep + cleanup。原顺序 sleep 400ms 开头 →
|
|
// host 已经 exit → Electron 还活着跑旧 close 回调弹 crash dialog。
|
|
// 新顺序:立刻 kill (host 这边 killInstallElectronSync 已经杀过,这里兜底),
|
|
// Electron 死得越快,旧回调越没机会跑。
|
|
if (process.platform === "darwin") {
|
|
await quitDarwinDesktop(info.execPath);
|
|
} else if (process.platform === "win32") {
|
|
killOtherDesktopProcesses(process.pid);
|
|
await sleep(500);
|
|
// 二次保险:再杀一次,防第一次漏掉的 pid
|
|
killOtherDesktopProcesses(process.pid);
|
|
} else {
|
|
await quitPosixDesktop(info.execPath);
|
|
}
|
|
for (const dir of cleanup) rmTree(dir);
|
|
if (info.helper) {
|
|
try {
|
|
fs.unlinkSync(info.helper);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (info.asar) asarReleased = await releaseSealedAsar(info.asar, info.execPath);
|
|
} else {
|
|
const gone = await waitPidGone(info.pid);
|
|
if (!gone) process.exit(1);
|
|
for (const dir of cleanup) rmTree(dir);
|
|
if (info.helper) {
|
|
try {
|
|
fs.unlinkSync(info.helper);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (info.asar) asarReleased = await releaseSealedAsar(info.asar, info.execPath);
|
|
}
|
|
|
|
if (info.respawn === false) {
|
|
process.exit(asarReleased ? 0 : 1);
|
|
}
|
|
|
|
if (info.asar && !asarReleased) {
|
|
log("asar still sealed; not relaunching");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!info.execPath) process.exit(1);
|
|
|
|
relaunchDesktop();
|
|
await sleep(400);
|
|
process.exit(0);
|