diff options
Diffstat (limited to 'packages/meshbay-client')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 125 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 1 |
2 files changed, 115 insertions, 11 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index f582b57..8e987d3 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -1459,6 +1459,34 @@ function registerBridge() { return r; } + // Every meshbay-node.exe running, 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 + // terminal, after `meshbay-node init` -- runs while the task reads "Ready". + // 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'], + { windowsHide: true }, (err, stdout) => { + if (err) return resolve([]); + resolve(String(stdout || '').split(/\r?\n/) + .map((l) => l.split('","')) + .filter((c) => c.length > 1 && /^\d+$/.test(c[1])) + .map((c) => Number(c[1]))); + }); + }); + } + + // 'active' when a node answers, 'unknown' when a node process is there but + // does not (starting, stopping, or stuck: Stop must still be offered), + // 'inactive' when there is none. + async function winNodeActivity() { + const [p, pids] = await Promise.all([probeNode(), winNodePids()]); + if (p) return { activeState: 'active', node: p }; + return { activeState: pids.length ? 'unknown' : 'inactive', node: null }; + } + // Start (or restart) through the CLI and return what answered, or throw // with the CLI's own words and where the log is. async function winNodeStartVia(args) { @@ -1691,14 +1719,15 @@ function registerBridge() { if (process.platform === 'win32') { const svc = await winServiceTaskStatus(); if (svc.installed) { - // Service mode: Task Scheduler already tracks running/not, directly — - // no need to probe the daemon's own API for this panel. - const running = /running/i.test(svc.state); + // Service mode. Not the task's state alone: a node started from a + // terminal runs while the task reads "Ready", and one still on its + // way out after a /end reads "Ready" too. + const { activeState } = await winNodeActivity(); return { supported: true, mode: 'service', installed: true, - activeState: running ? 'active' : 'inactive', + activeState, subState: svc.state, // Whether switching startup mode can actually elevate right now — // service-mode.ps1 is an extraResource, present in a packaged Full @@ -1715,7 +1744,7 @@ 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 [p, bin] = await Promise.all([probeNode(), findNodeBinary()]); + const [{ activeState }, bin] = await Promise.all([winNodeActivity(), findNodeBinary()]); return { supported: true, // Only claim "startup mode" once a node was actually found (bundled @@ -1726,8 +1755,8 @@ function registerBridge() { mode: bin ? 'startup' : null, installed: Boolean(bin), autostart: winAutostartInstalled(), - activeState: p ? 'active' : 'inactive', - subState: p ? 'running' : '', + activeState, + subState: activeState === 'active' ? 'running' : '', canElevate: winCanElevateServiceMode(), }; } @@ -1759,11 +1788,16 @@ function registerBridge() { async function nodeServiceStop() { if (process.platform === 'win32') { - await killNodeProcesses(); + const r = await killNodeProcesses(); nodeStartedByApp = false; - // Said only once nothing answers: this used to report success about a - // service node it could not reach from this session. - if (await probeNode()) { + // Said only once nothing answers and no node process is left: this used + // to report success about a service node it could not reach from this + // session, then about a process still running with its API closed. + if (!r.ok) { + throw new Error(`${r.out || 'the node could not be stopped'} +Its log: ${nodeLogHint()}`); + } + if (await probeNode() || (await winNodePids()).length) { throw new Error(`the node could not be stopped. Its log: ${nodeLogHint()}`); } return { stopped: true }; @@ -1980,7 +2014,76 @@ function registerBridge() { } } + // ── Windows: the node this application ships, set up for whoever signs in ── + // Installing MeshBay installs a node, and signing in is all it should take + // to have one running for that account. Nothing did it: node.toml was + // written by the Create Group wizard alone, which the home page offers only + // to an account with no groups -- a member of somebody else's group, or + // anyone who clicked "Skip setup", never saw it. A fresh install then left + // the service task starting a node that quit at once ("hub.username not + // set"), the account with no node key, and the sidebar without its Node + // section; the way out was `meshbay-node init` in a terminal and the key + // pasted on the profile page (the first Windows beta tester, 2026-10). + // + // It never overrides a choice: a node set up for another account or hub is + // left alone (node:start asks before changing that), and an account already + // linked to another node keeps it -- `PUT /me/node_key` replaces, and the + // node it replaced would stop working with nothing said. + let nodeEnsuring = null; + + async function ensureNode(opts) { + if (process.platform !== 'win32' || !hasBundledNode()) return { state: 'unsupported' }; + const hubUrl = String(config.hubBase || '').replace(/\/+$/, ''); + const username = String((opts && opts.username) || ''); + const token = opts && opts.token; + if (!hubUrl || !token || !USERNAME_RE.test(username)) return { state: 'unsupported' }; + const current = provisionedAs(); + if (current && (!sameHub(current.hubUrl, hubUrl) || current.username !== username)) { + return { state: 'other_account' }; + } + if (!current) provisionNode(hubUrl, username); + + let p = await probeNode(); + if (!p) { + try { + // However this machine is set up: the service task, or a process of + // its own -- and it reports what answered, not what it launched. + await winNodeStartVia(['autostart', 'start']); + } catch (err) { + console.error('[node] start after sign-in:', err.message); + return { state: 'not_started' }; + } + if ((await winStartupMode()) !== 'service') nodeStartedByApp = true; + p = await waitForNode(Date.now() + 15000); + if (!p) return { state: 'not_started' }; + } + if (p.status === 'running') return { state: 'running' }; + + let linked = ''; + try { + const r = await fetch(`${hubUrl}/v1/users/${encodeURIComponent(username)}/pubkeys`, { + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(5000), + }); + if (!r.ok) return { state: 'not_linked' }; + linked = (await r.json()).pk_node_ed25519 || ''; + } catch { return { state: 'not_linked' }; } + if (linked && linked !== p.pk_node_ed25519) return { state: 'other_node' }; + const ready = await linkNodeKeyAndAwaitRunning({ token, hubUrl }, Date.now() + 90000); + return { state: ready && ready.status === 'running' ? 'running' : 'not_linked' }; + } + + handle('node:ensure', async (_e, opts) => { + if (!nodeEnsuring) { + nodeEnsuring = ensureNode(opts).finally(() => { nodeEnsuring = null; }); + } + return nodeEnsuring; + }); + handle('node:start', async (_e, opts) => { + // One at a time: a start racing the sign-in's own would stop the node the + // other had just started. + if (nodeEnsuring) await nodeEnsuring.catch(() => {}); const already = await probeNode(); if (already && already.status === 'running') { return { started: true, ...already }; diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 0de76db..9b466f4 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -143,6 +143,7 @@ contextBridge.exposeInMainWorld('meshbay', { // PATH. Windows only; other platforms always resolve true. bundled: () => ipcRenderer.invoke('node:bundled'), start: (opts) => ipcRenderer.invoke('node:start', opts), + ensure: (opts) => ipcRenderer.invoke('node:ensure', opts), op: (name, args) => ipcRenderer.invoke('node:op', name, args), pairingCode: () => ipcRenderer.invoke('node:pairing-code'), setPairingCode: (code) => ipcRenderer.invoke('node:set-pairing-code', code), |