feat(host-cli): serve product frames headlessly with --serve - #439
feat(host-cli): serve product frames headlessly with --serve#439peetzweg wants to merge 1 commit into
Conversation
`signing-host` refused to start without a TTY unless it was given `--script` or `exec`, and both of those are one-shot. `script(1)` cannot lend it a pty from a spawned process either, because it reads `tcgetattr` on its own stdin, so no dev server could supervise a host. `--serve` resolves the signer, announces the endpoint, and stays up until it is stopped, logging one line per event. It refuses to combine with the one-shot modes, and says at startup which way confirmations will go, because a process with no terminal cannot prompt. Ctrl-C is awaited so the host owns its shutdown; SIGTERM keeps its default action. The pairing-responder block that `--script` and `exec` each carried moves into `spawn_pairing_responder`, shared by all three paths.
|
Verification detail, run on macOS 15 arm64 against Host, started headless and left running: truapi-host signing-host --serve --frame-listen 127.0.0.1:9957 \
--product-id dim2.dot --auto-accept > serve.log 2>&1 &Then a product client over the frame socket, using this crate's own The host logged the auto-approval while serving: Shutdown: Checks run locally: One thing worth flagging that is not from this branch: three tests in |
Closes #436
What
signing-host --serveruns the host with no terminal UI and no TTY, and stays up until it is stopped. That makes it supervisable, so a dev server can start it, wait for it, and use it:Output is one line per event:
Why
Until now there was no supported way to run a host next to a dev server.
signing-hostrefuses to start without a TTY unless it is given--scriptorexec, and both of those are one-shot: they run and exit.script(1)cannot lend the CLI a pty from a spawned process, because it readstcgetattron its own stdin, so a wrapper script cannot fake a terminal either.The only working headless recipe was a source checkout plus a script that never resolves (
--script keepalive.ts), which cannot report readiness and leaves an orphan behind. With--servethe host is an ordinary background process.The readiness line matters as much as the mode. The frame socket accepts connections well before a signer exists, so "port is open" is not "host is up".
Serving product frames until stoppedis last in every case and means both halves are up, so that is the line to wait for. (Signing host readylands either side of the endpoint line, depending on whether the session was cached or is being registered, which is why the README points at the serve line instead.)How
Small, because the scaffolding was already there.
interactivewas alreadyscript.is_none() && exec_input.is_none(),SystemEventalready prints line by line when no TUI is attached, andwith_frame_serveralready wraps a future in the frame server. So--serveis one more branch beside theexecbranch: resolve the signer, announce, then park until stopped.--servecannot combine with--scriptorexec, and the TTY error message now points at it.SystemEvent::ServeReady { url, auto_accept }. It names the endpoint, and says which way confirmations will go, because a process with no terminal cannot prompt: without--auto-acceptthey are denied, which is worth stating at startup rather than leaving to be discovered on the first signature.--scriptandexeceach carried is nowspawn_pairing_responder, shared by all three paths, so--deeplinkbehaves the same in serve mode.Verified
Run headlessly against paseo-next-v2 with an existing session, output captured from a pipe rather than a terminal, and a product client connected over the frame socket while it served:
getUserIdandgetAccountfor a product account both answered, and the process exited on signal. Details in a comment below.Companion to #438, which is the client half: with both, a browser product needs two commands and no glue.