; electron-builder NSIS customisation (auto-included: build/installer.nsh). ; ; Per-user install, no elevation at install time (package.json build.nsis) -- ; that part never changes. What this adds, all conditional on interactive setup ; (never ${Silent}): ; - the per-machine ("anyone who uses this computer") option removed from ; electron-builder's install-mode page: MeshBay is per-user only -- the ; keystore and the DPAPI-protected secrets are bound to the signed-in ; account (MESHBAY_DESIGN.md §11.2 / §7.5) -- so that option was only ever ; shown disabled here. customInstallMode forces current-user and the page ; is skipped entirely; ; - the bundled daemon dir on the user's PATH, so `meshbay-node` works in a ; terminal; ; - a custom page (radio buttons, like the rest of setup) choosing when the ; node runs: only while the MeshBay app is open, at each sign-in (a per-user ; Startup-folder launcher -- no admin -- see ; meshbay_node.platform.autostart_install), or as a background service ; (a boot-time S4U scheduled task -- one admin confirmation -- see ; meshbay_node.platform.service_install and packaging/win/service.ps1). ; Default: background service; ; - the inbound firewall rules, set up in EVERY mode (a node that silently ; accepts no connections is the failure mode called out in ; MESHBAY_DESIGN.md §7.5). Folded into the SAME elevation as the boot ; 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). ; ; Deliberately NOT touched: ; - %LOCALAPPDATA%\meshbay\ (node.toml, keystore.enc, unlock.key, data/) -- ; the keystore must survive an uninstall/reinstall; installers place files, ; never remove secrets. This is also why service mode needs no code ; changes to platform.py: it runs as this same user (S4U), so it is the ; same profile either way. !include "WinMessages.nsh" !include "WordFunc.nsh" !include "LogicLib.nsh" !include "nsDialogs.nsh" !insertmacro WordAdd !insertmacro un.WordAdd !define MB_PWSH "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" ; The dir electron-builder drops resources into. `meshbay-node.exe` and its ; frozen Python live directly in here. A fixed suffix of $INSTDIR, so both the ; add (install) and the remove (uninstall, where $INSTDIR is still known) match ; the exact same string. !define MB_NODE_BIN "$INSTDIR\resources\node-runtime" ; ── force per-user, skip the all-users / current-user page ────────────────── !macro customInstallMode StrCpy $isForceCurrentInstall "1" !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 ───────────────────────────────── ; MB_AutoMode: "0" only-while-open, "1" at sign-in, "2" background service. ; Declared here (not at file scope) so the uninstaller pass -- which inserts ; none of the macros that touch it -- does not warn about an unused Var, which ; electron-builder's makensis promotes to a hard error. customInit defaults it ; for silent installs, where this page never runs. !macro customPageAfterChangeDir Var MB_AutoMode Var MB_Dlg Var MB_RbOpen Var MB_RbSignin Var MB_RbService Page custom mbAutostartPageCreate mbAutostartPageLeave Function mbAutostartPageCreate !insertmacro MUI_HEADER_TEXT "MeshBay Node" "Choose when the node runs on this computer." nsDialogs::Create 1018 Pop $MB_Dlg ${If} $MB_Dlg == error Abort ${EndIf} ${NSD_CreateLabel} 0 0 100% 26u "The node makes your groups' content available to other members. It can run only while the MeshBay window is open, start on its own when you sign in, or run as a background service that is up even before you sign in." Pop $0 ${NSD_CreateRadioButton} 6u 34u 96% 12u "Only while MeshBay is open" Pop $MB_RbOpen ${NSD_CreateRadioButton} 6u 48u 96% 12u "Automatically when I sign in to Windows" Pop $MB_RbSignin ${NSD_CreateRadioButton} 6u 62u 96% 12u "As a background service (starts at boot, before sign-in) -- recommended" Pop $MB_RbService ${NSD_CreateLabel} 0 82u 100% 34u "Setup adds Windows Firewall rules for local-network connections in every case. The background-service option, and those firewall rules, together need one administrator confirmation now -- without it the node cannot be reached and the app is not operational." Pop $0 ${If} $MB_AutoMode == "0" ${NSD_Check} $MB_RbOpen ${ElseIf} $MB_AutoMode == "1" ${NSD_Check} $MB_RbSignin ${Else} ${NSD_Check} $MB_RbService ${EndIf} nsDialogs::Show FunctionEnd Function mbAutostartPageLeave ${NSD_GetState} $MB_RbOpen $0 ${If} $0 == ${BST_CHECKED} StrCpy $MB_AutoMode "0" ${EndIf} ${NSD_GetState} $MB_RbSignin $0 ${If} $0 == ${BST_CHECKED} StrCpy $MB_AutoMode "1" ${EndIf} ${NSD_GetState} $MB_RbService $0 ${If} $0 == ${BST_CHECKED} StrCpy $MB_AutoMode "2" ${EndIf} FunctionEnd !macroend !macro customInstall ; 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 ; already there, so a reinstall does not double it. New shells only; the ; broadcast tells already-open Explorer/shells to reload the environment. ReadRegStr $0 HKCU "Environment" "Path" ${WordAdd} "$0" ";" "+${MB_NODE_BIN}" $1 WriteRegExpandStr HKCU "Environment" "Path" "$1" SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 ${IfNot} ${Silent} ; The firewall rules go in for every autostart mode. Read their state ; first, unelevated -- Get-NetFirewallRule needs no admin, only New/Remove ; do -- so an upgrade or repair that changes nothing triggers no UAC. nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" check' Pop $R0 ; 0 = every rule already present ${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 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 = installed and current, 1 = absent, 2 = stale ${If} $R1 == 0 ${AndIf} $R0 == 0 Goto mb_auto_done ${EndIf} ExecShellWait "runas" "${MB_PWSH}" \ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\service-mode.ps1" -Action install' \ SW_HIDE Goto mb_auto_done ${EndIf} ; 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} 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 ; 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" ${un.WordAdd} "$0" ";" "-${MB_NODE_BIN}" $1 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. ${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 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. 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