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