aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--packages/meshbay-client/src/main.js61
-rw-r--r--packages/meshbay-client/src/preload.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/node-page.js19
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js21
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py51
15 files changed, 200 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
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index b66c847..bb81fee 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -678,6 +678,11 @@ export default {
'node.service_stopping': 'Wird angehalten…',
'node.service_restart': 'Neu starten',
'node.service_restarting': 'Wird neu gestartet…',
+ 'node.autostart_label': 'Automatisch bei der Anmeldung starten',
+ 'node.autostart_updating': 'Wird aktualisiert…',
+ 'node.service_mode_hint': 'Läuft als Hintergrunddienst — startet beim Booten, vor der Anmeldung.',
+ 'node.service_mode_label': 'Als Hintergrunddienst ausführen (startet beim Booten, vor der Anmeldung)',
+ 'node.service_mode_updating': 'Modus wird gewechselt — achten Sie auf eine Administrator-Eingabeaufforderung…',
'node.not_operator': 'Ihr Node konnte nicht erreicht werden. Stellen Sie sicher, dass er läuft.',
'node.offline': 'Node ist offline',
'node.retry': 'Erneut versuchen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 196b9cc..2817432 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -737,6 +737,8 @@ export default {
'node.autostart_label': 'Start automatically at sign-in',
'node.autostart_updating': 'Updating…',
'node.service_mode_hint': 'Running as a background service — it starts at boot, before sign-in.',
+ 'node.service_mode_label': 'Run as a background service (starts at boot, before sign-in)',
+ 'node.service_mode_updating': 'Switching mode — check for an administrator prompt…',
'node.offline': 'Node is offline',
'node.no_groups': 'No groups configured on this node.',
'node.retry': 'Retry',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index 8d06a2d..0f3ce21 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -674,6 +674,11 @@ export default {
'node.service_stopping': 'Deteniendo…',
'node.service_restart': 'Reiniciar',
'node.service_restarting': 'Reiniciando…',
+ 'node.autostart_label': 'Iniciar automáticamente al iniciar sesión',
+ 'node.autostart_updating': 'Actualizando…',
+ 'node.service_mode_hint': 'Se ejecuta como servicio en segundo plano — se inicia al arrancar, antes de iniciar sesión.',
+ 'node.service_mode_label': 'Ejecutar como servicio en segundo plano (se inicia al arrancar, antes de iniciar sesión)',
+ 'node.service_mode_updating': 'Cambiando de modo — compruebe si aparece un aviso de administrador…',
'node.not_operator': 'No se pudo contactar con su node. Asegúrese de que esté en ejecución.',
'node.offline': 'Node sin conexión',
'node.retry': 'Reintentar',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index fe38500..c3c4846 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -680,6 +680,8 @@ export default {
'node.autostart_label': 'Démarrer automatiquement à l\'ouverture de session',
'node.autostart_updating': 'Mise à jour…',
'node.service_mode_hint': 'Fonctionne comme service en arrière-plan — démarre au boot, avant l\'ouverture de session.',
+ 'node.service_mode_label': 'Exécuter comme service en arrière-plan (démarre au boot, avant l\'ouverture de session)',
+ 'node.service_mode_updating': 'Changement de mode — vérifiez une invite d\'administrateur…',
'node.not_operator': 'Impossible de joindre votre node. Vérifiez qu\'il est en cours d\'exécution.',
'node.offline': 'Node hors ligne',
'node.retry': 'Réessayer',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 52e18a8..f39ab3c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -676,6 +676,11 @@ export default {
'node.service_stopping': 'Arresto…',
'node.service_restart': 'Riavvia',
'node.service_restarting': 'Riavvio…',
+ 'node.autostart_label': 'Avvia automaticamente all\'accesso',
+ 'node.autostart_updating': 'Aggiornamento…',
+ 'node.service_mode_hint': 'In esecuzione come servizio in background — si avvia all\'avvio del sistema, prima dell\'accesso.',
+ 'node.service_mode_label': 'Esegui come servizio in background (si avvia all\'avvio del sistema, prima dell\'accesso)',
+ 'node.service_mode_updating': 'Cambio modalità — controlli se compare una richiesta di amministratore…',
'node.not_operator': 'Impossibile raggiungere il suo node. Si assicuri che sia in esecuzione.',
'node.offline': 'Node non in linea',
'node.retry': 'Riprova',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index c0560b3..1544940 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -664,6 +664,11 @@ export default {
'node.service_stopping': '停止中…',
'node.service_restart': '再起動',
'node.service_restarting': '再起動中…',
+ 'node.autostart_label': 'サインイン時に自動的に開始する',
+ 'node.autostart_updating': '更新中…',
+ 'node.service_mode_hint': 'バックグラウンドサービスとして実行中 — サインインより前、起動時に開始します。',
+ 'node.service_mode_label': 'バックグラウンドサービスとして実行する(サインインより前、起動時に開始)',
+ 'node.service_mode_updating': 'モードを切り替え中 — 管理者の確認ダイアログをご確認ください…',
'node.not_operator': 'node に接続できませんでした。node が実行中であることをご確認ください。',
'node.offline': 'Node はオフラインです',
'node.retry': '再試行',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index da6dc04..19feec2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -678,6 +678,11 @@ export default {
'node.service_stopping': 'Stoppen…',
'node.service_restart': 'Herstarten',
'node.service_restarting': 'Herstarten…',
+ 'node.autostart_label': 'Automatisch starten bij aanmelden',
+ 'node.autostart_updating': 'Bijwerken…',
+ 'node.service_mode_hint': 'Actief als achtergrondservice — start bij het opstarten, vóór het aanmelden.',
+ 'node.service_mode_label': 'Uitvoeren als achtergrondservice (start bij het opstarten, vóór het aanmelden)',
+ 'node.service_mode_updating': 'Modus wijzigen — let op een beheerdersprompt…',
'node.not_operator': 'Uw node is niet bereikbaar. Controleer of hij draait.',
'node.offline': 'Node is offline',
'node.retry': 'Opnieuw proberen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index 29feebe..512c4f3 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -696,6 +696,11 @@ export default {
'node.service_stopping': 'Zatrzymywanie…',
'node.service_restart': 'Uruchom ponownie',
'node.service_restarting': 'Ponowne uruchamianie…',
+ 'node.autostart_label': 'Uruchamiaj automatycznie przy logowaniu',
+ 'node.autostart_updating': 'Aktualizowanie…',
+ 'node.service_mode_hint': 'Działa jako usługa w tle — uruchamia się przy starcie systemu, przed zalogowaniem.',
+ 'node.service_mode_label': 'Uruchom jako usługę w tle (uruchamia się przy starcie systemu, przed zalogowaniem)',
+ 'node.service_mode_updating': 'Zmiana trybu — proszę sprawdzić, czy pojawiło się okno uprawnień administratora…',
'node.not_operator': 'Nie udało się połączyć z Pana/Pani node. Upewnij się, że działa.',
'node.offline': 'Node jest niedostępny',
'node.retry': 'Ponów',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index d2fc355..8e33d74 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -675,6 +675,11 @@ export default {
'node.service_stopping': 'Parando…',
'node.service_restart': 'Reiniciar',
'node.service_restarting': 'Reiniciando…',
+ 'node.autostart_label': 'Iniciar automaticamente ao entrar na sessão',
+ 'node.autostart_updating': 'Atualizando…',
+ 'node.service_mode_hint': 'Em execução como serviço em segundo plano — inicia na inicialização, antes do login.',
+ 'node.service_mode_label': 'Executar como serviço em segundo plano (inicia na inicialização, antes do login)',
+ 'node.service_mode_updating': 'Alternando modo — verifique se aparece um aviso de administrador…',
'node.not_operator': 'Não foi possível alcançar seu node. Verifique se ele está em execução.',
'node.offline': 'Node está off-line',
'node.retry': 'Tentar novamente',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index 6840774..299dc4f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -651,6 +651,11 @@ export default {
'node.service_stopping': '正在停止…',
'node.service_restart': '重启',
'node.service_restarting': '正在重启…',
+ 'node.autostart_label': '登录时自动启动',
+ 'node.autostart_updating': '正在更新…',
+ 'node.service_mode_hint': '以后台服务方式运行 — 在开机时启动,早于登录。',
+ 'node.service_mode_label': '以后台服务方式运行(在开机时启动,早于登录)',
+ 'node.service_mode_updating': '正在切换模式 — 请留意管理员权限提示…',
'node.not_operator': '无法连接到您的 node。请确保它正在运行。',
'node.offline': 'Node 已离线',
'node.retry': '重试',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
index d242b75..2a0b0d7 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js
@@ -53,6 +53,17 @@ function NodeServicePanel({ onChanged }) {
: platform.node.autostart.install()));
}, [act, info]);
+ // Switching mode itself — the installer's own choice is effectively one-shot
+ // (it skips the question once the firewall rules exist for any reason, and
+ // per-user mode sets those up on its own with no Scheduled Task), so this is
+ // the only way back in if service mode was declined, or out if it is no
+ // longer wanted. One elevation, task + firewall together, same script.
+ const toggleServiceMode = useCallback(() => {
+ act('serviceMode', () => (info && info.mode === 'service'
+ ? platform.node.serviceMode.remove()
+ : platform.node.serviceMode.install()));
+ }, [act, info]);
+
if (!platform.node.service.available) return null;
if (!info || info.supported === false) {
return html`<div class="node-service">
@@ -99,6 +110,14 @@ function NodeServicePanel({ onChanged }) {
${' '}${busy === 'autostart' ? t('node.autostart_updating') : t('node.autostart_label')}
</label>
`}
+ ${platform.node.serviceMode.available && typeof info.mode === 'string' && html`
+ <label class="toggle-switch ${busy ? 'toggle-switch-disabled' : ''}">
+ <input type="checkbox" checked=${info.mode === 'service'} disabled=${!!busy}
+ onChange=${toggleServiceMode} />
+ <span class="toggle-switch-track"><span class="toggle-switch-thumb"></span></span>
+ ${' '}${busy === 'serviceMode' ? t('node.service_mode_updating') : t('node.service_mode_label')}
+ </label>
+ `}
</div>`;
}
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index b1025c5..8f5d484 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -282,6 +282,27 @@ export const node = {
return bridge.node.autostart('remove');
},
},
+ /**
+ * Windows only: switch INTO or OUT OF service mode after install — the
+ * installer's own choice is effectively one-shot (build/installer.nsh skips
+ * it once the firewall rules exist for any reason, and per-user mode sets
+ * those up on its own with no Scheduled Task), so this is the only way back
+ * in if it was declined, or out if it was chosen and no longer wanted. One
+ * elevation, task + firewall together — same script the installer runs.
+ */
+ serviceMode: {
+ available: Boolean(bridge && bridge.node && bridge.node.serviceMode),
+ async install() {
+ if (!bridge || !bridge.node || !bridge.node.serviceMode)
+ throw new Error('Node bridge not available');
+ return bridge.node.serviceMode('install');
+ },
+ async remove() {
+ if (!bridge || !bridge.node || !bridge.node.serviceMode)
+ throw new Error('Node bridge not available');
+ return bridge.node.serviceMode('remove');
+ },
+ },
};
/**
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 31816d2..21aaf29 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -316,6 +316,57 @@ def test_service_status_reports_state_without_admin():
"calls status/run/end directly, unelevated")
+HUB_STATIC = ROOT / "packages" / "meshbay-hub" / "src" / "meshbay_hub" / "static"
+
+
+def test_service_mode_toggle_elevates_the_same_script_the_installer_runs():
+ """
+ The installer's own mode question is effectively one-shot (it skips
+ itself the moment the firewall rules 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 for any other reason, is a dead
+ end through setup alone. The Node page's toggle is the other door in
+ (and out), and it must drive service-mode.ps1 -- the exact script
+ installer.nsh runs -- so the two paths can never disagree about what
+ "service mode" means. One elevation (-Verb RunAs), no stored password.
+ """
+ main_js = (CLIENT / "src" / "main.js").read_text(encoding="utf-8")
+ assert "winElevateServiceMode" in main_js
+ fn = main_js.split("function winElevateServiceMode", 1)[1]
+ fn = fn[:fn.index("\n }\n")]
+ assert "service-mode.ps1" in fn
+ assert "-Verb RunAs" in fn or "'-Verb', 'RunAs'" in fn or "-Verb', 'RunAs'" in fn
+ assert "Get-Credential" not in fn
+
+ handler = main_js.split("ipcMain.handle('node:service-mode'", 1)[1]
+ handler = handler[:handler.index("ipcMain.handle(")]
+ assert "winElevateServiceMode" in handler
+ assert "'install'" in handler or '"install"' in handler
+ assert "'remove'" in handler or '"remove"' in handler
+
+
+def test_node_page_service_mode_toggle_is_wired_end_to_end():
+ """preload.js -> platform.js -> node-page.js, the same three-layer shape
+ the existing autostart toggle uses. A break anywhere in this chain means
+ the checkbox renders but does nothing, or never renders at all."""
+ preload = (CLIENT / "src" / "preload.js").read_text(encoding="utf-8")
+ assert "serviceMode:" in preload
+ assert "'node:service-mode'" in preload
+
+ platform_js = (HUB_STATIC / "platform.js").read_text(encoding="utf-8")
+ assert "serviceMode:" in platform_js
+ assert "bridge.node.serviceMode('install')" in platform_js
+ assert "bridge.node.serviceMode('remove')" in platform_js
+
+ node_page = (HUB_STATIC / "node-page.js").read_text(encoding="utf-8")
+ assert "platform.node.serviceMode.available" in node_page
+ assert "platform.node.serviceMode.install()" in node_page
+ assert "platform.node.serviceMode.remove()" in node_page
+ # Checked state must reflect the CURRENT mode, not a separate flag --
+ # otherwise the toggle and the status panel above it could disagree.
+ assert "info.mode === 'service'" in node_page
+
+
def test_the_help_smoke_test_joins_multiline_output_before_matching():
"""
`& exe --help 2>&1` is an ARRAY once the output wraps past one line, which