feat(tty-local): implement LocalTtyBackend (TtyBackend) branching on terminal Some/None
Implements the wiring task: LocalTtyBackend::allocate() dispatches to pty::allocate_pty when TtyParams.terminal is Some (PTY mode, stderr None) and pipe::allocate_pipe when None (runner mode, stderr Some). Validates non-empty cmd, ignores backend_params (local backend has no backend- specific selector fields), and resource_id() returns None (local backend creates its own resource — process). Adds async-trait dependency.
This commit is contained in:
1 parent
ad9afab145
commit
0ff0ceeac3
3 files changed
+167
-1
No files matched your search
Generated
+1
@@ -152,6 +152,7 @@ name = "alknet-tty-local"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alknet-tty",
|
"alknet-tty",
|
||||||
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ name = "alknet_tty_local"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
alknet-tty = { path = "../alknet-tty" }
|
alknet-tty = { path = "../alknet-tty" }
|
||||||
|
async-trait = "0.1"
|
||||||
portable-pty = "0.9"
|
portable-pty = "0.9"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
//! (the runner case, this crate's `pipe` module). One backend serves both
|
//! (the runner case, this crate's `pipe` module). One backend serves both
|
||||||
//! (ADR-054).
|
//! (ADR-054).
|
||||||
|
|
||||||
// TODO: implement
|
use alknet_tty::backend::{TtyBackend, TtyError, TtyHandle, TtyParams};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
/// Local TTY backend: implements `alknet_tty::TtyBackend` for local processes.
|
/// Local TTY backend: implements `alknet_tty::TtyBackend` for local processes.
|
||||||
///
|
///
|
||||||
@@ -13,4 +14,167 @@
|
|||||||
/// selects PTY mode (this crate's `pty` module); `None` selects pipe mode
|
/// selects PTY mode (this crate's `pty` module); `None` selects pipe mode
|
||||||
/// (the runner case, this crate's `pipe` module). One backend serves both
|
/// (the runner case, this crate's `pipe` module). One backend serves both
|
||||||
/// (ADR-054).
|
/// (ADR-054).
|
||||||
|
///
|
||||||
|
/// Takes no constructor dependencies — unlike `DockerTtyBackend` (wraps a
|
||||||
|
/// `bollard::Docker` client) or `SshTtyBackend` (wraps an SSH session), the
|
||||||
|
/// `portable_pty` system is process-global. The assembly layer constructs one
|
||||||
|
/// `LocalTtyBackend` and registers it as `"local"`.
|
||||||
pub struct LocalTtyBackend;
|
pub struct LocalTtyBackend;
|
||||||
|
|
||||||
|
impl LocalTtyBackend {
|
||||||
|
/// Construct a new `LocalTtyBackend`. The backend is dependency-free;
|
||||||
|
/// `portable_pty`'s native system is process-global.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LocalTtyBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TtyBackend for LocalTtyBackend {
|
||||||
|
/// Allocate a terminal/process session, branching on
|
||||||
|
/// `params.terminal`.
|
||||||
|
///
|
||||||
|
/// `Some(TerminalParams)` dispatches to [`pty::allocate_pty`] (PTY mode,
|
||||||
|
/// real terminal semantics — resize, process-group signal forwarding,
|
||||||
|
/// merged stdout/stderr). `None` dispatches to [`pipe::allocate_pipe`]
|
||||||
|
/// (pipe mode, the runner case — separate stdout/stderr, no-op resize,
|
||||||
|
/// pid-only signal).
|
||||||
|
///
|
||||||
|
/// `backend_params` is ignored: the local backend has no
|
||||||
|
/// backend-specific selector fields. An empty `cmd` returns
|
||||||
|
/// [`TtyError::AllocFailed`] (the adapter already checks this at
|
||||||
|
/// negotiation, but the backend fails gracefully if called directly).
|
||||||
|
async fn allocate(&self, params: &TtyParams) -> Result<TtyHandle, TtyError> {
|
||||||
|
if params.cmd.is_empty() {
|
||||||
|
return Err(TtyError::AllocFailed {
|
||||||
|
message: "cmd must be non-empty".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match ¶ms.terminal {
|
||||||
|
Some(terminal) => crate::pty::allocate_pty(
|
||||||
|
terminal.clone(),
|
||||||
|
params.cmd.clone(),
|
||||||
|
params.cwd.clone(),
|
||||||
|
params.env.clone(),
|
||||||
|
),
|
||||||
|
None => crate::pipe::allocate_pipe(
|
||||||
|
params.cmd.clone(),
|
||||||
|
params.cwd.clone(),
|
||||||
|
params.env.clone(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local backend creates its own resource (a process), so there is
|
||||||
|
/// no pre-existing resource for the ownership check. Returns `None`.
|
||||||
|
fn resource_id(&self, _params: &TtyParams) -> Option<(&'static str, String)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn term() -> alknet_tty::backend::TerminalParams {
|
||||||
|
alknet_tty::backend::TerminalParams {
|
||||||
|
term: None,
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
pixel_width: 0,
|
||||||
|
pixel_height: 0,
|
||||||
|
modes: serde_json::Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn params(
|
||||||
|
terminal: Option<alknet_tty::backend::TerminalParams>,
|
||||||
|
cmd: Vec<String>,
|
||||||
|
) -> TtyParams {
|
||||||
|
TtyParams {
|
||||||
|
terminal,
|
||||||
|
cmd,
|
||||||
|
cwd: None,
|
||||||
|
env: HashMap::new(),
|
||||||
|
backend_params: serde_json::Map::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn pty_dispatch_yields_no_stderr() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let handle = backend
|
||||||
|
.allocate(¶ms(
|
||||||
|
Some(term()),
|
||||||
|
vec!["echo".to_string(), "hi".to_string()],
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.expect("allocate");
|
||||||
|
assert!(handle.stderr.is_none(), "PTY backends merge stdout/stderr");
|
||||||
|
assert!(handle.control.is_some(), "PTY backends provide control");
|
||||||
|
let _ = handle.exit_code.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pipe_dispatch_yields_some_stderr() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let handle = backend
|
||||||
|
.allocate(¶ms(None, vec!["echo".to_string(), "hi".to_string()]))
|
||||||
|
.await
|
||||||
|
.expect("allocate");
|
||||||
|
assert!(
|
||||||
|
handle.stderr.is_some(),
|
||||||
|
"pipe backends have separate stderr"
|
||||||
|
);
|
||||||
|
assert!(handle.control.is_some(), "pipe backends provide control");
|
||||||
|
let _ = handle.exit_code.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn empty_cmd_returns_alloc_failed() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let result = backend.allocate(¶ms(None, vec![])).await;
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(TtyError::AllocFailed { .. })),
|
||||||
|
"expected AllocFailed for empty cmd"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn empty_cmd_with_terminal_returns_alloc_failed() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let result = backend.allocate(¶ms(Some(term()), vec![])).await;
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(TtyError::AllocFailed { .. })),
|
||||||
|
"expected AllocFailed for empty cmd even with terminal set"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resource_id_returns_none() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let params = params(None, vec!["true".to_string()]);
|
||||||
|
assert!(backend.resource_id(¶ms).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resource_id_returns_none_with_terminal() {
|
||||||
|
let backend = LocalTtyBackend::new();
|
||||||
|
let params = params(Some(term()), vec!["true".to_string()]);
|
||||||
|
assert!(backend.resource_id(¶ms).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_constructs() {
|
||||||
|
let backend = LocalTtyBackend::default();
|
||||||
|
let params = params(None, vec!["true".to_string()]);
|
||||||
|
assert!(backend.resource_id(¶ms).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user