Skip to main content

balise_cli/dev/
mod.rs

1//! The dev supervisor, the same on the machine (`--native`) and in the container
2//! (`--inside`): it watches the files, rebuilds, restarts the app, and writes one stream of
3//! logs. While a build runs, or fails, the last version that built keeps answering.
4
5mod app;
6mod build;
7pub mod docker;
8mod signal;
9mod watch;
10
11use std::io::Write;
12use std::path::Path;
13use std::time::{Duration, Instant};
14
15use balise_core::error_codes::{DEV0001, DEV0002};
16use balise_core::event::{Event, Level};
17
18use crate::{Failure, project};
19use app::App;
20use build::Built;
21use watch::Watcher;
22
23pub use signal::install as catch_signals;
24
25/// How often the files are read.
26const POLL: Duration = Duration::from_millis(300);
27
28/// A change is complete once the files stop changing for this long: an editor saving several
29/// files is one change.
30const SETTLE: Duration = Duration::from_millis(150);
31
32/// An app that stops sooner than this after starting would stop again: `balise dev` waits
33/// for a change instead of starting it again.
34const STARTUP: Duration = Duration::from_secs(2);
35
36/// How much longer than `shutdown.timeout` the app is given to stop before it is killed.
37pub const GRACE_MARGIN: Duration = Duration::from_secs(2);
38
39/// Runs the supervisor of the project at `root` until `stop` says so.
40pub fn supervise(
41    root: &Path,
42    out: &mut dyn Write,
43    color: bool,
44    stop: &dyn Fn() -> bool,
45) -> Result<(), Failure> {
46    let name = project::package_name(root).unwrap_or_else(|| "app".to_string());
47    let target = project::target_dir(root);
48    let binary = target
49        .join("debug")
50        .join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
51    let runs = target.join("balise-dev");
52    // The app gets the time its own shutdown takes, and a little more. The app inherits
53    // this process's variables: the configuration is read with them.
54    let variables: Vec<(String, String)> = std::env::vars().collect();
55    let grace = project::config(root, &variables).shutdown_timeout + GRACE_MARGIN;
56    let say = |out: &mut dyn Write, event: Event| {
57        let _ = writeln!(out, "{}", event.render_text(color));
58        let _ = out.flush();
59    };
60
61    let mut watcher = Watcher::new(root);
62    let mut app: Option<App> = None;
63    let mut generation = 0;
64    let mut rebuild = Some(format!("Building {name}"));
65    loop {
66        if stop() {
67            if let Some(app) = app.take() {
68                app.stop(grace);
69            }
70            return Ok(());
71        }
72        if let Some(reason) = rebuild.take() {
73            say(out, Event::new(Level::Info, reason));
74            match build::build(root)? {
75                Built::Ok { seconds, warnings } => {
76                    let warnings = warnings.map(|w| format!(", {w}")).unwrap_or_default();
77                    let restart = if app.is_some() {
78                        "; restarting the app"
79                    } else {
80                        ""
81                    };
82                    say(
83                        out,
84                        Event::new(
85                            Level::Info,
86                            format!("Built in {seconds:.1} s{warnings}{restart}"),
87                        ),
88                    );
89                    if let Some(old) = app.take() {
90                        old.stop(grace);
91                    }
92                    generation += 1;
93                    app = Some(start(&binary, root, &runs, generation)?);
94                }
95                Built::Failed { errors, lines } => {
96                    for line in lines {
97                        let _ = writeln!(out, "  {line}");
98                    }
99                    say(
100                        out,
101                        Event::coded(Level::Warn, DEV0002, &[("errors", &errors)]),
102                    );
103                }
104            }
105        }
106        std::thread::sleep(POLL);
107
108        let mut changes = watcher.changes();
109        if !changes.is_empty() {
110            let since = Instant::now();
111            while since.elapsed() < Duration::from_secs(2) {
112                std::thread::sleep(SETTLE);
113                let more = watcher.changes();
114                if more.is_empty() {
115                    break;
116                }
117                changes.extend(more);
118            }
119            changes.sort();
120            changes.dedup();
121            let what = match changes.as_slice() {
122                [one] => format!("{} changed", one.display()),
123                many => format!("{} files changed", many.len()),
124            };
125            rebuild = Some(format!("Rebuilding: {what}"));
126        }
127
128        if let Some(running) = app.as_mut()
129            && let Some(status) = running.exited()
130        {
131            let uptime = running.uptime();
132            app = None;
133            say(
134                out,
135                Event::coded(Level::Error, DEV0001, &[("status", &app::describe(status))]),
136            );
137            if uptime > STARTUP && rebuild.is_none() {
138                say(out, Event::new(Level::Info, "Starting the app again"));
139                generation += 1;
140                app = Some(start(&binary, root, &runs, generation)?);
141            }
142        }
143    }
144}
145
146fn start(binary: &Path, root: &Path, runs: &Path, generation: u64) -> Result<App, Failure> {
147    App::start(binary, root, runs, generation).map_err(|error| {
148        Failure::problem(
149            balise_core::error_codes::DEV0003,
150            &[(
151                "cause",
152                &format!("cannot start {}: {error}", binary.display()),
153            )],
154            &[],
155        )
156    })
157}
158
159/// Whether Ctrl-C or `SIGTERM` asked the supervisor to stop.
160pub fn stop_requested() -> bool {
161    signal::requested()
162}