Skip to main content

driver_debug/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Helper library for productionized driver debugging.
6
7pub use anyhow;
8pub use argh::{self, FromArgs, SubCommands};
9pub use fidl;
10pub use fidl_fuchsia_driver_debug;
11pub use zx;
12
13use core::future::Future;
14use fidl_fuchsia_driver_debug as fdebug;
15use futures::TryStreamExt as _;
16
17/// Result of executing a driver debug command.
18#[derive(Debug, PartialEq, Clone)]
19pub struct ExecutionResult {
20    pub stdout: String,
21    pub stderr: String,
22    pub exit_code: i32,
23}
24
25/// Driver debug commands.
26#[derive(FromArgs)]
27struct TopLevel<C: SubCommands> {
28    #[argh(subcommand)]
29    cmd: C,
30}
31
32/// Returns metadata for all supported subcommands of `C`.
33pub fn command_info<C: SubCommands>() -> Vec<fdebug::CommandInfo> {
34    C::COMMANDS
35        .iter()
36        .copied()
37        .chain(C::dynamic_commands().iter().copied())
38        .map(|info| fdebug::CommandInfo {
39            name: Some(info.name.to_string()),
40            description: Some(info.description.to_string()),
41            ..Default::default()
42        })
43        .collect()
44}
45
46/// Parses a slice of argument strings into the subcommand type `C`.
47///
48/// On `--help` or syntax error, returns an `Err(ExecutionResult)` ready to be sent back to the
49/// caller.
50pub fn parse_args<C: SubCommands>(args: &[impl AsRef<str>]) -> Result<C, ExecutionResult> {
51    let args_strs: Vec<&str> = args.iter().map(|s| s.as_ref()).collect();
52    match TopLevel::<C>::from_args(&["debug"], &args_strs) {
53        Ok(TopLevel { cmd }) => Ok(cmd),
54        Err(early_exit) => {
55            if early_exit.status.is_ok() {
56                Err(ExecutionResult {
57                    stdout: early_exit.output,
58                    stderr: String::new(),
59                    exit_code: 0,
60                })
61            } else {
62                Err(ExecutionResult {
63                    stdout: String::new(),
64                    stderr: early_exit.output,
65                    exit_code: 2,
66                })
67            }
68        }
69    }
70}
71
72/// Parses arguments into `C` and executes `handler` on the parsed subcommand, returning an
73/// [`ExecutionResult`].
74pub async fn dispatch<C, F, Fut>(args: &[impl AsRef<str>], handler: F) -> ExecutionResult
75where
76    C: SubCommands,
77    F: FnOnce(C) -> Fut,
78    Fut: Future<Output = Result<String, anyhow::Error>>,
79{
80    match parse_args::<C>(args) {
81        Ok(cmd) => match handler(cmd).await {
82            Ok(stdout) => ExecutionResult { stdout, stderr: String::new(), exit_code: 0 },
83            Err(err) => ExecutionResult {
84                stdout: String::new(),
85                stderr: format!("{err:#}\n"),
86                exit_code: 1,
87            },
88        },
89        Err(res) => res,
90    }
91}
92
93/// Responder for a single `fuchsia.driver.debug.Debug/Execute` request.
94pub struct CommandResponder {
95    stdout: zx::Socket,
96    stderr: zx::Socket,
97    responder: fdebug::DebugExecuteResponder,
98}
99
100impl CommandResponder {
101    /// Writes the command result (`Ok(stdout)` or `Err(error)`) to the appropriate socket and
102    /// sends the exit code over the FIDL responder.
103    pub fn send(
104        self,
105        result: Result<impl AsRef<str>, impl std::fmt::Display>,
106    ) -> Result<(), fidl::Error> {
107        let exec_result = match result {
108            Ok(stdout) => ExecutionResult {
109                stdout: stdout.as_ref().to_string(),
110                stderr: String::new(),
111                exit_code: 0,
112            },
113            Err(err) => ExecutionResult {
114                stdout: String::new(),
115                stderr: format!("{err:#}\n"),
116                exit_code: 1,
117            },
118        };
119        self.send_execution_result(exec_result)
120    }
121
122    /// Writes an [`ExecutionResult`] to the stdout/stderr sockets and sends the exit code over the
123    /// FIDL responder.
124    pub fn send_execution_result(self, result: ExecutionResult) -> Result<(), fidl::Error> {
125        let Self { stdout, stderr, responder } = self;
126        if !result.stdout.is_empty() {
127            let _ = stdout.write(result.stdout.as_bytes());
128        }
129        drop(stdout);
130        if !result.stderr.is_empty() {
131            let _ = stderr.write(result.stderr.as_bytes());
132        }
133        drop(stderr);
134        responder.send(Ok(result.exit_code))
135    }
136}
137
138/// Reads the next parsed subcommand from a `DebugRequestStream`.
139///
140/// Automatically responds to `ListCommands` requests using `C`'s subcommand metadata, as well as
141/// `--help` and syntax errors on `Execute` requests. Returns `Ok(None)` when the stream closes.
142pub async fn next_command<C: SubCommands>(
143    stream: &mut fdebug::DebugRequestStream,
144) -> Result<Option<(C, CommandResponder)>, fidl::Error> {
145    while let Some(request) = stream.try_next().await? {
146        match request {
147            fdebug::DebugRequest::ListCommands { responder } => {
148                let commands = command_info::<C>();
149                responder.send(Ok(&commands))?;
150            }
151            fdebug::DebugRequest::Execute { args, stdout, stderr, responder } => {
152                let cmd_responder = CommandResponder { stdout, stderr, responder };
153                match parse_args::<C>(&args) {
154                    Ok(cmd) => return Ok(Some((cmd, cmd_responder))),
155                    Err(result) => {
156                        cmd_responder.send_execution_result(result)?;
157                    }
158                }
159            }
160            fdebug::DebugRequest::_UnknownMethod { .. } => {}
161        }
162    }
163    Ok(None)
164}
165
166/// Serves a `DebugRequestStream` by parsing commands into `C` and invoking `handler` for each
167/// command.
168pub async fn serve<C, F, Fut>(
169    mut stream: fdebug::DebugRequestStream,
170    mut handler: F,
171) -> Result<(), fidl::Error>
172where
173    C: SubCommands,
174    F: FnMut(C) -> Fut,
175    Fut: Future<Output = Result<String, anyhow::Error>>,
176{
177    while let Some((cmd, responder)) = next_command::<C>(&mut stream).await? {
178        let result = handler(cmd).await;
179        responder.send(result)?;
180    }
181    Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use fidl::endpoints::create_proxy_and_stream;
188    use fuchsia_async as fasync;
189
190    #[derive(FromArgs, Debug, PartialEq)]
191    #[argh(subcommand, name = "ping")]
192    /// Ping the driver.
193    pub struct PingArgs {
194        #[argh(option, short = 'c', default = "1", description = "ping count")]
195        pub count: u32,
196    }
197
198    #[derive(FromArgs, Debug, PartialEq)]
199    #[argh(subcommand, name = "reset")]
200    /// Reset the driver.
201    pub struct ResetArgs {
202        #[argh(switch, description = "perform a hard reset")]
203        pub hard: bool,
204    }
205
206    #[derive(FromArgs, Debug, PartialEq)]
207    #[argh(subcommand, name = "fail")]
208    /// Trigger a failure.
209    pub struct FailArgs {}
210
211    #[derive(FromArgs, Debug, PartialEq)]
212    #[argh(subcommand)]
213    pub enum TestCommands {
214        Ping(PingArgs),
215        Reset(ResetArgs),
216        Fail(FailArgs),
217    }
218
219    async fn handle_cmd(cmd: TestCommands) -> Result<String, anyhow::Error> {
220        match cmd {
221            TestCommands::Ping(args) => {
222                let mut out = String::new();
223                for i in 1..=args.count {
224                    out.push_str(&format!("ping {i}\n"));
225                }
226                Ok(out)
227            }
228            TestCommands::Reset(args) => {
229                if args.hard {
230                    Ok("hard reset\n".to_string())
231                } else {
232                    Ok("soft reset\n".to_string())
233                }
234            }
235            TestCommands::Fail(_) => Err(anyhow::anyhow!("device failure")),
236        }
237    }
238
239    #[test]
240    fn test_command_info() {
241        let commands = command_info::<TestCommands>();
242        assert_eq!(commands.len(), 3);
243        assert_eq!(commands[0].name.as_deref(), Some("ping"));
244        assert_eq!(commands[0].description.as_deref(), Some("Ping the driver."));
245        assert_eq!(commands[1].name.as_deref(), Some("reset"));
246        assert_eq!(commands[1].description.as_deref(), Some("Reset the driver."));
247        assert_eq!(commands[2].name.as_deref(), Some("fail"));
248        assert_eq!(commands[2].description.as_deref(), Some("Trigger a failure."));
249    }
250
251    #[fasync::run_singlethreaded(test)]
252    async fn test_dispatch_success() {
253        let res = dispatch(&["ping", "-c", "2"], handle_cmd).await;
254        assert_eq!(res.exit_code, 0);
255        assert_eq!(res.stdout, "ping 1\nping 2\n");
256        assert_eq!(res.stderr, "");
257
258        let res = dispatch(&["reset", "--hard"], handle_cmd).await;
259        assert_eq!(res.exit_code, 0);
260        assert_eq!(res.stdout, "hard reset\n");
261        assert_eq!(res.stderr, "");
262    }
263
264    #[fasync::run_singlethreaded(test)]
265    async fn test_dispatch_help() {
266        let res = dispatch(&["--help"], handle_cmd).await;
267        assert_eq!(res.exit_code, 0);
268        assert!(res.stdout.contains("Usage:"));
269        assert_eq!(res.stderr, "");
270
271        let res = dispatch(&["ping", "--help"], handle_cmd).await;
272        assert_eq!(res.exit_code, 0);
273        assert!(res.stdout.contains("Usage:"));
274        assert_eq!(res.stderr, "");
275    }
276
277    #[fasync::run_singlethreaded(test)]
278    async fn test_dispatch_syntax_error() {
279        let res = dispatch(&["unknown-cmd"], handle_cmd).await;
280        assert_eq!(res.exit_code, 2);
281        assert_eq!(res.stdout, "");
282        assert!(res.stderr.contains("Unrecognized") || !res.stderr.is_empty());
283
284        let res = dispatch(&["ping", "--invalid"], handle_cmd).await;
285        assert_eq!(res.exit_code, 2);
286        assert_eq!(res.stdout, "");
287        assert!(res.stderr.contains("Unrecognized") || !res.stderr.is_empty());
288    }
289
290    #[fasync::run_singlethreaded(test)]
291    async fn test_dispatch_error() {
292        let res = dispatch(&["fail"], handle_cmd).await;
293        assert_eq!(res.exit_code, 1);
294        assert_eq!(res.stdout, "");
295        assert!(res.stderr.contains("device failure"));
296    }
297
298    #[fasync::run_singlethreaded(test)]
299    async fn test_next_command() {
300        use futures::io::AsyncReadExt;
301
302        let (proxy, mut stream) = create_proxy_and_stream::<fdebug::DebugMarker>();
303
304        let task = fasync::Task::spawn(async move {
305            while let Some((cmd, responder)) = next_command(&mut stream).await.unwrap() {
306                let res = handle_cmd(cmd).await;
307                responder.send(res).unwrap();
308            }
309        });
310
311        let commands = proxy.list_commands().await.unwrap().unwrap();
312        assert_eq!(commands.len(), 3);
313        assert_eq!(commands[0].name.as_deref(), Some("ping"));
314
315        let (stdout_local, stdout_remote) = zx::Socket::create_stream();
316        let (stderr_local, stderr_remote) = zx::Socket::create_stream();
317
318        let mut async_stdout = fasync::Socket::from_socket(stdout_local);
319        let mut async_stderr = fasync::Socket::from_socket(stderr_local);
320
321        let mut stdout_bytes = Vec::new();
322        let mut stderr_bytes = Vec::new();
323
324        let stdout_reader = async {
325            async_stdout.read_to_end(&mut stdout_bytes).await.unwrap();
326        };
327        let stderr_reader = async {
328            async_stderr.read_to_end(&mut stderr_bytes).await.unwrap();
329        };
330        let execute_fut = async {
331            proxy
332                .execute(
333                    &["ping".to_string(), "-c".to_string(), "3".to_string()],
334                    stdout_remote,
335                    stderr_remote,
336                )
337                .await
338                .unwrap()
339                .unwrap()
340        };
341
342        let (_, _, exit_code) = futures::join!(stdout_reader, stderr_reader, execute_fut);
343        assert_eq!(exit_code, 0);
344        assert_eq!(String::from_utf8(stdout_bytes).unwrap(), "ping 1\nping 2\nping 3\n");
345        assert_eq!(String::from_utf8(stderr_bytes).unwrap(), "");
346
347        drop(proxy);
348        task.await;
349    }
350
351    #[fasync::run_singlethreaded(test)]
352    async fn test_serve() {
353        use futures::io::AsyncReadExt;
354
355        let (proxy, stream) = create_proxy_and_stream::<fdebug::DebugMarker>();
356
357        let task = fasync::Task::spawn(async move {
358            serve(stream, handle_cmd).await.expect("serve");
359        });
360
361        let (stdout_local, stdout_remote) = zx::Socket::create_stream();
362        let (stderr_local, stderr_remote) = zx::Socket::create_stream();
363
364        let mut async_stdout = fasync::Socket::from_socket(stdout_local);
365        let mut async_stderr = fasync::Socket::from_socket(stderr_local);
366
367        let mut stdout_bytes = Vec::new();
368        let mut stderr_bytes = Vec::new();
369
370        let stdout_reader = async {
371            async_stdout.read_to_end(&mut stdout_bytes).await.unwrap();
372        };
373        let stderr_reader = async {
374            async_stderr.read_to_end(&mut stderr_bytes).await.unwrap();
375        };
376        let execute_fut = async {
377            proxy
378                .execute(&["reset".to_string(), "--hard".to_string()], stdout_remote, stderr_remote)
379                .await
380                .unwrap()
381                .unwrap()
382        };
383
384        let (_, _, exit_code) = futures::join!(stdout_reader, stderr_reader, execute_fut);
385        assert_eq!(exit_code, 0);
386        assert_eq!(String::from_utf8(stdout_bytes).unwrap(), "hard reset\n");
387        assert_eq!(String::from_utf8(stderr_bytes).unwrap(), "");
388
389        drop(proxy);
390        task.await;
391    }
392}