diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-14 23:51:36 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-14 23:51:36 +0200 |
| commit | e12570f9d1aa2645e6bb223b1417fa0e81957b65 (patch) | |
| tree | d3eda196ed925dc4fbedcb7671b38bd1f05f552d /packages/meshbay-hub | |
| parent | cd2745cecff12e894e0dfa702bff6a90f0e8734e (diff) | |
| download | meshbay-e12570f9d1aa2645e6bb223b1417fa0e81957b65.tar.gz | |
fix(win): a service-mode daemon can be replaced, and the Node page can link one
Two live-reproduced bugs in Windows node start/stop, found sideloading the
0.14.0 build:
- node:start's crash-recovery step killed a service-mode daemon with
taskkill/CTRL_BREAK, both of which fail with "Access is denied" against a
process running under the Scheduled Task's own S4U logon session (a
different session from the Electron app's). The daemon it was meant to
replace just kept running, unreplaced, and schtasks /run on a task Windows
still considered Running was then a silent no-op too. Route through
winServiceTaskEnd() (schtasks /end) first, the way nodeServiceStop/
nodeServiceRestart already correctly do. service-mode.ps1 also now starts
the task right after registering it -- Register-ScheduledTask's own
AtStartup trigger does not run it immediately, so nothing was listening
until the next reboot.
- The Node page's Start button called node.start() with no arguments, so an
unlinked node (a fresh install, or one whose hub-side link was lost) could
never link on Start alone -- only create-group-page.js's own call passed
{hubUrl, username, token}. Reproduced on a fresh non-service install signed
in to the real hub: Start hung for ~105s and failed with "could not link",
pointing at a "Link Node" control that lives on Settings, not the Node
page (that message is fixed too).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/app.js | 2 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/node-page.js | 23 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_transport_contracts.py | 50 |
3 files changed, 68 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 87c4e33..367774f 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1042,7 +1042,7 @@ function App() { .catch(() => {}); }} />`; } else if (route === '/node' && platform.capabilities.nodeAdmin && hasNodeKey) { - page = html`<${LazyNodePage} groups=${groups} />`; + page = html`<${LazyNodePage} groups=${groups} token=${user.token} username=${user.username} />`; } else if (route.startsWith('/group/')) { const groupId = route.slice(7); const group = groups.find(g => g.id === groupId); 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 6dcaa58..0f75edc 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/node-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/node-page.js @@ -4,6 +4,7 @@ import { import { t } from './i18n.js'; import * as platform from './platform.js'; import { Icon } from './icon.js'; +import { HUB } from './hub-client.js'; // ── Node management (D5) ──────────────────────────────────────────────────── // @@ -11,7 +12,7 @@ import { Icon } from './icon.js'; // (platform.node.call), not over MNP/WebRTC. The MNP protocol types remain // for potential future browser-side use. -function NodeServicePanel({ onChanged }) { +function NodeServicePanel({ onChanged, token, username }) { const [info, setInfo] = useState(null); const [busy, setBusy] = useState(''); const [err, setErr] = useState(''); @@ -77,6 +78,16 @@ function NodeServicePanel({ onChanged }) { }); }, [act, info]); + // The Start button below passes {hubUrl, username, token} to node:start, + // same as create-group-page.js's own startNode() -- node:start only links + // an unlinked node key to the hub account when given credentials to link + // it with (main.js's linkNodeKeyAndAwaitRunning). Without them, a node that + // is not linked yet (a fresh install, or one whose hub-side link was lost) + // just polls for up to 105s and fails with "could not link", pointing at a + // "Link Node" control that lives on Settings, not here. Reproduced live + // 2026-09-14: a fresh non-service install's own Start button hung and + // failed this way, the exact same account and key that had just linked + // fine through the Create Group wizard. if (!platform.node.service.available) return null; if (!info || info.supported === false) { return html`<div class="node-service"> @@ -108,7 +119,7 @@ function NodeServicePanel({ onChanged }) { ${err && html`<div class="error-msg">${err}</div>`} <div class="node-service-actions"> <button class="btn btn-small btn-secondary" disabled=${!!busy || running} - onClick=${() => act('start', () => platform.node.start())}> + onClick=${() => act('start', () => platform.node.start({ hubUrl: HUB, username, token }))}> ${busy === 'start' ? t('node.service_starting') : t('node.service_start')}</button> ${info.installed && html` <button class="btn btn-small btn-secondary" disabled=${!!busy || !running} @@ -171,7 +182,7 @@ async function saveCsv(filename, text) { URL.revokeObjectURL(url); } -export function NodePage({ groups }) { +export function NodePage({ groups, token, username }) { const [status, setStatus] = useState('idle'); const [error, setError] = useState(''); const [nodeGroups, setNodeGroups] = useState([]); @@ -664,14 +675,14 @@ export function NodePage({ groups }) { if (status === 'idle' || status === 'connecting') { return html`<div class="page-content"> <h2>${t('node.title')}</h2> - <${NodeServicePanel} onChanged=${fetchStatus} /> + <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> <p class="page-message"><span class="spinner"></span>${' '}${t('status.connecting')}</p> </div>`; } if (status === 'error') { return html`<div class="page-content"> <h2>${t('node.title')}</h2> - <${NodeServicePanel} onChanged=${fetchStatus} /> + <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> <p class="error-msg">${error}</p> <button class="btn btn-primary" onClick=${fetchStatus}> ${t('node.retry')}</button> @@ -686,7 +697,7 @@ export function NodePage({ groups }) { onClick=${reloadConfig}> ${t('node.reload')}</button> </div> - <${NodeServicePanel} onChanged=${fetchStatus} /> + <${NodeServicePanel} onChanged=${fetchStatus} token=${token} username=${username} /> ${actionMsg && html`<div class="node-message">${actionMsg}</div>`} ${!operatorPaired && html` <div class="node-pair-banner"> diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py index ab942e4..f780073 100644 --- a/packages/meshbay-hub/tests/test_transport_contracts.py +++ b/packages/meshbay-hub/tests/test_transport_contracts.py @@ -534,3 +534,53 @@ def test_search_connects_in_one_place(): index = code[code.index("async function fetchGroupIndex("): code.index("async function fetchAllIndexes(")] assert "connectToGroup(" in pool and "connectToGroup(" in index + + +# node:start only links an unlinked node key to the hub account when it is +# given credentials to link it with (main.js's linkNodeKeyAndAwaitRunning on +# Windows, the equivalent inline block on Linux -- both gate on `opts.token`). +# create-group-page.js's own startNode() passes {hubUrl, username, token}; +# the Node page's Start button, reachable independently of that wizard, +# passed none. Reproduced live 2026-09-14 on a fresh non-service Windows +# install signed in to the real hub: Start on a node that had never been +# linked (the ordinary state for anyone who has not gone through Create +# Group yet) polled for up to 105s and failed with "could not link" -- +# cross-platform, since both mains share this same frontend call. +NODE_PAGE = STATIC / "node-page.js" + + +@pytest.fixture(scope="module") +def node_page(): + return NODE_PAGE.read_text(encoding="utf-8") + + +def test_node_page_start_button_can_link_an_unlinked_node(node_page): + fn = node_page[node_page.index("function NodeServicePanel("):] + fn = fn[:fn.index("\nfunction ") if "\nfunction " in fn else len(fn)] + start_call = fn[fn.index("act('start'"):] + start_call = start_call[:start_call.index(")}>")] + assert "hubUrl" in start_call and "HUB" in start_call, ( + "the Start button must pass hubUrl (HUB) through to node:start, or " + "an unlinked node can never link on Start alone") + assert "username" in start_call and "token" in start_call, ( + "the Start button must pass username and token through to " + "node:start -- linkNodeKeyAndAwaitRunning needs both to PUT the key") + + assert "import { HUB" in node_page or "import {HUB" in node_page, ( + "HUB must come from hub-client.js, the one file allowed to decide " + "where the hub is" + ) + + +def test_node_page_receives_the_session_it_hands_to_start(): + """app.js is what actually has to hand token/username down; a fixed + node-page.js reading them off undefined props is the same bug moved up + one file.""" + app_src = APP.read_text(encoding="utf-8") + marker = "route === '/node' && platform.capabilities.nodeAdmin" + route = app_src[app_src.index(marker):] + route = route[:route.index(";")] + assert "LazyNodePage" in route + assert "token=" in route and "username=" in route, ( + "app.js renders the Node page without the session NodeServicePanel " + "now expects, so its Start button's opts are undefined again") |