fluentci_ext/
lib.rs

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}