fix(release): make Windows Squirrel app stop idempotent

This commit is contained in:
coso
2026-09-26 11:49:28 +08:00
parent 25040fa620
commit a123afaf53
3 changed files with 81 additions and 8 deletions
@@ -195,19 +195,40 @@ export async function findReadyElectronUpdaterPage(pages) {
return null;
}
export async function stopInstalledApp(executable) {
const script = [
export function buildStopInstalledAppScript() {
const matchingProcesses =
"@(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and [String]::Equals([System.IO.Path]::GetFullPath($_.ExecutablePath), $target, [StringComparison]::OrdinalIgnoreCase) })";
return [
"$ErrorActionPreference = 'Stop'",
"$target = [System.IO.Path]::GetFullPath($env:LIME_TARGET_EXECUTABLE)",
"$processes = @(Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and [String]::Equals([System.IO.Path]::GetFullPath($_.ExecutablePath), $target, [StringComparison]::OrdinalIgnoreCase) })",
"$processes | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop }",
`$processes = ${matchingProcesses}`,
"$processes | ForEach-Object { try { Stop-Process -Id $_.ProcessId -Force -ErrorAction Stop } catch { if ($_.FullyQualifiedErrorId -notmatch '^NoProcessFoundForGivenId(?:,|$)') { throw } } }",
'Write-Output "stopped=$($processes.Count)"',
].join("; ");
const result = await runProcess(
}
export async function stopInstalledApp(
executable,
{
runProcessImpl = runProcess,
waitForProcessExitImpl = waitForWindowsProcessExit,
timeoutMs = 30_000,
} = {},
) {
const result = await runProcessImpl(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-Command", script],
[
"-NoProfile",
"-NonInteractive",
"-Command",
buildStopInstalledAppScript(),
],
{
env: { ...process.env, LIME_TARGET_EXECUTABLE: executable },
timeoutMs: 30_000,
env: {
...process.env,
LIME_TARGET_EXECUTABLE: executable,
},
timeoutMs,
},
);
if (result.exitCode !== 0) {
@@ -215,6 +236,10 @@ export async function stopInstalledApp(executable) {
`failed to stop installed app at ${executable}: exit ${result.exitCode}`,
);
}
await waitForProcessExitImpl(executable, {
runProcessImpl,
timeoutMs,
});
return { executable, exitCode: result.exitCode };
}
@@ -18,6 +18,7 @@ import process from "node:process";
import { fileURLToPath } from "node:url";
import {
buildNMinusOneLaunchEnv,
buildStopInstalledAppScript,
buildWaitForWindowsProcessExitScript,
classifySquirrelUninstallResult,
compareVersions,
@@ -39,6 +40,7 @@ import {
export {
buildNMinusOneLaunchEnv,
buildStopInstalledAppScript,
buildWaitForWindowsProcessExitScript,
classifySquirrelUninstallResult,
compareVersions,
@@ -48,6 +50,7 @@ export {
resolveInstalledSquirrelPaths,
resolveSquirrelFeed,
selectNMinusOneVersion,
stopInstalledApp,
uninstallInstalledSquirrel,
waitForWindowsProcessExit,
};
@@ -7,6 +7,7 @@ import YAML from "yaml";
import {
buildNMinusOneLaunchEnv,
buildStopInstalledAppScript,
buildWaitForWindowsProcessExitScript,
buildWindowsRcSummary,
cleanupFromSummary,
@@ -19,6 +20,7 @@ import {
resolveSquirrelFeed,
selectNMinusOneVersion,
selectSquirrelInstaller,
stopInstalledApp,
uninstallInstalledSquirrel,
waitForWindowsProcessExit,
} from "./windows-squirrel-rc-smoke.mjs";
@@ -160,6 +162,49 @@ describe("Windows Squirrel RC smoke", () => {
);
});
it("停止已退出的应用时忽略 PID 竞态,但保留其他 Stop-Process 错误", async () => {
const script = buildStopInstalledAppScript();
expect(script).toContain("NoProcessFoundForGivenId");
expect(script).toContain("FullyQualifiedErrorId");
expect(script).toContain("throw");
const runProcessImpl = vi.fn().mockResolvedValue({ exitCode: 0 });
const waitForProcessExitImpl = vi.fn().mockResolvedValue({
executable: "C:\\runner\\Lime.exe",
exitCode: 0,
timeoutMs: 30_000,
});
await expect(
stopInstalledApp("C:\\runner\\Lime.exe", {
runProcessImpl,
waitForProcessExitImpl,
}),
).resolves.toEqual({
executable: "C:\\runner\\Lime.exe",
exitCode: 0,
});
expect(runProcessImpl).toHaveBeenCalledWith(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-Command",
script,
],
expect.objectContaining({
env: expect.objectContaining({
LIME_TARGET_EXECUTABLE: "C:\\runner\\Lime.exe",
}),
timeoutMs: 30_000,
}),
);
expect(waitForProcessExitImpl).toHaveBeenCalledWith(
"C:\\runner\\Lime.exe",
{ runProcessImpl, timeoutMs: 30_000 },
);
});
it("卸载只操作候选 Update.exe,并等待安装目录与快捷方式消失", async () => {
const updateExecutable =
"C:\\Users\\runner\\AppData\\Local\\lime\\Update.exe";