mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
Fix Windows installer cleanup and preflight
This commit is contained in:
@@ -128,6 +128,7 @@ RequestExecutionLevel user
|
||||
!include "MUI2.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
!include "nsDialogs.nsh"
|
||||
!include "WinMessages.nsh"
|
||||
|
||||
Name "${productName}"
|
||||
OutFile "\${OUTPUT_EXE}"
|
||||
@@ -141,6 +142,7 @@ ShowUninstDetails hide
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_ICON "\${APP_ICON}"
|
||||
!define MUI_UNICON "\${APP_ICON}"
|
||||
Page custom RunningInstancesPage RunningInstancesPageLeave
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE DirectoryPageLeave
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
@@ -163,7 +165,11 @@ ${createNsisLangString("RemoveDesktopShortcut", "Remove desktop shortcut", { LAN
|
||||
${createNsisLangString("RemoveLocalData", "Delete local data for this installation", { LANG_SIMPCHINESE: "删除此安装的本地数据" })}
|
||||
${createNsisLangString("UninstallOptionsTitle", "Uninstall options", { LANG_SIMPCHINESE: "卸载选项" })}
|
||||
${createNsisLangString("UninstallOptionsSubtitle", "Choose which local items to remove.", { LANG_SIMPCHINESE: "选择要删除的本地项目。" })}
|
||||
${createNsisLangString("RunningInstancesMessage", `${productName} is still running. Close all ${productName} windows and background processes, then choose Retry.`, { LANG_SIMPCHINESE: `${productName} 仍在运行。请关闭所有 ${productName} 窗口和后台进程,然后选择重试。` })}
|
||||
${createNsisLangString("RunningInstancesTitle", `${productName} is still running`, { LANG_SIMPCHINESE: `${productName} 仍在运行` })}
|
||||
${createNsisLangString("RunningInstancesSubtitle", "Close it before continuing installation.", { LANG_SIMPCHINESE: "继续安装前需要关闭它。" })}
|
||||
${createNsisLangString("RunningInstancesMessage", `${productName} must be closed before installation can continue.`, { LANG_SIMPCHINESE: `继续安装前需要关闭 ${productName}。` })}
|
||||
${createNsisLangString("CloseAndContinue", "Close and continue", { LANG_SIMPCHINESE: "关闭并继续" })}
|
||||
${createNsisLangString("RunningInstancesCloseFailed", `${productName} could not be closed. Close it manually, then try again.`, { LANG_SIMPCHINESE: `无法关闭 ${productName}。请手动关闭后重试。` })}
|
||||
${createNsisLangString("RunningInstancesSilentAbort", `${productName} is still running. Close it before running the installer silently.`, { LANG_SIMPCHINESE: `${productName} 仍在运行。请先关闭它,再运行静默安装。` })}
|
||||
${createNsisLangString("ExistingInstallMessage", `${productName} is already installed in the selected folder. Choose OK to overwrite it, or Cancel to stop installation.`, { LANG_SIMPCHINESE: `所选文件夹中已经安装了 ${productName}。选择确定覆盖,或取消安装。` })}
|
||||
${createNsisLangString("ExistingInstallSilentOverwrite", "Existing installation found; silent install will overwrite it.", { LANG_SIMPCHINESE: "发现已有安装;静默安装将覆盖它。" })}
|
||||
@@ -173,6 +179,7 @@ Var RemoveLocalDataCheckbox
|
||||
Var RemoveDesktopShortcutState
|
||||
Var RemoveLocalDataState
|
||||
Var RunningInstancesOutput
|
||||
Var ExistingInstallLocation
|
||||
|
||||
Function LogInstallerEvent
|
||||
Exch $0
|
||||
@@ -208,7 +215,7 @@ FunctionEnd
|
||||
Function DetectRunningInstances
|
||||
Push $0
|
||||
Push $1
|
||||
nsExec::ExecToStack 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$ns = ''${namespace}''; $flag = ''--od-stamp-namespace='' + $ns; $install = ''$INSTDIR''.ToLowerInvariant(); $matches = Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and ( $_.CommandLine.Contains($flag) -or ( $install.Length -gt 0 -and $_.ExecutablePath -and $_.ExecutablePath.ToLowerInvariant().StartsWith($install) ) ) }; if ($matches) { ($matches | ForEach-Object { [string]$_.ProcessId + '' '' + $_.Name }) -join ''; '' }"'
|
||||
nsExec::ExecToStack 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { param($$install, $$registered); $$paths = @($$install, $$registered) | Where-Object { $$_ } | ForEach-Object { $$_.TrimEnd([char]92).ToLowerInvariant() } | Select-Object -Unique; $$matches = Get-CimInstance Win32_Process | Where-Object { $$matched = $$false; $$exe = $$_.ExecutablePath; if ($$null -ne $$exe) { $$exe = $$exe.ToLowerInvariant(); foreach ($$path in $$paths) { if ($$path -and $$exe.StartsWith($$path)) { $$matched = $$true; break } } }; $$matched }; if ($$matches) { $$matches | ForEach-Object { [string]$$_.ProcessId + [char]32 + $$_.Name } } }" "$INSTDIR" "$ExistingInstallLocation"'
|
||||
Pop $0
|
||||
Pop $1
|
||||
\${If} $0 == "0"
|
||||
@@ -222,18 +229,29 @@ Function DetectRunningInstances
|
||||
Pop $0
|
||||
FunctionEnd
|
||||
|
||||
Function CloseRunningInstances
|
||||
Push $0
|
||||
Push $1
|
||||
nsExec::ExecToStack 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { param($$install, $$registered); $$paths = @($$install, $$registered) | Where-Object { $$_ } | ForEach-Object { $$_.TrimEnd([char]92).ToLowerInvariant() } | Select-Object -Unique; $$matches = Get-CimInstance Win32_Process | Where-Object { $$matched = $$false; $$exe = $$_.ExecutablePath; if ($$null -ne $$exe) { $$exe = $$exe.ToLowerInvariant(); foreach ($$path in $$paths) { if ($$path -and $$exe.StartsWith($$path)) { $$matched = $$true; break } } }; $$matched }; $$ids = @($$matches | ForEach-Object { $$_.ProcessId }); foreach ($$id in $$ids) { try { [void][System.Diagnostics.Process]::GetProcessById($$id).CloseMainWindow() } catch {} }; Start-Sleep -Milliseconds 1500; foreach ($$id in $$ids) { try { $$p = [System.Diagnostics.Process]::GetProcessById($$id); if (-not $$p.HasExited) { Stop-Process -Id $$id -Force -ErrorAction SilentlyContinue } } catch {} }; if ($$ids) { $$ids -join ([char]32) } }" "$INSTDIR" "$ExistingInstallLocation"'
|
||||
Pop $0
|
||||
Pop $1
|
||||
Push "running instances close exit=$0 output=$1"
|
||||
Call LogInstallerEvent
|
||||
Pop $1
|
||||
Pop $0
|
||||
FunctionEnd
|
||||
|
||||
Function .onInit
|
||||
SetShellVarContext current
|
||||
ReadRegStr $ExistingInstallLocation HKCU "${registryKey}" "InstallLocation"
|
||||
|
||||
check_running:
|
||||
IfSilent silent_check no_existing_install
|
||||
silent_check:
|
||||
Call DetectRunningInstances
|
||||
\${If} $RunningInstancesOutput != ""
|
||||
IfSilent 0 interactive_running
|
||||
Push "install aborted: running instances detected: $RunningInstancesOutput"
|
||||
Call LogInstallerEvent
|
||||
Abort "$(RunningInstancesSilentAbort)"
|
||||
interactive_running:
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(RunningInstancesMessage)$\\r$\\n$\\r$\\n$RunningInstancesOutput" IDRETRY check_running IDCANCEL cancel_install
|
||||
Push "install aborted: running instances detected: $RunningInstancesOutput"
|
||||
Call LogInstallerEvent
|
||||
Abort "$(RunningInstancesSilentAbort)"
|
||||
\${EndIf}
|
||||
|
||||
IfFileExists "$INSTDIR\\${exeName}" existing_install no_existing_install
|
||||
@@ -251,6 +269,45 @@ cancel_install:
|
||||
no_existing_install:
|
||||
FunctionEnd
|
||||
|
||||
Function RunningInstancesPage
|
||||
IfSilent done
|
||||
Call DetectRunningInstances
|
||||
\${If} $RunningInstancesOutput == ""
|
||||
Abort
|
||||
\${EndIf}
|
||||
Push "running instances detected before install: $RunningInstancesOutput"
|
||||
Call LogInstallerEvent
|
||||
|
||||
!insertmacro MUI_HEADER_TEXT "$(RunningInstancesTitle)" "$(RunningInstancesSubtitle)"
|
||||
nsDialogs::Create 1018
|
||||
Pop $0
|
||||
\${If} $0 == error
|
||||
Abort
|
||||
\${EndIf}
|
||||
|
||||
\${NSD_CreateLabel} 0 0 100% 36u "$(RunningInstancesMessage)"
|
||||
Pop $0
|
||||
|
||||
GetDlgItem $0 $HWNDPARENT 1
|
||||
SendMessage $0 \${WM_SETTEXT} 0 "STR:$(CloseAndContinue)"
|
||||
GetDlgItem $0 $HWNDPARENT 3
|
||||
ShowWindow $0 0
|
||||
|
||||
nsDialogs::Show
|
||||
done:
|
||||
FunctionEnd
|
||||
|
||||
Function RunningInstancesPageLeave
|
||||
Call CloseRunningInstances
|
||||
Call DetectRunningInstances
|
||||
\${If} $RunningInstancesOutput != ""
|
||||
Push "running instances still detected after close: $RunningInstancesOutput"
|
||||
Call LogInstallerEvent
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION "$(RunningInstancesCloseFailed)"
|
||||
Abort
|
||||
\${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function DirectoryPageLeave
|
||||
IfSilent done
|
||||
IfFileExists "$INSTDIR\\${exeName}" existing_install done
|
||||
@@ -265,12 +322,13 @@ FunctionEnd
|
||||
|
||||
Function CreateDesktopShortcut
|
||||
SetShellVarContext current
|
||||
SetOutPath "$INSTDIR"
|
||||
CreateShortCut "$DESKTOP\\${shortcutName}" "$INSTDIR\\${exeName}" "" "$INSTDIR\\${exeName}" 0
|
||||
FunctionEnd
|
||||
|
||||
Function RemoveInstallDir
|
||||
Push $0
|
||||
nsExec::ExecToLog 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "if (Test-Path -LiteralPath ''$INSTDIR'') { Remove-Item -LiteralPath ''$INSTDIR'' -Recurse -Force -ErrorAction SilentlyContinue }"'
|
||||
nsExec::ExecToLog 'cmd.exe /d /s /c if exist "$INSTDIR" rmdir /s /q "$INSTDIR"'
|
||||
Pop $0
|
||||
Push "install dir remove exit=$0"
|
||||
Call LogInstallerEvent
|
||||
@@ -308,7 +366,7 @@ FunctionEnd
|
||||
|
||||
Function un.RemoveInstallDirContents
|
||||
Push $0
|
||||
nsExec::ExecToLog 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "if (Test-Path -LiteralPath ''$INSTDIR'') { Get-ChildItem -LiteralPath ''$INSTDIR'' -Force | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }"'
|
||||
nsExec::ExecToLog 'cmd.exe /d /s /c if exist "$INSTDIR" rmdir /s /q "$INSTDIR"'
|
||||
Pop $0
|
||||
Push "install dir fast remove exit=$0"
|
||||
Call un.LogInstallerEvent
|
||||
@@ -317,7 +375,7 @@ FunctionEnd
|
||||
|
||||
Function un.RemoveLocalDataRoot
|
||||
Push $0
|
||||
nsExec::ExecToLog 'powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "if (Test-Path -LiteralPath ''${localDataRoot}'') { Remove-Item -LiteralPath ''${localDataRoot}'' -Recurse -Force -ErrorAction SilentlyContinue }"'
|
||||
nsExec::ExecToLog 'cmd.exe /d /s /c if exist "${localDataRoot}" rmdir /s /q "${localDataRoot}"'
|
||||
Pop $0
|
||||
Push "local data remove exit=$0"
|
||||
Call un.LogInstallerEvent
|
||||
@@ -352,6 +410,7 @@ prepare_install_dir:
|
||||
\${EndIf}
|
||||
|
||||
WriteUninstaller "$INSTDIR\\${uninstallerName}"
|
||||
SetOutPath "$INSTDIR"
|
||||
IfSilent 0 skip_silent_desktop_shortcut
|
||||
CreateShortCut "$DESKTOP\\${shortcutName}" "$INSTDIR\\${exeName}" "" "$INSTDIR\\${exeName}" 0
|
||||
skip_silent_desktop_shortcut:
|
||||
@@ -371,9 +430,15 @@ Section "Uninstall"
|
||||
SetShellVarContext current
|
||||
Push "uninstall section start"
|
||||
Call un.LogInstallerEvent
|
||||
IfSilent delete_desktop_shortcut check_desktop_shortcut_state
|
||||
check_desktop_shortcut_state:
|
||||
\${If} $RemoveDesktopShortcutState == \${BST_CHECKED}
|
||||
Delete "$DESKTOP\\${shortcutName}"
|
||||
\${EndIf}
|
||||
Goto after_desktop_shortcut
|
||||
delete_desktop_shortcut:
|
||||
Delete "$DESKTOP\\${shortcutName}"
|
||||
after_desktop_shortcut:
|
||||
Delete "$SMPROGRAMS\\${shortcutName}"
|
||||
DeleteRegKey HKCU "${registryKey}"
|
||||
DeleteRegKey HKCU "${appPathsKey}"
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
resolveWinProductNamespaceRoot,
|
||||
resolveWinProductUserDataRoot,
|
||||
} from "./paths.js";
|
||||
import { cleanupWinRegistryResidues, queryWinRegistryEntries } from "./registry.js";
|
||||
import { cleanupWinRegistryResidues, queryWinRegistryEntries, resolveWinRegisteredPaths } from "./registry.js";
|
||||
import type {
|
||||
WinCleanupResult,
|
||||
WinInspectResult,
|
||||
@@ -137,7 +137,7 @@ async function observeWinResidues(config: ToolPackConfig, paths = resolveWinPath
|
||||
productNamespaceRootExists: await pathExists(resolveWinProductNamespaceRoot(config)),
|
||||
productUserDataRootExists: await pathExists(resolveWinProductUserDataRoot()),
|
||||
publicDesktopShortcutExists: await pathExists(paths.publicDesktopShortcutPath),
|
||||
registryResidues: (await queryWinRegistryEntries(paths)).map((entry) => entry.keyPath),
|
||||
registryResidues: (await queryWinRegistryEntries(paths, config)).map((entry) => entry.keyPath),
|
||||
runtimeNamespaceRootExists: await pathExists(config.roots.runtime.namespaceRoot),
|
||||
startMenuShortcutExists: await pathExists(paths.startMenuShortcutPath),
|
||||
uninstallerExists: await pathExists(paths.uninstallerPath),
|
||||
@@ -147,18 +147,19 @@ async function observeWinResidues(config: ToolPackConfig, paths = resolveWinPath
|
||||
|
||||
export async function installPackedWinApp(config: ToolPackConfig): Promise<WinInstallResult> {
|
||||
const paths = resolveWinPaths(config);
|
||||
const registeredPaths = await resolveWinRegisteredPaths(config, paths);
|
||||
if (!(await pathExists(paths.setupPath))) throw new Error(`no windows installer found at ${paths.setupPath}; run tools-pack win build first`);
|
||||
if (await pathExists(paths.uninstallerPath)) {
|
||||
if (await pathExists(registeredPaths.uninstallerPath)) {
|
||||
await uninstallPackedWinApp(config);
|
||||
} else {
|
||||
await removeTree(paths.installDir);
|
||||
await removeTree(registeredPaths.installDir);
|
||||
}
|
||||
await mkdir(dirname(paths.installDir), { recursive: true });
|
||||
await runTimed(paths.installTimingPath, "install", async () => {
|
||||
await invokeNsis(paths, paths.setupPath, installArgs(config, paths), "install");
|
||||
});
|
||||
if (!(await pathExists(paths.installedExePath))) throw new Error(`installer completed but executable is missing at ${paths.installedExePath}`);
|
||||
const registryEntries = await queryWinRegistryEntries(paths);
|
||||
const registryEntries = await queryWinRegistryEntries(paths, config);
|
||||
const installPayload = await collectInstallPayloadReport(paths);
|
||||
await writeJsonMarker(paths.installMarkerPath, {
|
||||
installedAt: new Date().toISOString(),
|
||||
@@ -283,14 +284,15 @@ export async function readPackedWinLogs(config: ToolPackConfig) {
|
||||
|
||||
export async function uninstallPackedWinApp(config: ToolPackConfig): Promise<WinUninstallResult> {
|
||||
const paths = resolveWinPaths(config);
|
||||
const registeredPaths = await resolveWinRegisteredPaths(config, paths);
|
||||
const stop = await stopPackedWinApp(config);
|
||||
if (await pathExists(paths.uninstallerPath)) {
|
||||
if (await pathExists(registeredPaths.uninstallerPath)) {
|
||||
await runTimed(paths.uninstallTimingPath, "uninstall", async () => {
|
||||
await invokeNsis(paths, paths.uninstallerPath, config.silent ? ["/S"] : [], "uninstall");
|
||||
await invokeNsis(paths, registeredPaths.uninstallerPath, config.silent ? ["/S"] : [], "uninstall");
|
||||
});
|
||||
}
|
||||
await removeTree(paths.installDir);
|
||||
const registryResiduesRemoved = await cleanupWinRegistryResidues(paths);
|
||||
await removeTree(registeredPaths.installDir);
|
||||
const registryResiduesRemoved = await cleanupWinRegistryResidues(registeredPaths, config);
|
||||
const removalPlan = await createWinRemovalPlan(config);
|
||||
await writeJsonMarker(paths.uninstallMarkerPath, {
|
||||
namespace: config.namespace,
|
||||
@@ -315,24 +317,25 @@ export async function uninstallPackedWinApp(config: ToolPackConfig): Promise<Win
|
||||
removedProductUserDataRoot,
|
||||
removedSidecarRoot,
|
||||
removalPlan,
|
||||
residueObservation: await observeWinResidues(config, paths),
|
||||
residueObservation: await observeWinResidues(config, registeredPaths),
|
||||
stop,
|
||||
timingPath: paths.uninstallTimingPath,
|
||||
uninstallerPath: paths.uninstallerPath,
|
||||
uninstallerPath: registeredPaths.uninstallerPath,
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanupPackedWinNamespace(config: ToolPackConfig): Promise<WinCleanupResult> {
|
||||
const paths = resolveWinPaths(config);
|
||||
const registeredPaths = await resolveWinRegisteredPaths(config, paths);
|
||||
const removalPlan = await createWinRemovalPlan(config);
|
||||
if (await pathExists(paths.uninstallerPath)) {
|
||||
if (await pathExists(registeredPaths.uninstallerPath)) {
|
||||
await uninstallPackedWinApp(config);
|
||||
}
|
||||
const stop = await stopPackedWinApp(config);
|
||||
const removedOutputRoot = await pathExists(config.roots.output.namespaceRoot);
|
||||
const removedRuntimeNamespaceRoot = await pathExists(config.roots.runtime.namespaceRoot);
|
||||
const removedProductUserDataRoot = removalPlan.some((target) => target.scope === "product-user-data" && target.willRemove && target.exists);
|
||||
await cleanupWinRegistryResidues(paths);
|
||||
await cleanupWinRegistryResidues(registeredPaths, config);
|
||||
for (const target of removalPlan) {
|
||||
if (target.scope === "product-user-data" && target.willRemove) await removeTree(target.path);
|
||||
}
|
||||
@@ -344,14 +347,15 @@ export async function cleanupPackedWinNamespace(config: ToolPackConfig): Promise
|
||||
removedProductUserDataRoot,
|
||||
removedRuntimeNamespaceRoot,
|
||||
removalPlan,
|
||||
residueObservation: await observeWinResidues(config, paths),
|
||||
residueObservation: await observeWinResidues(config, registeredPaths),
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listPackedWinNamespaces(config: ToolPackConfig): Promise<WinListResult> {
|
||||
const paths = resolveWinPaths(config);
|
||||
const registryEntries = await queryWinRegistryEntries(paths);
|
||||
const registeredPaths = await resolveWinRegisteredPaths(config, paths);
|
||||
const registryEntries = await queryWinRegistryEntries(registeredPaths, config);
|
||||
const productNamespaceRoot = resolveWinProductNamespaceRoot(config);
|
||||
const productUserDataRoot = resolveWinProductUserDataRoot();
|
||||
const builtManifest = await readBuiltAppManifest(paths, { requireExecutable: true });
|
||||
@@ -360,9 +364,9 @@ export async function listPackedWinNamespaces(config: ToolPackConfig): Promise<W
|
||||
builtExecutableExists: builtManifest != null || await pathExists(paths.unpackedExePath),
|
||||
builtExecutablePath: builtManifest?.executablePath ?? ((await pathExists(paths.unpackedExePath)) ? paths.unpackedExePath : null),
|
||||
builtManifestPath: paths.builtManifestPath,
|
||||
installDir: paths.installDir,
|
||||
installedExeExists: await pathExists(paths.installedExePath),
|
||||
installedExePath: paths.installedExePath,
|
||||
installDir: registeredPaths.installDir,
|
||||
installedExeExists: await pathExists(registeredPaths.installedExePath),
|
||||
installedExePath: registeredPaths.installedExePath,
|
||||
namespace: config.namespace,
|
||||
publicDesktopShortcutExists: await pathExists(paths.publicDesktopShortcutPath),
|
||||
publicDesktopShortcutPath: paths.publicDesktopShortcutPath,
|
||||
@@ -379,8 +383,8 @@ export async function listPackedWinNamespaces(config: ToolPackConfig): Promise<W
|
||||
setupPath: paths.setupPath,
|
||||
startMenuShortcutExists: await pathExists(paths.startMenuShortcutPath),
|
||||
startMenuShortcutPath: paths.startMenuShortcutPath,
|
||||
uninstallerExists: await pathExists(paths.uninstallerPath),
|
||||
uninstallerPath: paths.uninstallerPath,
|
||||
uninstallerExists: await pathExists(registeredPaths.uninstallerPath),
|
||||
uninstallerPath: registeredPaths.uninstallerPath,
|
||||
userDesktopShortcutExists: await pathExists(paths.userDesktopShortcutPath),
|
||||
userDesktopShortcutPath: paths.userDesktopShortcutPath,
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import type { ToolPackConfig } from "../config.js";
|
||||
import { pathExists } from "./fs.js";
|
||||
import { resolveWinInstallIdentity } from "./identity.js";
|
||||
import type { WinPaths, WindowsUninstallRegistryEntry } from "./types.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -10,7 +13,7 @@ function normalizeRegistryPath(value: string | null | undefined): string {
|
||||
return (value ?? "").replace(/[\\/]+$/, "").toLowerCase();
|
||||
}
|
||||
|
||||
function stripRegistryQuotedValue(value: string | null | undefined): string {
|
||||
export function stripRegistryQuotedValue(value: string | null | undefined): string {
|
||||
const trimmed = (value ?? "").trim();
|
||||
if (trimmed.startsWith('"')) {
|
||||
const closingQuote = trimmed.indexOf('"', 1);
|
||||
@@ -32,11 +35,27 @@ function createEmptyRegistryEntry(keyPath: string): WindowsUninstallRegistryEntr
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRegistryKeyPath(value: string): string {
|
||||
return value
|
||||
.replace(/^HKCU\\/i, "HKEY_CURRENT_USER\\")
|
||||
.replace(/^HKLM\\/i, "HKEY_LOCAL_MACHINE\\")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function namespaceRegistryKeyPath(config: Pick<ToolPackConfig, "namespace">): string {
|
||||
return normalizeRegistryKeyPath(`HKCU\\${resolveWinInstallIdentity(config).registryKey}`);
|
||||
}
|
||||
|
||||
async function execReg(args: string[], cwd: string): Promise<{ stdout: string; stderr: string }> {
|
||||
return await execFileAsync("reg.exe", args, { cwd, env: process.env, windowsHide: true });
|
||||
}
|
||||
|
||||
function registryEntryMatches(paths: WinPaths, entry: WindowsUninstallRegistryEntry): boolean {
|
||||
function registryEntryMatches(
|
||||
paths: WinPaths,
|
||||
entry: WindowsUninstallRegistryEntry,
|
||||
config?: Pick<ToolPackConfig, "namespace">,
|
||||
): boolean {
|
||||
if (config != null && normalizeRegistryKeyPath(entry.keyPath) === namespaceRegistryKeyPath(config)) return true;
|
||||
const targetInstallDir = normalizeRegistryPath(paths.installDir);
|
||||
const targetUninstaller = normalizeRegistryPath(paths.uninstallerPath);
|
||||
const installLocation = normalizeRegistryPath(entry.installLocation);
|
||||
@@ -51,7 +70,10 @@ function registryEntryMatches(paths: WinPaths, entry: WindowsUninstallRegistryEn
|
||||
);
|
||||
}
|
||||
|
||||
export async function queryWinRegistryEntries(paths: WinPaths): Promise<WindowsUninstallRegistryEntry[]> {
|
||||
export async function queryWinRegistryEntries(
|
||||
paths: WinPaths,
|
||||
config?: Pick<ToolPackConfig, "namespace">,
|
||||
): Promise<WindowsUninstallRegistryEntry[]> {
|
||||
const roots = [
|
||||
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
||||
"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
||||
@@ -67,7 +89,7 @@ export async function queryWinRegistryEntries(paths: WinPaths): Promise<WindowsU
|
||||
}
|
||||
let current: WindowsUninstallRegistryEntry | null = null;
|
||||
const collect = () => {
|
||||
if (current != null && registryEntryMatches(paths, current)) entries.push(current);
|
||||
if (current != null && registryEntryMatches(paths, current, config)) entries.push(current);
|
||||
};
|
||||
for (const rawLine of stdout.split(/\r?\n/)) {
|
||||
const line = rawLine.trimEnd();
|
||||
@@ -94,8 +116,58 @@ export async function queryWinRegistryEntries(paths: WinPaths): Promise<WindowsU
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function cleanupWinRegistryResidues(paths: WinPaths): Promise<string[]> {
|
||||
const entries = await queryWinRegistryEntries(paths);
|
||||
export async function queryWinNamespaceRegistryEntry(
|
||||
config: Pick<ToolPackConfig, "namespace">,
|
||||
paths: WinPaths,
|
||||
): Promise<WindowsUninstallRegistryEntry | null> {
|
||||
const identity = resolveWinInstallIdentity(config);
|
||||
let stdout = "";
|
||||
try {
|
||||
({ stdout } = await execReg(
|
||||
["query", `HKCU\\${identity.registryKey}`],
|
||||
await pathExists(paths.appBuilderOutputRoot) ? paths.appBuilderOutputRoot : process.cwd(),
|
||||
));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const entry = createEmptyRegistryEntry(`HKEY_CURRENT_USER\\${identity.registryKey}`);
|
||||
for (const rawLine of stdout.split(/\r?\n/)) {
|
||||
const line = rawLine.trimEnd();
|
||||
if (line.length === 0 || line.startsWith("HKEY_")) continue;
|
||||
const [name, , ...valueParts] = line.trim().split(/\s{2,}/);
|
||||
if (name == null || valueParts.length === 0) continue;
|
||||
const value = valueParts.join(" ");
|
||||
if (name === "DisplayIcon") entry.displayIcon = value;
|
||||
else if (name === "DisplayName") entry.displayName = value;
|
||||
else if (name === "DisplayVersion") entry.displayVersion = value;
|
||||
else if (name === "InstallLocation") entry.installLocation = value;
|
||||
else if (name === "Publisher") entry.publisher = value;
|
||||
else if (name === "QuietUninstallString") entry.quietUninstallString = value;
|
||||
else if (name === "UninstallString") entry.uninstallString = value;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
export async function resolveWinRegisteredPaths(config: ToolPackConfig, paths: WinPaths): Promise<WinPaths> {
|
||||
const entry = await queryWinNamespaceRegistryEntry(config, paths);
|
||||
if (entry == null) return paths;
|
||||
const identity = resolveWinInstallIdentity(config);
|
||||
const uninstallerFromRegistry = stripRegistryQuotedValue(entry.quietUninstallString) || stripRegistryQuotedValue(entry.uninstallString);
|
||||
const installDir = stripRegistryQuotedValue(entry.installLocation) || (uninstallerFromRegistry.length > 0 ? dirname(uninstallerFromRegistry) : paths.installDir);
|
||||
const uninstallerPath = uninstallerFromRegistry.length > 0 ? uninstallerFromRegistry : join(installDir, identity.uninstallerName);
|
||||
return {
|
||||
...paths,
|
||||
installDir,
|
||||
installedExePath: join(installDir, identity.exeName),
|
||||
uninstallerPath,
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanupWinRegistryResidues(
|
||||
paths: WinPaths,
|
||||
config?: Pick<ToolPackConfig, "namespace">,
|
||||
): Promise<string[]> {
|
||||
const entries = await queryWinRegistryEntries(paths, config);
|
||||
const removed: string[] = [];
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user