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