refactor(tty): split control channel into STREAM_CTRL_IN/STREAM_CTRL_OUT halves (Phase 7)
The single STREAM_CONTROL = 3 was documented as bidirectional but the
adapter had to ignore Exit from the client because the two directions
were indistinguishable on the same stream_type — half-duplex in
disguise. Phase 7 splits it into two halves so the bidirectionality is
literal on the wire.
Changes:
- wire.rs: STREAM_CTRL_IN = 3 (client→server), STREAM_CTRL_OUT = 4
(server→client); InvalidStreamType bound > 3 → > 4; Chunk::control
→ Chunk::ctrl_in/ctrl_out; ChunkWriter::write_control_json →
write_ctrl_in_json/write_ctrl_out_json; tests split accordingly.
- control.rs: ControlMessage doc updated with the stream_type column;
JSON shape unchanged.
- adapter.rs: pump_client_to_backend dispatches on STREAM_CTRL_IN
(Resize/Signal/Eof; Exit on ctrl_in is a protocol violation,
ignored); send_exit_chunk emits on STREAM_CTRL_OUT; STREAM_CTRL_OUT
from the client is a protocol violation, ignored. 3 new tests for
the direction enforcement; existing tests updated to the new
stream_types.
- negotiation.rs: framing-disambiguation doc updated (server-sent
stream_type set is {1, 2, 4}).
- alknet-tty-local/tests: common/mod.rs, pty.rs, pipe.rs updated to
the new constants.
Specs:
- ADR-052 amended (§4a 'Control channel split (Phase 7 amendment)').
- tty-wire.md + tty-adapter.md updated (last_updated 2026-07-18).
Verification:
- cargo test -p alknet-tty: 65 passed (was 61; +4 new tests).
- cargo test -p alknet-tty-local: 19 passed.
- cargo test --workspace --all-features: 1017 passed, 0 failed.
- cargo clippy --workspace --all-features: clean.
- cargo fmt --all: clean.
This commit is contained in:
1 parent
859ad35896
commit
c2b7055a64
11 files changed
+571
-142
No files matched your search
@@ -21,7 +21,9 @@ use std::sync::Arc;
|
||||
use alknet_core::auth::Identity;
|
||||
use alknet_tty::adapter::drive_session;
|
||||
use alknet_tty::backend::TtyBackend;
|
||||
use alknet_tty::wire::{ChunkReader, STREAM_CONTROL, STREAM_STDERR, STREAM_STDOUT};
|
||||
use alknet_tty::wire::{
|
||||
ChunkReader, STREAM_CTRL_IN, STREAM_CTRL_OUT, STREAM_STDERR, STREAM_STDOUT,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use tokio::io::duplex;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
@@ -70,10 +72,11 @@ impl ClientSide {
|
||||
self.write.flush().await.unwrap();
|
||||
}
|
||||
|
||||
/// Write a control chunk (stream_type 3) carrying a serialized
|
||||
/// `ControlMessage` JSON payload.
|
||||
/// Write a client→server control chunk (`STREAM_CTRL_IN`, stream_type
|
||||
/// 3) carrying a serialized `ControlMessage` JSON payload (`Resize`,
|
||||
/// `Signal`, or `Eof`).
|
||||
pub async fn write_control(&mut self, json: &[u8]) {
|
||||
self.write_chunk(STREAM_CONTROL, json).await;
|
||||
self.write_chunk(STREAM_CTRL_IN, json).await;
|
||||
}
|
||||
|
||||
/// Read one raw chunk from the server. Returns the `stream_type`
|
||||
@@ -145,7 +148,7 @@ impl ClientSide {
|
||||
stderr.extend_from_slice(&bytes);
|
||||
}
|
||||
}
|
||||
STREAM_CONTROL => {
|
||||
STREAM_CTRL_OUT => {
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
if v["type"] == "exit" {
|
||||
return Some((stdout, stderr, v["code"].as_i64().unwrap() as i32));
|
||||
@@ -177,7 +180,7 @@ impl ClientSide {
|
||||
stderr.extend_from_slice(&bytes);
|
||||
}
|
||||
}
|
||||
STREAM_CONTROL => {
|
||||
STREAM_CTRL_OUT => {
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
if v["type"] == "exit" {
|
||||
return Some((stdout, stderr, v["code"].as_i64().unwrap() as i32));
|
||||
|
||||
@@ -199,7 +199,7 @@ async fn pipe_echo_emits_stdout_chunk_then_sentinel() {
|
||||
if st == STREAM_STDOUT && !bytes.is_empty() {
|
||||
saw_nonempty_stdout = true;
|
||||
}
|
||||
if st == alknet_tty::wire::STREAM_CONTROL {
|
||||
if st == alknet_tty::wire::STREAM_CTRL_OUT {
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
if v["type"] == "exit" {
|
||||
break;
|
||||
|
||||
@@ -17,7 +17,7 @@ mod common;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use alknet_tty::wire::{STREAM_CONTROL, STREAM_STDIN};
|
||||
use alknet_tty::wire::{STREAM_CTRL_OUT, STREAM_STDIN};
|
||||
use alknet_tty_local::LocalTtyBackend;
|
||||
use common::{negotiate_pty_json, spawn_session};
|
||||
|
||||
@@ -245,7 +245,7 @@ async fn pty_exit_chunk_is_last() {
|
||||
if saw_exit {
|
||||
panic!("chunk arrived after exit: stream_type={st}, bytes={bytes:?} (ADR-055)");
|
||||
}
|
||||
if st == STREAM_CONTROL {
|
||||
if st == STREAM_CTRL_OUT {
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
|
||||
if v["type"] == "exit" {
|
||||
assert_eq!(v["code"], 0);
|
||||
|
||||
Reference in new issue
Block a user