asimov_cli/commands/
help.rs1use clientele::SubcommandsProvider;
4use clientele::SysexitsError::{self, *};
5use rayon::prelude::*;
6use std::process::Stdio;
7
8use crate::Result;
9
10pub struct CommandDescription {
11 pub name: String,
12 pub description: String,
13 pub usage: Option<String>,
14}
15
16pub struct Help;
18
19impl Help {
20 pub fn execute(&self) -> Vec<CommandDescription> {
21 let output = self.collect_output();
22
23 let mut result = vec![];
24 for (name, description) in output {
25 let lines = description.lines().collect::<Vec<_>>();
26 let usage = lines.iter().find(|line| line.starts_with("Usage:"));
27
28 let description = lines
30 .iter()
31 .map_while(|line| {
32 if !line.trim().is_empty() {
33 Some(line.to_string())
34 } else {
35 None
36 }
37 })
38 .collect::<Vec<_>>()
39 .join("\n");
40
41 result.push(CommandDescription {
42 name,
43 description,
44 usage: usage.map(|usage| usage.to_string()),
45 });
46 }
47
48 result
49 }
50
51 fn is_child_running(&self, child: &mut std::process::Child) -> Result<bool, std::io::Error> {
52 Ok(child.try_wait()?.is_none())
53 }
54
55 fn collect_output(&self) -> Vec<(String, String)> {
56 const MAX_WAIT_TIME: std::time::Duration = std::time::Duration::from_secs(1);
57
58 let provider = SubcommandsProvider::collect("asimov-", 1);
59 let commands = provider.get_commands();
60
61 let start_time = std::time::Instant::now();
62
63 commands
64 .par_iter()
65 .filter_map(|cmd| {
66 let Ok(mut child) = std::process::Command::new(&cmd.path)
67 .args(["--help"])
68 .stdin(Stdio::inherit())
69 .stdout(Stdio::piped())
70 .stderr(Stdio::piped())
71 .spawn()
72 else {
73 return None;
74 };
75
76 loop {
77 let Ok(is_running) = self.is_child_running(&mut child) else {
78 return None;
79 };
80
81 if !is_running {
82 break;
83 }
84
85 let now = std::time::Instant::now();
86 if now.duration_since(start_time) > MAX_WAIT_TIME {
87 child.kill();
88 drop(child);
89 return None;
90 }
91
92 rayon::yield_local();
93 }
94
95 let output = child.wait_with_output().unwrap();
96 if !output.status.success() {
97 return None;
98 }
99
100 let stdout = String::from_utf8_lossy(&output.stdout);
101 Some((cmd.name.clone(), stdout.to_string()))
102 })
103 .collect()
104 }
105}