1#![forbid(unsafe_code)]
7
8mod cli;
11mod clients;
12mod commands;
13mod debug;
14mod env;
15mod error;
16mod input;
17mod json;
18mod output;
19mod pagination;
20mod render;
21mod signals;
22mod store;
23mod tenant;
24mod views;
25
26use clap::FromArgMatches;
27use commands::{Invocation, Plan};
28use error::{CliError, EXIT_OK, EXIT_OPERATIONAL, EXIT_USAGE};
29use output::Output;
30use std::ffi::OsString;
31use std::io::{ErrorKind, Write};
32
33pub const VERSION: &str = env!("CARGO_PKG_VERSION");
35
36pub fn run<I, T>(args: I) -> i32
38where
39 I: IntoIterator<Item = T>,
40 T: Into<OsString>,
41{
42 let args: Vec<OsString> = args.into_iter().map(Into::into).collect();
43 if version_requested(&args) {
44 return print_version();
45 }
46 let parsed = cli::command()
47 .try_get_matches_from(&args)
48 .and_then(|matches| cli::Cli::from_arg_matches(&matches));
49 let cli = match parsed {
50 Ok(cli) => cli,
51 Err(error) => {
52 let error = cli::redacted_parse_error(&args, &error).unwrap_or(error);
55 let _ = error.print();
56 return error.exit_code();
57 }
58 };
59 let mut output = Output::stdio(cli.global.quiet);
60 output.set_debug(cli.global.debug);
61 let plan = match commands::plan(&cli) {
62 Ok(plan) => plan,
63 Err(error) => {
64 output.report(&error);
65 return error.exit_code(None);
66 }
67 };
68 if let Plan::GroupHelp(path) = plan {
69 output.write_stderr(&cli::group_help(path));
70 return EXIT_USAGE;
71 }
72 let mut invocation = Invocation::new(output, cli.global.tenant.clone());
73 if cli.global.debug
75 && plan.debug_policy() == debug::DebugPolicy::Log
76 && let Err(error) = invocation.open_debug_log()
77 {
78 invocation.output.report(&error);
79 return error.exit_code(None);
80 }
81 execute(plan, invocation)
82}
83
84pub fn version_requested(args: &[OsString]) -> bool {
87 args.iter()
88 .skip(1)
89 .take_while(|arg| *arg != "--")
90 .any(|arg| arg == "-V" || arg == "--version")
91}
92
93fn stdout_write_failed() -> CliError {
96 CliError::operational("AISEC_SDK_ERROR:Output could not be written to stdout")
97}
98
99fn print_version() -> i32 {
100 let mut stdout = std::io::stdout().lock();
101 match writeln!(stdout, "{VERSION}").and_then(|()| stdout.flush()) {
102 Err(error) if error.kind() != ErrorKind::BrokenPipe => {
103 let text = stdout_write_failed().render_colored(render::style::stderr_color());
104 let _ = std::io::stderr().write_all(text.as_bytes());
105 EXIT_OPERATIONAL
106 }
107 _ => EXIT_OK,
108 }
109}
110
111fn execute(plan: Plan, mut invocation: Invocation) -> i32 {
112 let runtime = match tokio::runtime::Builder::new_current_thread()
113 .enable_all()
114 .build()
115 {
116 Ok(runtime) => runtime,
117 Err(_) => {
118 let error = CliError::operational("AISEC_SDK_ERROR:The async runtime could not start");
119 invocation.output.report(&error);
120 return EXIT_OPERATIONAL;
121 }
122 };
123 let signals = signals::install(invocation.session.clone());
124 signals.set_quiet(invocation.output.quiet());
125 invocation.output.attach_signals(signals.clone());
126 let fallback = plan.interrupt_context();
127 let result = runtime.block_on(commands::dispatch(&mut invocation, plan));
128 let fallback = if invocation.committed {
130 error::InterruptContext::ChangeSaved
131 } else {
132 fallback
133 };
134 runtime.shutdown_background();
137 let signal = signals.code();
138 let result = match (result, signal) {
140 (Err(CliError::Interrupted(context)), _) => Err(CliError::Interrupted(context)),
141 (_, Some(_)) => Err(CliError::Interrupted(fallback)),
142 (result, None) => result,
143 };
144 finish(&mut invocation.output, result, signal)
145}
146
147fn finish(output: &mut Output, result: Result<(), CliError>, signal: Option<i32>) -> i32 {
148 match result {
149 Ok(()) => match output.commit() {
150 Ok(()) => EXIT_OK,
151 Err(error) if error.kind() == ErrorKind::BrokenPipe => EXIT_OK,
153 Err(_) => {
154 let error = match output.pending_signal() {
156 Some(_) => CliError::Interrupted(error::InterruptContext::OutputIncomplete),
157 None => stdout_write_failed(),
158 };
159 output.report(&error);
160 error.exit_code(output.pending_signal())
161 }
162 },
163 Err(error) => {
164 output.discard();
165 output.report(&error);
166 error.exit_code(signal)
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn args(list: &[&str]) -> Vec<OsString> {
176 list.iter().map(OsString::from).collect()
177 }
178
179 #[test]
180 fn version_pre_scan() {
181 assert!(version_requested(&args(&["airs-cli-rs", "-V"])));
182 assert!(version_requested(&args(&[
183 "airs-cli-rs",
184 "runtime",
185 "scan",
186 "--profile",
187 "-V",
188 "hi"
189 ])));
190 assert!(version_requested(&args(&[
191 "airs-cli-rs",
192 "doctor",
193 "--version"
194 ])));
195 assert!(!version_requested(&args(&[
196 "airs-cli-rs",
197 "runtime",
198 "scan",
199 "--profile",
200 "p",
201 "--",
202 "-V"
203 ])));
204 assert!(
205 !version_requested(&args(&["-V"])),
206 "the program name is skipped"
207 );
208 assert!(!version_requested(&args(&["airs-cli-rs", "-v"])));
209 }
210
211 #[test]
212 fn finish_maps_results_to_exit_codes() {
213 let (mut out, stdout, stderr) = output::testing::capture(false);
214 out.data_line("data");
215 assert_eq!(finish(&mut out, Ok(()), None), 0);
216 assert_eq!(stdout.text(), "data\n");
217
218 let (mut out, stdout, stderr2) = output::testing::capture(false);
219 out.data_line("partial");
220 assert_eq!(finish(&mut out, Err(CliError::usage("bad")), None), 2);
221 assert_eq!(stdout.text(), "");
222 assert_eq!(stderr2.text(), "\n ✗ bad\n\n");
223
224 let (mut out, _, stderr3) = output::testing::capture(false);
225 let code = finish(
226 &mut out,
227 Err(CliError::Interrupted(
228 error::InterruptContext::NoDataWritten,
229 )),
230 Some(143),
231 );
232 assert_eq!(code, 143);
233 assert!(stderr3.text().contains("Interrupted; no data was written"));
234 assert_eq!(stderr.text(), "");
235 }
236
237 #[test]
238 fn broken_pipe_is_success() {
239 struct Closed;
240 impl Write for Closed {
241 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
242 Err(std::io::Error::from(ErrorKind::BrokenPipe))
243 }
244 fn flush(&mut self) -> std::io::Result<()> {
245 Ok(())
246 }
247 }
248 let mut out = Output::with_sinks(false, Box::new(Closed), Box::new(std::io::sink()));
249 out.data_line("data");
250 assert_eq!(finish(&mut out, Ok(()), None), 0);
251 }
252
253 #[test]
254 fn other_stdout_write_failures_are_reported() {
255 struct Full;
256 impl Write for Full {
257 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
258 Err(std::io::Error::from(ErrorKind::StorageFull))
259 }
260 fn flush(&mut self) -> std::io::Result<()> {
261 Ok(())
262 }
263 }
264 let stderr = output::testing::Capture::default();
265 let mut out = Output::with_sinks(false, Box::new(Full), Box::new(stderr.clone()));
266 out.data_line("data");
267 assert_eq!(finish(&mut out, Ok(()), None), 1);
268 assert_eq!(
269 stderr.text(),
270 "\n ✗ Error: AISEC_SDK_ERROR:Output could not be written to stdout\n\n"
271 );
272 }
273
274 #[test]
275 fn a_signal_before_the_final_write_reports_the_interruption() {
276 let (mut out, stdout, stderr) = output::testing::capture(false);
277 let signals = std::sync::Arc::new(signals::SignalState::default());
278 assert_eq!(signals.on_signal(143), signals::Action::Cancel);
279 out.attach_signals(signals);
280 out.data_line("data");
281 assert_eq!(finish(&mut out, Ok(()), None), 143);
282 assert_eq!(stdout.text(), "", "nothing is written after a signal");
283 assert_eq!(
284 stderr.text(),
285 "\n ✗ Error: Interrupted; the output was not completely written\n\n"
286 );
287 }
288}