blob: 3c55b3434f59bda1090b3bc1796fe373e6098a57 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Helper library for productionized driver debugging.
pub use anyhow;
pub use argh::{self, FromArgs, SubCommands};
pub use fidl;
pub use fidl_fuchsia_driver_debug;
pub use zx;
use core::future::Future;
use fidl_fuchsia_driver_debug as fdebug;
use futures::TryStreamExt as _;
/// Result of executing a driver debug command.
#[derive(Debug, PartialEq, Clone)]
pub struct ExecutionResult {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
/// Driver debug commands.
#[derive(FromArgs)]
struct TopLevel<C: SubCommands> {
#[argh(subcommand)]
cmd: C,
}
/// Returns metadata for all supported subcommands of `C`.
pub fn command_info<C: SubCommands>() -> Vec<fdebug::CommandInfo> {
C::COMMANDS
.iter()
.copied()
.chain(C::dynamic_commands().iter().copied())
.map(|info| fdebug::CommandInfo {
name: Some(info.name.to_string()),
description: Some(info.description.to_string()),
..Default::default()
})
.collect()
}
/// Parses a slice of argument strings into the subcommand type `C`.
///
/// On `--help` or syntax error, returns an `Err(ExecutionResult)` ready to be sent back to the
/// caller.
pub fn parse_args<C: SubCommands>(args: &[impl AsRef<str>]) -> Result<C, ExecutionResult> {
let args_strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
match TopLevel::<C>::from_args(&["debug"], &args_strs) {
Ok(TopLevel { cmd }) => Ok(cmd),
Err(early_exit) => {
if early_exit.status.is_ok() {
Err(ExecutionResult {
stdout: early_exit.output,
stderr: String::new(),
exit_code: 0,
})
} else {
Err(ExecutionResult {
stdout: String::new(),
stderr: early_exit.output,
exit_code: 2,
})
}
}
}
}
/// Parses arguments into `C` and executes `handler` on the parsed subcommand, returning an
/// [`ExecutionResult`].
pub async fn dispatch<C, F, Fut>(args: &[impl AsRef<str>], handler: F) -> ExecutionResult
where
C: SubCommands,
F: FnOnce(C) -> Fut,
Fut: Future<Output = Result<String, anyhow::Error>>,
{
match parse_args::<C>(args) {
Ok(cmd) => match handler(cmd).await {
Ok(stdout) => ExecutionResult { stdout, stderr: String::new(), exit_code: 0 },
Err(err) => ExecutionResult {
stdout: String::new(),
stderr: format!("{err:#}\n"),
exit_code: 1,
},
},
Err(res) => res,
}
}
/// Responder for a single `fuchsia.driver.debug.Debug/Execute` request.
pub struct CommandResponder {
stdout: zx::Socket,
stderr: zx::Socket,
responder: fdebug::DebugExecuteResponder,
}
impl CommandResponder {
/// Writes the command result (`Ok(stdout)` or `Err(error)`) to the appropriate socket and
/// sends the exit code over the FIDL responder.
pub fn send(
self,
result: Result<impl AsRef<str>, impl std::fmt::Display>,
) -> Result<(), fidl::Error> {
let exec_result = match result {
Ok(stdout) => ExecutionResult {
stdout: stdout.as_ref().to_string(),
stderr: String::new(),
exit_code: 0,
},
Err(err) => ExecutionResult {
stdout: String::new(),
stderr: format!("{err:#}\n"),
exit_code: 1,
},
};
self.send_execution_result(exec_result)
}
/// Writes an [`ExecutionResult`] to the stdout/stderr sockets and sends the exit code over the
/// FIDL responder.
pub fn send_execution_result(self, result: ExecutionResult) -> Result<(), fidl::Error> {
let Self { stdout, stderr, responder } = self;
if !result.stdout.is_empty() {
let _ = stdout.write(result.stdout.as_bytes());
}
drop(stdout);
if !result.stderr.is_empty() {
let _ = stderr.write(result.stderr.as_bytes());
}
drop(stderr);
responder.send(Ok(result.exit_code))
}
}
/// Reads the next parsed subcommand from a `DebugRequestStream`.
///
/// Automatically responds to `ListCommands` requests using `C`'s subcommand metadata, as well as
/// `--help` and syntax errors on `Execute` requests. Returns `Ok(None)` when the stream closes.
pub async fn next_command<C: SubCommands>(
stream: &mut fdebug::DebugRequestStream,
) -> Result<Option<(C, CommandResponder)>, fidl::Error> {
while let Some(request) = stream.try_next().await? {
match request {
fdebug::DebugRequest::ListCommands { responder } => {
let commands = command_info::<C>();
responder.send(Ok(&commands))?;
}
fdebug::DebugRequest::Execute { args, stdout, stderr, responder } => {
let cmd_responder = CommandResponder { stdout, stderr, responder };
match parse_args::<C>(&args) {
Ok(cmd) => return Ok(Some((cmd, cmd_responder))),
Err(result) => {
cmd_responder.send_execution_result(result)?;
}
}
}
fdebug::DebugRequest::_UnknownMethod { .. } => {}
}
}
Ok(None)
}
/// Serves a `DebugRequestStream` by parsing commands into `C` and invoking `handler` for each
/// command.
pub async fn serve<C, F, Fut>(
mut stream: fdebug::DebugRequestStream,
mut handler: F,
) -> Result<(), fidl::Error>
where
C: SubCommands,
F: FnMut(C) -> Fut,
Fut: Future<Output = Result<String, anyhow::Error>>,
{
while let Some((cmd, responder)) = next_command::<C>(&mut stream).await? {
let result = handler(cmd).await;
responder.send(result)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use fidl::endpoints::create_proxy_and_stream;
use fuchsia_async as fasync;
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "ping")]
/// Ping the driver.
pub struct PingArgs {
#[argh(option, short = 'c', default = "1", description = "ping count")]
pub count: u32,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "reset")]
/// Reset the driver.
pub struct ResetArgs {
#[argh(switch, description = "perform a hard reset")]
pub hard: bool,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "fail")]
/// Trigger a failure.
pub struct FailArgs {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum TestCommands {
Ping(PingArgs),
Reset(ResetArgs),
Fail(FailArgs),
}
async fn handle_cmd(cmd: TestCommands) -> Result<String, anyhow::Error> {
match cmd {
TestCommands::Ping(args) => {
let mut out = String::new();
for i in 1..=args.count {
out.push_str(&format!("ping {i}\n"));
}
Ok(out)
}
TestCommands::Reset(args) => {
if args.hard {
Ok("hard reset\n".to_string())
} else {
Ok("soft reset\n".to_string())
}
}
TestCommands::Fail(_) => Err(anyhow::anyhow!("device failure")),
}
}
#[test]
fn test_command_info() {
let commands = command_info::<TestCommands>();
assert_eq!(commands.len(), 3);
assert_eq!(commands[0].name.as_deref(), Some("ping"));
assert_eq!(commands[0].description.as_deref(), Some("Ping the driver."));
assert_eq!(commands[1].name.as_deref(), Some("reset"));
assert_eq!(commands[1].description.as_deref(), Some("Reset the driver."));
assert_eq!(commands[2].name.as_deref(), Some("fail"));
assert_eq!(commands[2].description.as_deref(), Some("Trigger a failure."));
}
#[fasync::run_singlethreaded(test)]
async fn test_dispatch_success() {
let res = dispatch(&["ping", "-c", "2"], handle_cmd).await;
assert_eq!(res.exit_code, 0);
assert_eq!(res.stdout, "ping 1\nping 2\n");
assert_eq!(res.stderr, "");
let res = dispatch(&["reset", "--hard"], handle_cmd).await;
assert_eq!(res.exit_code, 0);
assert_eq!(res.stdout, "hard reset\n");
assert_eq!(res.stderr, "");
}
#[fasync::run_singlethreaded(test)]
async fn test_dispatch_help() {
let res = dispatch(&["--help"], handle_cmd).await;
assert_eq!(res.exit_code, 0);
assert!(res.stdout.contains("Usage:"));
assert_eq!(res.stderr, "");
let res = dispatch(&["ping", "--help"], handle_cmd).await;
assert_eq!(res.exit_code, 0);
assert!(res.stdout.contains("Usage:"));
assert_eq!(res.stderr, "");
}
#[fasync::run_singlethreaded(test)]
async fn test_dispatch_syntax_error() {
let res = dispatch(&["unknown-cmd"], handle_cmd).await;
assert_eq!(res.exit_code, 2);
assert_eq!(res.stdout, "");
assert!(res.stderr.contains("Unrecognized") || !res.stderr.is_empty());
let res = dispatch(&["ping", "--invalid"], handle_cmd).await;
assert_eq!(res.exit_code, 2);
assert_eq!(res.stdout, "");
assert!(res.stderr.contains("Unrecognized") || !res.stderr.is_empty());
}
#[fasync::run_singlethreaded(test)]
async fn test_dispatch_error() {
let res = dispatch(&["fail"], handle_cmd).await;
assert_eq!(res.exit_code, 1);
assert_eq!(res.stdout, "");
assert!(res.stderr.contains("device failure"));
}
#[fasync::run_singlethreaded(test)]
async fn test_next_command() {
use futures::io::AsyncReadExt;
let (proxy, mut stream) = create_proxy_and_stream::<fdebug::DebugMarker>();
let task = fasync::Task::spawn(async move {
while let Some((cmd, responder)) = next_command(&mut stream).await.unwrap() {
let res = handle_cmd(cmd).await;
responder.send(res).unwrap();
}
});
let commands = proxy.list_commands().await.unwrap().unwrap();
assert_eq!(commands.len(), 3);
assert_eq!(commands[0].name.as_deref(), Some("ping"));
let (stdout_local, stdout_remote) = zx::Socket::create_stream();
let (stderr_local, stderr_remote) = zx::Socket::create_stream();
let mut async_stdout = fasync::Socket::from_socket(stdout_local);
let mut async_stderr = fasync::Socket::from_socket(stderr_local);
let mut stdout_bytes = Vec::new();
let mut stderr_bytes = Vec::new();
let stdout_reader = async {
async_stdout.read_to_end(&mut stdout_bytes).await.unwrap();
};
let stderr_reader = async {
async_stderr.read_to_end(&mut stderr_bytes).await.unwrap();
};
let execute_fut = async {
proxy
.execute(
&["ping".to_string(), "-c".to_string(), "3".to_string()],
stdout_remote,
stderr_remote,
)
.await
.unwrap()
.unwrap()
};
let (_, _, exit_code) = futures::join!(stdout_reader, stderr_reader, execute_fut);
assert_eq!(exit_code, 0);
assert_eq!(String::from_utf8(stdout_bytes).unwrap(), "ping 1\nping 2\nping 3\n");
assert_eq!(String::from_utf8(stderr_bytes).unwrap(), "");
drop(proxy);
task.await;
}
#[fasync::run_singlethreaded(test)]
async fn test_serve() {
use futures::io::AsyncReadExt;
let (proxy, stream) = create_proxy_and_stream::<fdebug::DebugMarker>();
let task = fasync::Task::spawn(async move {
serve(stream, handle_cmd).await.expect("serve");
});
let (stdout_local, stdout_remote) = zx::Socket::create_stream();
let (stderr_local, stderr_remote) = zx::Socket::create_stream();
let mut async_stdout = fasync::Socket::from_socket(stdout_local);
let mut async_stderr = fasync::Socket::from_socket(stderr_local);
let mut stdout_bytes = Vec::new();
let mut stderr_bytes = Vec::new();
let stdout_reader = async {
async_stdout.read_to_end(&mut stdout_bytes).await.unwrap();
};
let stderr_reader = async {
async_stderr.read_to_end(&mut stderr_bytes).await.unwrap();
};
let execute_fut = async {
proxy
.execute(&["reset".to_string(), "--hard".to_string()], stdout_remote, stderr_remote)
.await
.unwrap()
.unwrap()
};
let (_, _, exit_code) = futures::join!(stdout_reader, stderr_reader, execute_fut);
assert_eq!(exit_code, 0);
assert_eq!(String::from_utf8(stdout_bytes).unwrap(), "hard reset\n");
assert_eq!(String::from_utf8(stderr_bytes).unwrap(), "");
drop(proxy);
task.await;
}
}