From aaf96eb94a0652c8ba130dcd6b5d62ce45133418 Mon Sep 17 00:00:00 2001 From: Sergey Pak Date: Tue, 2 Jun 2026 15:47:35 +0200 Subject: [PATCH] Windows shim: preserve update on unreadable zip + add upgrade logging The Windows shim computed the downloaded zip's SHA-256 via Get-FileHash and treated ANY non-matching result as a checksum mismatch, deleting both the manifest and the zip. When the zip cannot be read at hash time -- typically because antivirus is still scanning/locking the freshly written file -- Get-FileHash emits nothing, so ACTUAL_SHA is empty. The old code read that empty value as 'mismatch', deleted the artifacts, and launched the old version; the binary then re-queued the same update on the next start, producing an endless loop that never updates (the empty 'Got:' a customer reported). Hardening: branch on an empty hash. An empty Get-FileHash result is now treated as a transient, retryable condition -- the claimed manifest is renamed back to pending-update.json and the zip is kept, so the update is re-attempted on the next launch instead of being destroyed. A genuine hash mismatch (non-empty but different) still drops the artifacts as before. Logging: add a persistent, append-only upgrade log mirroring the POSIX shim. It lives under %USERPROFILE%\.junie\logs\upgrade.log (override via JUNIE_LOG_DIR), stamps each line with a per-launch session id, and records every step of apply_pending_update (claim, parse, checksum start/ok/ unreadable/fail, extract, validate, swap, success). Writes are best-effort and never block launching. A goto :eof guard before the new :log_upgrade subroutine preserves junie.exe's exit code. Regenerated install.ps1 / install-eap.ps1 / install-nightly.ps1 / install-experimental.ps1 from the template. Co-authored-by: Junie --- install-eap.ps1 | 51 ++++++++++++++++++++++++++++++++++++++++ install-experimental.ps1 | 51 ++++++++++++++++++++++++++++++++++++++++ install-nightly.ps1 | 51 ++++++++++++++++++++++++++++++++++++++++ install.ps1 | 51 ++++++++++++++++++++++++++++++++++++++++ templates/junie.shim.bat | 51 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+) diff --git a/install-eap.ps1 b/install-eap.ps1 index 92aba410..e2c275ce 100644 --- a/install-eap.ps1 +++ b/install-eap.ps1 @@ -193,6 +193,17 @@ set "CURRENT_FILE=%JUNIE_DATA%\current" set "PENDING_UPDATE=%UPDATES_DIR%\pending-update.json" set "PROCESSING=%UPDATES_DIR%\pending-update.json.processing" +:: Persistent upgrade log (mirrors the POSIX shim). Lives under +:: %USERPROFILE%\.junie\logs -- separate from the data dir so a reinstall that +:: wipes ~/.local/share/junie does not lose update history. Always appended, +:: never truncated. Override the directory with JUNIE_LOG_DIR. A per-invocation +:: session id (LOG_SID) is stamped on every line so one launch can be followed. +set "UPGRADE_LOG_DIR=%USERPROFILE%\.junie\logs" +if defined JUNIE_LOG_DIR set "UPGRADE_LOG_DIR=%JUNIE_LOG_DIR%" +set "UPGRADE_LOG=%UPGRADE_LOG_DIR%\upgrade.log" +set "LOG_SID=%RANDOM%%RANDOM%" +if not exist "%UPGRADE_LOG_DIR%" mkdir "%UPGRADE_LOG_DIR%" 2>nul + ::: === Defensive Sanitization === ::: ::: JUNIE-2957 defense: drop pending-update.json / pending-update.json.processing @@ -207,6 +218,7 @@ if exist "%PENDING_UPDATE%" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '%PENDING_UPDATE%' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PEND_OK=%%V" if /i not "!PEND_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: %PENDING_UPDATE% 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json" del /f /q "%PENDING_UPDATE%" 2>nul ) ) @@ -215,6 +227,7 @@ if exist "!PROCESSING!" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '!PROCESSING!' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PROC_OK=%%V" if /i not "!PROC_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: !PROCESSING! 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json.processing" del /f /q "!PROCESSING!" 2>nul ) ) @@ -230,39 +243,60 @@ if errorlevel 1 ( goto :resolve_version ) set "CLAIMED_UPDATE=%UPDATES_DIR%\pending-update.json.processing" +call :log_upgrade "apply: claimed pending update for processing" :: Parse the claimed manifest using PowerShell (reliable JSON parsing) for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).version"`) do set "UPD_VERSION=%%V" for /f "usebackq delims=" %%Z in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).zipPath"`) do set "UPD_ZIP=%%Z" for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).sha256"`) do set "UPD_SHA256=%%S" +call :log_upgrade "apply: manifest parsed version=!UPD_VERSION! zipPath=!UPD_ZIP! sha256=!UPD_SHA256!" if not defined UPD_VERSION ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing version); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not defined UPD_ZIP ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing zipPath); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not exist "!UPD_ZIP!" ( echo [Junie] Update file not found: !UPD_ZIP! 1>&2 + call :log_upgrade "apply: update archive not found at !UPD_ZIP!; dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) :: Verify SHA-256 checksum if defined UPD_SHA256 ( + call :log_upgrade "checksum: starting SHA-256 validation expected=!UPD_SHA256!" + set "ACTUAL_SHA=" for /f "usebackq delims=" %%H in (`powershell -NoProfile -Command "(Get-FileHash '!UPD_ZIP!' -Algorithm SHA256).Hash.ToLower()"`) do set "ACTUAL_SHA=%%H" + if not defined ACTUAL_SHA ( + :: Get-FileHash produced no output -- the zip could not be read/hashed, + :: almost always because it is still locked or being scanned by antivirus. + :: This is a transient condition, NOT corruption: PRESERVE the manifest and + :: zip and retry on the next launch. Deleting them here (as a plain mismatch + :: would) makes the binary re-download and re-fail every startup -- an + :: endless update loop that never recovers. + echo [Junie] Could not read update zip ^(locked or AV scan?^); will retry next launch 1>&2 + call :log_upgrade "checksum: UNREADABLE -- Get-FileHash returned no output; preserving manifest and archive for retry" + rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul + goto :resolve_version + ) if /i not "!ACTUAL_SHA!"=="!UPD_SHA256!" ( echo [Junie] Checksum mismatch, skipping update 1>&2 echo [Junie] Expected: !UPD_SHA256! 1>&2 echo [Junie] Got: !ACTUAL_SHA! 1>&2 + call :log_upgrade "checksum: FAIL expected=!UPD_SHA256! got=!ACTUAL_SHA! -- dropping manifest and archive" del /f /q "!CLAIMED_UPDATE!" 2>nul del /f /q "!UPD_ZIP!" 2>nul goto :resolve_version ) + call :log_upgrade "checksum: OK" ) :: Extract to a staging dir on the same filesystem as %VERSIONS_DIR%, validate @@ -279,9 +313,11 @@ if exist "!UPD_STAGING!" rmdir /s /q "!UPD_STAGING!" 2>nul mkdir "!UPD_STAGING!" 2>nul echo [Junie] Applying pending update to version !UPD_VERSION!... 1>&2 +call :log_upgrade "extract: starting Expand-Archive src=!UPD_ZIP! dst=!UPD_STAGING!" powershell -NoProfile -Command "Expand-Archive -Path '!UPD_ZIP!' -DestinationPath '!UPD_STAGING!' -Force" 2>nul if errorlevel 1 ( echo [Junie] Failed to extract update; preserving for retry 1>&2 + call :log_upgrade "extract: FAIL (Expand-Archive non-zero); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -290,6 +326,7 @@ if errorlevel 1 ( :: Validate extracted binary if not exist "!UPD_STAGING!\junie\junie.exe" ( echo [Junie] Extracted payload missing junie\junie.exe; preserving for retry 1>&2 + call :log_upgrade "validate: FAIL (junie\junie.exe missing in payload); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -300,6 +337,7 @@ if exist "!UPD_TARGET!" rename "!UPD_TARGET!" "!UPD_OLD_NAME!" 2>nul move "!UPD_STAGING!" "!UPD_TARGET!" >nul 2>nul if errorlevel 1 ( echo [Junie] Failed to install new version; preserving for retry 1>&2 + call :log_upgrade "swap: FAIL (could not move staging into place); rolled back and preserving manifest for retry" if exist "!UPD_OLD!" if not exist "!UPD_TARGET!" rename "!UPD_OLD!" "!UPD_VERSION!" 2>nul rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul @@ -315,6 +353,7 @@ if exist "!UPD_OLD!" rmdir /s /q "!UPD_OLD!" 2>nul :: Cleanup downloaded zip and manifest only after a successful swap del /f /q "!UPD_ZIP!" 2>nul del /f /q "!CLAIMED_UPDATE!" 2>nul +call :log_upgrade "apply: SUCCESS updated to version !UPD_VERSION!" echo [Junie] Updated to version !UPD_VERSION! 1>&2 :: === Resolve Version === @@ -378,6 +417,18 @@ for %%A in (%*) do ( ) endlocal & "%JUNIE_EXE%" %FILTERED_ARGS% + +:: Jump past the subroutines to end of file, preserving junie.exe's exit code. +goto :eof + +:: === Subroutines === + +:log_upgrade +:: Append a timestamped, session-tagged line to the persistent upgrade log. +:: Best-effort: a failure to write (e.g. read-only profile) must never break +:: launching, so the error is swallowed. %~1 is the (already-expanded) message. +echo [%DATE% %TIME%] [sid=%LOG_SID%] %~1>> "%UPGRADE_LOG%" 2>nul +exit /b 0 '@ [System.IO.File]::WriteAllText($SHIM_PATH, $SHIM_CONTENT) diff --git a/install-experimental.ps1 b/install-experimental.ps1 index 847695af..591cacc4 100644 --- a/install-experimental.ps1 +++ b/install-experimental.ps1 @@ -193,6 +193,17 @@ set "CURRENT_FILE=%JUNIE_DATA%\current" set "PENDING_UPDATE=%UPDATES_DIR%\pending-update.json" set "PROCESSING=%UPDATES_DIR%\pending-update.json.processing" +:: Persistent upgrade log (mirrors the POSIX shim). Lives under +:: %USERPROFILE%\.junie\logs -- separate from the data dir so a reinstall that +:: wipes ~/.local/share/junie does not lose update history. Always appended, +:: never truncated. Override the directory with JUNIE_LOG_DIR. A per-invocation +:: session id (LOG_SID) is stamped on every line so one launch can be followed. +set "UPGRADE_LOG_DIR=%USERPROFILE%\.junie\logs" +if defined JUNIE_LOG_DIR set "UPGRADE_LOG_DIR=%JUNIE_LOG_DIR%" +set "UPGRADE_LOG=%UPGRADE_LOG_DIR%\upgrade.log" +set "LOG_SID=%RANDOM%%RANDOM%" +if not exist "%UPGRADE_LOG_DIR%" mkdir "%UPGRADE_LOG_DIR%" 2>nul + ::: === Defensive Sanitization === ::: ::: JUNIE-2957 defense: drop pending-update.json / pending-update.json.processing @@ -207,6 +218,7 @@ if exist "%PENDING_UPDATE%" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '%PENDING_UPDATE%' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PEND_OK=%%V" if /i not "!PEND_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: %PENDING_UPDATE% 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json" del /f /q "%PENDING_UPDATE%" 2>nul ) ) @@ -215,6 +227,7 @@ if exist "!PROCESSING!" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '!PROCESSING!' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PROC_OK=%%V" if /i not "!PROC_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: !PROCESSING! 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json.processing" del /f /q "!PROCESSING!" 2>nul ) ) @@ -230,39 +243,60 @@ if errorlevel 1 ( goto :resolve_version ) set "CLAIMED_UPDATE=%UPDATES_DIR%\pending-update.json.processing" +call :log_upgrade "apply: claimed pending update for processing" :: Parse the claimed manifest using PowerShell (reliable JSON parsing) for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).version"`) do set "UPD_VERSION=%%V" for /f "usebackq delims=" %%Z in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).zipPath"`) do set "UPD_ZIP=%%Z" for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).sha256"`) do set "UPD_SHA256=%%S" +call :log_upgrade "apply: manifest parsed version=!UPD_VERSION! zipPath=!UPD_ZIP! sha256=!UPD_SHA256!" if not defined UPD_VERSION ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing version); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not defined UPD_ZIP ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing zipPath); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not exist "!UPD_ZIP!" ( echo [Junie] Update file not found: !UPD_ZIP! 1>&2 + call :log_upgrade "apply: update archive not found at !UPD_ZIP!; dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) :: Verify SHA-256 checksum if defined UPD_SHA256 ( + call :log_upgrade "checksum: starting SHA-256 validation expected=!UPD_SHA256!" + set "ACTUAL_SHA=" for /f "usebackq delims=" %%H in (`powershell -NoProfile -Command "(Get-FileHash '!UPD_ZIP!' -Algorithm SHA256).Hash.ToLower()"`) do set "ACTUAL_SHA=%%H" + if not defined ACTUAL_SHA ( + :: Get-FileHash produced no output -- the zip could not be read/hashed, + :: almost always because it is still locked or being scanned by antivirus. + :: This is a transient condition, NOT corruption: PRESERVE the manifest and + :: zip and retry on the next launch. Deleting them here (as a plain mismatch + :: would) makes the binary re-download and re-fail every startup -- an + :: endless update loop that never recovers. + echo [Junie] Could not read update zip ^(locked or AV scan?^); will retry next launch 1>&2 + call :log_upgrade "checksum: UNREADABLE -- Get-FileHash returned no output; preserving manifest and archive for retry" + rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul + goto :resolve_version + ) if /i not "!ACTUAL_SHA!"=="!UPD_SHA256!" ( echo [Junie] Checksum mismatch, skipping update 1>&2 echo [Junie] Expected: !UPD_SHA256! 1>&2 echo [Junie] Got: !ACTUAL_SHA! 1>&2 + call :log_upgrade "checksum: FAIL expected=!UPD_SHA256! got=!ACTUAL_SHA! -- dropping manifest and archive" del /f /q "!CLAIMED_UPDATE!" 2>nul del /f /q "!UPD_ZIP!" 2>nul goto :resolve_version ) + call :log_upgrade "checksum: OK" ) :: Extract to a staging dir on the same filesystem as %VERSIONS_DIR%, validate @@ -279,9 +313,11 @@ if exist "!UPD_STAGING!" rmdir /s /q "!UPD_STAGING!" 2>nul mkdir "!UPD_STAGING!" 2>nul echo [Junie] Applying pending update to version !UPD_VERSION!... 1>&2 +call :log_upgrade "extract: starting Expand-Archive src=!UPD_ZIP! dst=!UPD_STAGING!" powershell -NoProfile -Command "Expand-Archive -Path '!UPD_ZIP!' -DestinationPath '!UPD_STAGING!' -Force" 2>nul if errorlevel 1 ( echo [Junie] Failed to extract update; preserving for retry 1>&2 + call :log_upgrade "extract: FAIL (Expand-Archive non-zero); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -290,6 +326,7 @@ if errorlevel 1 ( :: Validate extracted binary if not exist "!UPD_STAGING!\junie\junie.exe" ( echo [Junie] Extracted payload missing junie\junie.exe; preserving for retry 1>&2 + call :log_upgrade "validate: FAIL (junie\junie.exe missing in payload); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -300,6 +337,7 @@ if exist "!UPD_TARGET!" rename "!UPD_TARGET!" "!UPD_OLD_NAME!" 2>nul move "!UPD_STAGING!" "!UPD_TARGET!" >nul 2>nul if errorlevel 1 ( echo [Junie] Failed to install new version; preserving for retry 1>&2 + call :log_upgrade "swap: FAIL (could not move staging into place); rolled back and preserving manifest for retry" if exist "!UPD_OLD!" if not exist "!UPD_TARGET!" rename "!UPD_OLD!" "!UPD_VERSION!" 2>nul rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul @@ -315,6 +353,7 @@ if exist "!UPD_OLD!" rmdir /s /q "!UPD_OLD!" 2>nul :: Cleanup downloaded zip and manifest only after a successful swap del /f /q "!UPD_ZIP!" 2>nul del /f /q "!CLAIMED_UPDATE!" 2>nul +call :log_upgrade "apply: SUCCESS updated to version !UPD_VERSION!" echo [Junie] Updated to version !UPD_VERSION! 1>&2 :: === Resolve Version === @@ -378,6 +417,18 @@ for %%A in (%*) do ( ) endlocal & "%JUNIE_EXE%" %FILTERED_ARGS% + +:: Jump past the subroutines to end of file, preserving junie.exe's exit code. +goto :eof + +:: === Subroutines === + +:log_upgrade +:: Append a timestamped, session-tagged line to the persistent upgrade log. +:: Best-effort: a failure to write (e.g. read-only profile) must never break +:: launching, so the error is swallowed. %~1 is the (already-expanded) message. +echo [%DATE% %TIME%] [sid=%LOG_SID%] %~1>> "%UPGRADE_LOG%" 2>nul +exit /b 0 '@ [System.IO.File]::WriteAllText($SHIM_PATH, $SHIM_CONTENT) diff --git a/install-nightly.ps1 b/install-nightly.ps1 index 283bd79c..dcc26540 100644 --- a/install-nightly.ps1 +++ b/install-nightly.ps1 @@ -193,6 +193,17 @@ set "CURRENT_FILE=%JUNIE_DATA%\current" set "PENDING_UPDATE=%UPDATES_DIR%\pending-update.json" set "PROCESSING=%UPDATES_DIR%\pending-update.json.processing" +:: Persistent upgrade log (mirrors the POSIX shim). Lives under +:: %USERPROFILE%\.junie\logs -- separate from the data dir so a reinstall that +:: wipes ~/.local/share/junie does not lose update history. Always appended, +:: never truncated. Override the directory with JUNIE_LOG_DIR. A per-invocation +:: session id (LOG_SID) is stamped on every line so one launch can be followed. +set "UPGRADE_LOG_DIR=%USERPROFILE%\.junie\logs" +if defined JUNIE_LOG_DIR set "UPGRADE_LOG_DIR=%JUNIE_LOG_DIR%" +set "UPGRADE_LOG=%UPGRADE_LOG_DIR%\upgrade.log" +set "LOG_SID=%RANDOM%%RANDOM%" +if not exist "%UPGRADE_LOG_DIR%" mkdir "%UPGRADE_LOG_DIR%" 2>nul + ::: === Defensive Sanitization === ::: ::: JUNIE-2957 defense: drop pending-update.json / pending-update.json.processing @@ -207,6 +218,7 @@ if exist "%PENDING_UPDATE%" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '%PENDING_UPDATE%' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PEND_OK=%%V" if /i not "!PEND_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: %PENDING_UPDATE% 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json" del /f /q "%PENDING_UPDATE%" 2>nul ) ) @@ -215,6 +227,7 @@ if exist "!PROCESSING!" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '!PROCESSING!' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PROC_OK=%%V" if /i not "!PROC_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: !PROCESSING! 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json.processing" del /f /q "!PROCESSING!" 2>nul ) ) @@ -230,39 +243,60 @@ if errorlevel 1 ( goto :resolve_version ) set "CLAIMED_UPDATE=%UPDATES_DIR%\pending-update.json.processing" +call :log_upgrade "apply: claimed pending update for processing" :: Parse the claimed manifest using PowerShell (reliable JSON parsing) for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).version"`) do set "UPD_VERSION=%%V" for /f "usebackq delims=" %%Z in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).zipPath"`) do set "UPD_ZIP=%%Z" for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).sha256"`) do set "UPD_SHA256=%%S" +call :log_upgrade "apply: manifest parsed version=!UPD_VERSION! zipPath=!UPD_ZIP! sha256=!UPD_SHA256!" if not defined UPD_VERSION ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing version); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not defined UPD_ZIP ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing zipPath); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not exist "!UPD_ZIP!" ( echo [Junie] Update file not found: !UPD_ZIP! 1>&2 + call :log_upgrade "apply: update archive not found at !UPD_ZIP!; dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) :: Verify SHA-256 checksum if defined UPD_SHA256 ( + call :log_upgrade "checksum: starting SHA-256 validation expected=!UPD_SHA256!" + set "ACTUAL_SHA=" for /f "usebackq delims=" %%H in (`powershell -NoProfile -Command "(Get-FileHash '!UPD_ZIP!' -Algorithm SHA256).Hash.ToLower()"`) do set "ACTUAL_SHA=%%H" + if not defined ACTUAL_SHA ( + :: Get-FileHash produced no output -- the zip could not be read/hashed, + :: almost always because it is still locked or being scanned by antivirus. + :: This is a transient condition, NOT corruption: PRESERVE the manifest and + :: zip and retry on the next launch. Deleting them here (as a plain mismatch + :: would) makes the binary re-download and re-fail every startup -- an + :: endless update loop that never recovers. + echo [Junie] Could not read update zip ^(locked or AV scan?^); will retry next launch 1>&2 + call :log_upgrade "checksum: UNREADABLE -- Get-FileHash returned no output; preserving manifest and archive for retry" + rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul + goto :resolve_version + ) if /i not "!ACTUAL_SHA!"=="!UPD_SHA256!" ( echo [Junie] Checksum mismatch, skipping update 1>&2 echo [Junie] Expected: !UPD_SHA256! 1>&2 echo [Junie] Got: !ACTUAL_SHA! 1>&2 + call :log_upgrade "checksum: FAIL expected=!UPD_SHA256! got=!ACTUAL_SHA! -- dropping manifest and archive" del /f /q "!CLAIMED_UPDATE!" 2>nul del /f /q "!UPD_ZIP!" 2>nul goto :resolve_version ) + call :log_upgrade "checksum: OK" ) :: Extract to a staging dir on the same filesystem as %VERSIONS_DIR%, validate @@ -279,9 +313,11 @@ if exist "!UPD_STAGING!" rmdir /s /q "!UPD_STAGING!" 2>nul mkdir "!UPD_STAGING!" 2>nul echo [Junie] Applying pending update to version !UPD_VERSION!... 1>&2 +call :log_upgrade "extract: starting Expand-Archive src=!UPD_ZIP! dst=!UPD_STAGING!" powershell -NoProfile -Command "Expand-Archive -Path '!UPD_ZIP!' -DestinationPath '!UPD_STAGING!' -Force" 2>nul if errorlevel 1 ( echo [Junie] Failed to extract update; preserving for retry 1>&2 + call :log_upgrade "extract: FAIL (Expand-Archive non-zero); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -290,6 +326,7 @@ if errorlevel 1 ( :: Validate extracted binary if not exist "!UPD_STAGING!\junie\junie.exe" ( echo [Junie] Extracted payload missing junie\junie.exe; preserving for retry 1>&2 + call :log_upgrade "validate: FAIL (junie\junie.exe missing in payload); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -300,6 +337,7 @@ if exist "!UPD_TARGET!" rename "!UPD_TARGET!" "!UPD_OLD_NAME!" 2>nul move "!UPD_STAGING!" "!UPD_TARGET!" >nul 2>nul if errorlevel 1 ( echo [Junie] Failed to install new version; preserving for retry 1>&2 + call :log_upgrade "swap: FAIL (could not move staging into place); rolled back and preserving manifest for retry" if exist "!UPD_OLD!" if not exist "!UPD_TARGET!" rename "!UPD_OLD!" "!UPD_VERSION!" 2>nul rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul @@ -315,6 +353,7 @@ if exist "!UPD_OLD!" rmdir /s /q "!UPD_OLD!" 2>nul :: Cleanup downloaded zip and manifest only after a successful swap del /f /q "!UPD_ZIP!" 2>nul del /f /q "!CLAIMED_UPDATE!" 2>nul +call :log_upgrade "apply: SUCCESS updated to version !UPD_VERSION!" echo [Junie] Updated to version !UPD_VERSION! 1>&2 :: === Resolve Version === @@ -378,6 +417,18 @@ for %%A in (%*) do ( ) endlocal & "%JUNIE_EXE%" %FILTERED_ARGS% + +:: Jump past the subroutines to end of file, preserving junie.exe's exit code. +goto :eof + +:: === Subroutines === + +:log_upgrade +:: Append a timestamped, session-tagged line to the persistent upgrade log. +:: Best-effort: a failure to write (e.g. read-only profile) must never break +:: launching, so the error is swallowed. %~1 is the (already-expanded) message. +echo [%DATE% %TIME%] [sid=%LOG_SID%] %~1>> "%UPGRADE_LOG%" 2>nul +exit /b 0 '@ [System.IO.File]::WriteAllText($SHIM_PATH, $SHIM_CONTENT) diff --git a/install.ps1 b/install.ps1 index e7e5f224..04b1b5ca 100644 --- a/install.ps1 +++ b/install.ps1 @@ -193,6 +193,17 @@ set "CURRENT_FILE=%JUNIE_DATA%\current" set "PENDING_UPDATE=%UPDATES_DIR%\pending-update.json" set "PROCESSING=%UPDATES_DIR%\pending-update.json.processing" +:: Persistent upgrade log (mirrors the POSIX shim). Lives under +:: %USERPROFILE%\.junie\logs -- separate from the data dir so a reinstall that +:: wipes ~/.local/share/junie does not lose update history. Always appended, +:: never truncated. Override the directory with JUNIE_LOG_DIR. A per-invocation +:: session id (LOG_SID) is stamped on every line so one launch can be followed. +set "UPGRADE_LOG_DIR=%USERPROFILE%\.junie\logs" +if defined JUNIE_LOG_DIR set "UPGRADE_LOG_DIR=%JUNIE_LOG_DIR%" +set "UPGRADE_LOG=%UPGRADE_LOG_DIR%\upgrade.log" +set "LOG_SID=%RANDOM%%RANDOM%" +if not exist "%UPGRADE_LOG_DIR%" mkdir "%UPGRADE_LOG_DIR%" 2>nul + ::: === Defensive Sanitization === ::: ::: JUNIE-2957 defense: drop pending-update.json / pending-update.json.processing @@ -207,6 +218,7 @@ if exist "%PENDING_UPDATE%" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '%PENDING_UPDATE%' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PEND_OK=%%V" if /i not "!PEND_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: %PENDING_UPDATE% 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json" del /f /q "%PENDING_UPDATE%" 2>nul ) ) @@ -215,6 +227,7 @@ if exist "!PROCESSING!" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '!PROCESSING!' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PROC_OK=%%V" if /i not "!PROC_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: !PROCESSING! 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json.processing" del /f /q "!PROCESSING!" 2>nul ) ) @@ -230,39 +243,60 @@ if errorlevel 1 ( goto :resolve_version ) set "CLAIMED_UPDATE=%UPDATES_DIR%\pending-update.json.processing" +call :log_upgrade "apply: claimed pending update for processing" :: Parse the claimed manifest using PowerShell (reliable JSON parsing) for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).version"`) do set "UPD_VERSION=%%V" for /f "usebackq delims=" %%Z in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).zipPath"`) do set "UPD_ZIP=%%Z" for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).sha256"`) do set "UPD_SHA256=%%S" +call :log_upgrade "apply: manifest parsed version=!UPD_VERSION! zipPath=!UPD_ZIP! sha256=!UPD_SHA256!" if not defined UPD_VERSION ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing version); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not defined UPD_ZIP ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing zipPath); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not exist "!UPD_ZIP!" ( echo [Junie] Update file not found: !UPD_ZIP! 1>&2 + call :log_upgrade "apply: update archive not found at !UPD_ZIP!; dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) :: Verify SHA-256 checksum if defined UPD_SHA256 ( + call :log_upgrade "checksum: starting SHA-256 validation expected=!UPD_SHA256!" + set "ACTUAL_SHA=" for /f "usebackq delims=" %%H in (`powershell -NoProfile -Command "(Get-FileHash '!UPD_ZIP!' -Algorithm SHA256).Hash.ToLower()"`) do set "ACTUAL_SHA=%%H" + if not defined ACTUAL_SHA ( + :: Get-FileHash produced no output -- the zip could not be read/hashed, + :: almost always because it is still locked or being scanned by antivirus. + :: This is a transient condition, NOT corruption: PRESERVE the manifest and + :: zip and retry on the next launch. Deleting them here (as a plain mismatch + :: would) makes the binary re-download and re-fail every startup -- an + :: endless update loop that never recovers. + echo [Junie] Could not read update zip ^(locked or AV scan?^); will retry next launch 1>&2 + call :log_upgrade "checksum: UNREADABLE -- Get-FileHash returned no output; preserving manifest and archive for retry" + rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul + goto :resolve_version + ) if /i not "!ACTUAL_SHA!"=="!UPD_SHA256!" ( echo [Junie] Checksum mismatch, skipping update 1>&2 echo [Junie] Expected: !UPD_SHA256! 1>&2 echo [Junie] Got: !ACTUAL_SHA! 1>&2 + call :log_upgrade "checksum: FAIL expected=!UPD_SHA256! got=!ACTUAL_SHA! -- dropping manifest and archive" del /f /q "!CLAIMED_UPDATE!" 2>nul del /f /q "!UPD_ZIP!" 2>nul goto :resolve_version ) + call :log_upgrade "checksum: OK" ) :: Extract to a staging dir on the same filesystem as %VERSIONS_DIR%, validate @@ -279,9 +313,11 @@ if exist "!UPD_STAGING!" rmdir /s /q "!UPD_STAGING!" 2>nul mkdir "!UPD_STAGING!" 2>nul echo [Junie] Applying pending update to version !UPD_VERSION!... 1>&2 +call :log_upgrade "extract: starting Expand-Archive src=!UPD_ZIP! dst=!UPD_STAGING!" powershell -NoProfile -Command "Expand-Archive -Path '!UPD_ZIP!' -DestinationPath '!UPD_STAGING!' -Force" 2>nul if errorlevel 1 ( echo [Junie] Failed to extract update; preserving for retry 1>&2 + call :log_upgrade "extract: FAIL (Expand-Archive non-zero); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -290,6 +326,7 @@ if errorlevel 1 ( :: Validate extracted binary if not exist "!UPD_STAGING!\junie\junie.exe" ( echo [Junie] Extracted payload missing junie\junie.exe; preserving for retry 1>&2 + call :log_upgrade "validate: FAIL (junie\junie.exe missing in payload); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -300,6 +337,7 @@ if exist "!UPD_TARGET!" rename "!UPD_TARGET!" "!UPD_OLD_NAME!" 2>nul move "!UPD_STAGING!" "!UPD_TARGET!" >nul 2>nul if errorlevel 1 ( echo [Junie] Failed to install new version; preserving for retry 1>&2 + call :log_upgrade "swap: FAIL (could not move staging into place); rolled back and preserving manifest for retry" if exist "!UPD_OLD!" if not exist "!UPD_TARGET!" rename "!UPD_OLD!" "!UPD_VERSION!" 2>nul rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul @@ -315,6 +353,7 @@ if exist "!UPD_OLD!" rmdir /s /q "!UPD_OLD!" 2>nul :: Cleanup downloaded zip and manifest only after a successful swap del /f /q "!UPD_ZIP!" 2>nul del /f /q "!CLAIMED_UPDATE!" 2>nul +call :log_upgrade "apply: SUCCESS updated to version !UPD_VERSION!" echo [Junie] Updated to version !UPD_VERSION! 1>&2 :: === Resolve Version === @@ -378,6 +417,18 @@ for %%A in (%*) do ( ) endlocal & "%JUNIE_EXE%" %FILTERED_ARGS% + +:: Jump past the subroutines to end of file, preserving junie.exe's exit code. +goto :eof + +:: === Subroutines === + +:log_upgrade +:: Append a timestamped, session-tagged line to the persistent upgrade log. +:: Best-effort: a failure to write (e.g. read-only profile) must never break +:: launching, so the error is swallowed. %~1 is the (already-expanded) message. +echo [%DATE% %TIME%] [sid=%LOG_SID%] %~1>> "%UPGRADE_LOG%" 2>nul +exit /b 0 '@ [System.IO.File]::WriteAllText($SHIM_PATH, $SHIM_CONTENT) diff --git a/templates/junie.shim.bat b/templates/junie.shim.bat index 2a1203e8..50c960e2 100644 --- a/templates/junie.shim.bat +++ b/templates/junie.shim.bat @@ -22,6 +22,17 @@ set "CURRENT_FILE=%JUNIE_DATA%\current" set "PENDING_UPDATE=%UPDATES_DIR%\pending-update.json" set "PROCESSING=%UPDATES_DIR%\pending-update.json.processing" +:: Persistent upgrade log (mirrors the POSIX shim). Lives under +:: %USERPROFILE%\.junie\logs -- separate from the data dir so a reinstall that +:: wipes ~/.local/share/junie does not lose update history. Always appended, +:: never truncated. Override the directory with JUNIE_LOG_DIR. A per-invocation +:: session id (LOG_SID) is stamped on every line so one launch can be followed. +set "UPGRADE_LOG_DIR=%USERPROFILE%\.junie\logs" +if defined JUNIE_LOG_DIR set "UPGRADE_LOG_DIR=%JUNIE_LOG_DIR%" +set "UPGRADE_LOG=%UPGRADE_LOG_DIR%\upgrade.log" +set "LOG_SID=%RANDOM%%RANDOM%" +if not exist "%UPGRADE_LOG_DIR%" mkdir "%UPGRADE_LOG_DIR%" 2>nul + ::: === Defensive Sanitization === ::: ::: JUNIE-2957 defense: drop pending-update.json / pending-update.json.processing @@ -36,6 +47,7 @@ if exist "%PENDING_UPDATE%" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '%PENDING_UPDATE%' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PEND_OK=%%V" if /i not "!PEND_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: %PENDING_UPDATE% 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json" del /f /q "%PENDING_UPDATE%" 2>nul ) ) @@ -44,6 +56,7 @@ if exist "!PROCESSING!" ( for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "try { $c = Get-Content -Raw -LiteralPath '!PROCESSING!' -ErrorAction Stop; if ([string]::IsNullOrWhiteSpace($c)) { 'bad' } else { $j = $c ^| ConvertFrom-Json; if ($j.version -and $j.zipPath) { 'ok' } else { 'bad' } } } catch { 'bad' }"`) do set "PROC_OK=%%V" if /i not "!PROC_OK!"=="ok" ( echo [Junie] Removing invalid update artifact: !PROCESSING! 1>&2 + call :log_upgrade "sanitize: removing invalid update artifact pending-update.json.processing" del /f /q "!PROCESSING!" 2>nul ) ) @@ -59,39 +72,60 @@ if errorlevel 1 ( goto :resolve_version ) set "CLAIMED_UPDATE=%UPDATES_DIR%\pending-update.json.processing" +call :log_upgrade "apply: claimed pending update for processing" :: Parse the claimed manifest using PowerShell (reliable JSON parsing) for /f "usebackq delims=" %%V in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).version"`) do set "UPD_VERSION=%%V" for /f "usebackq delims=" %%Z in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).zipPath"`) do set "UPD_ZIP=%%Z" for /f "usebackq delims=" %%S in (`powershell -NoProfile -Command "(Get-Content '!CLAIMED_UPDATE!' | ConvertFrom-Json).sha256"`) do set "UPD_SHA256=%%S" +call :log_upgrade "apply: manifest parsed version=!UPD_VERSION! zipPath=!UPD_ZIP! sha256=!UPD_SHA256!" if not defined UPD_VERSION ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing version); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not defined UPD_ZIP ( echo [Junie] Invalid pending update manifest, skipping 1>&2 + call :log_upgrade "apply: invalid manifest (missing zipPath); dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) if not exist "!UPD_ZIP!" ( echo [Junie] Update file not found: !UPD_ZIP! 1>&2 + call :log_upgrade "apply: update archive not found at !UPD_ZIP!; dropping manifest" del /f /q "!CLAIMED_UPDATE!" 2>nul goto :resolve_version ) :: Verify SHA-256 checksum if defined UPD_SHA256 ( + call :log_upgrade "checksum: starting SHA-256 validation expected=!UPD_SHA256!" + set "ACTUAL_SHA=" for /f "usebackq delims=" %%H in (`powershell -NoProfile -Command "(Get-FileHash '!UPD_ZIP!' -Algorithm SHA256).Hash.ToLower()"`) do set "ACTUAL_SHA=%%H" + if not defined ACTUAL_SHA ( + :: Get-FileHash produced no output -- the zip could not be read/hashed, + :: almost always because it is still locked or being scanned by antivirus. + :: This is a transient condition, NOT corruption: PRESERVE the manifest and + :: zip and retry on the next launch. Deleting them here (as a plain mismatch + :: would) makes the binary re-download and re-fail every startup -- an + :: endless update loop that never recovers. + echo [Junie] Could not read update zip ^(locked or AV scan?^); will retry next launch 1>&2 + call :log_upgrade "checksum: UNREADABLE -- Get-FileHash returned no output; preserving manifest and archive for retry" + rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul + goto :resolve_version + ) if /i not "!ACTUAL_SHA!"=="!UPD_SHA256!" ( echo [Junie] Checksum mismatch, skipping update 1>&2 echo [Junie] Expected: !UPD_SHA256! 1>&2 echo [Junie] Got: !ACTUAL_SHA! 1>&2 + call :log_upgrade "checksum: FAIL expected=!UPD_SHA256! got=!ACTUAL_SHA! -- dropping manifest and archive" del /f /q "!CLAIMED_UPDATE!" 2>nul del /f /q "!UPD_ZIP!" 2>nul goto :resolve_version ) + call :log_upgrade "checksum: OK" ) :: Extract to a staging dir on the same filesystem as %VERSIONS_DIR%, validate @@ -108,9 +142,11 @@ if exist "!UPD_STAGING!" rmdir /s /q "!UPD_STAGING!" 2>nul mkdir "!UPD_STAGING!" 2>nul echo [Junie] Applying pending update to version !UPD_VERSION!... 1>&2 +call :log_upgrade "extract: starting Expand-Archive src=!UPD_ZIP! dst=!UPD_STAGING!" powershell -NoProfile -Command "Expand-Archive -Path '!UPD_ZIP!' -DestinationPath '!UPD_STAGING!' -Force" 2>nul if errorlevel 1 ( echo [Junie] Failed to extract update; preserving for retry 1>&2 + call :log_upgrade "extract: FAIL (Expand-Archive non-zero); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -119,6 +155,7 @@ if errorlevel 1 ( :: Validate extracted binary if not exist "!UPD_STAGING!\junie\junie.exe" ( echo [Junie] Extracted payload missing junie\junie.exe; preserving for retry 1>&2 + call :log_upgrade "validate: FAIL (junie\junie.exe missing in payload); preserving manifest and archive for retry" rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul goto :resolve_version @@ -129,6 +166,7 @@ if exist "!UPD_TARGET!" rename "!UPD_TARGET!" "!UPD_OLD_NAME!" 2>nul move "!UPD_STAGING!" "!UPD_TARGET!" >nul 2>nul if errorlevel 1 ( echo [Junie] Failed to install new version; preserving for retry 1>&2 + call :log_upgrade "swap: FAIL (could not move staging into place); rolled back and preserving manifest for retry" if exist "!UPD_OLD!" if not exist "!UPD_TARGET!" rename "!UPD_OLD!" "!UPD_VERSION!" 2>nul rmdir /s /q "!UPD_STAGING!" 2>nul rename "!CLAIMED_UPDATE!" "pending-update.json" 2>nul @@ -144,6 +182,7 @@ if exist "!UPD_OLD!" rmdir /s /q "!UPD_OLD!" 2>nul :: Cleanup downloaded zip and manifest only after a successful swap del /f /q "!UPD_ZIP!" 2>nul del /f /q "!CLAIMED_UPDATE!" 2>nul +call :log_upgrade "apply: SUCCESS updated to version !UPD_VERSION!" echo [Junie] Updated to version !UPD_VERSION! 1>&2 :: === Resolve Version === @@ -207,3 +246,15 @@ for %%A in (%*) do ( ) endlocal & "%JUNIE_EXE%" %FILTERED_ARGS% + +:: Jump past the subroutines to end of file, preserving junie.exe's exit code. +goto :eof + +:: === Subroutines === + +:log_upgrade +:: Append a timestamped, session-tagged line to the persistent upgrade log. +:: Best-effort: a failure to write (e.g. read-only profile) must never break +:: launching, so the error is swallowed. %~1 is the (already-expanded) message. +echo [%DATE% %TIME%] [sid=%LOG_SID%] %~1>> "%UPGRADE_LOG%" 2>nul +exit /b 0