The wave-2 gate's booked finding: observability on the decline paths. `tracing` rides the manifest per the ADR-002 toolkit but nothing in src/ emitted — a decline's "reason" was only the routing decision documented per build. - src/observe.rs: the event vocabulary, adopting the family posture read from alktty/alktunnels/alkcall/alkhttp (free tracing fns, message-last lowercase, flat fields; no info!/trace!, no macros, no spans): feature_build + row + channel_id + decline_class (open/request/serving/auth) + reason + exec_route (exec rows); declines at debug, the serving-path failure arm the vocabulary's one warn - handler.rs: every decline site emits — auth dispositions (non-ed25519 / fingerprint-unresolved / the four unimplemented methods), open-level declines (unfeatured / never-feature / cap-exceeded / tunnel connect-failed stub), request-level capability declines (exec route-aware, subsystem, env, x11, agent, the -R listen forms), and the seam-stub serving declines with the spawned-path lifecycle (started / ended / failed — the shape the wave-3 mappings inherit) - tests/observe.rs: the event-presence pin — a hand-rolled global capture subscriber (the recorded test convention; no event-capture precedent exists in the family), field-matching rows across the default/git/session/tunnel/--all-features builds, and the safety scan (no username, no fingerprint ever in an event) - helper-level exact shapes pinned in src/observe.rs unit rows Verification: cargo test (default, --features git/session/tunnel, --all-features all green), clippy x2 --all-targets -D warnings, fmt --check, cargo doc --no-deps 0 warnings, taskgraph validate.
97 lines
4.1 KiB
Rust
97 lines
4.1 KiB
Rust
//! The spawn-and-return plumbing (`w2-spawn-return`) — ADR-006
|
||
//! decision 4, the structural constraint of the whole spine, in code.
|
||
//!
|
||
//! russh at this pin runs server handler callbacks **inline inside the
|
||
//! session's packet loop** — a blocking handler suspends all packet
|
||
//! routing for the connection and deadlocks against the peer's
|
||
//! immediate follow-up bytes (POC-3 proved by construction with
|
||
//! receive-pack). The crate's answer is mechanical, not conventional:
|
||
//!
|
||
//! - **Every serving path is spawned** ([`spawn_serving_path`]) — the
|
||
//! handler hook clones the session handle
|
||
//! (`Session::handle()`), hands it into its serving future, spawns,
|
||
//! and returns. No serving path is ever awaited inline; the dispatch
|
||
//! seam stubs ([`crate::handler`]) hang off this helper, so the
|
||
//! wave-3 mappings inherit the pattern mechanically from the stubs.
|
||
//! The serving-path *events* (started / ended / failure —
|
||
//! [`crate::observe`]) are the spawned task's own actions: the stub
|
||
//! body emits them, the mappings' real paths will too.
|
||
//! - **Exit/status reporting rides the cloned handle** — the spawned
|
||
//! task's only report surface is the handle clone it was handed;
|
||
//! [`report_exit_status_then_close`] preserves the wire ordering the
|
||
//! `mapping.md` `exit_status` row pins: `exit-status` sent **before**
|
||
//! close, from the spawned task.
|
||
//!
|
||
//! The *derivation contract's rules* (which exit code → which SSH exit
|
||
//! shape, `mapping.md` §Exit chunk → SSH exit reporting rules 1–6) are
|
||
//! wave-3 session-mapping material; this module is the mechanics only.
|
||
//!
|
||
//! ## Test surface
|
||
//!
|
||
//! Both helpers are re-exported `#[doc(hidden)]` from the crate root —
|
||
//! the `handler.rs` §Test surface pattern: the mechanics-proof rows in
|
||
//! `tests/spawn_return.rs` compose them over the real wire (a minimal
|
||
//! proof handler standing in for the crate's own auth/dispatch hooks,
|
||
//! which have their own contract suites) to pin the constraint as
|
||
//! tests, not convention.
|
||
|
||
use std::future::Future;
|
||
|
||
use russh::ChannelId;
|
||
use russh::server::Handle;
|
||
|
||
/// Spawns one serving path on its own task (ADR-006 decision 4): the
|
||
/// caller — the handler hook — clones the session handle, hands it
|
||
/// into the serving future, calls this, and returns immediately.
|
||
/// Nothing here is ever awaited inline: an inline serve deadlocks
|
||
/// against the peer's follow-up bytes (POC-3 proved by construction).
|
||
pub fn spawn_serving_path(serving: impl Future<Output = ()> + Send + 'static) {
|
||
tokio::spawn(serving);
|
||
}
|
||
|
||
/// The spawned task's exit report through the cloned handle:
|
||
/// `exit-status(N)` then close — the ordering contract
|
||
/// (`mapping.md`'s `exit_status` row: sent **before** close, from the
|
||
/// spawned task). A failed send means the session is gone; nothing is
|
||
/// reportable then.
|
||
pub async fn report_exit_status_then_close(handle: &Handle, channel: ChannelId, exit_code: u32) {
|
||
let _ = handle.exit_status_request(channel, exit_code).await;
|
||
let _ = handle.close(channel).await;
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
|
||
use super::spawn_serving_path;
|
||
|
||
/// - *Contract stamp:* ADR-006 decision 4 (spawn-and-return: the
|
||
/// handler call returns while the serving path is still running
|
||
/// — handler-returns-fast is structural).
|
||
#[tokio::test]
|
||
async fn spawn_returns_while_the_serving_path_is_still_pending() {
|
||
let (release, waited) = tokio::sync::oneshot::channel::<()>();
|
||
let done = Arc::new(AtomicBool::new(false));
|
||
let flag = Arc::clone(&done);
|
||
|
||
spawn_serving_path(async move {
|
||
let _ = waited.await;
|
||
flag.store(true, Ordering::Release);
|
||
});
|
||
|
||
assert!(
|
||
!done.load(Ordering::Acquire),
|
||
"the spawn call returned while the serving path was still gated — \
|
||
no serving path is awaited inline"
|
||
);
|
||
|
||
release
|
||
.send(())
|
||
.expect("the gate releases exactly once, to the still-running path");
|
||
while !done.load(Ordering::Acquire) {
|
||
tokio::task::yield_now().await;
|
||
}
|
||
}
|
||
}
|