Skip to main content

bacon/exec/
executor.rs

1use {
2    crate::*,
3    std::{
4        io::{
5            self,
6            BufRead,
7            BufReader,
8        },
9        process::{
10            Child,
11            Command,
12        },
13        thread,
14        time::Instant,
15    },
16    termimad::crossbeam::channel::{
17        self,
18        Receiver,
19        Sender,
20    },
21};
22
23/// an executor calling a cargo (or similar) command in a separate
24/// thread when asked to and sending the lines of output in a channel,
25/// and finishing by None.
26/// Channel sizes are designed to avoid useless computations.
27pub struct MissionExecutor {
28    command_builder: CommandBuilder,
29    kill_command: Option<Vec<String>>,
30}
31
32/// Dedicated to one execution of the job (so there's usually
33/// several task executors during the lifetime of a mission executor).
34pub struct TaskExecutor {
35    /// the thread running the current task
36    child_thread: thread::JoinHandle<()>,
37    stop_sender: Sender<StopMessage>,
38    grace_period_start: Option<Instant>, // forgotten at end of grace period
39    grace_period: Period,
40    /// Kept to hold the line channel open: once the task's own threads are
41    /// gone, this keeps the channel *connected* so the main loop's `select!`
42    /// blocks on `line_receiver` instead of busy-looping on a disconnected one.
43    _line_sender: Sender<CommandExecInfo>,
44    /// receiver for this task's output lines
45    pub line_receiver: Receiver<CommandExecInfo>,
46}
47
48/// A message sent to the `child_thread` on end
49#[derive(Clone, Copy)]
50enum StopMessage {
51    SendStatus, // process already finished, just get status
52    Kill,       // kill the process, don't bother about the status
53}
54
55impl TaskExecutor {
56    /// Interrupt the process
57    pub fn interrupt(self) {
58        let _ = self.stop_sender.send(StopMessage::Kill);
59    }
60    /// Kill the process, and wait until it finished
61    pub fn die(self) {
62        if let Err(e) = self.stop_sender.send(StopMessage::Kill) {
63            debug!("failed to send 'die' signal: {e}");
64        }
65        if self.child_thread.join().is_err() {
66            warn!("child_thread.join() failed"); // should not happen
67        }
68    }
69    pub fn is_in_grace_period(&mut self) -> bool {
70        if let Some(grace_period_start) = self.grace_period_start {
71            if grace_period_start.elapsed() < self.grace_period.duration {
72                return true;
73            }
74            self.grace_period_start = None;
75        }
76        false
77    }
78}
79
80impl MissionExecutor {
81    /// Prepare the executor (no task/process/thread is started at this point)
82    pub fn new(mission: &Mission) -> anyhow::Result<Self> {
83        let command_builder = mission.get_command()?;
84        let kill_command = mission.kill_command();
85        Ok(Self {
86            command_builder,
87            kill_command,
88        })
89    }
90
91    /// Start the job's command, once, with the given settings
92    ///
93    /// # Panics
94    ///
95    /// Will panic if the `MissionBuilder` doesn't pipe stderr
96    pub fn start(
97        &mut self,
98        task: Task,
99    ) -> anyhow::Result<TaskExecutor> {
100        info!("start task {task:?}");
101        let grace_period = task.grace_period;
102        let grace_period_start = if grace_period.is_zero() {
103            None
104        } else {
105            Some(Instant::now())
106        };
107        let mut command_builder = self.command_builder.clone();
108        if let Some(backtrace) = task.backtrace {
109            command_builder.env("RUST_BACKTRACE", backtrace);
110        }
111        let kill_command = self.kill_command.clone();
112        let with_stdout = command_builder.is_with_stdout();
113        let (line_sender, line_receiver) = channel::unbounded();
114        let keepalive_sender = line_sender.clone();
115        let (stop_sender, stop_receiver) = channel::bounded(1);
116        let err_stop_sender = stop_sender.clone();
117
118        // Global task executor thread
119        let child_thread = thread::spawn(move || {
120            // before starting the command, we wait some time, so that a bunch
121            // of quasi-simultaneous file events can be finished before the command
122            // starts (during this time, no other command is started by bacon in app.rs)
123            if !grace_period.is_zero() {
124                thread::sleep(grace_period.duration);
125            }
126
127            let mut cmd = command_builder.build();
128            let mut child = match cmd.spawn() {
129                Ok(child) => child,
130                Err(e) => {
131                    let _ = line_sender.send(CommandExecInfo::Error(
132                        anyhow::anyhow!(e).context(format!("failed to spawn {cmd:?}")),
133                    ));
134                    return;
135                }
136            };
137
138            // thread piping the stdout lines
139            if with_stdout {
140                let sender = line_sender.clone();
141                let Some(stdout) = child.stdout.take() else {
142                    warn!("process has no stdout"); // unlikely
143                    return;
144                };
145                let mut buf_reader = BufReader::new(stdout);
146                thread::spawn(move || {
147                    let mut line = String::new();
148                    loop {
149                        match buf_reader.read_line(&mut line) {
150                            Err(e) => {
151                                warn!("error : {e}");
152                            }
153                            Ok(0) => {
154                                // there won't be anything more, quitting
155                                break;
156                            }
157                            Ok(_) => {
158                                let response = CommandExecInfo::Line(RawCommandOutputLine {
159                                    content: line.clone(),
160                                    origin: CommandStream::StdOut,
161                                });
162                                if sender.send(response).is_err() {
163                                    break; // channel closed
164                                }
165                            }
166                        }
167                        line.clear();
168                    }
169                });
170            }
171
172            // starting a thread to handle stderr lines until program
173            // ends (then ask the child_thread to send status)
174            let err_line_sender = line_sender.clone();
175            // stderr is piped in CommandBuilder, so the following statement can't fail
176            // unless you break the CommandBuilder
177            let stderr = child
178                .stderr
179                .take()
180                .expect("MissionExecutor requires piped stderr");
181            let mut buf_reader = BufReader::new(stderr);
182            thread::spawn(move || {
183                let mut line = String::new();
184                loop {
185                    match buf_reader.read_line(&mut line) {
186                        Err(e) => {
187                            warn!("error : {e}");
188                        }
189                        Ok(0) => {
190                            if let Err(e) = err_stop_sender.send(StopMessage::SendStatus) {
191                                warn!("sending stop message failed: {e}");
192                            }
193                            break;
194                        }
195                        Ok(_) => {
196                            let response = CommandExecInfo::Line(RawCommandOutputLine {
197                                content: line.clone(),
198                                origin: CommandStream::StdErr,
199                            });
200                            if err_line_sender.send(response).is_err() {
201                                break; // channel closed
202                            }
203                        }
204                    }
205                    line.clear();
206                }
207            });
208
209            // now waiting for the stop event
210            let natural_end = match stop_receiver.recv() {
211                Ok(StopMessage::SendStatus) => true, // the process finished on its own
212                Ok(StopMessage::Kill) => {
213                    debug!("explicit interrupt received");
214                    kill(kill_command.as_deref(), &mut child);
215                    false
216                }
217                Err(e) => {
218                    debug!("recv error: {e}"); // probably just the executor dropped
219                    kill(kill_command.as_deref(), &mut child);
220                    false
221                }
222            };
223            // reap the child exactly once (also required after the SIGKILL in
224            // kill(), which doesn't wait itself) and keep its status
225            let status = child.wait();
226            if let Err(ref e) = status {
227                warn!("waiting for child failed: {e}");
228            }
229            // announce completion only for a natural end (a killed task reports
230            // no status). The reader threads are intentionally left detached: a
231            // surviving grandchild could hold the pipe open, so joining them
232            // could block forever — and their output goes to this task's channel,
233            // which is dropped when the next task starts.
234            if natural_end {
235                if let Ok(status) = status {
236                    let _ = line_sender.send(CommandExecInfo::End { status });
237                }
238            }
239        });
240        Ok(TaskExecutor {
241            child_thread,
242            stop_sender,
243            grace_period_start,
244            grace_period,
245            _line_sender: keepalive_sender,
246            line_receiver,
247        })
248    }
249}
250
251/// kill the child process, either by using a specific command or by
252/// using the default platform kill method if the specific command
253/// failed or wasn't provided.
254fn kill(
255    kill_command: Option<&[String]>,
256    child: &mut Child,
257) {
258    if let Some(kill_command) = kill_command {
259        info!("launch specific kill command {kill_command:?}");
260        let Err(e) = run_kill_command(kill_command, child) else {
261            return;
262        };
263        warn!("specific kill command failed: {e}");
264    }
265    if let Err(e) = child.kill() {
266        // e.g. the process already exited; nothing more we can do, and panicking
267        // here would skip the caller's child.wait() and orphan the process
268        warn!("failed to kill child process: {e}");
269    }
270}
271
272fn run_kill_command(
273    kill_command: &[String],
274    child: &mut Child,
275) -> io::Result<()> {
276    let (exe, args) = kill_command
277        .split_first()
278        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "empty kill command"))?;
279    let mut kill = Command::new(exe);
280    kill.args(args);
281    kill.arg(child.id().to_string());
282    let mut proc = kill.spawn()?;
283    let status = proc.wait()?;
284    if !status.success() {
285        return Err(io::Error::other(format!(
286            "kill command returned nonzero status: {status}"
287        )));
288    }
289    child.wait()?;
290    Ok(())
291}