diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-27 22:20:35 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-27 22:20:35 +0200 |
| commit | 7662484cae8e74b7d9aa383bd6cd0dad4690aadc (patch) | |
| tree | 057609311e339c839dcad04b33de62e2541a9943 /packages/meshbay-node/src/meshbay_node/cli/dispatch.py | |
| parent | 326d7796c79f616f5a0f2058386df2bded657c78 (diff) | |
| download | meshbay-7662484cae8e74b7d9aa383bd6cd0dad4690aadc.tar.gz | |
fix(node): a Windows daemon that stops properly, starts honestly and runs once
Found by installing the builds and driving every startup mode live:
- Stop through the node's own control API first (POST /api/shutdown, loopback
and per-run token): the one channel that reaches a daemon in any session
without elevation -- a service node runs in session 0 -- and the one that
runs its shutdown. Then Task Scheduler, then a forced stop. Nine stops in a
row used to log no shutdown at all: each was a TerminateProcess.
- The forced stop spares the command running it. The frozen meshbay-node.exe
is the daemon and every CLI verb, so `taskkill /IM meshbay-node.exe` killed
`autostart stop` and `restart-daemon` themselves: exit 1, no output, and no
node after a restart. It excludes its own pid and its parent's, and /T takes
a venv launcher's python child and a daemon's ffmpeg children with it.
- Start and restart report the version that answered, never "started" about a
node nobody asked; `service start` says so when no node answered, and where
the log is.
- A second instance fails before it touches anything. The daemon wrote
ui-token, then failed to bind inside uvicorn's task and exited with the
reason on a hidden console; the node still running then refused every stop
and status, its token file naming a dead process. The control port is now
bound first (exclusively on Windows, where SO_REUSEADDR would share it), and
a refusal is logged and exits 2. Linux had the same order.
- The daemon logs to %LOCALAPPDATA%\meshbay\state\node.log: Task Scheduler
discards its stderr. Only the daemon run opens it, never a CLI verb.
- Hub sign-in waits are interruptible, a stop requested before the node is up
is honoured, and a hub that answers 429 or restarts leaves the node in
waiting_for_hub rather than looking dead.
- operator_paired is null until the roster is read, instead of a false that
showed "No operator paired" about a node whose pairing was intact.
The node test conftest also points HOME, USERPROFILE, LOCALAPPDATA and APPDATA
at a throwaway directory for every test, and keeps log_file() away from the
developer's own node: redirecting HOME alone isolates nothing on Windows, and
the CLI tests had been writing invite and pairing codes into the real profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/dispatch.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/cli/dispatch.py | 27 |
1 files changed, 26 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/dispatch.py b/packages/meshbay-node/src/meshbay_node/cli/dispatch.py index 849d589..fc5712f 100644 --- a/packages/meshbay-node/src/meshbay_node/cli/dispatch.py +++ b/packages/meshbay-node/src/meshbay_node/cli/dispatch.py @@ -28,11 +28,36 @@ def start(): "reset") logging.basicConfig( level=logging.ERROR if quiet else getattr(logging, args.log_level), - format="%(asctime)s %(levelname)-8s %(name)s: %(message)s", + format=LOG_FORMAT, ) + if args.command is None: + add_log_file() return args +LOG_FORMAT = "%(asctime)s %(levelname)-8s %(name)s: %(message)s" + + +def add_log_file() -> None: + """Also log the daemon to platform.log_file(), where there is one.""" + from logging.handlers import RotatingFileHandler + + from meshbay_node.platform import log_file + path = log_file() + if path is None: + return + try: + path.parent.mkdir(parents=True, exist_ok=True) + # delay: nothing is created until there is something to write. + handler = RotatingFileHandler(path, maxBytes=5 * 1024 * 1024, backupCount=3, + encoding="utf-8", delay=True) + except OSError as e: + logging.getLogger(__name__).warning("cannot open the log file %s: %s", path, e) + return + handler.setFormatter(logging.Formatter(LOG_FORMAT)) + logging.getLogger().addHandler(handler) + + # Each verb and what runs it. A command line naming none of them starts the # daemon, which is daemon.main's to do. VERBS = { |