diff options
| -rw-r--r-- | packages/meshbay-client/build/installer-light.nsh | 13 | ||||
| -rw-r--r-- | packages/meshbay-client/build/installer.nsh | 30 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_packaging_win.py | 31 | ||||
| -rw-r--r-- | packaging/win/firewall.ps1 | 11 |
4 files changed, 54 insertions, 31 deletions
diff --git a/packages/meshbay-client/build/installer-light.nsh b/packages/meshbay-client/build/installer-light.nsh index 86e41ba..7717331 100644 --- a/packages/meshbay-client/build/installer-light.nsh +++ b/packages/meshbay-client/build/installer-light.nsh @@ -53,15 +53,16 @@ !macroend !macro customUnInstall - ; Opt-in, default No -- a stale allow-rule is inert, so this should not - ; nag. A silent uninstall skips it entirely (no UAC prompt of its own). + ; Same as the full installer: the UAC prompt alone, no question before it, + ; and only when a rule is still there. A silent uninstall skips it (it may + ; not raise UI). ${IfNot} ${Silent} - MessageBox MB_YESNO|MB_ICONQUESTION \ - "Remove MeshBay Light's Windows Firewall rules? This needs one administrator confirmation. They are harmless if left." \ - /SD IDNO IDNO mb_keep_firewall + nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" any' + Pop $R1 ; 0 = at least one rule exists + ${If} $R1 == 0 ExecShellWait "runas" "${MB_PWSH}" \ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" remove' \ SW_HIDE - mb_keep_firewall: + ${EndIf} ${EndIf} !macroend diff --git a/packages/meshbay-client/build/installer.nsh b/packages/meshbay-client/build/installer.nsh index ef9c82a..2337088 100644 --- a/packages/meshbay-client/build/installer.nsh +++ b/packages/meshbay-client/build/installer.nsh @@ -24,8 +24,8 @@ ; task when service mode is chosen; their own single elevation otherwise. ; One UAC prompt for an install, never two, never zero; ; - cleanup of whichever of those is outside $INSTDIR on the way out (the -; Startup .vbs; the scheduled task and firewall rules, together, if the -; user opts in at uninstall time). +; Startup .vbs; the scheduled task and firewall rules, together, in one +; elevation asked for only when one of them is still there). ; ; Deliberately NOT touched: ; - %LOCALAPPDATA%\meshbay\ (node.toml, keystore.enc, unlock.key, data/) -- @@ -268,22 +268,26 @@ Var pid WriteRegExpandStr HKCU "Environment" "Path" "$1" SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 - ; Offer to take the firewall rules, and the service task if one was set up, - ; back out together (needs admin again -- one prompt for both, same as - ; install). Both underlying removes are no-ops when there is nothing to - ; remove, so this is safe to run unconditionally regardless of which mode - ; was chosen. Stale rules/tasks are inert if left, so this is opt-in and - ; default-No; a silent uninstall skips it entirely. customUnInstall runs - ; before the files are removed, so service-mode.ps1 is still there. + ; The firewall rules, and the service task if one was set up, go too: one + ; elevation for both, the UAC prompt alone, no question before it. There + ; used to be a Yes/No asking first, default No, so an uninstall left them + ; behind unless the person thought to say yes. Both are read unelevated + ; first, so a machine where neither is left sees no prompt at all. A + ; silent uninstall skips it (it may not raise UI); so does an upgrade, + ; whose install keeps both. customUnInstall runs before the files are + ; removed, so the scripts are still there. ${IfNot} ${Silent} ${AndIfNot} ${isUpdated} - MessageBox MB_YESNO|MB_ICONQUESTION \ - "Remove MeshBay's Windows Firewall rules and its boot-time service task, if you set one up? This needs one administrator confirmation. Both are harmless if left." \ - /SD IDNO IDNO mb_keep_privileged + nsExec::Exec 'schtasks /query /tn "MeshBay Node"' + Pop $R0 ; 0 = the boot task exists + nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" any' + Pop $R1 ; 0 = at least one rule exists + ${If} $R0 == 0 + ${OrIf} $R1 == 0 ExecShellWait "runas" "${MB_PWSH}" \ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service-mode.ps1" -Action uninstall' \ SW_HIDE - mb_keep_privileged: + ${EndIf} ${EndIf} ; meshbay_node.platform.autostart_install() -- if the user picked "at sign-in" diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py index 96351db..ad366d0 100644 --- a/packages/meshbay-node/tests/test_packaging_win.py +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -554,23 +554,32 @@ def test_reinstalling_with_everything_already_in_place_asks_nothing(): assert "Goto mb_auto_done" in install -def test_the_uninstaller_offers_to_remove_everything_privileged_default_no(): +def test_the_uninstaller_removes_everything_privileged_without_asking(): """ - Opt-in on the way out too, and defaulting to No: a stale allow-rule or - Scheduled Task is inert, so this should not nag. One elevation removes - both, unconditionally — service-mode.ps1's own remove actions are each - no-ops when there is nothing to remove, so this is safe to run whether or - not service mode was ever chosen. + No Yes/No first: there used to be one, default No, and an uninstall left + the firewall rules and the boot task behind unless the person thought to + say yes. The UAC prompt is the only question, and it is asked only when a + rule or the task is still there, read unelevated beforehand. One elevation + removes both. """ nsh = NSH.read_text(encoding="utf-8") uninstall = _macro_body(nsh, "customUnInstall") + assert "MessageBox" not in uninstall assert "${IfNot} ${Silent}" in uninstall assert "${AndIfNot} ${isUpdated}" in uninstall, "not while an upgrade replaces it" - assert "/SD IDNO" in uninstall, "the uninstall prompt should default to No" + i_task = uninstall.index('schtasks /query /tn "MeshBay Node"') + i_rules = uninstall.index('firewall.ps1" any') + i_runas = uninstall.index('ExecShellWait "runas"') + assert i_task < i_runas and i_rules < i_runas, "read first, elevate after" # uninstall, not remove: remove keeps the firewall rules (a mode switch). assert 'service-mode.ps1" -Action uninstall' in uninstall - assert 'ExecShellWait "runas"' in uninstall + + +def test_the_firewall_helper_can_tell_whether_any_rule_is_left(): + ps1 = (ROOT / "packaging" / "win" / "firewall.ps1").read_text(encoding="utf-8") + assert '"any"' in ps1.split("ValidateSet", 1)[1].split(")", 1)[0] + assert '$Action -eq "any"' in ps1 def test_an_upgrade_keeps_the_sign_in_launcher(): @@ -1163,13 +1172,13 @@ def test_light_installer_never_touches_a_co_installed_full_clients_node(): "belongs to a co-installed Full client, not to Light") -def test_light_uninstaller_offers_to_remove_the_firewall_rules_default_no(): +def test_light_uninstaller_removes_the_firewall_rules_without_asking(): nsh = LIGHT_NSH.read_text(encoding="utf-8") uninstall = _macro_body(nsh, "customUnInstall") + assert "MessageBox" not in uninstall assert "${IfNot} ${Silent}" in uninstall - assert "/SD IDNO" in uninstall + assert uninstall.index('firewall.ps1" any') < uninstall.index('ExecShellWait "runas"') assert 'firewall.ps1" remove' in uninstall - assert 'ExecShellWait "runas"' in uninstall # ── the app-side gaps a bundle-less build would otherwise hit ────────────── diff --git a/packaging/win/firewall.ps1 b/packaging/win/firewall.ps1 index 9b3687a..939a58e 100644 --- a/packaging/win/firewall.ps1 +++ b/packaging/win/firewall.ps1 @@ -39,10 +39,13 @@ add (default) create/replace the rules remove delete them check exit 0 if all rules already exist, 1 otherwise -- no admin needed + any exit 0 if at least one rule exists, 1 otherwise -- no admin + needed; the uninstallers elevate only when this says there is + something to remove #> [CmdletBinding()] param( - [ValidateSet("add", "remove", "check")] + [ValidateSet("add", "remove", "check", "any")] [string]$Action = "add" ) @@ -76,6 +79,12 @@ if ($Action -eq "check") { exit 0 } +if ($Action -eq "any") { + $present = $rules | Where-Object { Get-NetFirewallRule -DisplayName $_.Name -ErrorAction SilentlyContinue } + if ($present) { exit 0 } + exit 1 +} + $ErrorActionPreference = "Stop" try { foreach ($r in $rules) { |