Files
alkssh/src/serve.rs
T
glm-5.3-flash 58d0b86ec5 feat(observe): land the decline-path event vocabulary (w3-decline-tracing)
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.
2026-10-28 17:50:49 +00:00

97 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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;
}
}
}