Skip to content

fix(windows): avoid encoded PowerShell updater - #36

Merged
elkaix merged 5 commits into
mainfrom
fix/windows-updater-av-heuristics
May 31, 2026
Merged

fix(windows): avoid encoded PowerShell updater#36
elkaix merged 5 commits into
mainfrom
fix/windows-updater-av-heuristics

Conversation

@elkaix

@elkaix elkaix commented May 31, 2026

Copy link
Copy Markdown
Member

Summary

  • replace Windows native update PowerShell -EncodedCommand helper with direct Inno installer launch
  • use visible /SILENT progress plus Restart Manager flags instead of fully suppressed setup
  • sign bundled Windows PE files and let Inno sign setup/uninstaller/temp copies when signing credentials are configured
  • update bootstrap installer copies, docs, changelog, and regression tests

Verification

  • make check-pythinker-code
  • uv run pytest tests/ui_and_conv/test_shell_update.py tests/ui_and_conv/test_native_update_parity.py tests/test_release_update_pipeline.py tests/test_installation_docs.py -q
  • PowerShell parser check for packages/windows-installer/build.ps1, scripts/install.ps1, docs/public/install.ps1, web/public/install.ps1

Summary by CodeRabbit

  • Improvements

    • Windows updates now launch the signed installer executable directly (no PowerShell wrapper), show visible silent progress, more reliably close apps, and handle restart behavior better.
    • Update process now uses a shared staging area that prunes stale update data and exits the updater when spawning the installer.
  • Documentation

    • Windows upgrade instructions updated to use the revised installer flags for silent/close/restart behavior.

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1a384017-096d-42ed-9d44-30824dc218b2

📥 Commits

Reviewing files that changed from the base of the PR and between 00276e3 and 7f41289.

📒 Files selected for processing (4)
  • packages/windows-installer/installer.iss
  • src/pythinker_code/ui/shell/update.py
  • tests/test_release_update_pipeline.py
  • tests/ui_and_conv/test_shell_update.py

📝 Walkthrough

Walkthrough

Refactors Windows native updates to spawn installers directly (no PowerShell -EncodedCommand), standardizes Inno installer flags across bootstraps to /SILENT with close-app flags, adds conditional signing for bundled PE and Inno artifacts in the build, and introduces shared Windows update staging with stale-prune logic.

Changes

Windows Installer and Update Refactoring

Layer / File(s) Summary
Installer argument standardization across bootstrap scripts
README.md, docs/public/install.ps1, scripts/install.ps1, web/public/install.ps1, tests/test_installation_docs.py, CHANGELOG.md
Replaced /VERYSILENT//SUPPRESSMSGBOXES with /SILENT, added /CLOSEAPPLICATIONS and /NORESTARTAPPLICATIONS across docs and bootstrap scripts; test updated to require new flags and forbid old ones.
Windows installer signing pipeline and Inno integration
packages/windows-installer/build.ps1, packages/windows-installer/installer.iss, tests/test_release_update_pipeline.py
Adds Invoke-PythinkerSign and signing-config detection, recursively signs .exe/.dll/.pyd under dist\pythinker, injects PowerShell sign wrapper into Inno compile when configured, and conditionally enables Inno SignTool/SignedUninstaller. New tests verify signing wiring and ordering.
Installer.iss PATH handling
packages/windows-installer/installer.iss
Introduces PathWithoutEntry, changes AddToPath to prefix the new entry and uses the helper in RemoveFromPath, and calls AddToPath based solely on task selection.
Python update logic: PowerShell removal and staging
src/pythinker_code/ui/shell/update.py
Removes encoded-PowerShell helpers and base64 usage; directly spawns upgrade and installer executables with Windows creation flags; adds WINDOWS_UPDATE_STAGING_MAX_AGE_SECONDS, shared staging parent, stale-prune _cleanup_stale_windows_update_staging(), staging-aware _make_native_update_tmpdir(), and _windows_native_installer_args(). Updates messaging and exits the caller after successful detached spawn.
Tests and parity updates
tests/ui_and_conv/test_native_update_parity.py, tests/ui_and_conv/test_shell_update.py, tests/test_installation_docs.py, tests/test_release_update_pipeline.py
Replaces tests that relied on PowerShell-encoded commands with assertions for direct Popen calls, exact Inno argument lists, and detached process creation flags; adds tests validating installer signing wiring and PATH concatenation order.

Sequence Diagram

sequenceDiagram
  participant Updater
  participant OS
  participant InstallerExe
  Updater->>OS: spawn_installer
  OS->>InstallerExe: execute_installer
  InstallerExe->>Updater: detached_exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.47% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional commits format with 'fix' type and Windows scope, accurately describing the main change of removing PowerShell encoding from the updater.
Description check ✅ Passed Description covers key changes with verification steps, though missing explicit issue link and checklist items per template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/windows-updater-av-heuristics

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/public/install.ps1`:
- Line 201: The script assigns to the automatic PowerShell variable $args which
shadows the built-in; rename the local variable (for example to $installArgs or
$scriptArgs) everywhere it is defined and referenced in docs/public/install.ps1
(look for the $args = @( ... ) declaration and subsequent uses) to avoid
PSAvoidAssignmentToAutomaticVariable; ensure all references (parameter
expansion, passing to functions, splatting, etc.) are updated consistently and
run the script to verify behavior remains identical.

In `@packages/windows-installer/build.ps1`:
- Around line 100-108: The /SPythinkerSign argument can lose embedded quotes on
PowerShell 5.1 when passed to the native iscc.exe because $signCommand embeds
quoted -File "$signScript"; fix by avoiding nested quoted paths: either (A)
build $signCommand to call a small wrapper .cmd (e.g., create a sign-wrapper.cmd
that runs powershell -NoProfile -NonInteractive -File "%~1" %2 and set
$signCommand to "cmd.exe /C \"path\to\sign-wrapper.cmd\" `"$signScript`" `$f"),
or (B) convert $signScript to its short (8.3) path via GetShortPathName and use
that short path in $signCommand; then append that safer $signCommand into
$isccArgs (still gated by $signingConfigured) and add a diagnostic log of
$isccArgs before invoking & $iscc.Source so you can confirm the exact argument
delivered to iscc.exe.

In `@scripts/install.ps1`:
- Line 201: The script assigns to PowerShell's automatic variable $args which
shadows the built-in and triggers PSAvoidAssignmentToAutomaticVariable; rename
the local variable (for example to $scriptArgs or $installArgs) everywhere it is
constructed and referenced (the $args = @(...) assignment and subsequent uses)
so no code assigns to or reads from the automatic $args; update any dependent
references to use the new name consistently.

In `@src/pythinker_code/ui/shell/update.py`:
- Around line 777-785: The returned installer argument list in
_windows_native_installer_args omits the /CURRENTUSER flag, causing in-app
updates to attempt system-wide installs; update the function
_windows_native_installer_args to include "/CURRENTUSER" in the returned list
(alongside "/SILENT", "/NORESTART", "/CLOSEAPPLICATIONS",
"/NORESTARTAPPLICATIONS") so the Inno Setup installer runs per-user consistent
with the PowerShell bootstraps.
- Around line 1007-1008: Move the module-level constant
WINDOWS_UPDATE_STAGING_MAX_AGE_SECONDS from its current mid-file location into
the top-of-file constants block (immediately after imports alongside the other
constants) so all configuration constants are grouped together; then delete the
duplicate definition where it currently appears mid-file to avoid redundancy and
ensure the code references the single top-level constant.

In `@tests/ui_and_conv/test_shell_update.py`:
- Around line 749-775: Update the test expectations in
test_spawn_detached_windows_installer_uses_inno_directly_not_powershell to
include the missing /CURRENTUSER flag so it matches the installer args produced
by update._windows_native_installer_args() and consumed by
update._spawn_detached_windows_installer(); specifically, adjust the expected
args list in that test (the launched variable assertion) to include
"/CURRENTUSER" among the flags so the assertion reflects the actual arguments
returned by the update module.

In `@web/public/install.ps1`:
- Line 201: The script assigns to the automatic PowerShell variable $args which
shadows built-in parameters; rename that variable (for example to $installArgs
or $scriptArgs) wherever it is declared (the line with "$args = @(") and update
all subsequent references to use the new name so you no longer assign to the
automatic $args (this will satisfy PSAvoidAssignmentToAutomaticVariable and
avoid confusing shadowing).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6e09222d-f686-4edc-8c56-6601b0f36bde

📥 Commits

Reviewing files that changed from the base of the PR and between 47e5c5e and d7b99b2.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • README.md
  • docs/public/install.ps1
  • packages/windows-installer/build.ps1
  • packages/windows-installer/installer.iss
  • scripts/install.ps1
  • src/pythinker_code/ui/shell/update.py
  • tests/test_installation_docs.py
  • tests/test_release_update_pipeline.py
  • tests/ui_and_conv/test_native_update_parity.py
  • tests/ui_and_conv/test_shell_update.py
  • web/public/install.ps1

Comment thread docs/public/install.ps1 Outdated
Comment thread packages/windows-installer/build.ps1
Comment thread scripts/install.ps1 Outdated
Comment thread src/pythinker_code/ui/shell/update.py Outdated
Comment thread src/pythinker_code/ui/shell/update.py Outdated
Comment thread tests/ui_and_conv/test_shell_update.py
Comment thread web/public/install.ps1 Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_release_update_pipeline.py`:
- Around line 81-89: Replace the fragile white-box assertions in the test that
check exact PowerShell command and log text with contract-focused checks: assert
that signing is enabled by checking "/DUseInnoSignTool=1" is in build_script,
assert the env-driven sign script path is wired by checking
"PYTHINKER_INNO_SIGN_SCRIPT" (and that "/SPythinkerSign=$signCommand" remains
present), and keep the negative assertion that the old encoded-command form
'-File `"$signScript`"' is not present; remove assertions that pin the exact
cmd.exe/powershell invocation string and the specific "build.ps1: invoking Inno
Setup with arguments:" log text so the test validates behavior not internal
quoting/logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e5cbb92f-6354-4ea6-abea-a3b21e022a7f

📥 Commits

Reviewing files that changed from the base of the PR and between d7b99b2 and 87f114d.

📒 Files selected for processing (9)
  • README.md
  • docs/public/install.ps1
  • packages/windows-installer/build.ps1
  • scripts/install.ps1
  • src/pythinker_code/ui/shell/update.py
  • tests/test_release_update_pipeline.py
  • tests/ui_and_conv/test_native_update_parity.py
  • tests/ui_and_conv/test_shell_update.py
  • web/public/install.ps1

Comment thread tests/test_release_update_pipeline.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pythinker_code/ui/shell/update.py (1)

827-833: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle fallback installer spawn failure explicitly

Line 827 currently performs a second subprocess.Popen(...) without a guard. If that call raises, /update can crash with a traceback instead of a clear next step for the user.

Proposed fix
 def _run_native_installer(installer_path: Path) -> None:
@@
     if _spawn_detached_windows_installer(installer_path):
         sys.exit(0)
-    subprocess.Popen(
-        [str(installer_path), *_windows_native_installer_args()],
-        creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
-        | getattr(subprocess, "DETACHED_PROCESS", 0),
-        close_fds=True,
-    )
-    sys.exit(0)
+    try:
+        subprocess.Popen(
+            [str(installer_path), *_windows_native_installer_args()],
+            creationflags=getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
+            | getattr(subprocess, "DETACHED_PROCESS", 0),
+            close_fds=True,
+        )
+    except OSError as exc:
+        logger.exception("Failed to launch Windows installer:")
+        raise typer.Exit("Failed to launch installer. Please run /update again or install manually.") from exc
+    sys.exit(0)

As per coding guidelines, **/*.py: Keep exceptions actionable. User-facing CLI errors should explain what to do next.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pythinker_code/ui/shell/update.py` around lines 827 - 833, The second
subprocess.Popen call that launches the fallback installer (using installer_path
and _windows_native_installer_args with creationflags) can raise and currently
crashes /update; wrap this subprocess.Popen(...) plus the subsequent sys.exit(0)
in a try/except that catches Exception, emit a clear, actionable user-facing
message (e.g., via the module's CLI logger or click.echo/print) explaining the
installer spawn failed and what to do next (path to installer, manual run
instructions), and then exit with a non-zero status; ensure the exception
details are included in the log for debugging while keeping the message
user-friendly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/windows-installer/installer.iss`:
- Around line 104-106: The current use of StringChangeEx(OrigPath, Param, '',
True) is unsafe because it removes Param as a substring anywhere in OrigPath;
remove that raw substring replacement and only perform delimiter-aware removals
using the existing StringChangeEx calls that target ';' + Param, Param + ';'
(and if desired also ';' + Param + ';' or a normalized ';' + OrigPath + ';'
approach) so only exact PATH entries are removed; update the logic around the
StringChangeEx calls (referencing StringChangeEx, OrigPath and Param) to
normalize/boundary-wrap the path string or explicitly search/replace ';' + Param
+ ';' variants instead of the plain Param replacement.

In `@tests/test_release_update_pipeline.py`:
- Around line 75-76: The test currently asserts exact source snippets ("NewPath
:= Param + ';' + OrigPath" and its negation) which couples the test to
implementation text; replace these fragile asserts with behavior-based checks
against the installer_script variable—e.g., assert that the NewPath assignment
puts Param before OrigPath (check the order or match a looser regex for "NewPath
:= ... Param ... OrigPath") and that the reversed order does not occur; update
the assertions in tests/test_release_update_pipeline.py to verify
ordering/behavior rather than exact source-string equality using the
installer_script identifier.

---

Outside diff comments:
In `@src/pythinker_code/ui/shell/update.py`:
- Around line 827-833: The second subprocess.Popen call that launches the
fallback installer (using installer_path and _windows_native_installer_args with
creationflags) can raise and currently crashes /update; wrap this
subprocess.Popen(...) plus the subsequent sys.exit(0) in a try/except that
catches Exception, emit a clear, actionable user-facing message (e.g., via the
module's CLI logger or click.echo/print) explaining the installer spawn failed
and what to do next (path to installer, manual run instructions), and then exit
with a non-zero status; ensure the exception details are included in the log for
debugging while keeping the message user-friendly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5c49019b-91e3-4299-ad78-b2ff29e76194

📥 Commits

Reviewing files that changed from the base of the PR and between 3ddb41b and 00276e3.

📒 Files selected for processing (4)
  • packages/windows-installer/installer.iss
  • src/pythinker_code/ui/shell/update.py
  • tests/test_release_update_pipeline.py
  • tests/ui_and_conv/test_native_update_parity.py

Comment thread packages/windows-installer/installer.iss Outdated
Comment thread tests/test_release_update_pipeline.py Outdated
@elkaix
elkaix merged commit 25c2f20 into main May 31, 2026
25 checks passed
@elkaix
elkaix deleted the fix/windows-updater-av-heuristics branch July 17, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant