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 config = project::config(root, &variables);
56 let grace = config.shutdown_timeout + GRACE_MARGIN;
57 let ready = format!(
59 "Ready: http://localhost:{}{}",
60 config.port,
61 config.api_base()
62 );
63 balise_core::journal::open(root);
64 let say = |out: &mut dyn Write, event: Event| {
65 let _ = writeln!(out, "{}", event.render_text(color));
66 let _ = out.flush();
67 balise_core::journal::event(&event);
68 };
69
70 let mut watcher = Watcher::new(root);
71 let mut app: Option<App> = None;
72 let mut generation = 0;
73 let mut starting = false;
75 let mut rebuild = Some(format!("Building {name}"));
76 loop {
77 if stop() {
78 if let Some(app) = app.take() {
79 app.stop(grace);
80 }
81 return Ok(());
82 }
83 if let Some(reason) = rebuild.take() {
84 say(out, Event::new(Level::Info, reason));
85 match build::build(root)? {
86 Built::Ok { seconds, warnings } => {
87 let warnings = warnings.map(|w| format!(", {w}")).unwrap_or_default();
88 let restart = if app.is_some() {
89 "; restarting the app"
90 } else {
91 ""
92 };
93 say(
94 out,
95 Event::new(
96 Level::Info,
97 format!("Built in {seconds:.1} s{warnings}{restart}"),
98 ),
99 );
100 if let Some(old) = app.take() {
101 old.stop(grace);
102 }
103 generation += 1;
104 app = Some(start(&binary, root, &runs, generation)?);
105 starting = true;
106 }
107 Built::Failed { errors, lines } => {
108 for line in lines {
109 let _ = writeln!(out, " {line}");
110 }
111 say(
112 out,
113 Event::coded(Level::Warn, DEV0002, &[("errors", &errors)]),
114 );
115 }
116 }
117 }
118 std::thread::sleep(POLL);
119 if starting && app.is_some() && listens(config.port) {
120 starting = false;
121 say(out, Event::new(Level::Info, ready.as_str()));
122 }
123
124 let mut changes = watcher.changes();
125 if !changes.is_empty() {
126 let since = Instant::now();
127 while since.elapsed() < Duration::from_secs(2) {
128 std::thread::sleep(SETTLE);
129 let more = watcher.changes();
130 if more.is_empty() {
131 break;
132 }
133 changes.extend(more);
134 }
135 changes.sort();
136 changes.dedup();
137 let what = match changes.as_slice() {
138 [one] => format!("{} changed", one.display()),
139 many => format!("{} files changed", many.len()),
140 };
141 rebuild = Some(format!("Rebuilding: {what}"));
142 }
143
144 if let Some(running) = app.as_mut()
145 && let Some(status) = running.exited()
146 {
147 let uptime = running.uptime();
148 app = None;
149 say(
150 out,
151 Event::coded(Level::Error, DEV0001, &[("status", &app::describe(status))]),
152 );
153 if uptime > STARTUP && rebuild.is_none() {
154 say(out, Event::new(Level::Info, "Starting the app again"));
155 generation += 1;
156 app = Some(start(&binary, root, &runs, generation)?);
157 starting = true;
158 }
159 }
160 }
161}
162
163fn listens(port: u16) -> bool {
165 let address = (std::net::Ipv4Addr::LOCALHOST, port).into();
166 std::net::TcpStream::connect_timeout(&address, Duration::from_millis(200)).is_ok()
167}
168
169fn start(binary: &Path, root: &Path, runs: &Path, generation: u64) -> Result<App, Failure> {
170 App::start(binary, root, runs, generation).map_err(|error| {
171 Failure::problem(
172 balise_core::error_codes::DEV0003,
173 &[(
174 "cause",
175 &format!("cannot start {}: {error}", binary.display()),
176 )],
177 &[],
178 )
179 })
180}
181
182pub fn stop_requested() -> bool {
184 signal::requested()
185}