1pub 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#[derive(Debug, PartialEq, Clone)]
19pub struct ExecutionResult {
20 pub stdout: String,
21 pub stderr: String,
22 pub exit_code: i32,
23}
24
25#[derive(FromArgs)]
27struct TopLevel<C: SubCommands> {
28 #[argh(subcommand)]
29 cmd: C,
30}
31
32pub 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
46pub 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
72pub 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
93pub struct CommandResponder {
95 stdout: zx::Socket,
96 stderr: zx::Socket,
97 responder: fdebug::DebugExecuteResponder,
98}
99
100impl CommandResponder {
101 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 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
138pub 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
166pub 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 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 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 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}