vertigo_cli/watch/
watch_run.rs1use actix_cors::Cors;
2use actix_web::{App, HttpServer, rt::System, web};
3use notify::RecursiveMode;
4use std::{path::Path, process::exit, sync::Arc, time::Duration};
5use tokio::sync::{Notify, watch::Sender};
6use tokio_retry::{Retry, strategy::FibonacciBackoff};
7
8use crate::build::{Workspace, get_workspace};
9use crate::commons::{
10 ErrorCode,
11 spawn::{SpawnOwner, term_signal},
12};
13
14use super::{
15 ignore_agent::IgnoreAgents, is_http_server_listening::is_http_server_listening,
16 sse::handler_sse, watch_opts::WatchOpts,
17};
18
19const SETTLE_TIME: Duration = Duration::from_millis(200);
21
22#[derive(Clone, Debug, Default, PartialEq)]
23pub enum Status {
24 #[default]
25 Building,
26 Version(u32),
27 Errors,
28}
29
30pub async fn run(mut opts: WatchOpts) -> Result<(), ErrorCode> {
31 log::info!("watch params => {opts:#?}");
32
33 let ws = match get_workspace() {
34 Ok(ws) => ws,
35 Err(err) => {
36 log::error!("Can't read workspace");
37 return Err(err);
38 }
39 };
40
41 let package_name = match opts.build.package_name.as_deref() {
42 Some(name) => name.to_string(),
43 None => match ws.infer_package_name() {
44 Some(name) => {
45 log::info!("Inferred package name = {name}");
46 opts.build.package_name = Some(name.clone());
47 name
48 }
49 None => {
50 log::error!(
51 "Can't find vertigo project in {} (no cdylib member)",
52 ws.get_root_dir()
53 );
54 return Err(ErrorCode::CantFindCdylibMember);
55 }
56 },
57 };
58
59 log::info!("package_name ==> {package_name:?}");
60
61 let root = ws.find_package_path(&package_name);
62 log::info!("path ==> {root:?}");
63
64 let Some(root) = root else {
65 log::error!("package not found ==> {:?}", opts.build.package_name);
66 return Err(ErrorCode::PackageNameNotFound);
67 };
68
69 if opts.build.release_mode.is_none() {
71 opts.build.release_mode = Some(false);
72 }
73 if opts.build.wasm_opt.is_none() {
74 opts.build.wasm_opt = Some(false);
75 }
76
77 let excludes = [root.join("target"), root.join(opts.common.dest_dir.clone())];
78
79 let notify_build = Arc::new(Notify::new());
80
81 let watch_result = notify::recommended_watcher({
82 let notify_build = notify_build.clone();
83
84 let ignore_agents = IgnoreAgents::new(&ws.get_root_dir().into(), &opts);
86
87 move |res: Result<notify::Event, _>| match res {
88 Ok(event) => {
89 if let notify::EventKind::Access(_) = event.kind {
90 return;
91 }
92
93 if event.paths.iter().all(|path| {
94 for exclude_path in &excludes {
96 if path.starts_with(exclude_path) {
97 return true;
98 }
99 }
100 ignore_agents.should_be_ignored(path)
102 }) {
103 return;
104 }
105 notify_build.notify_one();
106 }
107 Err(err) => {
108 log::error!("watch error: {err:?}");
109 }
110 }
111 });
112
113 let mut watcher = match watch_result {
114 Ok(watcher) => watcher,
115 Err(error) => {
116 log::error!("error watcher => {error}");
117 return Err(ErrorCode::WatcherError);
118 }
119 };
120
121 let (tx, rx) = tokio::sync::watch::channel(Status::default());
122 let tx = Arc::new(tx);
123
124 let watch_server = HttpServer::new(move || {
125 App::new()
126 .wrap(Cors::permissive())
127 .service(web::resource("/events").route(web::get().to(handler_sse)))
128 .app_data(web::Data::new(rx.clone()))
129 })
130 .workers(1)
131 .bind("127.0.0.1:5555")
132 .map_err(|err| {
133 log::error!("Watch server bind error: {err}");
134 ErrorCode::WatcherError
135 })?
136 .disable_signals()
137 .client_disconnect_timeout(Duration::from_secs(1))
138 .shutdown_timeout(1)
139 .run();
140
141 let watch_handle = watch_server.handle();
142
143 std::thread::spawn(move || System::new().block_on(watch_server));
144
145 use notify::Watcher;
146 watcher
147 .watch(&root, RecursiveMode::Recursive)
148 .map_err(|err| {
149 log::error!("Can't watch root dir {}: {err}", root.to_string_lossy());
150 ErrorCode::CantAddWatchDir
151 })?;
152
153 for watch_path in &opts.add_watch_path {
154 match watcher.watch(Path::new(watch_path), RecursiveMode::Recursive) {
155 Ok(()) => {
156 log::info!("Added `{watch_path}` to watched directories");
157 }
158 Err(err) => {
159 log::error!("Error adding watch dir `{watch_path}`: {err}");
160 return Err(ErrorCode::CantAddWatchDir);
161 }
162 }
163 }
164 let mut version = 0;
165
166 loop {
167 version += 1;
168
169 if let Err(err) = tx.send(Status::Building) {
170 log::error!("Can't contact the browser: {err} (Other watch process already running?)");
171 return Err(ErrorCode::OtherProcessAlreadyRunning);
172 };
173
174 while tokio::time::timeout(SETTLE_TIME, notify_build.notified())
179 .await
180 .is_ok()
181 {}
182
183 log::info!("Build run...");
184
185 let spawn = build_and_watch(version, tx.clone(), &opts, &ws);
186
187 tokio::select! {
188 msg = term_signal() => {
189 log::info!("{msg} received, shutting down");
190 spawn.off();
191 watch_handle.stop(true).await;
192 return Ok(());
193 }
194 _ = notify_build.notified() => {
195 log::info!("Notify build received, shutting down");
196 spawn.off();
197 }
198 }
199 }
200}
201
202fn build_and_watch(
203 version: u32,
204 tx: Arc<Sender<Status>>,
205 opts: &WatchOpts,
206 ws: &Workspace,
207) -> SpawnOwner {
208 let opts = opts.clone();
209 let ws = ws.clone();
210 SpawnOwner::new(async move {
211 match crate::build::run_with_ws(opts.to_build_opts(), &ws, true) {
212 Ok(()) => {
213 log::info!("Build successful.");
214
215 let check_spawn = SpawnOwner::new(async move {
216 let _ = Retry::start(
217 FibonacciBackoff::from_millis(100).max_delay(Duration::from_secs(4)),
218 || is_http_server_listening(opts.serve.port),
219 )
220 .await;
221
222 let Ok(()) = tx.send(Status::Version(version)) else {
223 exit(ErrorCode::WatchPipeBroken as i32)
224 };
225 });
226
227 let opts = opts.clone();
228
229 log::info!("Spawning serve command...");
230 let (mut serve_params, port_watch) = opts.to_serve_opts();
231
232 if serve_params.inner.threads.is_none() {
233 serve_params.inner.threads = Some(2);
234 }
235
236 if let Err(error_code) = crate::serve::run(serve_params, Some(port_watch)).await {
237 log::error!("Error {} while running server", error_code as i32);
238 exit(error_code as i32)
239 }
240
241 check_spawn.off();
242 }
243 Err(_) => {
244 log::error!("Build run failed. Waiting for changes...");
245
246 let Ok(()) = tx.send(Status::Errors) else {
247 exit(ErrorCode::WatchPipeBroken as i32)
248 };
249 }
250 };
251 })
252}