variants/linux: harden the PTY console and add stdin input - #10
Open
mmmorks wants to merge 1 commit into
Open
Conversation
mmmorks
force-pushed
the
pr/05-pty-console
branch
from
September 7, 2026 22:57
9e63079 to
bb016db
Compare
meshcorectlBuilds on l5yth#25's PTY console (PtyConsole/LinuxConsole) and keeps its shape: a pseudo-terminal published as a stable symlink, so meshcore-cli -r -s and any serial tool attach directly, and the CLI's traffic mirrored to stdout so journald records what was run. Four things change underneath it. The daemon holds a descriptor of its own on the slave. Without one the master reports POLLHUP and read() fails with EIO from the moment the last client closes until the next one opens -- a level condition the poll() loop introduced in the previous commit could only throttle, not clear, so one detached client cost a wake-up per millisecond for the rest of the run. With it a detached console is idle: poll() sleeps, read() says "nothing yet", and the next client attaches as if the first had never left. The master and holder are registered with the event loop as permanent descriptors, so the daemon wakes on console input. Every descriptor is close-on-exec and reboot() unpublishes the console before re-exec. An inherited master would keep the old /dev/pts/N alive with nobody reading it; a symlink left behind would point at a number the exec has just freed, which the next image can only guess about. begin() now tries candidates in order -- console_path, /run/meshcored/console when the unit's RuntimeDirectory is present (so the packaged service needs no INI edit), $XDG_RUNTIME_DIR/meshcore/console, /tmp/meshcore-<uid>/console -- and declines one that a live console holds (its symlink resolves to an existing device) or that is not a symlink at all, since the path is operator input and unlink() does not care what it removes. A dangling symlink from a crashed daemon is reclaimed. stdin is a second door onto the same CLI when meshcored runs in a terminal: raw, non-blocking, no kernel echo (the CLI echoes), restored on end(). Replies follow the command to its source -- always to stdout, and to the PTY only when the command arrived there -- so a foreground session's output does not queue in the PTY to greet the next client. The PTY engine joins the native test build under PIO_UNIT_TESTING, which is what l5yth#25 wanted and reverted: the "SIGHUP" its author saw is PlatformIO's runner mapping a plain exit status 1 (a failed assertion) to signal 1. The suite covers the symlink and its mode, both directions and the newline map, the holder (a detached client leaves poll() sleeping and the event loop timing out cleanly), close-on-exec for every descriptor begin() opens, the live/stale/non-symlink candidate rules, the XDG default, stdin routing with a real terminal on fd 0, and end()/begin() across a simulated reboot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXSCjgNEbJfHwLjD2WSHW4
mmmorks
force-pushed
the
pr/05-pty-console
branch
from
September 8, 2026 04:11
bb016db to
b6620be
Compare
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
Built on top of l5yth#25 and keeps its design: the CLI is served on a pseudo-terminal published as a stable symlink, so
meshcore-cli -r -s /run/meshcored/consoleand any serial tool attach directly, and CLI traffic is mirrored to stdout so journald records what was run. This PR changes what is underneath it. (A dependency-free client,meshcorectl, follows as a separate PR stacked on this one.)POLLHUPand readsEIOuntil the next client opens. With thepoll()-based main loop from the previous PR that is a level condition: one detachedmeshcore-clicost a wake-up per millisecond for the rest of the run. The daemon now holds a descriptor of its own on the slave, so a detached console is idle, and the master is registered with the event loop so the daemon wakes on console input.reboot. Every descriptor is close-on-exec, andreboot()unpublishes the console before re-exec, so the new image never inherits a master (which would keep the old/dev/pts/Nalive with nobody reading it) or a symlink pointing at a number the exec just freed.console_path, then/run/meshcored/consolewhen the unit'sRuntimeDirectoryis present, then$XDG_RUNTIME_DIR/meshcore/console, then/tmp/meshcore-<uid>/console. A candidate held by a live console (its symlink resolves to an existing device) is declined rather than taken over; a regular file at the path is refused rather than unlinked; a dangling symlink from a crash is reclaimed. Each decision is logged.meshcoreddirectly (raw, non-blocking, no kernel echo, restored on exit). Replies follow the command to its source: always to stdout, and to the PTY only when the command arrived there, so a foreground session's output does not queue in the PTY to greet the next client.PtyConsole/LinuxConsolecompile underPIO_UNIT_TESTING, which is what variants/linux: local CLI console over a PTY l5yth/meshcore-linux#25 wanted and reverted. The "SIGHUP" its author saw is PlatformIO's native runner mapping a plain exit status1(one failed assertion) to signal 1; nothing tty-related. The suite runs on macOS and Linux.What changed
src/helpers/PtyConsole.{h,cpp}: holder descriptor, close-on-exec, candidate order and the live/stale/non-symlink rules,fd()accessor.src/helpers/LinuxConsole.{h,cpp}: stdin input, per-source output routing,end()restoring the terminal,ptyFd()/stdinFd(); the console is now the globalConsole(defined in the.cpp) soLinuxBoardcan reach it.examples/simple_repeater/main.cpp: usesConsole; logs the PTY path or the reason there is none.variants/linux/LinuxBoard.{h,cpp}:reboot()callsConsole.end();idleUntilEvent()registers the console's descriptors.platformio.ini: the two sources join the native build.test/test_linux_console: 13 tests against real/dev/ptsnodes in a temp dir, including a real terminal on fd 0 for the routing test.meshcored.ini,meshcored.service: a new## The control CLIsection (path order, permissions and the decline rules, other serial tools, stdin); the "uncommentconsole_path" instruction is gone because the default finds/run/meshcoredon its own.How it was tested
pio test -e native) passes on macOS and in the arm64 bookworm container. The holder test fails without the holder on Linux (poll()returnsPOLLHUPimmediately) and the close-on-exec test fails without thefcntlcalls.linux_repeaterbuilds for arm64 in the container.Dependencies
Stacked on
pr/04-event-loop-cad, with l5yth#25 replayed on top of it as the base commit (authored by @rgrizzell, unchanged apart from theconsole_pathkey going through the validated INI loader). Review the last commit only. If l5yth#25 lands first, this rebases onto it with no change in content.Shared code touched
src/helpers/PtyConsole.{h,cpp},src/helpers/LinuxConsole.{h,cpp}(Linux-only by guard; also compiled in the native test build)examples/simple_repeater/main.cpp(Linux-only block; no change for other targets)platformio.ini(native test env only)