1mod 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
25const POLL: Duration = Duration::from_millis(300);
27
28const SETTLE: Duration = Duration::from_millis(150);
31
32const STARTUP: Duration = Duration::from_secs(2);
35
36pub const GRACE_MARGIN: Duration = Duration::from_secs(2);
38
39pub 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 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
159pub fn stop_requested() -> bool {
161 signal::requested()
162}