Files
DeskClaw/scripts/installer.nsh

368 lines
17 KiB
NSIS
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; ClawX Custom NSIS Installer/Uninstaller Script
;
; Install: enables long paths, adds resources\cli to user PATH for openclaw CLI.
; Uninstall: removes the PATH entry and optionally deletes user data.
!ifndef nsProcess::FindProcess
!include "nsProcess.nsh"
!endif
!macro customHeader
; Show install details by default so users can see what stage is running.
ShowInstDetails show
ShowUninstDetails show
!macroend
!macro customCheckAppRunning
; Make stage logs visible on assisted installers (defaults to hidden).
SetDetailsPrint both
DetailPrint "Preparing installation..."
DetailPrint "Extracting ClawX runtime files. This can take a few minutes on slower disks or while antivirus scanning is active."
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
${if} $R0 == 0
${if} ${isUpdated}
# Auto-update: the app is already shutting down (quitAndInstall was called).
# The before-quit handler needs up to 8s to gracefully stop the Gateway
# process tree (5s timeout + force-terminate + re-quit). Wait for the
# app to exit on its own before resorting to force-kill.
DetailPrint `Waiting for "${PRODUCT_NAME}" to finish shutting down...`
Sleep 8000
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
${if} $R0 != 0
# App exited cleanly. Still kill long-lived child processes (gateway,
# uv, python) which may not have followed the app's graceful exit.
nsExec::ExecToStack 'taskkill /F /IM openclaw-gateway.exe'
Pop $0
Pop $1
Goto done_killing
${endIf}
# App didn't exit in time; fall through to force-kill
${endIf}
${if} ${isUpdated} ; skip the dialog for auto-updates
${else}
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(appRunning)" /SD IDOK IDOK doStopProcess
Quit
${endIf}
doStopProcess:
DetailPrint `Closing running "${PRODUCT_NAME}"...`
# Kill ALL processes whose executable lives inside $INSTDIR.
# This covers ClawX.exe (multiple Electron processes), openclaw-gateway.exe,
# python.exe (skills runtime), uv.exe (package manager), and any other
# child process that might hold file locks in the installation directory.
#
# Use PowerShell Get-CimInstance for path-based matching (most reliable),
# with taskkill name-based fallback for restricted environments.
# Note: Using backticks ` ` for the NSIS string allows us to use single quotes inside.
nsExec::ExecToStack `"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance -ClassName Win32_Process | Where-Object { $$_.ExecutablePath -and $$_.ExecutablePath.StartsWith('$INSTDIR', [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"`
Pop $0
Pop $1
${if} $0 != 0
# PowerShell failed (policy restriction, etc.) — fall back to name-based kill
nsExec::ExecToStack 'taskkill /F /T /IM "${APP_EXECUTABLE_FILENAME}"'
Pop $0
Pop $1
${endIf}
# Also kill well-known child processes that may have detached from the
# Electron process tree or run from outside $INSTDIR (e.g. system python).
nsExec::ExecToStack 'taskkill /F /IM openclaw-gateway.exe'
Pop $0
Pop $1
# Wait for Windows to fully release file handles after process termination.
# 5 seconds accommodates slow antivirus scanners and filesystem flush delays.
Sleep 5000
DetailPrint "Processes terminated. Continuing installation..."
done_killing:
${nsProcess::Unload}
${endIf}
; Even if ClawX.exe was not detected as running, orphan child processes
; (python.exe, openclaw-gateway.exe, uv.exe, etc.) from a previous crash
; or unclean shutdown may still hold file locks inside $INSTDIR.
; Unconditionally kill any process whose executable lives in the install dir.
nsExec::ExecToStack `"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance -ClassName Win32_Process | Where-Object { $$_.ExecutablePath -and $$_.ExecutablePath.StartsWith('$INSTDIR', [System.StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { Stop-Process -Id $$_.ProcessId -Force -ErrorAction SilentlyContinue }"`
Pop $0
Pop $1
; Always kill known process names as a belt-and-suspenders approach.
; PowerShell path-based kill may miss processes if the old ClawX was installed
; in a different directory than $INSTDIR (e.g., per-machine -> per-user migration).
; taskkill is name-based and catches processes regardless of their install location.
nsExec::ExecToStack 'taskkill /F /T /IM "${APP_EXECUTABLE_FILENAME}"'
Pop $0
Pop $1
nsExec::ExecToStack 'taskkill /F /IM openclaw-gateway.exe'
Pop $0
Pop $1
; Note: we intentionally do NOT kill uv.exe globally — it is a popular
; Python package manager and other users/CI jobs may have uv running.
; The PowerShell path-based kill above already handles uv inside $INSTDIR.
; Brief wait for handle release (main wait was already done above if app was running)
Sleep 2000
; Release NSIS's CWD on $INSTDIR BEFORE the rename check.
; NSIS sets CWD to $INSTDIR in .onInit; Windows refuses to rename a directory
; that any process (including NSIS itself) has as its CWD.
SetOutPath $TEMP
; Pre-emptively clear the old installation directory so that the 7z
; extraction `CopyFiles` step in extractAppPackage.nsh won't fail on
; locked files. electron-builder's extractUsing7za macro extracts to a
; temp folder first, then uses `CopyFiles /SILENT` to copy into $INSTDIR.
; If ANY file in $INSTDIR is still locked, CopyFiles fails and triggers a
; "Can't modify ClawX's files" retry loop -> "ClawX 无法关闭" dialog.
;
; Strategy: rename (move) the old $INSTDIR out of the way. Rename works
; even when AV/indexer have files open for reading (they use
; FILE_SHARE_DELETE sharing mode), whereas CopyFiles fails because it
; needs write/overwrite access which some AV products deny.
; Check if a previous installation exists ($INSTDIR is a directory).
; Use trailing backslash — the correct NSIS idiom for directory existence.
; (IfFileExists "$INSTDIR\*.*" only matches files containing a dot and
; would fail for extensionless files or pure-subdirectory layouts.)
IfFileExists "$INSTDIR\" 0 _instdir_clean
; Find the first available stale directory name (e.g. $INSTDIR._stale_0)
; This ensures we NEVER have to synchronously delete old leftovers before
; renaming the current $INSTDIR. We just move it out of the way instantly.
StrCpy $R8 0
_find_free_stale:
IfFileExists "$INSTDIR._stale_$R8\" 0 _found_free_stale
IntOp $R8 $R8 + 1
Goto _find_free_stale
_found_free_stale:
ClearErrors
Rename "$INSTDIR" "$INSTDIR._stale_$R8"
IfErrors 0 _stale_moved
; Rename still failed — a process reopened a file or holds CWD in $INSTDIR.
; We must delete forcibly and synchronously to make room for CopyFiles.
; This can be slow (~1-3 minutes) if there are 10,000+ files and AV is active.
nsExec::ExecToStack 'cmd.exe /c rd /s /q "$INSTDIR"'
Pop $0
Pop $1
Sleep 2000
CreateDirectory "$INSTDIR"
Goto _instdir_clean
_stale_moved:
CreateDirectory "$INSTDIR"
_instdir_clean:
; Pre-emptively remove the old uninstall registry entry so that
; electron-builder's uninstallOldVersion skips the old uninstaller entirely.
;
; Why: uninstallOldVersion has a hardcoded 5-retry loop that runs the old
; uninstaller repeatedly. The old uninstaller's atomicRMDir fails on locked
; files (antivirus, indexing) causing a blocking "ClawX 无法关闭" dialog.
; Deleting UninstallString makes uninstallOldVersion return immediately.
; The new installer will overwrite / extract all files on top of the old dir.
; registryAddInstallInfo will write the correct new entries afterwards.
; Clean both SHELL_CONTEXT and HKCU to cover cross-hive upgrades
; (e.g. old install was per-user, new install is per-machine or vice versa).
DeleteRegValue SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" UninstallString
DeleteRegValue SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY}" QuietUninstallString
DeleteRegValue HKCU "${UNINSTALL_REGISTRY_KEY}" UninstallString
DeleteRegValue HKCU "${UNINSTALL_REGISTRY_KEY}" QuietUninstallString
!ifdef UNINSTALL_REGISTRY_KEY_2
DeleteRegValue SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY_2}" UninstallString
DeleteRegValue SHELL_CONTEXT "${UNINSTALL_REGISTRY_KEY_2}" QuietUninstallString
DeleteRegValue HKCU "${UNINSTALL_REGISTRY_KEY_2}" UninstallString
DeleteRegValue HKCU "${UNINSTALL_REGISTRY_KEY_2}" QuietUninstallString
!endif
!macroend
; Override electron-builder's handleUninstallResult to prevent the
; "ClawX 无法关闭" retry dialog when the old uninstaller fails.
;
; During upgrades, electron-builder copies the old uninstaller to a temp dir
; and runs it silently. The old uninstaller uses atomicRMDir to rename every
; file out of $INSTDIR. If ANY file is still locked (antivirus scanner,
; Windows Search indexer, delayed kernel handle release after taskkill), it
; aborts with a non-zero exit code. The default handler retries 5× then shows
; a blocking MessageBox.
;
; This macro clears the error and lets the new installer proceed — it will
; simply overwrite / extract new files on top of the (partially cleaned) old
; installation directory. This is safe because:
; 1. Processes have already been force-killed in customCheckAppRunning.
; 2. The new installer extracts a complete, self-contained file tree.
; 3. Any leftover old files that weren't removed are harmless.
!macro customUnInstallCheck
${if} $R0 != 0
DetailPrint "Old uninstaller exited with code $R0. Continuing with overwrite install..."
${endIf}
ClearErrors
!macroend
; Same safety net for the HKEY_CURRENT_USER uninstall path.
; Without this, handleUninstallResult would show a fatal error and Quit.
!macro customUnInstallCheckCurrentUser
${if} $R0 != 0
DetailPrint "Old uninstaller (current user) exited with code $R0. Continuing..."
${endIf}
ClearErrors
!macroend
!macro customInstall
; Async cleanup of old dirs left by the rename loop in customCheckAppRunning.
; Wait 60s before starting deletion to avoid I/O contention with ClawX's
; first launch (Windows Defender scan, ASAR mapping, etc.).
; ExecShell SW_HIDE is completely detached from NSIS and avoids pipe blocking.
IfFileExists "$INSTDIR._stale_0\" 0 _ci_stale_cleaned
; Use PowerShell to extract the basename of $INSTDIR so the glob works
; even when the user picked a custom install folder name.
; E.g. $INSTDIR = D:\Apps\MyClaw → glob = MyClaw._stale_*
ExecShell "" "cmd.exe" `/c ping -n 61 127.0.0.1 >nul & cd /d "$INSTDIR\.." & for /d %D in ("$INSTDIR._stale_*") do rd /s /q "%D"` SW_HIDE
_ci_stale_cleaned:
DetailPrint "Core files extracted. Finalizing system integration..."
; Enable Windows long path support (Windows 10 1607+ / Windows 11).
; pnpm virtual store paths can exceed the default MAX_PATH limit of 260 chars.
; Writing to HKLM requires admin privileges; on per-user installs without
; elevation this call silently fails — no crash, just no key written.
DetailPrint "Enabling long-path support (if permissions allow)..."
WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1
; Add $INSTDIR to Windows Defender exclusion list so that real-time scanning
; doesn't block the first app launch (Defender scans every newly-created file,
; causing 10-30s startup delay on a fresh install). Requires elevation;
; silently fails on non-admin per-user installs (no harm done).
DetailPrint "Configuring Windows Defender exclusion..."
nsExec::ExecToStack `"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Add-MpPreference -ExclusionPath '$INSTDIR' -ErrorAction SilentlyContinue"`
Pop $0
Pop $1
; Use PowerShell to update the current user's PATH.
; This avoids NSIS string-buffer limits and preserves long PATH values.
DetailPrint "Updating user PATH for the OpenClaw CLI..."
InitPluginsDir
ClearErrors
File "/oname=$PLUGINSDIR\update-user-path.ps1" "${PROJECT_DIR}\resources\cli\win32\update-user-path.ps1"
nsExec::ExecToStack '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\update-user-path.ps1" -Action add -CliDir "$INSTDIR\resources\cli"'
Pop $0
Pop $1
StrCmp $0 "error" 0 +2
DetailPrint "Warning: Failed to launch PowerShell while updating PATH."
StrCmp $0 "timeout" 0 +2
DetailPrint "Warning: PowerShell PATH update timed out."
StrCmp $0 "0" 0 +2
Goto _ci_done
DetailPrint "Warning: PowerShell PATH update exited with code $0."
_ci_done:
DetailPrint "Installation steps complete."
!macroend
!macro customUnInstall
; Remove Windows Defender exclusion added during install
nsExec::ExecToStack `"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Remove-MpPreference -ExclusionPath '$INSTDIR' -ErrorAction SilentlyContinue"`
Pop $0
Pop $1
; Remove resources\cli from user PATH via PowerShell so long PATH values are handled safely
InitPluginsDir
ClearErrors
File "/oname=$PLUGINSDIR\update-user-path.ps1" "${PROJECT_DIR}\resources\cli\win32\update-user-path.ps1"
nsExec::ExecToStack '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\update-user-path.ps1" -Action remove -CliDir "$INSTDIR\resources\cli"'
Pop $0
Pop $1
StrCmp $0 "error" 0 +2
DetailPrint "Warning: Failed to launch PowerShell while removing PATH entry."
StrCmp $0 "timeout" 0 +2
DetailPrint "Warning: PowerShell PATH removal timed out."
StrCmp $0 "0" 0 +2
Goto _cu_pathDone
DetailPrint "Warning: PowerShell PATH removal exited with code $0."
_cu_pathDone:
; Ask user if they want to remove AppData (preserves .openclaw)
MessageBox MB_YESNO|MB_ICONQUESTION \
"Do you want to remove ClawX application data?$\r$\n$\r$\nThis will delete:$\r$\n • AppData\Local\clawx (local app data)$\r$\n • AppData\Roaming\clawx (roaming app data)$\r$\n$\r$\nYour .openclaw folder (configuration & skills) will be preserved.$\r$\nSelect 'No' to keep all data for future reinstallation." \
/SD IDNO IDYES _cu_removeData IDNO _cu_skipRemove
_cu_removeData:
; Kill any lingering ClawX processes (and their child process trees) to
; release file locks on electron-store JSON files, Gateway sockets, etc.
${nsProcess::FindProcess} "${APP_EXECUTABLE_FILENAME}" $R0
${if} $R0 == 0
nsExec::ExecToStack 'taskkill /F /T /IM "${APP_EXECUTABLE_FILENAME}"'
Pop $0
Pop $1
${endIf}
${nsProcess::Unload}
; Wait for processes to fully exit and release file handles
Sleep 2000
; --- Always remove current user's AppData first ---
; NOTE: .openclaw directory is intentionally preserved (user configuration & skills)
RMDir /r "$LOCALAPPDATA\clawx"
RMDir /r "$APPDATA\clawx"
; --- Retry: if directories still exist (locked files), wait and try again ---
; Check AppData\Local\clawx
IfFileExists "$LOCALAPPDATA\clawx\*.*" 0 _cu_localDone
Sleep 3000
RMDir /r "$LOCALAPPDATA\clawx"
IfFileExists "$LOCALAPPDATA\clawx\*.*" 0 _cu_localDone
nsExec::ExecToStack 'cmd.exe /c rd /s /q "$LOCALAPPDATA\clawx"'
Pop $0
Pop $1
_cu_localDone:
; Check AppData\Roaming\clawx
IfFileExists "$APPDATA\clawx\*.*" 0 _cu_roamingDone
Sleep 3000
RMDir /r "$APPDATA\clawx"
IfFileExists "$APPDATA\clawx\*.*" 0 _cu_roamingDone
nsExec::ExecToStack 'cmd.exe /c rd /s /q "$APPDATA\clawx"'
Pop $0
Pop $1
_cu_roamingDone:
; --- Final check: warn user if any directories could not be removed ---
StrCpy $R3 ""
IfFileExists "$LOCALAPPDATA\clawx\*.*" 0 +2
StrCpy $R3 "$R3$\r$\n • $LOCALAPPDATA\clawx"
IfFileExists "$APPDATA\clawx\*.*" 0 +2
StrCpy $R3 "$R3$\r$\n • $APPDATA\clawx"
StrCmp $R3 "" _cu_cleanupOk
MessageBox MB_OK|MB_ICONEXCLAMATION \
"Some data directories could not be removed (files may be in use):$\r$\n$R3$\r$\n$\r$\nPlease delete them manually after restarting your computer."
_cu_cleanupOk:
; --- For per-machine (all users) installs, enumerate all user profiles ---
StrCpy $R0 0
_cu_enumLoop:
EnumRegKey $R1 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" $R0
StrCmp $R1 "" _cu_enumDone
ReadRegStr $R2 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$R1" "ProfileImagePath"
StrCmp $R2 "" _cu_enumNext
; ExpandEnvStrings requires distinct src and dest registers
ExpandEnvStrings $R3 $R2
StrCmp $R3 $PROFILE _cu_enumNext
; NOTE: .openclaw directory is intentionally preserved for all users
RMDir /r "$R3\AppData\Local\clawx"
RMDir /r "$R3\AppData\Roaming\clawx"
_cu_enumNext:
IntOp $R0 $R0 + 1
Goto _cu_enumLoop
_cu_enumDone:
_cu_skipRemove:
!macroend