From 8c7e39b6dca758badec6867ab6610fd5e8d93d1e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sun, 27 Sep 2026 22:20:53 +0200 Subject: fix: Windows installer and desktop app start and stop the node one way A 0.16 upgrade in service mode left the previous node running: setup's unelevated taskkill cannot reach session 0, and it ran in customInstall, which electron-builder inserts after the files are copied. The locked exe was not replaced, and the new app talked to the old node ("started but could not link", "No operator paired"). Installer (build/installer.nsh, build/stop-node.ps1): - customCheckAppRunning, which runs before uninstallOldVersion and extraction, stops the node with an embedded stop-node.ps1: control API, then schtasks /end, then Stop-Process, and refuses to half-upgrade if one survives. - An upgrade keeps the mode it finds (task, launcher, previous install), restores the sign-in launcher the old uninstaller deletes, and restarts the node the way that mode runs it. A silent upgrade of an "at sign-in" install used to end with no autostart and no node. - The uninstaller removes the task and firewall rules only on a real uninstall, not on an update. Desktop app (src/main.js): - Start, Stop, Restart and node:start go through the CLI's lifecycle verbs instead of a second implementation; a child spawned by Electron also held Electron's sockets after the app quit. - "Only while MeshBay is open" is a real mode: the app starts a provisioned node at launch and stops the one it started when it quits. - Switching modes stops the node first -- deleting a task does not end its instance, and a new service found the port taken -- keeps the firewall rules every mode needs, and starts the node again. A declined or unanswered UAC prompt restores the node instead of leaving it stopped, and says that nothing changed. - waiting_for_hub counts as a node that is up; linking waits for a node that answers, with a longer deadline, and reports a version mismatch. Packaging (packaging/win): - The service task gets no 72-hour limit, runs on battery and ignores a second start; service.ps1 status reports a stale registration so setup re-registers it; remove ends the running instance before deleting the task. - build-node-runtime.ps1 starts the frozen daemon in a throwaway profile (smoke-node-runtime.ps1) instead of only asking for --help. The mode that was "Off (start manually)" is labelled "Only while MeshBay is open" in all ten catalogues. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-client/build/installer.nsh | 123 +++++++++++++++++++++++----- packages/meshbay-client/build/stop-node.ps1 | 45 ++++++++++ 2 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 packages/meshbay-client/build/stop-node.ps1 (limited to 'packages/meshbay-client/build') diff --git a/packages/meshbay-client/build/installer.nsh b/packages/meshbay-client/build/installer.nsh index 3157f22..ef9c82a 100644 --- a/packages/meshbay-client/build/installer.nsh +++ b/packages/meshbay-client/build/installer.nsh @@ -55,7 +55,55 @@ !macroend !macro customInit + ; What this machine already runs, before the previous version's uninstaller + ; deletes the sign-in launcher: an upgrade keeps the mode it finds, instead + ; of defaulting to "background service" -- which a silent upgrade cannot even + ; set up (no elevation), so an "at sign-in" install came out of one with no + ; autostart at all and its node stopped (found upgrading a real install). + ; A fresh install still defaults to the service. StrCpy $MB_AutoMode "2" + nsExec::Exec 'schtasks /query /tn "MeshBay Node"' + Pop $0 + ${If} $0 == 0 + StrCpy $MB_AutoMode "2" + ${ElseIf} ${FileExists} "$APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MeshBay Node.vbs" + StrCpy $MB_AutoMode "1" + ${ElseIf} ${FileExists} "$INSTDIR\${APP_EXECUTABLE_FILENAME}" + StrCpy $MB_AutoMode "0" + ${EndIf} +!macroend + +; ── stop the node before a single file is touched ───────────────────────── +; electron-builder inserts customCheckAppRunning in place of its own +; app-running check, which it runs before uninstallOldVersion and before the +; files are extracted (installSection.nsh); customInstall only runs after both. +; A service-mode daemon lives in the task's S4U logon session, where an +; unelevated taskkill gets "Access is denied". Left running, it keeps +; meshbay-node.exe and its DLLs locked, their copy fails, and electron-builder's +; last-resort extract ignores the failure. build/stop-node.ps1 asks the node to +; stop through its own control API first -- any session, no elevation, a proper +; shutdown -- then Task Scheduler, then taskkill. It is embedded and run from +; the plugins directory: the installed copy of anything may be what is being +; replaced. + +; Defining customCheckAppRunning makes allowOnlyOneInstallerInstance.nsh skip +; these two, which its own _CHECK_APP_RUNNING (inserted below) still needs. +!include "getProcessInfo.nsh" +Var pid + +!macro customCheckAppRunning + InitPluginsDir + File "/oname=$PLUGINSDIR\mb-stop-node.ps1" "${BUILD_RESOURCES_DIR}\stop-node.ps1" + mb_stop_node: + DetailPrint "Stopping the MeshBay node..." + nsExec::Exec `"${MB_PWSH}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\mb-stop-node.ps1"` + Pop $0 + ${If} $0 != 0 + MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "The MeshBay node is still running and holds files that setup must replace. Stop it (meshbay-node service stop, or end meshbay-node.exe in Task Manager), then click Retry." /SD IDCANCEL IDRETRY mb_stop_node + Quit + ${EndIf} + !insertmacro IS_POWERSHELL_AVAILABLE + !insertmacro _CHECK_APP_RUNNING !macroend ; ── the autostart choice, as a radio page ───────────────────────────────── @@ -123,9 +171,8 @@ !macroend !macro customInstall - ; resources\node-runtime\meshbay-node.exe is about to be overwritten; a - ; daemon still running from a previous version holds the file open. - nsExec::Exec 'taskkill /IM meshbay-node.exe /F' + ; The node was stopped by customCheckAppRunning, before the files were + ; copied -- by the time this runs they already have been. ; Add the daemon dir to the per-user PATH (HKCU\Environment). WordAdd is a ; stock NSIS macro over a ';'-delimited list -- it is a no-op if the entry is @@ -146,9 +193,12 @@ ${If} $MB_AutoMode == "2" ; Background service: the boot-time Scheduled Task AND the firewall ; rules, in ONE elevation (service-mode.ps1 does both). Skip it only - ; when the task already exists and the rules are already there. + ; when the task is already there and current and so are the rules. A + ; stale task (2: another executable, or the 72-hour / battery defaults + ; of an older setup) is registered again -- the owner cannot change it + ; without elevation. nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service.ps1" status' - Pop $R1 ; 0 = task installed + Pop $R1 ; 0 = installed and current, 1 = absent, 2 = stale ${If} $R1 == 0 ${AndIf} $R0 == 0 Goto mb_auto_done @@ -159,28 +209,58 @@ Goto mb_auto_done ${EndIf} - ; Modes 0 and 1: no scheduled task. One elevation for the firewall rules, - ; and only if one is actually missing. + ; Modes 0 and 1. A boot task left from an earlier "background service" + ; choice would start the node a second time, at boot and at sign-in: take + ; it out (service-mode.ps1 remove keeps the firewall rules every mode needs). + nsExec::Exec 'schtasks /query /tn "MeshBay Node"' + Pop $R1 + ${If} $R1 == 0 + ExecShellWait "runas" "${MB_PWSH}" \ + '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service-mode.ps1" -Action remove' \ + SW_HIDE + ${EndIf} + ; One elevation for the firewall rules, and only if one is actually missing. ${If} $R0 != 0 ExecShellWait "runas" "${MB_PWSH}" \ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" add' \ SW_HIDE ${EndIf} - ; Mode 1 also drops the per-user sign-in launcher (no admin -- it is just - ; a .vbs in this account's Startup folder). Idempotent, so a repeat run is - ; harmless. meshbay_node.platform.service_install() removes this itself if - ; the user later switches to service mode from the Node page. - ${If} $MB_AutoMode == "1" - nsExec::Exec '"${MB_NODE_BIN}\meshbay-node.exe" autostart install' - ${EndIf} - mb_auto_done: ${EndIf} + + ; The sign-in launcher as the mode wants it -- silent installs too: during an + ; upgrade the previous version's uninstaller has just deleted it. No admin, + ; idempotent; `autostart install` refuses while a boot task exists. + ${If} $MB_AutoMode == "1" + nsExec::Exec '"${MB_NODE_BIN}\meshbay-node.exe" autostart install' + ${Else} + nsExec::Exec '"${MB_NODE_BIN}\meshbay-node.exe" autostart remove' + ${EndIf} + Pop $R2 + + ; Start the node customCheckAppRunning stopped, the way this mode runs it, + ; rather than leave it down until the next boot or sign-in. Only a node that + ; has been set up: a fresh install's has no account yet, and node:start + ; provisions and starts it. Mode 0 is the app's to start (runAfterFinish). + ${If} ${FileExists} "$LOCALAPPDATA\meshbay\node.toml" + ${If} $MB_AutoMode == "2" + nsExec::Exec 'schtasks /query /tn "MeshBay Node"' + Pop $R2 + ${If} $R2 == 0 + nsExec::Exec 'schtasks /run /tn "MeshBay Node"' + Pop $R2 + ${EndIf} + ${ElseIf} $MB_AutoMode == "1" + nsExec::Exec '"${MB_NODE_BIN}\meshbay-node.exe" autostart start' + Pop $R2 + ${EndIf} + ${EndIf} !macroend !macro customUnInstall - nsExec::Exec 'taskkill /IM meshbay-node.exe /F' + ; The node is already stopped: the uninstaller runs customCheckAppRunning + ; (un.checkAppRunning) before this. ; Take our entry back out of PATH, leaving the rest of it alone. ReadRegStr $0 HKCU "Environment" "Path" @@ -196,17 +276,22 @@ ; default-No; a silent uninstall skips it entirely. customUnInstall runs ; before the files are removed, so service-mode.ps1 is 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 ExecShellWait "runas" "${MB_PWSH}" \ - '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service-mode.ps1" -Action remove' \ + '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service-mode.ps1" -Action uninstall' \ SW_HIDE mb_keep_privileged: ${EndIf} ; meshbay_node.platform.autostart_install() -- if the user picked "at sign-in" ; (here, or later in the client), this points wscript at the binary we are - ; about to delete, and would error at every sign-in. - Delete "$APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MeshBay Node.vbs" + ; about to delete, and would error at every sign-in. Not in an upgrade: the + ; new version is about to take this path's place, and deleting the launcher + ; here is what left upgraded "at sign-in" installs with no autostart at all. + ${IfNot} ${isUpdated} + Delete "$APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MeshBay Node.vbs" + ${EndIf} !macroend diff --git a/packages/meshbay-client/build/stop-node.ps1 b/packages/meshbay-client/build/stop-node.ps1 new file mode 100644 index 0000000..cfaf2f8 --- /dev/null +++ b/packages/meshbay-client/build/stop-node.ps1 @@ -0,0 +1,45 @@ +# Stop every MeshBay node on this machine before setup touches its files, or +# before the uninstaller removes them. Embedded in the installer and run from +# its plugins directory: the installed copy of anything may be the one being +# replaced. +# +# 1. Ask the node through its own control API (/api/shutdown, loopback, per-run +# token): the only stop that reaches a node in any session with no +# elevation, and the one that lets it shut down properly -- WebRTC sessions +# closed, transcodes stopped. +# 2. Task Scheduler for a background-service node (the owner may end the task; +# taskkill from here gets "Access is denied" in its session). +# 3. taskkill for anything left in this session. +# Exit 0 once no meshbay-node.exe is left, 1 otherwise. +$ErrorActionPreference = "SilentlyContinue" + +function NodeLeft { [bool](Get-Process -Name meshbay-node -ErrorAction SilentlyContinue) } +function WaitGone([int]$Seconds) { + $deadline = (Get-Date).AddSeconds($Seconds) + while ((NodeLeft) -and (Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 250 } + -not (NodeLeft) +} + +if (-not (NodeLeft)) { exit 0 } + +$cfg = Join-Path $env:LOCALAPPDATA "meshbay" +$port = 18000 +$toml = Join-Path $cfg "node.toml" +if (Test-Path $toml) { + $m = Select-String -Path $toml -Pattern '^\s*ui_port\s*=\s*(\d+)' | Select-Object -First 1 + if ($m) { $port = [int]$m.Matches[0].Groups[1].Value } +} +$tokenFile = Join-Path $cfg "data\ui-token" +if (Test-Path $tokenFile) { + $token = (Get-Content $tokenFile -Raw).Trim() + try { + Invoke-WebRequest -UseBasicParsing -Method Post -TimeoutSec 5 ` + -Uri "http://127.0.0.1:$port/api/shutdown?t=$token" | Out-Null + if (WaitGone 20) { exit 0 } + } catch { } +} + +& schtasks /end /tn "MeshBay Node" 2>&1 | Out-Null +Get-Process -Name meshbay-node -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue +if (WaitGone 20) { exit 0 } +exit 1 -- cgit v1.2.3