feat(tty): implement TtyAdapter (ProtocolHandler) and three-pump session driver (ADR-052, 053, 055, 056)
Implement TtyAdapter (ProtocolHandler on alknet/tty) and the
drive_session three-pump bidirectional driver in adapter.rs.
TtyAdapter holds a HashMap<String, Arc<dyn TtyBackend>> keyed by the
negotiation frame's backend string and an optional OwnershipProvider.
handle() loops accept_bi and spawns drive_session per stream.
drive_session proceeds in three phases:
1. Negotiation — read length-prefixed JSON, parse NegotiateRequest,
validate (carriage == raw, cmd non-empty), look up backend, scope-gate
(tty:open), resource ownership check via backend.resource_id() +
OwnershipProvider::owns(), construct TtyParams. Errors sent as JSON
in negotiation framing, stream closed.
2. Allocation — backend.allocate(¶ms). On failure send
allocate_failed error, close.
3. Raw carriage — three concurrent pumps:
A. stdout/stderr → client (stream_type 1/2), zero-length sentinel on EOF
B. client → backend: stdin chunks → TtyHandle.stdin, control chunks →
Resize/Signal/Eof dispatch (Exit ignored, unknown types ignored)
C. exit → exit chunk: await exit_code; on resolve enqueue exit chunk
Exit-chunk-is-last invariant (ADR-055): adapter waits for BOTH
stdout/stderr pumps to complete AND exit_code to resolve before
enqueueing the exit chunk (tokio::join!). On TtyError → code -1.
Cancel cleanup (ADR-056): on connection drop/stream reset, pump tasks
drop, TtyHandle drops, exit_code future drops without resolve →
backend kill-on-Drop fires. Client write-half close is NOT a cancel —
session runs to completion.
Access control (ADR-050): scope gate at negotiation (tty:open scope,
configurable constant), resource ownership via backend.resource_id() +
OwnershipProvider::owns() when wired.
18 integration tests with a TestBackend fixture over tokio::io::duplex:
happy path, exit-chunk-is-last, stdin EOF, resize/signal control,
unknown control ignored, exit control ignored, unknown_backend,
malformed_negotiation (bad JSON, carriage != raw, empty cmd),
allocate_failed, exit error (-1), cancel cleanup, scope gate forbidden,
ownership check deny/allow, stderr pump, eof control closes stdin.
Also fix two clippy unused_mut warnings in negotiation.rs tests.
This commit is contained in:
1 parent
42643be338
commit
d5612f1a0e
2 files changed
+1391
-3
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -440,7 +440,7 @@ mod tests {
|
||||
let read = reader.read_frame().await.expect("read");
|
||||
assert_eq!(read.as_ref(), body);
|
||||
|
||||
let mut reclaimed = reader.into_inner();
|
||||
let reclaimed = reader.into_inner();
|
||||
let mut leftover = [0u8; 4];
|
||||
a.write_all(b"tail").await.expect("write leftover");
|
||||
a.flush().await.expect("flush");
|
||||
@@ -456,7 +456,7 @@ mod tests {
|
||||
let (mut a, mut b) = duplex(8 * 1024);
|
||||
let mut writer = NegotiationWriter::new(&mut a);
|
||||
writer.write_frame(b"x").await.expect("write");
|
||||
let mut reclaimed = writer.into_inner();
|
||||
let reclaimed = writer.into_inner();
|
||||
reclaimed.write_all(b"raw").await.expect("write raw");
|
||||
reclaimed.flush().await.expect("flush");
|
||||
|
||||
|
||||
Reference in new issue
Block a user