| // 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; |
| } |
| } |