Frame one-command GPU invocation broker
Record the approved managed-launch contract, daemon lifecycle and process-group rules, strict GPU CLI behavior, headless acceptance seam, and the complete acceptance matrix before implementation.
This commit is contained in:
parent
96d0baebb7
commit
821835b8c5
|
|
@ -0,0 +1,613 @@
|
|||
# GPU invocation — one-command broker framing
|
||||
|
||||
**Revision 3 — pre-implementation. Ground truth: canonical `main` @
|
||||
`96d0bae`, protocol v19, 2026-07-23.**
|
||||
|
||||
The GPU editor works, but reaching it is still a development-session ritual:
|
||||
build two packages with different feature requirements, keep a foreground
|
||||
daemon alive in one terminal, reconstruct its resolved Unix-socket path, and
|
||||
pass that raw path to a second binary in another terminal. This framing makes
|
||||
the normal local GPU path one explicit command:
|
||||
|
||||
```sh
|
||||
pmacs --gpu
|
||||
pmacs --gpu --socket research
|
||||
```
|
||||
|
||||
This is an additive first stage. It does **not** yet change bare `pmacs` from
|
||||
TUI to GUI, and it does not pretend that `pmacs --gpu FILE` works before the
|
||||
daemon has a real per-frontend initial-file contract. It preserves the
|
||||
separate `pmacs-gpu` binary and its independent dependency graph.
|
||||
|
||||
Revision 2 closes the first review round: the spawned daemon is isolated from
|
||||
the launcher's foreground process group; the required Vterm headless probe is
|
||||
retained; retry/error/reaping behavior is complete; a real display-less
|
||||
managed-attach seam is named; bare `--socket` stops being silently ignored;
|
||||
and the non-CRDT gate is explicitly justified as default-socket protection.
|
||||
|
||||
Revision 3 makes the managed probe deterministic for signal/reaper tests,
|
||||
keeps `Interrupted` / `WouldBlock` transient inside the post-spawn retry
|
||||
window, states the process-group signal simulation in CI-executable terms,
|
||||
and distinguishes the socket type check from liveness inference.
|
||||
|
||||
## Ground truth
|
||||
|
||||
### Current user path
|
||||
|
||||
The daemon is a mode of the root `pmacs` binary; there is no
|
||||
`pmacs-daemon` executable (`Cargo.toml`, `src/main.rs`). `pmacs-gpu` is a
|
||||
separate unpublished workspace package and binary
|
||||
(`pmacs-gpu/Cargo.toml`). The current source-checkout path is:
|
||||
|
||||
```sh
|
||||
cargo build --release --workspace --features pmacs/crdt
|
||||
|
||||
# terminal 1
|
||||
target/release/pmacs --daemon
|
||||
|
||||
# terminal 2
|
||||
runtime="${XDG_RUNTIME_DIR:-/tmp/pmacs-$(id -u)}"
|
||||
target/release/pmacs-gpu --attach "$runtime/pmacs/default.sock"
|
||||
```
|
||||
|
||||
The unified workspace build above succeeds. The README currently documents
|
||||
two separate build commands instead. Its `cargo run --release -- <file>`
|
||||
example is not runnable as written: the root package has no `default-run`,
|
||||
and Cargo reports that it cannot choose among `pmacs`, `pmacs-audit`,
|
||||
`pmacs_fake_lsp`, and `pmacs_fake_mcp`.
|
||||
|
||||
A live smoke on this base exposed the cost of independent builds: the first
|
||||
`target/release/pmacs-gpu` was protocol v15 while the daemon was v19. The GPU
|
||||
opened but rejected the attach. Rebuilding `pmacs-gpu` produced a successful
|
||||
v19 attach. The handshake caught the mismatch correctly; the invocation path
|
||||
made it easy to create.
|
||||
|
||||
### Existing CLI and process boundaries
|
||||
|
||||
- `pmacs [FILE]` runs the in-process TUI. `-nw` / `--no-window` is already
|
||||
parsed as an explicit TUI choice, though it is currently equivalent to the
|
||||
default (`src/main.rs:94-131,288-318`).
|
||||
- The source comment at `src/main.rs:28-49` reserves the future shape:
|
||||
explicit `-nw` wins, a future `--gui` can select GUI, then an environment /
|
||||
display-based default may choose GUI. None of that GUI selection is
|
||||
implemented today.
|
||||
- `pmacs --daemon [--socket NAME|PATH]` runs in the foreground. Bare names
|
||||
resolve to `<runtime>/pmacs/NAME.sock`; omission means `default.sock`; a
|
||||
value containing `/` is used as a path (`src/socket_path.rs`).
|
||||
- `<runtime>` is nonempty `$XDG_RUNTIME_DIR`, otherwise
|
||||
`/tmp/pmacs-<uid>`. The daemon creates a private parent, locks a sibling
|
||||
lockfile, removes a stale socket only after acquiring the lock, and binds
|
||||
the socket owner-only (`src/socket_path.rs`, `src/lockfile.rs`,
|
||||
`src/daemon.rs:437-530`).
|
||||
- `pmacs-gpu --attach <socket>` takes a raw pathname. It has no default/name
|
||||
resolver and no file positional (`pmacs-gpu/src/main.rs:548-565,802-848`).
|
||||
- `pmacs-gpu` has three current modes: bare hello-world, direct `--attach`,
|
||||
and the test/acceptance seam `--headless-probe <socket> <report>`. The
|
||||
required Vterm Stage 3 acceptance invokes that third mode as a real
|
||||
subprocess (`tests/vterm_stage3_acceptance.rs:679-691`).
|
||||
- Bare `pmacs-gpu` still opens the inert Session-2 `hello, pmacs` window. It
|
||||
is scaffolding, not an editor session.
|
||||
- The GPU parser consumes the attach/probe operands but does not reject later
|
||||
argv, so trailing values are silently ignored.
|
||||
- The GPU package intentionally depends on `pmacs-protocol`, not the root
|
||||
editor crate. The distribution decision in
|
||||
`docs/pmacs-gpu-design.md:195-202` keeps wgpu, winit, font, and
|
||||
window-system dependencies out of TUI-only installs. That boundary still
|
||||
holds.
|
||||
|
||||
### Capability and attach constraints
|
||||
|
||||
A usable GPU daemon must advertise all of `multi_frontend`, `crdt_replica`,
|
||||
and `semantic_render`. The root `crdt` feature enables those capabilities;
|
||||
`pmacs-gpu` itself has no Cargo features. The GPU validates the daemon's
|
||||
`Hello` before sending `AttachRequest` and produces an actionable capability
|
||||
mismatch instead of waiting forever (`pmacs-gpu/src/attach.rs`).
|
||||
|
||||
`pmacs-gpu` currently attempts one `UnixStream::connect` from
|
||||
`ApplicationHandler::resumed`. A connect/handshake failure stays visible in
|
||||
the window but is not retried. Later disconnect also requires a manual
|
||||
relaunch. Automatic reconnect is an existing named deferral, separate from
|
||||
startup (`docs/gpu-attach-robustness-framing.md:175-186`).
|
||||
|
||||
The root already has daemon auto-start precedent in `src/daemon_attach.rs`:
|
||||
try an existing socket, otherwise spawn `current_exe --daemon --socket PATH`,
|
||||
wait up to five seconds, then enter its byte bridge. That helper must retain
|
||||
the successful connection because a disposable connect probe makes the
|
||||
daemon send `Hello` into a stream the probe drops, producing a broken pipe.
|
||||
The GPU cannot directly reuse the helper: it lives in the root crate and
|
||||
returns the stream to the stdio bridge, while `pmacs-gpu` owns its own direct
|
||||
Unix transport.
|
||||
|
||||
The SSH-side daemon auto-start precedent does not isolate the daemon into a
|
||||
new process group: `daemon_attach.rs` relies on the SSH spawn having no
|
||||
controlling terminal (`src/daemon_attach.rs:53-60`). A local `pmacs --gpu`
|
||||
launcher does have one. Without an explicit process-group split, root, GPU,
|
||||
and the auto-started daemon inherit the foreground group; terminal Ctrl-C
|
||||
reaches all three, and the daemon deliberately treats SIGINT as graceful
|
||||
shutdown (`src/daemon.rs:617-633`). SIGHUP is already ignored, so foreground
|
||||
SIGINT is the specific lifecycle gap.
|
||||
|
||||
### Initial-file constraint
|
||||
|
||||
Every daemon attachment currently receives a fresh scratch view in
|
||||
`handle_session_established` (`src/daemon.rs:1533-1552`). The code explicitly
|
||||
names cloning or taking an initial-buffer argument as future work. Opening a
|
||||
file in the daemon before attach would not put that file in the new GPU
|
||||
frontend's view. There is no frontend `OpenPath` event and no initial target
|
||||
in `AttachRequest` (`pmacs-protocol/src/message.rs:2023-2040`).
|
||||
|
||||
Therefore a launcher that accepts `FILE` without new daemon/session work
|
||||
would either ignore it, drive the minibuffer by synthetic keys, or open it in
|
||||
the wrong view. All three are rejected.
|
||||
|
||||
### Distribution gaps
|
||||
|
||||
There is no editor installation recipe, desktop entry, user service,
|
||||
Make/Just target, Cargo alias, or launcher wrapper. The only repository
|
||||
script is the test-development helper `scripts/bite`. Cargo does not install
|
||||
repository shell scripts, and `pmacs-gpu` has `publish = false`.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Q#GI1 — Add the explicit root command `pmacs --gpu [--socket NAME|PATH]`
|
||||
|
||||
The first-stage public surface is:
|
||||
|
||||
```text
|
||||
pmacs --gpu [--socket NAME|PATH]
|
||||
```
|
||||
|
||||
It is additive. Bare `pmacs` and `pmacs FILE` continue to run the local TUI;
|
||||
`pmacs -nw` remains the explicit TUI spelling. `--gpu` is mutually exclusive
|
||||
with `-nw` / `--no-window`, `--daemon`, `--attach`, and `--daemon-attach`.
|
||||
It rejects every positional argument with:
|
||||
|
||||
```text
|
||||
pmacs: --gpu does not yet accept FILE; open it from the GPU with C-x C-f
|
||||
```
|
||||
|
||||
The parser also closes the adjacent existing hole: `--socket` without one of
|
||||
`--daemon`, `--attach`, `--daemon-attach`, or `--gpu` exits 2 instead of being
|
||||
silently discarded by `Mode::Local`. A flag is used rather than a `pmacs gpu`
|
||||
subcommand because `gpu` is a valid existing positional filename. No
|
||||
`PMACS_FRONTEND` environment selection and no display auto-detection land in
|
||||
this stage.
|
||||
|
||||
Rejected alternatives:
|
||||
|
||||
- **Bare `pmacs` becomes GUI immediately** — this mixes launcher correctness,
|
||||
daemon lifecycle, initial-file semantics, and a default-behavior change in
|
||||
one cut. `-nw` reserves that eventual migration; it does not make an
|
||||
incomplete migration safe.
|
||||
- **A shell wrapper** — not installed by Cargo, duplicates readiness and path
|
||||
policy, and makes version-skewed binaries easier to combine.
|
||||
- **Link GPU into the root binary** — violates the deliberate independent
|
||||
dependency graphs and adds wgpu/window dependencies to TUI-only builds.
|
||||
|
||||
### Q#GI2 — Root owns policy and paths; `pmacs-gpu` owns the successful connection
|
||||
|
||||
The root launcher owns:
|
||||
|
||||
1. CLI validation;
|
||||
2. the canonical `resolve_socket_path` call;
|
||||
3. the CRDT-build gate;
|
||||
4. discovery of the separate `pmacs-gpu` executable;
|
||||
5. waiting for that executable and reflecting its outcome.
|
||||
|
||||
The GPU child owns the actual connection that becomes the session. For the
|
||||
managed launch, root passes two hidden/internal arguments: the resolved raw
|
||||
socket path and `current_exe()` as the daemon executable. The child first
|
||||
tries the socket itself; the stream that completes `Hello` / `AttachRequest`
|
||||
is retained as its real `AttachClient`. There is no disposable readiness
|
||||
connection and therefore no deliberate broken-pipe noise.
|
||||
|
||||
The hidden argument shape is not a second user-facing launcher. Direct users
|
||||
keep `pmacs-gpu --attach PATH`; the documented managed surface is
|
||||
`pmacs --gpu`.
|
||||
|
||||
### Q#GI3 — Managed GPU attach may start the supplied daemon, then retries boundedly
|
||||
|
||||
Managed attach follows this state machine before creating the window:
|
||||
|
||||
1. Try the resolved Unix socket once.
|
||||
2. If connection reaches `Hello`, perform the normal version and capability
|
||||
validation. Either failure is final and surfaced; never start a
|
||||
replacement daemon over a live incompatible instance.
|
||||
3. `NotFound` authorizes daemon startup. `ConnectionRefused` authorizes it
|
||||
only when `metadata(socket)` says the existing entry is a Unix socket, or
|
||||
the entry disappeared in the race between connect and metadata. An
|
||||
existing non-socket file is a final error and is never handed to
|
||||
`pmacs --daemon`, whose established stale-path transaction would otherwise
|
||||
unlink it after acquiring the sibling lock.
|
||||
4. Every other initial connect error (`PermissionDenied`, invalid path shape,
|
||||
resource exhaustion, `Interrupted`, `WouldBlock`, and other errno classes)
|
||||
is final, surfaced with the socket path, and invokes no daemon spawner.
|
||||
During the post-spawn retry window, `NotFound`, authorized
|
||||
`ConnectionRefused`, `Interrupted`, and `WouldBlock` / `EAGAIN` continue to
|
||||
the deadline; all other errors remain final. This tolerates a signal-
|
||||
interrupted `connect(2)` and the just-bound daemon's temporarily full
|
||||
AF_UNIX accept backlog without broadening what may trigger daemon startup.
|
||||
5. Spawn the supplied executable as
|
||||
`pmacs --daemon --socket <resolved-path>`, with the process-group isolation
|
||||
in Q#GI13.
|
||||
6. Retry the real GPU connect every 50 ms for up to five seconds, matching
|
||||
`AUTO_START_POLL_INTERVAL` / `AUTO_START_TIMEOUT` in
|
||||
`daemon_attach.rs:149-160`. The first successful stream becomes the real
|
||||
attach; no probe is dropped.
|
||||
7. If the spawned child exits during startup, reap it, retain its status, and
|
||||
keep retrying until the deadline: another concurrent launcher may have won
|
||||
the socket lock and be about to listen.
|
||||
8. At the deadline, exit nonzero with the socket path, timeout, and spawned
|
||||
child status when available.
|
||||
|
||||
Connection/retry occurs before `run_app`, so the winit event thread never
|
||||
freezes for five seconds behind startup polling. On success, events sent to
|
||||
the already-created `EventLoopProxy` may queue until `run_app` starts; the
|
||||
window then assembles with the connected frontend id. Existing direct
|
||||
`--attach` behavior may retain its in-window failure banner.
|
||||
|
||||
During startup the managed connector owns `Option<Child>` so every early exit
|
||||
is reaped. After a successful attach, a still-running spawned daemon moves to
|
||||
a named reaper thread whose only job is blocking `Child::wait`; this prevents
|
||||
a daemon that later crashes or quits from remaining a zombie throughout a
|
||||
long GPU session. Closing the GPU process still leaves a live daemon orphaned
|
||||
and long-lived per Q#GI5.
|
||||
|
||||
Do not extract a generic launcher crate for two constants, a short loop, and
|
||||
one child reaper.
|
||||
|
||||
### Q#GI4 — Auto-start is race-safe through the existing daemon lock
|
||||
|
||||
Two simultaneous `pmacs --gpu` commands may both observe a missing socket and
|
||||
spawn a daemon. The sibling lockfile is the arbiter: one daemon binds, the
|
||||
other exits. Both GPU children continue their bounded connect loop and attach
|
||||
to the winner. The loser child is reaped; its lock error is not treated as a
|
||||
launch failure if the socket becomes usable.
|
||||
|
||||
A stale socket follows the existing daemon transaction: acquire the lock,
|
||||
unlink the stale socket, bind the replacement. The launcher may read
|
||||
filesystem metadata solely to distinguish a socket entry from non-socket data
|
||||
(Q#GI3); it neither deletes entries nor treats path existence as proof of
|
||||
liveness. The daemon lock and successful protocol connection remain the
|
||||
arbiters.
|
||||
|
||||
### Q#GI5 — An auto-started daemon remains long-lived
|
||||
|
||||
Closing the GPU window detaches that frontend but does not kill a daemon the
|
||||
launcher started. This matches pmacs's long-lived-instance architecture and
|
||||
the existing `--daemon-attach` auto-start behavior. A later `pmacs --gpu`
|
||||
reuses the same default/named daemon and retains buffers, processes, and
|
||||
language services.
|
||||
|
||||
The startup child has null stdin/stdout/stderr, matching the existing
|
||||
background auto-start precedent. Startup failure is reported through child
|
||||
status + timeout; detailed daemon diagnostics remain available by running
|
||||
`pmacs --daemon --socket ...` directly. Q#GI13, not the SSH precedent,
|
||||
defines the local terminal's signal isolation. The launcher does not
|
||||
daemonize, install a service, or invent idle shutdown in this stage.
|
||||
|
||||
### Q#GI6 — Require a CRDT-capable root build before launching
|
||||
|
||||
In a root binary compiled without `feature = "crdt"`, `pmacs --gpu` exits
|
||||
before executable discovery or any socket connection with an actionable
|
||||
message:
|
||||
|
||||
```text
|
||||
pmacs: --gpu requires pmacs built with --features crdt
|
||||
```
|
||||
|
||||
This prevents default-socket poisoning: without the gate, a non-CRDT root
|
||||
could auto-start an incapable daemon that successfully owns `default.sock`;
|
||||
the GPU would then reject its capabilities, and Q#GI3's correct
|
||||
never-replace-a-live-instance rule would make later managed launches keep
|
||||
failing until the user manually stopped that daemon.
|
||||
|
||||
The deliberate trade-off is that a non-CRDT root also refuses the managed
|
||||
fast path when a separate capable daemon is already listening. The broker's
|
||||
root executable is its daemon-start authority and must be capable for
|
||||
deterministic behavior. Advanced users may still attach the independent GPU
|
||||
directly to a known capable daemon with `pmacs-gpu --attach PATH`; that path
|
||||
retains its own Hello capability validation.
|
||||
|
||||
### Q#GI7 — Prefer the sibling GPU binary, then fall back to `PATH`
|
||||
|
||||
Discovery order:
|
||||
|
||||
1. test-only `PMACS_TEST_GPU_BIN` override;
|
||||
2. `current_exe().parent()/pmacs-gpu` when that path exists;
|
||||
3. `pmacs-gpu` through `PATH`.
|
||||
|
||||
Sibling-first keeps ordinary source builds and side-by-side installations on
|
||||
the same release/protocol build. The handshake remains authoritative: path
|
||||
co-location is not proof of protocol compatibility. Failure names the sibling
|
||||
candidate and PATH fallback rather than reporting a generic spawn error.
|
||||
|
||||
There is no production `PMACS_GPU_BIN` configuration knob in v1. A permanent
|
||||
override would become distribution policy; tests need substitution, users
|
||||
need an installation that places the two shipped binaries coherently.
|
||||
|
||||
### Q#GI8 — Root waits for the GPU child and reflects failure
|
||||
|
||||
`pmacs --gpu` remains the terminal-visible parent while the window is open.
|
||||
A successful GPU exit returns success; a nonzero exit returns failure and
|
||||
prints which GPU executable failed. Spawn failure is immediate and
|
||||
actionable. No shell, `nohup`, or detached launcher process sits between the
|
||||
user and the frontend.
|
||||
|
||||
The daemon is independent after startup (Q#GI5); root waits only for the GPU
|
||||
child.
|
||||
|
||||
### Q#GI9 — Retire bare `pmacs-gpu` hello-world and make argv strict
|
||||
|
||||
The hello-world window was Session-2 dependency scaffolding and no longer
|
||||
serves a user workflow. Bare `pmacs-gpu` exits with usage that points to
|
||||
`pmacs --gpu` for managed startup and `pmacs-gpu --attach PATH` for direct
|
||||
debugging.
|
||||
|
||||
The existing test-only
|
||||
`pmacs-gpu --headless-probe <socket> <report>` mode is retained byte-for-byte
|
||||
as the Vterm Stage 3 real daemon + PTY + wgpu seam. Its parser becomes strict
|
||||
about exactly those two operands; `tests/vterm_stage3_acceptance.rs` keeps its
|
||||
current subprocess command and semantics. Q#GI14 adds a separate managed
|
||||
headless probe rather than overloading this vterm contract.
|
||||
|
||||
The GPU parser rejects:
|
||||
|
||||
- trailing argv after direct-attach or probe operands;
|
||||
- a missing attach/probe operand;
|
||||
- user attempts to invoke the hidden managed modes without every broker
|
||||
operand;
|
||||
- unknown flags.
|
||||
|
||||
Add `pmacs-gpu --version` so reports can name both package version and
|
||||
protocol version without opening a window. `--help` labels direct attach as
|
||||
an advanced/manual path and does not advertise internal broker/probe
|
||||
arguments.
|
||||
|
||||
### Q#GI10 — No protocol change and no initial file in this stage
|
||||
|
||||
The broker changes process orchestration only. It sends the existing
|
||||
`AttachRequest`, negotiates protocol v19, and uses the existing semantic/CRDT
|
||||
session. `SUPPORTED` and every wire discriminant remain unchanged.
|
||||
|
||||
`pmacs --gpu FILE` is rejected rather than accepted partially. The future
|
||||
file feature must target the authenticated source frontend's view, preserve
|
||||
non-UTF-8 local paths, define relative-path resolution, surface open failure,
|
||||
and avoid a scratch-buffer flash. It receives its own framing and protocol
|
||||
review.
|
||||
|
||||
### Q#GI11 — Fix checkout build/run instructions with the broker
|
||||
|
||||
Set root package `default-run = "pmacs"`, making the existing README
|
||||
`cargo run --release -- ...` family unambiguous. Document one coherent build:
|
||||
|
||||
```sh
|
||||
cargo build --release --workspace --features pmacs/crdt
|
||||
```
|
||||
|
||||
Then document:
|
||||
|
||||
```sh
|
||||
target/release/pmacs --gpu
|
||||
# explicit TUI remains:
|
||||
target/release/pmacs -nw [FILE]
|
||||
```
|
||||
|
||||
The build line deliberately compiles both binaries together. Keep the
|
||||
advanced two-process commands in a troubleshooting/manual-attach subsection,
|
||||
including the canonical XDG fallback rather than Linux-only `$UID` prose.
|
||||
Do not claim `cargo install` support until installation is exercised and the
|
||||
unpublished GPU package has a deliberate distribution story.
|
||||
|
||||
### Q#GI12 — Scope stays on invocation, not reconnect or packaging
|
||||
|
||||
The managed startup retry ends when the first attach succeeds. A later daemon
|
||||
disconnect retains today's `(daemon disconnected)` state and manual relaunch.
|
||||
Desktop files, system services, release bundles, package managers, and remote
|
||||
GPU transports are not smuggled into this feature.
|
||||
|
||||
### Q#GI13 — Put the auto-started daemon in its own process group
|
||||
|
||||
Before spawning the managed daemon, call the safe Unix
|
||||
`std::os::unix::process::CommandExt::process_group(0)`. The daemon becomes the
|
||||
leader of a new process group while the root broker and GPU child remain in
|
||||
the terminal's foreground group. Terminal Ctrl-C therefore terminates the
|
||||
foreground launcher/frontend without delivering SIGINT to the daemon or any
|
||||
other attached frontend.
|
||||
|
||||
This is process-group isolation, not a new session or full daemonization.
|
||||
Direct `pmacs --daemon` remains foreground and keeps its established
|
||||
SIGINT/SIGTERM graceful-shutdown contract. Terminal close remains safe through
|
||||
the daemon's existing SIGHUP no-op. No `unsafe`, `setsid` helper process, or
|
||||
platform-specific FFI enters the codebase.
|
||||
|
||||
### Q#GI14 — Add a real display-less managed-attach acceptance seam
|
||||
|
||||
Extract the pre-`run_app` production path as
|
||||
`connect_managed_with_sink(socket, daemon_exe, sink)`. The normal managed
|
||||
window calls it with the existing `EventLoopProxy` sink. A new hidden strict
|
||||
subprocess mode:
|
||||
|
||||
```text
|
||||
pmacs-gpu --headless-managed-probe <socket> <report> <daemon-exe>
|
||||
```
|
||||
|
||||
calls the same function with a channel sink and keeps processing that channel
|
||||
after the real `BufferSnapshot`. At the snapshot checkpoint it atomically
|
||||
writes a complete report with `phase=ready` and initial named facts (protocol,
|
||||
whether this invocation spawned, child status so far), then holds the live
|
||||
session until stdin reaches EOF. A dedicated stdin-reader thread reports EOF
|
||||
to the probe loop; the loop itself remains free to receive disconnects and
|
||||
reaper observations. Each such lifecycle observation atomically refreshes the
|
||||
`phase=ready` report, so a harness can wait for a named fact such as
|
||||
`daemon_reaped=true` without sleeping. On EOF the probe atomically replaces
|
||||
the report with `phase=complete` plus final child/reaping facts and exits
|
||||
without winit or wgpu.
|
||||
|
||||
Tests that need a hold spawn the probe with piped stdin, wait for
|
||||
`phase=ready`, perform the signal/daemon action, wait for any required named
|
||||
fact, then close stdin when they want normal probe completion. An ordinary
|
||||
invocation with null stdin advances immediately from ready to complete. There
|
||||
is no timing-based linger duration.
|
||||
|
||||
This is the acceptance seam for Q#GI3–GI6 and Q#GI13: real binary, real Unix
|
||||
socket, real Hello/capability negotiation, and real daemon subprocess. A
|
||||
decoded-message fixture or a second test-only connect implementation is not
|
||||
accepted evidence.
|
||||
|
||||
The existing `--headless-probe <socket> <report>` remains separate and still
|
||||
drives real offscreen wgpu for Vterm Stage 3 (Q#GI9). Narrow injected-spawner
|
||||
unit tests pin rare errno/timeout branches, including post-spawn
|
||||
`Interrupted` / `WouldBlock` followed by success, but they do not replace the
|
||||
managed subprocess acceptance.
|
||||
|
||||
## Categorical bets
|
||||
|
||||
1. **An explicit broker is enough to validate lifecycle policy before changing
|
||||
the default frontend.** Users gain a one-command GPU path without forcing
|
||||
GUI startup into scripts, `$EDITOR`, SSH sessions, or terminals that rely
|
||||
on today's bare `pmacs` TUI.
|
||||
2. **Sibling-first discovery covers source and coherent installed layouts.**
|
||||
A fallback to PATH handles split prefixes; the protocol handshake catches
|
||||
stale or foreign binaries.
|
||||
3. **The existing lock is the correct concurrency arbiter.** Launcher-side
|
||||
PID files, path-existence checks, or socket deletion would duplicate and
|
||||
weaken the daemon's established ownership transaction.
|
||||
4. **The successful connect must be the session connect.** Disposable probes
|
||||
are observably wrong because the daemon speaks first; retrying the actual
|
||||
GPU connect avoids false BrokenPipe logs and frontend-id churn.
|
||||
5. **A five-second pre-window startup bound is acceptable.** Existing remote
|
||||
daemon auto-start uses the same bound. A missing/broken daemon fails before
|
||||
creating a misleading inert window.
|
||||
6. **Persistent auto-start matches user expectation.** The instance owns
|
||||
buffers and services across frontend lifetime; killing it on window close
|
||||
would turn the daemon split into implementation overhead with no persistence
|
||||
benefit.
|
||||
7. **Foreground job control must not own the daemon.** A local launcher's
|
||||
terminal process group is not the SSH no-controlling-terminal precedent;
|
||||
a safe process-group split preserves the daemon across Ctrl-C without full
|
||||
daemonization.
|
||||
8. **Rejecting FILE is better than synthetic input.** Driving `C-x C-f` or the
|
||||
minibuffer from a launcher is timing-dependent, configuration-dependent,
|
||||
and cannot provide an atomic initial view.
|
||||
9. **Non-socket paths are data, not stale sockets.** Managed auto-start never
|
||||
feeds a regular file to the daemon's existing unlink-and-bind transaction.
|
||||
10. **No new shared crate is warranted.** Root already resolves paths and
|
||||
hands the result to the GPU; the GPU adds only bounded connect-or-spawn
|
||||
behavior around its existing transport.
|
||||
|
||||
## Deferred (named)
|
||||
|
||||
- **GUI as the automatic default.** Complete the reserved
|
||||
`FrontendChoice::Auto` plan after the broker is proven: display detection,
|
||||
`PMACS_FRONTEND`, and `pmacs -nw` precedence. This is a user-default change,
|
||||
not part of additive startup.
|
||||
- **Initial file(s) for an attached frontend.** A real per-session target that
|
||||
opens/switches in the authenticated source's view, reports errors, handles
|
||||
path bytes and relative cwd, and avoids scratch flash. This is prerequisite
|
||||
to honest `pmacs --gpu FILE` and eventual bare `pmacs FILE` GUI startup.
|
||||
- **Automatic reconnect/resync.** Startup retry does not reconcile an
|
||||
optimistic replica after a live connection drops; retain the attach-
|
||||
robustness deferral.
|
||||
- **Direct `pmacs-gpu --socket NAME`.** Managed users go through root, which
|
||||
already owns canonical resolution. Revisit only if direct frontend use is a
|
||||
supported standalone workflow.
|
||||
- **Remote GPU attach.** The TUI owns SSH/daemon-attach transport today;
|
||||
generic GPU stream transports need separate latency, clipboard, and
|
||||
frontend-resource semantics.
|
||||
- **Daemon service management.** systemd/launchd units, socket activation,
|
||||
idle shutdown, logs, restart policy, and a `pmacs --stop-daemon` command.
|
||||
- **Distribution/install bundles.** Cargo install, release archives, desktop
|
||||
entries, icons, and ensuring both binaries land together.
|
||||
- **Multiple files and client/server open commands.** Follow the initial-file
|
||||
contract rather than widening this stage's rejected positional grammar.
|
||||
- **GPU executable override for users.** Keep only the test override until a
|
||||
real packaging use case establishes precedence and diagnostics.
|
||||
|
||||
## Acceptance
|
||||
|
||||
CLI and launcher cases run against the real built binaries where process
|
||||
behavior is the contract. Pure parsing/discovery helpers receive unit tests;
|
||||
no source-text assertions substitute for subprocess behavior. Managed
|
||||
connection cases use Q#GI14's real display-less binary seam; the existing
|
||||
Vterm probe continues to cover offscreen wgpu.
|
||||
|
||||
1. **Root CLI grammar:** `pmacs --gpu` and `pmacs --gpu --socket research`
|
||||
select managed GPU mode. Combinations with `-nw`, `--daemon`, `--attach`,
|
||||
`--daemon-attach`, or a positional file exit 2 with the conflicting
|
||||
argument named. Bare `pmacs --socket research` (with or without a local
|
||||
file / `-nw`) exits 2 and says which owning mode is required.
|
||||
2. **Non-CRDT build fails before socket or spawn:** a default-feature
|
||||
`pmacs --gpu` names `--features crdt`; a capable daemon already listening
|
||||
does not weaken the gate, and fake GPU/daemon executables record zero
|
||||
invocations. The default socket remains unowned.
|
||||
3. **Sibling discovery wins:** with executable fixtures at the current-exe
|
||||
sibling and on PATH, the sibling receives the managed arguments. Removing
|
||||
it uses PATH. With neither, the error names both lookup attempts.
|
||||
4. **Existing daemon fast path:** start a real CRDT daemon on a private socket,
|
||||
run `--headless-managed-probe`, and assert a v19 session establishes and a
|
||||
real `BufferSnapshot` arrives without invoking the supplied daemon spawner.
|
||||
5. **Missing daemon auto-start:** from no socket/lock, the managed probe starts
|
||||
the supplied real CRDT daemon, completes the real Hello/capability
|
||||
handshake, receives the first `BufferSnapshot`, and leaves the daemon
|
||||
connectable after the probe exits.
|
||||
6. **Ctrl-C does not kill the daemon:** spawn the managed probe/broker in its
|
||||
own process group, wait for the probe's `phase=ready`, attach a second real
|
||||
frontend, then simulate terminal Ctrl-C with `kill(-pgid, SIGINT)`. Assert
|
||||
the launcher/frontend exit while the separately grouped daemon and second
|
||||
session remain usable. This does not require a controlling terminal or a
|
||||
foreground-process-group claim in CI. Direct foreground `pmacs --daemon`
|
||||
still exits cleanly on SIGINT.
|
||||
7. **Concurrent launchers converge:** start two managed probes together on an
|
||||
absent named socket. Exactly one daemon owns the lock; both clients
|
||||
establish sessions; the losing daemon child is reaped rather than becoming
|
||||
a zombie or aborting its frontend.
|
||||
8. **Stale socket recovery stays daemon-owned:** leave a stale Unix socket
|
||||
with no lock owner, launch managed GPU, and assert the daemon replaces it
|
||||
and the frontend attaches. The launcher itself performs no unlink.
|
||||
9. **Other connect errors fail closed; retry transients survive:** a
|
||||
permission-denied initial socket path invokes no daemon spawner and reports
|
||||
the path/error immediately. A regular file at the socket path survives
|
||||
unchanged, invokes no daemon, and reports that managed startup refuses to
|
||||
replace a non-socket entry. An injected post-spawn sequence of
|
||||
`Interrupted`, `WouldBlock`, then a successful real connection stays
|
||||
within the deadline and establishes the session.
|
||||
10. **Live capability mismatch is not replaced:** against a non-CRDT daemon,
|
||||
managed launch reports the existing capability mismatch, invokes no
|
||||
second daemon, and leaves the live daemon untouched.
|
||||
11. **Live protocol mismatch is not replaced:** a real/fake-Hello listener
|
||||
advertising an unsupported protocol produces the version-mismatch error,
|
||||
invokes no daemon spawner, and leaves the listener/path untouched.
|
||||
12. **Bounded startup failure:** substitute a daemon executable that exits
|
||||
nonzero without binding. Managed GPU exits after five seconds (50 ms
|
||||
polls) and reports socket + child status. A concurrent-winner fixture
|
||||
proves an early losing-child exit does not abort while another process
|
||||
binds before the deadline.
|
||||
13. **Post-attach daemon exit is reaped:** let the spawned daemon establish a
|
||||
managed session, wait for the probe's `phase=ready`, then terminate the
|
||||
daemon while keeping the probe's stdin open. Poll the atomically replaced
|
||||
ready report until it records both disconnect and named reaper completion,
|
||||
close stdin, and assert the `phase=complete` report retains the wait
|
||||
outcome. The daemon never remains a zombie until frontend exit.
|
||||
14. **Root reflects the GPU outcome:** fake GPU success makes `pmacs --gpu`
|
||||
succeed; fake nonzero and spawn failure make it fail with the executable
|
||||
named.
|
||||
15. **GPU argv is strict without breaking probes:** bare invocation points to
|
||||
`pmacs --gpu`; direct `--attach PATH`, existing
|
||||
`--headless-probe SOCKET REPORT`, and hidden
|
||||
`--headless-managed-probe SOCKET REPORT DAEMON_EXE` accept exactly their
|
||||
operands. Missing/trailing/unknown/incomplete args exit 2. `--version`
|
||||
prints package and protocol versions without initializing winit/wgpu.
|
||||
16. **Existing direct and Vterm paths remain intact:** rebuilt
|
||||
`pmacs-gpu --attach RAW_PATH` still renders an existing CRDT daemon, and
|
||||
`tests/vterm_stage3_acceptance.rs` still invokes its unchanged
|
||||
`--headless-probe` command and passes the real daemon + PTY + wgpu
|
||||
criterion.
|
||||
17. **One-command visible smoke:** on a Vulkan/display-capable machine, build
|
||||
the workspace once, run only `target/release/pmacs --gpu`, observe the GPU
|
||||
scratch buffer attach at protocol v19, close the window, then invoke the
|
||||
same command again and observe reuse of the still-running daemon.
|
||||
18. **Documentation commands are executable:** the README's unified release
|
||||
build succeeds; `cargo run --release -- --version` selects `pmacs` through
|
||||
`default-run`; no documented command requires users to spell the resolved
|
||||
socket pathname for managed GPU startup.
|
||||
Loading…
Reference in New Issue