1use std::{
2 env,
3 io::{BufRead, BufReader},
4 process::{Command, ExitStatus, Stdio},
5 sync::mpsc::{self, Receiver, Sender},
6 thread,
7 time::Instant,
8};
9
10use anyhow::Error;
11use fluentci_types::Output;
12use owo_colors::OwoColorize;
13use superconsole::{
14 style::Stylize, Component, Dimensions, DrawMode, Line, Lines, Span, SuperConsole,
15};
16
17pub mod archive;
18pub mod cache;
19pub mod devbox;
20pub mod devenv;
21pub mod envhub;
22pub mod flox;
23pub mod git;
24pub mod git_checkout;
25pub mod git_last_commit;
26pub mod hash;
27pub mod hermit;
28pub mod http;
29pub mod mise;
30pub mod nix;
31pub mod pixi;
32pub mod pkgx;
33pub mod proto;
34pub mod runner;
35pub mod service;
36
37pub trait Extension {
38 fn exec(
39 &mut self,
40 cmd: &str,
41 tx: Sender<String>,
42 out: Output,
43 last_cmd: bool,
44 work_dir: &str,
45 ) -> Result<ExitStatus, Error>;
46 fn setup(&self) -> Result<(), Error>;
47 fn post_setup(&self, tx: Sender<String>) -> Result<ExitStatus, Error> {
48 tx.send("".into())?;
49 Ok(ExitStatus::default())
50 }
51 fn format_command(&self, cmd: &str) -> String {
52 format!("{}", cmd)
53 }
54}
55
56pub fn exec(
57 cmd: &str,
58 tx: Sender<String>,
59 out: Output,
60 last_cmd: bool,
61 work_dir: &str,
62) -> Result<ExitStatus, Error> {
63 let (stdout_tx, stdout_rx): (Sender<String>, Receiver<String>) = mpsc::channel();
64 let (stderr_tx, stderr_rx): (Sender<String>, Receiver<String>) = mpsc::channel();
65
66 let mut child = Command::new("bash")
67 .arg("-c")
68 .arg(cmd)
69 .current_dir(work_dir)
70 .stdout(Stdio::piped())
71 .stderr(Stdio::piped())
72 .spawn()?;
73
74 let stdout_tx_clone = stdout_tx.clone();
75 let stdout = child.stdout.take().unwrap();
76 let stderr = child.stderr.take().unwrap();
77
78 let out_clone = out.clone();
79 let tx_clone = tx.clone();
80 let namespace = cmd.to_string().clone();
81
82 thread::spawn(move || {
83 if let Some(mut console) = SuperConsole::new() {
84 if env::var("FLUENTCI_CONSOLE").unwrap_or_default() == "0" {
85 let mut stdout = String::new();
86 while let Ok(line) = stdout_rx.recv() {
87 println!("{}", line);
88 match fluentci_logging::info(&line, &namespace) {
89 Ok(_) => {}
90 Err(_) => {}
91 }
92 stdout.push_str(&line);
93 stdout.push_str("\n");
94 }
95
96 if out_clone == Output::Stdout && last_cmd {
97 match tx_clone.send(stdout) {
98 Ok(_) => {}
99 Err(_) => {}
100 }
101 }
102 return;
103 }
104 let created = Instant::now();
105 let mut stdout = String::new();
106 let mut lines = vec![];
107 while let Ok(line) = stdout_rx.recv() {
108 lines.push(line.clone());
109 if lines.len() > 20 {
110 lines.remove(0);
111 }
112
113 console
114 .render(&Log {
115 command: &namespace,
116 lines: lines.clone(),
117 created,
118 now: Instant::now(),
119 })
120 .unwrap();
121 match fluentci_logging::info(&line, &namespace) {
122 Ok(_) => {}
123 Err(e) => {
124 println!("Error: {}", e);
125 }
126 }
127 stdout.push_str(&line);
128 stdout.push_str("\n");
129 }
130
131 console
132 .finalize(&Log {
133 command: &namespace,
134 lines: vec![],
135 created,
136 now: Instant::now(),
137 })
138 .unwrap();
139 if out_clone == Output::Stdout && last_cmd {
140 match tx_clone.send(stdout) {
141 Ok(_) => {}
142 Err(_) => {}
143 }
144 }
145 return;
146 }
147
148 let mut stdout = String::new();
149 while let Ok(line) = stdout_rx.recv() {
150 println!("{}", line);
151 match fluentci_logging::info(&line, &namespace) {
152 Ok(_) => {}
153 Err(e) => {
154 println!("Error: {}", e);
155 }
156 }
157 stdout.push_str(&line);
158 stdout.push_str("\n");
159 }
160
161 if out_clone == Output::Stdout && last_cmd {
162 match tx_clone.send(stdout) {
163 Ok(_) => {}
164 Err(_) => {}
165 }
166 }
167 });
168
169 let namespace = cmd.to_string().clone();
170
171 thread::spawn(move || {
172 let mut stderr = String::new();
173 while let Ok(line) = stderr_rx.recv() {
174 println!("{}", line);
175 match fluentci_logging::info(&line, &namespace) {
176 Ok(_) => {}
177 Err(_) => {}
178 }
179 stderr.push_str(&line);
180 stderr.push_str("\n");
181 }
182 if out == Output::Stderr && last_cmd {
183 match tx.send(stderr) {
184 Ok(_) => {}
185 Err(_) => {}
186 }
187 }
188 });
189
190 thread::spawn(move || {
191 let reader = BufReader::new(stdout);
192 for line in reader.lines() {
193 match stdout_tx_clone.send(line.unwrap()) {
194 Ok(_) => {}
195 Err(_) => {}
196 }
197 }
198 });
199
200 thread::spawn(move || {
201 let reader = BufReader::new(stderr);
202 for line in reader.lines() {
203 stderr_tx.send(line.unwrap()).unwrap();
204 }
205 });
206
207 child.wait().map_err(Error::from)
208}
209
210struct Log<'a> {
211 command: &'a str,
212 lines: Vec<String>,
213 created: Instant,
214 now: Instant,
215}
216
217impl<'a> Component for Log<'a> {
218 fn draw_unchecked(&self, _dimensions: Dimensions, mode: DrawMode) -> anyhow::Result<Lines> {
219 Ok(match mode {
220 DrawMode::Normal => {
221 let mut lines: Vec<Line> = self
222 .lines
223 .iter()
224 .map(|l| vec![l.clone()].try_into().unwrap_or_default())
225 .collect();
226 lines.push(
227 vec![format!(
228 " {} {}",
229 "Executing".bright_green().bold(),
230 self.command.bright_purple(),
231 )]
232 .try_into()
233 .unwrap(),
234 );
235 Lines(lines)
236 }
237 DrawMode::Final => {
238 const FINISHED: &str = " Finished ";
239 let finished = Span::new_styled(FINISHED.to_owned().green().bold())?;
240 let completion = format!(
241 "{} in {}{}",
242 self.command.bright_purple(),
243 (self.now.duration_since(self.created).as_millis() as f64 / 1000.0)
244 .to_string()
245 .bright_blue(),
246 "s".bright_blue()
247 );
248 Lines(vec![Line::from_iter([
249 finished,
250 Span::new_unstyled(&completion)?,
251 ])])
252 }
253 })
254 }
255}