diff options
Diffstat (limited to 'packages/meshbay-client')
| -rw-r--r-- | packages/meshbay-client/build/appx-extensions.xml | 16 | ||||
| -rw-r--r-- | packages/meshbay-client/build/appx-manifest.xml | 68 | ||||
| -rw-r--r-- | packages/meshbay-client/src/main.js | 85 |
3 files changed, 157 insertions, 12 deletions
diff --git a/packages/meshbay-client/build/appx-extensions.xml b/packages/meshbay-client/build/appx-extensions.xml index 1ee1cca..3326dd8 100644 --- a/packages/meshbay-client/build/appx-extensions.xml +++ b/packages/meshbay-client/build/appx-extensions.xml @@ -1,3 +1,15 @@ - <desktop:Extension Category="windows.startupTask" Executable="app\resources\node-runtime\meshbay-node.exe" EntryPoint="Windows.FullTrustApplication"> - <desktop:StartupTask TaskId="MeshBayNodeStartup" Enabled="true" DisplayName="MeshBay Node" /> + <!-- At sign-in: off until the user picks that mode on the Node page, which + switches it through `meshbay-node autostart`. meshbay-nodew.exe, the build + without a console: Windows runs a startup task's executable as is, and + the console one opened a window whose close button stopped the node. --> + <desktop:Extension Category="windows.startupTask" Executable="app\resources\node-runtime\meshbay-nodew.exe" EntryPoint="Windows.FullTrustApplication"> + <desktop:StartupTask TaskId="MeshBayNodeStartup" Enabled="false" DisplayName="MeshBay Node" /> </desktop:Extension> + <!-- `meshbay-node` in a terminal: %LOCALAPPDATA%\Microsoft\WindowsApps is + on PATH already and this alias keeps its path across versions, where a + PATH entry naming the install folder went stale at each update. --> + <uap5:Extension Category="windows.appExecutionAlias" Executable="app\resources\node-runtime\meshbay-node.exe" EntryPoint="Windows.FullTrustApplication"> + <uap5:AppExecutionAlias> + <uap5:ExecutionAlias Alias="meshbay-node.exe" /> + </uap5:AppExecutionAlias> + </uap5:Extension> diff --git a/packages/meshbay-client/build/appx-manifest.xml b/packages/meshbay-client/build/appx-manifest.xml new file mode 100644 index 0000000..a8dba41 --- /dev/null +++ b/packages/meshbay-client/build/appx-manifest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- electron-builder's own appx template (app-builder-lib/templates/appx/), + plus what only a package-level element can declare: the node's firewall + rules. AppxTarget.js fills in its macros as for the stock one, and fails + on any it does not know, even inside a comment; re-check against that + file when electron-builder is upgraded. --> +<Package + xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" + xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" + xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> + <!-- use single quotes to avoid double quotes escaping in the publisher value --> + <Identity Name="${identityName}" + ProcessorArchitecture="${arch}" + Publisher='${publisher}' + Version="${version}" /> + <Properties> + <DisplayName>${displayName}</DisplayName> + <PublisherDisplayName>${publisherDisplayName}</PublisherDisplayName> + <Description>${description}</Description> + <Logo>${logo}</Logo> + </Properties> + <Resources> + ${resourceLanguages} + </Resources> + <Dependencies> + <TargetDeviceFamily Name="Windows.Desktop" MinVersion="${minVersion}" MaxVersionTested="${maxVersionTested}" /> + </Dependencies> + ${capabilities} + <Applications> + <Application Id="${applicationId}" Executable="${executable}" EntryPoint="Windows.FullTrustApplication"> + <uap:VisualElements + BackgroundColor="${backgroundColor}" + DisplayName="${displayName}" + Square150x150Logo="${square150x150Logo}" + Square44x44Logo="${square44x44Logo}" + Description="${description}"> + ${lockScreen} + ${defaultTile} + ${splashScreen} + </uap:VisualElements> + ${extensions} + </Application> + </Applications> + <!-- The node accepts connections from peers (WebRTC, QUIC, casting). Rules + declared here are created by Windows at install with no administrator + prompt, follow the executable's versioned path on every update, and go + with the package. A rule the app made itself named that path and was + stale after the next update; the network capabilities create rules for + sandboxed apps only, which a full-trust process is not. Both measured + on a real install. --> + <Extensions> + <desktop2:Extension Category="windows.firewallRules"> + <desktop2:FirewallRules Executable="app\resources\node-runtime\meshbay-node.exe"> + <desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all" /> + <desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all" /> + </desktop2:FirewallRules> + </desktop2:Extension> + <desktop2:Extension Category="windows.firewallRules"> + <desktop2:FirewallRules Executable="app\resources\node-runtime\meshbay-nodew.exe"> + <desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all" /> + <desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all" /> + </desktop2:FirewallRules> + </desktop2:Extension> + </Extensions> +</Package> diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index f01b8f2..603dedc 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -1361,6 +1361,10 @@ function registerBridge() { // dialog over. function winEnsureNodeOnPath() { if (process.platform !== 'win32' || !hasBundledNode()) return; + // The Store package declares meshbay-node.exe as an execution alias, in a + // folder already on PATH and the same for every version; an entry added + // here would name the versioned install folder, deleted by the next update. + if (WIN_STORE) return; const script = path.join(process.resourcesPath, 'ensure-node-path.ps1'); if (!fs.existsSync(script)) return; // dev run, or an older build without it const nodeDir = path.join(process.resourcesPath, 'node-runtime'); @@ -1428,6 +1432,54 @@ function registerBridge() { try { fs.rmSync(WIN_STARTUP_VBS, { force: true }); } catch { /* not there */ } } + // ── Windows, Store package: the startup task stands in for the launcher ── + // The .vbs names the node by its path, and in the Store package that path is + // C:\Program Files\WindowsApps\MeshBay.MeshBay_<version>_..., which every + // update deletes; right after sign-in Windows also refused the launcher the + // file outright ("Permission denied"). Both found on a real install. The + // package declares a startup task instead (build/appx-extensions.xml, off by + // default): Windows runs it, lists it in Settings > Apps > Startup, keeps its + // state across updates and removes it with the package. Only a process with + // the package's identity may switch it, and Windows gives that to the + // executables inside the package but not to one started from elsewhere: a + // powershell.exe run from here got "Element not found". The node's CLI is + // inside, so `autostart install | remove | status` does it, and its first + // line says "startup task <state>" (platform.startup_task). + const WIN_STORE = process.platform === 'win32' && Boolean(process.windowsStore); + + // The Node page asks for the status every couple of seconds, and each answer + // here is a process. The user can also switch the task in Windows Settings, + // so the answer is kept for a while, not for good. + const STARTUP_TASK_TTL_MS = 15000; + let startupTaskKnown = null; // { state, at } + + async function winStartupTask(sub) { + const r = await winNodeCli(['autostart', sub]); + const m = /^startup task (\S+)/m.exec(r.out); + if (m) startupTaskKnown = { state: m[1], at: Date.now() }; + if (!r.ok || !m) { + // The state line is for this file; the rest is the CLI's reason. + const why = r.out.replace(/^startup task \S+\s*/m, '').trim(); + throw new Error(why || r.out || 'the startup task did not answer'); + } + return m[1]; + } + + // Whether the node starts at sign-in: the startup task in the Store package, + // the Startup-folder launcher otherwise. + async function winSigninEnabled() { + if (!WIN_STORE) return winAutostartInstalled(); + if (startupTaskKnown && Date.now() - startupTaskKnown.at < STARTUP_TASK_TTL_MS) { + return startupTaskKnown.state.startsWith('Enabled'); + } + try { + return (await winStartupTask('status')).startsWith('Enabled'); + } catch (err) { + console.error('[node]', err.message); + return false; + } + } + // Windows: starting, stopping and restarting the node is the CLI's, and only // the CLI's (meshbay_node/cli/lifecycle.py) -- one implementation behind // every front door, the Node page, the tray, node:start and a terminal @@ -1459,7 +1511,8 @@ function registerBridge() { return r; } - // Every meshbay-node.exe running, in any session (tasklist lists session 0, + // Every node process running (meshbay-node.exe, and meshbay-nodew.exe, the + // build without a console the Store package's startup task runs), in any session (tasklist lists session 0, // where a service node runs). Whether a node is there is a question for the // process list as well as its control API: the API closes first on the way // down, and a node started some other way than the service task -- from a @@ -1467,7 +1520,7 @@ function registerBridge() { // Both made the Node page say "Stopped" about a node that was running. function winNodePids() { return new Promise((resolve) => { - execFile('tasklist', ['/FI', 'IMAGENAME eq meshbay-node.exe', '/NH', '/FO', 'CSV'], + execFile('tasklist', ['/FI', 'IMAGENAME eq meshbay-node*', '/NH', '/FO', 'CSV'], { windowsHide: true }, (err, stdout) => { if (err) return resolve([]); resolve(String(stdout || '').split(/\r?\n/) @@ -1521,7 +1574,7 @@ function registerBridge() { // boot task, the sign-in launcher, or neither -- "only while MeshBay is open". async function winStartupMode() { if ((await winServiceTaskStatus()).installed) return 'service'; - if (winAutostartInstalled()) return 'signin'; + if (await winSigninEnabled()) return 'signin'; return 'open'; } @@ -1688,7 +1741,7 @@ function registerBridge() { const [bin, svc] = await Promise.all([findNodeBinary(), winServiceTaskStatus()]); return { installed: Boolean(bin), - autostart: winAutostartInstalled(), + autostart: await winSigninEnabled(), service: svc.installed, }; } @@ -1728,7 +1781,7 @@ function registerBridge() { // check below, with an actionable error) -- correct but a dead click the // Node page should not offer in the first place. function winCanElevateServiceMode() { - return app.isPackaged + return app.isPackaged && !WIN_STORE && fs.existsSync(path.join(process.resourcesPath, 'service-mode.ps1')); } @@ -1761,7 +1814,8 @@ function registerBridge() { // gated on `installed`, so with no autostart configured they silently // vanished — the daemon was perfectly manageable, just not launchable // at sign-in. `autostart` carries that state as its own field instead. - const [{ activeState }, bin] = await Promise.all([winNodeActivity(), findNodeBinary()]); + const [{ activeState }, bin, autostart] = await Promise.all( + [winNodeActivity(), findNodeBinary(), winSigninEnabled()]); return { supported: true, // Only claim "startup mode" once a node was actually found (bundled @@ -1771,10 +1825,13 @@ function registerBridge() { // a string, so `null` here hides that whole row. mode: bin ? 'startup' : null, installed: Boolean(bin), - autostart: winAutostartInstalled(), + autostart, activeState, subState: activeState === 'active' ? 'running' : '', canElevate: winCanElevateServiceMode(), + // The Store package starts the node at sign-in at most: at boot is the + // .exe installer's (a boot task names the versioned WindowsApps path). + store: WIN_STORE, }; } if (process.platform !== 'linux') return { supported: false }; @@ -1857,7 +1914,8 @@ Its log: ${nodeLogHint()}`); return { restarted: true }; } - // Install / remove the Windows Startup-folder launcher, and query it. + // Install / remove the Windows Startup-folder launcher (the startup task in + // the Store package), and query it. handle('node:autostart', async (_e, action) => { if (process.platform !== 'win32') return { supported: false }; if (action === 'install') { @@ -1865,16 +1923,23 @@ Its log: ${nodeLogHint()}`); if ((await winServiceTaskStatus()).installed) { throw new Error('the node already runs as a background service'); } + if (WIN_STORE) { + // Refused, with the CLI's reason, when the user switched it off in + // Windows Settings: only they can switch it back on, there. + await winStartupTask('install'); + return { supported: true, installed: true }; + } const bin = await findNodeBinary(); if (!bin) throw new Error('meshbay-node not found on PATH'); winAutostartInstall(bin); return { supported: true, installed: true }; } if (action === 'remove') { - winAutostartRemove(); + if (WIN_STORE) await winStartupTask('remove'); + else winAutostartRemove(); return { supported: true, installed: false }; } - return { supported: true, installed: winAutostartInstalled() }; + return { supported: true, installed: await winSigninEnabled() }; }); // Turn service mode on or off after install — one elevation, task + firewall |