diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-05 09:28:03 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-05 09:28:03 +0200 |
| commit | 7601991ccb1d75637c055062c38b1852eeef9700 (patch) | |
| tree | 022d6ce76e632d005417c82e92e0277614a92689 /packages/meshbay-client/src | |
| parent | 301c8998bfdcac80ad3302e2d2ebe853e2ea6de1 (diff) | |
| download | meshbay-7601991ccb1d75637c055062c38b1852eeef9700.tar.gz | |
feat(client): a Node-page toggle to switch into/out of service mode
The installer's own mode question is effectively one-shot: customInstall
skips it entirely once the firewall rules already exist, for any reason --
and per-user mode sets those up on its own, with no Scheduled Task involved.
So declining once (or the rules existing from something unrelated, as
happened on a dev machine this session) was a dead end: no reinstall, repair,
or uninstall/reinstall cycle could ever bring the question back, since
uninstall defaults to leaving both alone.
Add the other door in (and out): a checkbox on the Node page, next to the
existing per-user autostart toggle, wired main.js -> preload.js -> platform.js
-> node-page.js. It runs packaging/win/service-mode.ps1 -- the exact script
installer.nsh already runs -- via one Start-Process -Verb RunAs elevation, so
the two paths can never disagree about what service mode means. The elevation
helper writes a tiny param()-based .ps1 to %TEMP% so the target script path
and its arguments bind through real PowerShell parameters instead of nested
string-quoting.
Also fixes a real pre-existing gap found while checking this: 8 of the 10
locale catalogues (all but en/fr) were missing the autostart/service-mode
keys added in an earlier commit this session (b782886) -- test_locales.py's
key-set-parity check uses a for-loop with an inline assert, so it stopped at
the first mismatch (fr) and never actually reached the other eight. Backfilled
all five keys (three pre-existing, two new) in de/es/it/ja/nl/pl/pt-BR/zh-CN.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 61 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 4 |
2 files changed, 65 insertions, 0 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 15c3cc0..dd7f38e 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -882,6 +882,54 @@ function registerBridge() { }); } + // ── Windows: switching INTO or OUT OF service mode after install ─────────── + // build/installer.nsh's mode question is effectively one-shot: it skips + // itself the moment the firewall rules already exist, and per-user mode + // sets those up on its own, with no Scheduled Task involved. So declining + // once (or the rules existing for any other reason) is a dead end through + // the installer alone — this is the other door in, driven from the Node + // page instead of setup. It runs the exact same packaging/win/service-mode.ps1 + // the installer does (task + firewall, one elevation), so the two paths + // can never disagree about what "service mode" means. + const MB_PWSH = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + + function winElevateServiceMode(action) { + return new Promise((resolve, reject) => { + const script = path.join(process.resourcesPath, 'service-mode.ps1'); + if (!fs.existsSync(script)) { + reject(new Error('service-mode.ps1 not found — only available in an installed build')); + return; + } + // Start-Process -Verb RunAs is the one UAC prompt; -Wait -PassThru hands + // its exit code back to this unelevated process, so a decline ("The + // operation was canceled by the user") surfaces as a rejection here + // instead of silently doing nothing. Written to a temp .ps1 and run via + // -File (not -Command) so the target path and its own arguments bind + // through real PowerShell parameters instead of nested string quoting. + const elevator = path.join(os.tmpdir(), 'meshbay-elevate-service-mode.ps1'); + const elevatorSrc = [ + 'param([string]$Target, [string]$TargetArgs)', + '$ErrorActionPreference = "Stop"', + '$p = Start-Process -FilePath $Target -ArgumentList $TargetArgs -Verb RunAs -Wait -PassThru', + 'exit $p.ExitCode', + '', + ].join('\r\n'); + fs.writeFileSync(elevator, elevatorSrc); + const targetArgs = + `-NoProfile -ExecutionPolicy Bypass -File "${script}" -Action ${action}`; + execFile(MB_PWSH, + ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', elevator, + '-Target', MB_PWSH, '-TargetArgs', targetArgs], + (err) => { + if (err) { + reject(new Error('Elevation was declined, or the operation failed.')); + return; + } + resolve(); + }); + }); + } + async function spawnNodeDetached() { const bin = await findNodeBinary(); if (!bin) throw new Error('meshbay-node not found on PATH'); @@ -1050,6 +1098,19 @@ function registerBridge() { return { supported: true, installed: winAutostartInstalled() }; }); + // Turn service mode on or off after install — one elevation, task + firewall + // together, via the same service-mode.ps1 the installer runs. See + // winElevateServiceMode() above for why this is needed at all. + ipcMain.handle('node:service-mode', async (_e, action) => { + if (process.platform !== 'win32') return { supported: false }; + if (action !== 'install' && action !== 'remove') { + throw new Error(`unknown service-mode action: ${action}`); + } + await winElevateServiceMode(action); + const svc = await winServiceTaskStatus(); + return { supported: true, installed: svc.installed }; + }); + async function probeNode() { const nc = readNodeConfig(); const dataDir = nc ? nc.dataDir : meshbayDataDir(); diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 38c1a57..97a5063 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -110,6 +110,10 @@ contextBridge.exposeInMainWorld('meshbay', { // action: 'install' | 'remove' | 'status' (default). Elsewhere returns // { supported: false }. autostart: (action) => ipcRenderer.invoke('node:autostart', action), + // Windows only: switch into/out of the boot-time service (Scheduled Task + // + firewall, one elevation). action: 'install' | 'remove'. Elsewhere + // returns { supported: false }. + serviceMode: (action) => ipcRenderer.invoke('node:service-mode', action), }, // LAN cast relay. The main process runs a local HTTP server and the |