aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client')
-rw-r--r--packages/meshbay-client/build/appx-extensions.xml16
-rw-r--r--packages/meshbay-client/build/appx-manifest.xml68
-rw-r--r--packages/meshbay-client/src/main.js85
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