variants/linux: control-socket console and meshcorectl - #5
Closed
mmmorks wants to merge 1 commit into
Closed
Conversation
On Linux the Arduino Serial object is an output-only stub, so the firmware CLI printed but never received a command: a packaged meshcored could be configured only over the mesh or through meshcored.ini. LinuxConsole is a Stream the CLI reads from and writes to, sourcing bytes from a Unix-domain control socket -- /run/meshcored/meshcored.sock under the unit's new RuntimeDirectory, else $XDG_RUNTIME_DIR, else /tmp -- or from stdin when the daemon runs in a foreground terminal. meshcorectl is a dependency-free Python client: a readline REPL with history and Tab completion of the real command set, a one-shot form, and a piped form, exiting 0 only if every command was actually run by the daemon (the echo is the proof; text without it is the daemon explaining why nothing ran). The socket carries the unauthenticated admin CLI, so the details are the point. It is created at 0600 under umask(0177) and widened to 0660 afterwards, in that order, so there is no window at umask-derived permissions. try_bind() probes each candidate with connect() and only reclaims a path that answers ECONNREFUSED -- a socket file gives no other way to tell stale from live, and unlinking first let a second meshcored silently take a running first one's path -- and it refuses to unlink anything that is not a socket, since the path is operator input. Only one client is served: a second connection is answered with "ERR: control socket busy" and closed, rather than parked in the listen backlog to have its command run minutes later on behalf of a process that has exited. A client in a persistent read-error state is dropped instead of retried at 1 kHz. Every descriptor is close-on-exec and reboot() tears the console down before re-exec, because a listener that reached the new image answered its own liveness probe and made the daemon decline its own socket. The console registers exactly the descriptors it will drain next with the event loop, so the daemon wakes on console traffic and on nothing else; a registered descriptor nothing reads stays POLLIN forever and reinstates the busy loop. That precedence lives in LinuxConsole rather than the board, next to rawReadByte(), so the two cannot drift apart. PeekableStream hoists the one-byte-lookahead Stream idiom so the console (and the GPS stream that follows) supply only rawReadByte(). Verified: native tests over real sockets in a temp dir (accept/echo, hangup, persistent read error, busy refusal, non-socket refusal, both path-length reports, close-on-exec), each fix confirmed to fail its test when reverted; two daemons run concurrently in the arm64 container -- the first keeps its socket on the same inode, the second declines it, a stale socket is reclaimed once its owner dies; meshcorectl end to end on a Pi in one-shot, piped and REPL form. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXSCjgNEbJfHwLjD2WSHW4
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On Linux the Arduino
Serialobject is an output-only stub, so the firmware's serial CLI printed but could never receive a command: a packagedmeshcoredwas configurable only over the mesh or throughmeshcored.ini. This addsLinuxConsole, aStreamthe CLI reads from and writes to, backed by a Unix-domain control socket (or stdin when run in a foreground terminal), andmeshcorectl, a dependency-free Python client for it.Relationship to l5yth#25. @rgrizzell's l5yth#25 solves the same problem with a pseudo-terminal, and the two are alternative designs rather than complementary ones — both are Linux-only, both hook the same point in
main.cpp. The trade-offs, as neutrally as I can put them:meshcore-cli -r -s,minicom,screen. That matters on hosts too small for Python.0only if every command was actually run by the daemon — includingreboot, whose only reply is its echo), a single-client rule with an explicitERR: control socket busyrefusal instead of silently queueing a second client's command, a liveness probe so a second daemon cannot steal a live socket, close-on-exec descriptors so arebootre-exec does not inherit the listener, and the socket path resolution (RuntimeDirectory→$XDG_RUNTIME_DIR→/tmp) with the permissions story written down. Raw tools still work (socat - UNIX-CONNECT:...).Happy for the maintainer to pick one. If l5yth#25 is preferred, the parts of this PR worth keeping regardless are
PeekableStream(also used by the GPS stream PR) and the descriptor registration with the event loop; if this one is preferred, exposing a PTY alongside the socket formeshcore-cliis a contained addition toLinuxConsole.What changed
variants/linux/LinuxConsole.{h,cpp}: the console. A live socket client takes priority over stdin. The socket is created at mode0600underumask(0177)and widened to0660afterwards (no window at umask-derived permissions).try_bind()probes each candidate path withconnect()first and only reclaims one that answersECONNREFUSED; it refuses to unlink anything that is not a socket (the path is operator input viaMESHCORED_CONTROL_SOCKET). A client in a persistent read-error state is dropped rather than retried at 1 kHz; a second concurrent client is answered and closed.registerPollFds()tells the event loop exactly which descriptorsrawReadByte()will drain next, so the daemon wakes on console traffic and on nothing else.variants/linux/PeekableStream.h: the one-byte-lookaheadStreamidiom (available()/peek()/read()over a non-blocking descriptor), so subclasses supply onlyrawReadByte().variants/linux/meshcorectl: the client. Completion tables checked against the dispatch literals inCommonCLI.cppandMyMesh.cpp(getandsethave separate key pools; region subcommands listed). Its docstring records the firmware behaviours it cannot fix (clock syncneeds a sender timestamp the socket path passes as 0;region loadis multi-line).examples/simple_repeater/main.cpp: the CLI reads fromConsoleon Linux (MC_CLI,Serialelsewhere);Console.begin()insetup().variants/linux/LinuxBoard.{h,cpp}:reboot()tears the console down before the re-exec;idleUntilEvent()registers the console's descriptors.variants/linux/meshcored.service:RuntimeDirectory=meshcored(mode0750), so/run/meshcored/meshcored.sockexists and is reachable from outsidePrivateTmp.test/test_linux_console: lifecycle over real sockets in a temp dir — accept and echo, hangup releasing the listener, persistent read error, the busy refusal, the non-socket refusal, both path-length reports, close-on-exec, andPeekableStream's contract.LinuxConsole.cppjoins the native build, which is whyMSG_NOSIGNALis guarded (macOS usesSO_NOSIGPIPE).meshcorectlinstalled alongsidemeshcored; a## The control CLIsection (socket path order, permissions, the/tmpfallback's caveat, the three ways to drive the client, the one-client rule, stdin when run in a terminal).How it was tested
linux_repeaterbuild for arm64 in the container; two daemons run concurrently in the container to confirm the first keeps its socket (same inode) and the second declines it, and that a stale socket is reclaimed once its owner dies.meshcorectlexercised against a stand-in socket for each failure mode (live, busy, silent, dying mid-script, missing) and end to end against the real console in one-shot, piped and REPL form on a Pi.Dependencies
Stacked on
pr/04-event-loop-cad(the console registers its descriptors with the event loop, andreboot()'s teardown assumes that PR'sLinuxBoard.cpp). Review the last commit only until that merges.pr/06-gpsadds an identicalPeekableStream.h; git merges identical additions cleanly.Shared code touched
examples/simple_repeater/main.cpp(a#defineselectingConsoleon Linux andSerialeverywhere else; no change for other targets)platformio.ini(native test env only)