feat(serve): land the spawn-and-return plumbing + exit/status mechanics (w2-spawn-return)
- src/serve.rs (new): the ADR-006 decision-4 mechanics in code — spawn_serving_path (the one choke point; nothing awaited inline) and report_exit_status_then_close (exit-status before close via the cloned Session::handle()), #[doc(hidden)] contract-test re-exports; a unit row pins spawn-returns-while-pending. - src/handler.rs: the four session-row seam stubs became spawned serving paths (failure-then-close unchanged; stubs are plain fns — an async stub holding &Session across the spawn is non-Send); open- level declines and the tunnel seam stub stay inline by class, per the new module-doc section. - tests/spawn_return.rs (new): two end-to-end wire rows — the client observes exit-status(7) strictly before close and after the handler returned; the handler-returns-fast row routes follow-up traffic while the serving path is still gated (POC-3 deadlock inversion). - tasks/implementation/w2-spawn-return.md — completed with notes. Verification (all green): cargo test; per-feature dispatch matrices (--features git/session/tunnel/sftp, --all-features); cargo clippy --all-targets/-all-features with -D warnings; cargo fmt --check; cargo doc --no-deps (no warnings); taskgraph validate.
This commit is contained in:
1 parent
68d3723f6a
commit
2e5a7c9cee
5 files changed
+640
-36
No files matched your search
+68
-28
@@ -69,7 +69,30 @@
|
||||
//! the store-and-take handoff (`channels.remove(&id)` → `into_stream`
|
||||
//! in the spawned serving task) is wave-3 mapping material; the
|
||||
//! routing decisions around it are already this wave's, because the
|
||||
//! routing stubs decline at the seam.
|
||||
//! routing stubs spawn their decline at the seam.
|
||||
//!
|
||||
//! ## The spawn-and-return structural constraint (ADR-006 decision 4)
|
||||
//!
|
||||
//! russh at this pin runs handler callbacks **inline inside the
|
||||
//! session's packet loop** — a blocking handler suspends all packet
|
||||
//! routing and deadlocks against the peer's follow-up bytes (POC-3
|
||||
//! proved by construction with receive-pack). Every serving path is
|
||||
//! therefore spawned through [`crate::serve::spawn_serving_path`] and
|
||||
//! the handler returns fast: the session-row seam stubs clone
|
||||
//! `session.handle()`, spawn their decline through the clone
|
||||
//! ([`crate::serve`]), and are gone from the hook's stack — no hook
|
||||
//! awaits a serving path inline. The decline keeps its wire shape
|
||||
//! (failure then close), riding the spawn. Exit/status reporting from
|
||||
//! spawned tasks rides the same cloned handle
|
||||
//! ([`crate::serve::report_exit_status_then_close`] — the mechanics;
|
||||
//! the derivation rules are wave-3 `mapping.md` material), pinned
|
||||
//! end-to-end by the mechanics rows in `tests/spawn_return.rs`.
|
||||
//!
|
||||
//! The open-level declines (unfeatured/never-feature rows, the tunnel
|
||||
//! seam stub) stay inline by class: they answer the open *before* any
|
||||
//! channel is owned — nothing is served yet, so nothing spawns. Wave
|
||||
//! 3's accept-only-after-fabric-open flow is where the tunnel row
|
||||
//! gains its spawned serving path, per the same constraint.
|
||||
//!
|
||||
//! Request-level declines: `subsystem_request` declines every name —
|
||||
//! `sftp` is the OQ-13 placeholder (the feature exists, the route
|
||||
@@ -101,7 +124,9 @@
|
||||
//! usable (the POC-1 §5 session-survives-declines posture); a
|
||||
//! *serving-path* decline (the wave-3 seam stubs) answers failure
|
||||
//! and closes the channel — the vanilla-clean shape for a row that
|
||||
//! will spawn its serving path next wave.
|
||||
//! owns the channel. The serving-path decline now rides the spawned
|
||||
//! serving task (ADR-006 decision 4, §The spawn-and-return
|
||||
//! structural constraint above); the wire shape is unchanged.
|
||||
//!
|
||||
//! ## Test surface
|
||||
//!
|
||||
@@ -118,13 +143,16 @@ use std::sync::Arc;
|
||||
|
||||
use alkcall::core::auth::Identity;
|
||||
use russh::keys::Algorithm;
|
||||
use russh::server::{Auth, ChannelOpenHandle, Msg, PublicKeyOffer, Session, VerifiedPublicKey};
|
||||
use russh::server::{
|
||||
Auth, ChannelOpenHandle, Handle, Msg, PublicKeyOffer, Session, VerifiedPublicKey,
|
||||
};
|
||||
use russh::{Channel, ChannelId, Pty};
|
||||
|
||||
use crate::config::ServeConfig;
|
||||
use crate::dispatch;
|
||||
use crate::error::AdapterError;
|
||||
use crate::fingerprint::fingerprint;
|
||||
use crate::serve;
|
||||
|
||||
/// The per-connection handler (one per mounted stream): russh constructs
|
||||
/// nothing here — [`crate::SshAdapter::handle`] builds one per accepted
|
||||
@@ -403,14 +431,14 @@ impl russh::server::Handler for SessionHandler {
|
||||
match dispatch::classify_exec(command) {
|
||||
dispatch::ExecRoute::Git => {
|
||||
if self.serves_git_row() {
|
||||
git_row_stub(channel, session).await;
|
||||
git_row_stub(channel, session);
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
}
|
||||
dispatch::ExecRoute::Pipe => {
|
||||
if self.serves_session_rows() {
|
||||
pipe_row_stub(channel, session).await;
|
||||
pipe_row_stub(channel, session);
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
@@ -433,7 +461,7 @@ impl russh::server::Handler for SessionHandler {
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.serves_session_rows() {
|
||||
pty_row_stub(channel, session).await;
|
||||
pty_row_stub(channel, session);
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
@@ -450,7 +478,7 @@ impl russh::server::Handler for SessionHandler {
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.serves_session_rows() {
|
||||
shell_row_stub(channel, session).await;
|
||||
shell_row_stub(channel, session);
|
||||
} else {
|
||||
decline_unfeatured_request(channel, session).await;
|
||||
}
|
||||
@@ -571,45 +599,57 @@ async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
|
||||
}
|
||||
|
||||
/// The family-seam decline shape for a **routed-but-not-yet-served**
|
||||
/// row: fail the request and close the channel — the vanilla-clean
|
||||
/// shape for a serving path that would have owned the channel (wave 3
|
||||
/// replaces these stubs with the spawned serving paths; a declined
|
||||
/// seam never leaks a half-served channel, since the decline IS the
|
||||
/// row this wave). Decline-first-alloc-never holds structurally:
|
||||
/// nothing is allocated before the failure lands.
|
||||
async fn seam_stub_decline(channel: ChannelId, session: &mut Session) {
|
||||
let _ = session.channel_failure(channel);
|
||||
let _ = session.close(channel);
|
||||
/// row — now the spawned serving task's body: fail the request and
|
||||
/// close the channel, through the cloned handle the task was handed.
|
||||
/// The vanilla-clean shape for a serving path that owns the channel
|
||||
/// (wave 3 replaces these stubs with the real serving paths; a
|
||||
/// declined seam never leaks a half-served channel, since the decline
|
||||
/// IS the row this wave). Decline-first-alloc-never holds structurally:
|
||||
/// nothing is allocated before the failure lands — the spawn's handle
|
||||
/// clone is the only resource ever taken.
|
||||
async fn seam_stub_decline(handle: Handle, channel: ChannelId) {
|
||||
let _ = handle.channel_failure(channel).await;
|
||||
let _ = handle.close(channel).await;
|
||||
}
|
||||
|
||||
/// Spawns the seam-stub decline as the row's serving path (ADR-006
|
||||
/// decision 4): the handle is cloned here, the decline runs on its own
|
||||
/// task, and the hook returns — no serving path awaits inline.
|
||||
fn spawn_seam_stub(channel: ChannelId, session: &Session) {
|
||||
let handle = session.handle();
|
||||
serve::spawn_serving_path(seam_stub_decline(handle, channel));
|
||||
}
|
||||
|
||||
/// The git row's seam stub — wave 3 lands the alkgit seam here
|
||||
/// (`parse command → Service + repo → serve_duplex_resolved`), spawned
|
||||
/// per ADR-006 decision 4; the skeleton declines at the seam.
|
||||
async fn git_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
/// per ADR-006 decision 4; the skeleton declines at the seam, on its
|
||||
/// own task.
|
||||
fn git_row_stub(channel: ChannelId, session: &Session) {
|
||||
spawn_seam_stub(channel, session);
|
||||
}
|
||||
|
||||
/// The session pipe row's seam stub — wave 3 lands the alktty pipe
|
||||
/// seam here (`wrapper ++ [cmd]`, the OQ-08 argv append), spawned per
|
||||
/// ADR-006 decision 4; the skeleton declines at the seam.
|
||||
async fn pipe_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
/// ADR-006 decision 4; the skeleton declines at the seam, on its own
|
||||
/// task.
|
||||
fn pipe_row_stub(channel: ChannelId, session: &Session) {
|
||||
spawn_seam_stub(channel, session);
|
||||
}
|
||||
|
||||
/// The session pty row's seam stub — wave 3 lands the alktty
|
||||
/// `NegotiateRequest.tty` construction here
|
||||
/// (`drive_session_pre_negotiated`), spawned per ADR-006 decision 4;
|
||||
/// the skeleton declines at the seam.
|
||||
async fn pty_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
/// the skeleton declines at the seam, on its own task.
|
||||
fn pty_row_stub(channel: ChannelId, session: &Session) {
|
||||
spawn_seam_stub(channel, session);
|
||||
}
|
||||
|
||||
/// The session shell row's seam stub — wave 3 lands the alktty
|
||||
/// `NegotiateRequest.cmd` = serve-config shell argv here (OQ-08, no
|
||||
/// crate default), spawned per ADR-006 decision 4; the skeleton
|
||||
/// declines at the seam.
|
||||
async fn shell_row_stub(channel: ChannelId, session: &mut Session) {
|
||||
seam_stub_decline(channel, session).await;
|
||||
/// declines at the seam, on its own task.
|
||||
fn shell_row_stub(channel: ChannelId, session: &Session) {
|
||||
spawn_seam_stub(channel, session);
|
||||
}
|
||||
|
||||
/// The tunnel row's seam stub: the explicit `ConnectFailed` reject
|
||||
|
||||
Reference in new issue
Block a user