Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ install:
- git submodule init
- git submodule update

# The appveyor image contains cmake 4.1 only, which doesn't support VS 2016.
# The appveyor image contains cmake 4.1 only, which doesn't support VS 2026.
# Download cmake 4.3.0 and add it to the PATH.
- appveyor DownloadFile https://cmake.org/files/v4.3/cmake-4.3.0-windows-x86_64.zip -FileName cmake43.zip
- appveyor DownloadFile https://github.com/Kitware/CMake/releases/download/v4.3.0/cmake-4.3.0-windows-x86_64.zip -FileName cmake43.zip
- 7z x cmake43.zip -oC:\cmake43
- set "PATH=C:\cmake43\cmake-4.3.0-windows-x86_64\bin;%PATH%"
- cmake --version # should print 4.3.0
Expand Down
69 changes: 24 additions & 45 deletions installer/installer.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
!addplugindir /x86-unicode "StdUtils.2015-11-16\Plugins\Release_Unicode"
!include "StdUtils.nsh" ; for ExecShellAsUser()

; We need the MD5 plugin
!addplugindir /x86-unicode "md5dll\UNICODE"

; We need the INetC (internet client) plugin
; https://nsis.sourceforge.io/Inetc_plug-in
Expand Down Expand Up @@ -133,27 +131,15 @@ Function uninstallOldVersion
!insertmacro UnregisterTextService 64 "$INSTDIR\x64\PIMETextService.dll"
${ElseIf} ${IsNativeARM64}
!insertmacro UnregisterTextService 64 "$INSTDIR\arm64\PIMETextService.dll"
${If} ${FileExists} "$INSTDIR\x64"
!insertmacro UnregisterTextService 64 "$INSTDIR\x64\PIMETextService.dll"
!insertmacro SafeDeleteFile "$INSTDIR\x64\PIMETextService.dll"
RMDir /REBOOTOK "$INSTDIR\x64"
${EndIf}
${EndIf}

; Remove the launcher from auto-start and clean registry
!insertmacro CleanPIMERegistry

; NOTE: Why binary files (DLLs, EXE) and binary folders (x86, x64, arm64) are NOT deleted here:
; 1. Folders: On Windows, scheduling directories for deletion on reboot (via RMDir /REBOOTOK)
; while this installer is about to install new files into those exact same directories
; would cause Windows PendingFileRenameOperations to wipe the newly installed directories
; on the next system reboot.
; 2. Binaries: PIMELauncher.exe and PIMETextService.dll are handled cleanly by InstallOrSkip
; later in the installation sections. If their MD5 checksum matches the new version, they
; are kept untouched without unnecessary writes or reboot requirements. If their MD5
; differs and they are currently locked by active applications, InstallOrSkip safely renames
; the locked file to a unique .deleted temporary file, freeing the path so the new binary
; can be installed immediately without forcing a reboot.
; Safely remove or rename old binaries so the destination paths are free for writing.
; If files are locked by running applications, SafeDeleteFile renames them to unique
; temporary files scheduled for deletion on reboot without blocking immediate installation.
!insertmacro CleanOldBinaries

; Clean old backend and data files
Delete "$INSTDIR\backends.json"
Expand Down Expand Up @@ -223,16 +209,6 @@ Function .onInit
Reboot
${EndIf}

; Extract candidate binaries into $PLUGINSDIR (NSIS temporary plugin directory,
; which Windows automatically cleans up when the installer exits).
; This allows InstallOrSkip to compute MD5 checksums of the new candidates against
; existing files on disk, and copy them safely via CopyFiles without triggering
; raw NSIS "File write error: Abort/Retry/Ignore" popups when files are in use.
File "/oname=$PLUGINSDIR\PIMETextService_x86.dll" "..\build\PIMETextService\Release\PIMETextService.dll"
File "/oname=$PLUGINSDIR\PIMETextService_x64.dll" "..\build64\PIMETextService\Release\PIMETextService.dll"
File "/oname=$PLUGINSDIR\PIMETextService_arm64.dll" "..\build_arm64\PIMETextService\Release\PIMETextService_arm64.dll"
File "/oname=$PLUGINSDIR\PIMETextService_arm64x.dll" "..\build_arm64\PIMETextService\Release\PIMETextService.dll"
File "/oname=$PLUGINSDIR\PIMELauncher.exe" "..\build\PIMELauncher\PIMELauncher.exe"

StrCpy $INST_PYTHON "False"
StrCpy $INST_CINBASE "False"
Expand Down Expand Up @@ -317,8 +293,14 @@ Section $(SECTION_MAIN) SecMain
; Install backend informations
File "..\backends.json"

; Ensure any existing in-use binaries in $INSTDIR are safely cleared before writing
!insertmacro CleanOldBinaries
${If} ${RebootFlag}
Call .onInstFailed
${EndIf}

; Install the launcher responsible to launch the backends
!insertmacro InstallOrSkip "$INSTDIR\PIMELauncher.exe" "$PLUGINSDIR\PIMELauncher.exe"
File "..\build\PIMELauncher\PIMELauncher.exe"
SectionEnd

SectionGroup /e $(PYTHON_SECTION_GROUP) python_section_group
Expand Down Expand Up @@ -510,20 +492,23 @@ Section "" Register

; Install the text service dlls
${If} ${IsNativeAMD64} ; This is a 64-bit x86-64/x64 Windows system
!insertmacro InstallOrSkip "$INSTDIR\x64\PIMETextService.dll" "$PLUGINSDIR\PIMETextService_x64.dll"
SetOutPath "$INSTDIR\x64"
File "..\build64\PIMETextService\Release\PIMETextService.dll"
!insertmacro RegisterTextService 64 "$INSTDIR\x64\PIMETextService.dll"
${ElseIf} ${IsNativeARM64} ; This is a native ARM64 Windows system
; ARM64X forwarder DLL
!insertmacro InstallOrSkip "$INSTDIR\arm64\PIMETextService.dll" "$PLUGINSDIR\PIMETextService_arm64x.dll"
; Real native ARM64 DLL
!insertmacro InstallOrSkip "$INSTDIR\arm64\PIMETextService_arm64.dll" "$PLUGINSDIR\PIMETextService_arm64.dll"
; Real x64 DLL for emulated x64 / ARM64EC processes
!insertmacro InstallOrSkip "$INSTDIR\arm64\PIMETextService_x64.dll" "$PLUGINSDIR\PIMETextService_x64.dll"
SetOutPath "$INSTDIR\arm64"
; ARM64X forwarder DLL (built in build_arm64\PIMETextService\Release\PIMETextService.dll)
File "..\build_arm64\PIMETextService\Release\PIMETextService.dll"
; Real native ARM64 DLL (built in build_arm64\PIMETextService\Release\PIMETextService_arm64.dll)
File "..\build_arm64\PIMETextService\Release\PIMETextService_arm64.dll"
; Real x64 DLL for emulated x64 / ARM64EC processes (built in build64\PIMETextService\Release\PIMETextService.dll)
File "/oname=PIMETextService_x64.dll" "..\build64\PIMETextService\Release\PIMETextService.dll"
; Register COM objects (only register the ARM64X forwarder DLL)
!insertmacro RegisterTextService 64 "$INSTDIR\arm64\PIMETextService.dll"
${EndIf}

!insertmacro InstallOrSkip "$INSTDIR\x86\PIMETextService.dll" "$PLUGINSDIR\PIMETextService_x86.dll"
SetOutPath "$INSTDIR\x86"
File "..\build\PIMETextService\Release\PIMETextService.dll"
!insertmacro RegisterTextService 32 "$INSTDIR\x86\PIMETextService.dll"

; Launch the python server on startup
Expand Down Expand Up @@ -603,7 +588,6 @@ Section "Uninstall"

; Try to terminate running PIMELauncher and the server process
!insertmacro StopPIMELauncher
!insertmacro SafeDeleteFile "$INSTDIR\PIMELauncher.exe"

; Unregister COM objects (NSIS UnRegDLL command is broken and cannot be used)
!insertmacro UnregisterTextService 32 "$INSTDIR\x86\PIMETextService.dll"
Expand All @@ -619,20 +603,15 @@ Section "Uninstall"
; Remove auto-start, uninstall, and product keys from registry
!insertmacro CleanPIMERegistry

; Safely delete text service DLLs (which may be locked by running processes)
!insertmacro SafeDeleteFile "$INSTDIR\x86\PIMETextService.dll"
; Safely delete binaries (which may be locked by running processes)
!insertmacro CleanOldBinaries
RMDir /REBOOTOK "$INSTDIR\x86"

${If} ${IsNativeAMD64}
!insertmacro SafeDeleteFile "$INSTDIR\x64\PIMETextService.dll"
RMDir /REBOOTOK "$INSTDIR\x64"
${ElseIf} ${IsNativeARM64}
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService.dll"
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService_arm64.dll"
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService_x64.dll"
RMDir /REBOOTOK "$INSTDIR\arm64"
${If} ${FileExists} "$INSTDIR\x64"
!insertmacro SafeDeleteFile "$INSTDIR\x64\PIMETextService.dll"
RMDir /REBOOTOK "$INSTDIR\x64"
${EndIf}
${EndIf}
Expand Down
164 changes: 38 additions & 126 deletions installer/macros.nsh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
; NOTE: This file MUST be saved as UTF-8 with BOM (utf-8-sig) so that
; NOTE: This file MUST be saved as UTF-8 with BOM (utf-8-sig) so that
; NSIS (makensis) natively detects Unicode strings without breaking Git diffs.
; Do NOT convert this file to UTF-16.
;
Expand Down Expand Up @@ -28,50 +28,6 @@
!define PIME_INSTALLER_MACROS_NSH


; ==============================================================================
; Macro: CheckFileMD5Equal
; ------------------------------------------------------------------------------
; Compares the MD5 checksums of two files using the md5dll plugin.
;
; Parameters:
; FILE_A - Full path to first file.
; FILE_B - Full path to second file.
; OUT_VAR - Variable to store result: "True" if both files exist and have
; matching checksums; "False" otherwise.
; ==============================================================================
!macro CheckFileMD5Equal FILE_A FILE_B OUT_VAR
Push $R0
Push $R1

!define md5A $R0
!define md5B $R1
!define isMatch $R1 ; reuse R1 for result

StrCpy ${isMatch} "False"
${If} ${FileExists} "${FILE_A}"
${AndIf} ${FileExists} "${FILE_B}"
md5dll::GetMD5File "${FILE_A}"
Pop ${md5A}
md5dll::GetMD5File "${FILE_B}"
Pop ${md5B}
${If} ${md5A} == ${md5B}
StrCpy ${isMatch} "True"
${EndIf}
${EndIf}

Push ${isMatch} ; push result onto stack

!undef md5A
!undef md5B
!undef isMatch

Exch 2
Pop $R0
Pop $R1
Pop ${OUT_VAR}
!macroend


; ==============================================================================
; Macro: SafeDeleteFile
; ------------------------------------------------------------------------------
Expand All @@ -84,19 +40,28 @@
; - Generates a unique temporary filename in the same directory.
; - Attempts to rename TARGET_FILE to this unique temporary file.
; - If rename succeeds: schedules only the temporary file for deletion on
; reboot using Delete /REBOOTOK.
; reboot using Delete /REBOOTOK, and restores RebootFlag if no reboot was
; previously pending (as the destination path is now free for writing).
; - If rename fails (hard lock): schedules original TARGET_FILE on reboot
; and sets RebootFlag.
; and sets RebootFlag true.
;
; Parameters:
; TARGET_FILE - Path to file to delete.
; ==============================================================================
!macro SafeDeleteFile TARGET_FILE
Push $R0
Push $R1
Push $R2

!define sdf_tempDeletedFile $R0
!define sdf_targetDir $R1
!define sdf_priorRebootFlag $R2

; Save pre-existing RebootFlag state so we don't accidentally clear a reboot requested earlier
StrCpy ${sdf_priorRebootFlag} "0"
${If} ${RebootFlag}
StrCpy ${sdf_priorRebootFlag} "1"
${EndIf}

${If} ${FileExists} "${TARGET_FILE}"
ClearErrors
Expand All @@ -115,8 +80,14 @@
${IfNot} ${Errors}
; Renamed: schedule only the unique file for deletion on reboot
Delete /REBOOTOK "${sdf_tempDeletedFile}"
; Delete /REBOOTOK sets RebootFlag internally. Since rename succeeded,
; the destination path is now vacant and can be replaced immediately
; without rebooting. Restore prior RebootFlag state.
${If} ${sdf_priorRebootFlag} == "0"
SetRebootFlag false
${EndIf}
${Else}
; Hard-locked: schedule original target on reboot
; Hard-locked: schedule original target on reboot and flag reboot requirement
Delete /REBOOTOK "${TARGET_FILE}"
SetRebootFlag true
${EndIf}
Expand All @@ -125,97 +96,38 @@

!undef sdf_tempDeletedFile
!undef sdf_targetDir
!undef sdf_priorRebootFlag

Pop $R2
Pop $R1
Pop $R0
!macroend


; ==============================================================================
; Macro: InstallOrSkip
; Macro: CleanOldBinaries
; ------------------------------------------------------------------------------
; Installs a binary file (DLL / EXE) to a destination path, skipping the write
; if the target file already exists and has identical MD5 checksum.
;
; Expected Behavior:
; 1. Fast path: attempts direct CopyFiles. If target is not locked, this succeeds
; immediately with 0 hash calculations.
; 2. Target is locked: calculates MD5 of both target and source.
; 3. If MD5 matches: cleanly skips overwriting the file.
; 4. If MD5 differs: calls SafeDeleteFile to safely rename the locked target
; to a unique <temp>.deleted file, freeing the destination path so the new
; file can be copied directly without requiring a reboot.
; 5. If SafeDeleteFile hit a hard lock (rename failed): fails cleanly and prompts
; user to restart the computer.
; Safely removes or renames all PIME binary executables and DLLs in INSTDIR.
; If files are locked by running applications, renames them to temporary files
; and schedules them for deletion on reboot, freeing destination paths so new
; files can be installed immediately.
;
; Parameters:
; TARGET_FILE - Destination file path (e.g. "$INSTDIR\x86\PIMETextService.dll").
; SOURCE_FILE - Source candidate file (e.g. "$PLUGINSDIR\PIMETextService_x86.dll").
; None (operates on $INSTDIR, RunningX64, IsNativeARM64).
; ==============================================================================
!macro InstallOrSkip TARGET_FILE SOURCE_FILE
Push $R0
Push $R1
Push $R2

!define ios_isMD5Equal $R0
!define ios_targetDir $R1
!define ios_priorRebootFlag $R2

; Save pre-existing RebootFlag state so we don't accidentally clear a reboot requested earlier
StrCpy ${ios_priorRebootFlag} "0"
${If} ${RebootFlag}
StrCpy ${ios_priorRebootFlag} "1"
${EndIf}

; Ensure parent directory exists
GetFullPathName ${ios_targetDir} "${TARGET_FILE}\.."
CreateDirectory "${ios_targetDir}"

; 1. Try unconditional write first (succeeds if file is new or not in use)
ClearErrors
CopyFiles /SILENT "${SOURCE_FILE}" "${TARGET_FILE}"
${If} ${Errors}
; 2. Write failed: target is locked / in use.
; Check MD5: if identical, we don't need to overwrite it! Skip cleanly.
!insertmacro CheckFileMD5Equal "${TARGET_FILE}" "${SOURCE_FILE}" ${ios_isMD5Equal}
${If} ${ios_isMD5Equal} != "True"
; 3. Target is locked and MD5 differs: safely remove/rename the old file
!insertmacro SafeDeleteFile "${TARGET_FILE}"
${IfNot} ${FileExists} "${TARGET_FILE}"
; Rename succeeded: path is now freed on disk, copy new file into place
ClearErrors
CopyFiles /SILENT "${SOURCE_FILE}" "${TARGET_FILE}"
${If} ${Errors}
; Target write failed even after path was freed
SetRebootFlag true
MessageBox MB_YESNO|MB_ICONSTOP $(REBOOT_QUESTION) IDNO +2
Reboot
Quit
${Else}
; Successfully replaced in place.
; Delete /REBOOTOK on the renamed temporary file sets NSIS RebootFlag
; internally. If no reboot was requested before this macro ran, clear
; the flag so we don't prompt the user for an unnecessary reboot.
${If} ${ios_priorRebootFlag} == "0"
SetRebootFlag false
${EndIf}
${EndIf}
${Else}
; 4. Hard lock: rename failed, cannot replace target without reboot.
MessageBox MB_YESNO|MB_ICONSTOP $(REBOOT_QUESTION) IDNO +2
Reboot
Quit
${EndIf}
!macro CleanOldBinaries
!insertmacro SafeDeleteFile "$INSTDIR\PIMELauncher.exe"
!insertmacro SafeDeleteFile "$INSTDIR\x86\PIMETextService.dll"
${If} ${IsNativeAMD64}
!insertmacro SafeDeleteFile "$INSTDIR\x64\PIMETextService.dll"
${ElseIf} ${IsNativeARM64}
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService.dll"
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService_arm64.dll"
!insertmacro SafeDeleteFile "$INSTDIR\arm64\PIMETextService_x64.dll"
${If} ${FileExists} "$INSTDIR\x64\PIMETextService.dll"
!insertmacro SafeDeleteFile "$INSTDIR\x64\PIMETextService.dll"
${EndIf}
${EndIf}

!undef ios_isMD5Equal
!undef ios_targetDir
!undef ios_priorRebootFlag

Pop $R2
Pop $R1
Pop $R0
!macroend


Expand Down
Binary file removed installer/md5dll/ANSI/md5dll.dll
Binary file not shown.
Loading