Skip to content

Commit 93674d2

Browse files
authored
Fix watch startup build locking (#8413)
* Fix watch startup build locking * Fix * Fix
1 parent 8290333 commit 93674d2

1 file changed

Lines changed: 151 additions & 101 deletions

File tree

rewatch/src/watcher.rs

Lines changed: 151 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use notify::event::ModifyKind;
1515
use notify::{Config, Error, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
1616
use std::path::{Path, PathBuf};
1717
use std::sync::Arc;
18-
use std::sync::Mutex;
18+
use std::sync::atomic::{AtomicBool, Ordering};
1919
use std::time::{Duration, Instant};
2020

2121
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
@@ -25,6 +25,13 @@ enum CompileType {
2525
None,
2626
}
2727

28+
type WatchPaths = Vec<(PathBuf, RecursiveMode)>;
29+
type StartupBuildResult = (
30+
BuildCommandState,
31+
WatchPaths,
32+
Option<(Instant, build::CompilationOutcome)>,
33+
);
34+
2835
fn is_rescript_file(path_buf: &Path) -> bool {
2936
let extension = path_buf.extension().and_then(|ext| ext.to_str());
3037

@@ -58,6 +65,31 @@ fn matches_filter(path_buf: &Path, filter: &Option<regex::Regex>) -> bool {
5865
filter.as_ref().map(|re| !re.is_match(&name)).unwrap_or(true)
5966
}
6067

68+
fn finish_successful_watch_compile(
69+
after_build: Option<String>,
70+
timing_total: Instant,
71+
show_progress: bool,
72+
plain_output: bool,
73+
finished_message: &str,
74+
compilation_kind: Option<&str>,
75+
outcome: build::CompilationOutcome,
76+
) {
77+
if let Some(a) = after_build {
78+
cmd::run(a)
79+
}
80+
let timing_total_elapsed = timing_total.elapsed();
81+
if show_progress {
82+
if plain_output {
83+
println!("{finished_message}")
84+
} else {
85+
println!(
86+
"\n{}\n",
87+
build::format_finished_compilation_message(compilation_kind, outcome, timing_total_elapsed)
88+
);
89+
}
90+
}
91+
}
92+
6193
/// Computes the list of paths to watch based on the build state.
6294
/// Returns tuples of (path, recursive_mode) for each watch target.
6395
fn compute_watch_paths(build_state: &BuildCommandState, root: &Path) -> Vec<(PathBuf, RecursiveMode)> {
@@ -178,13 +210,8 @@ fn carry_forward_compile_warnings(previous: &BuildCommandState, next: &mut Build
178210
}
179211
}
180212

181-
fn should_clear_screen(
182-
clear_screen: bool,
183-
show_progress: bool,
184-
plain_output: bool,
185-
initial_build: bool,
186-
) -> bool {
187-
clear_screen && show_progress && !plain_output && !initial_build
213+
fn should_clear_screen(clear_screen: bool, show_progress: bool, plain_output: bool) -> bool {
214+
clear_screen && show_progress && !plain_output
188215
}
189216

190217
fn clear_terminal_screen() {
@@ -203,11 +230,24 @@ fn print_build_failed_footer() {
203230
println!("\nBuild failed. Watching for changes...");
204231
}
205232

233+
fn cleanup_before_watch_exit(
234+
path: &Path,
235+
build_state: &BuildCommandState,
236+
show_progress: bool,
237+
message: &str,
238+
) {
239+
if show_progress {
240+
println!("{message}");
241+
}
242+
build::with_build_lock(path, || clean::cleanup_after_build(build_state));
243+
}
244+
206245
struct AsyncWatchArgs<'a> {
207246
watcher: &'a mut RecommendedWatcher,
208247
current_watch_paths: Vec<(PathBuf, RecursiveMode)>,
209248
initial_build_state: BuildCommandState,
210249
q: Arc<FifoQueue<Result<Event, Error>>>,
250+
ctrlc_pressed: Arc<AtomicBool>,
211251
path: &'a Path,
212252
show_progress: bool,
213253
filter: &'a Option<regex::Regex>,
@@ -225,6 +265,7 @@ async fn async_watch(
225265
mut current_watch_paths,
226266
initial_build_state,
227267
q,
268+
ctrlc_pressed,
228269
path,
229270
show_progress,
230271
filter,
@@ -237,26 +278,10 @@ async fn async_watch(
237278
}: AsyncWatchArgs<'_>,
238279
) -> Result<()> {
239280
let mut build_state = initial_build_state;
240-
let mut needs_compile_type = CompileType::Incremental;
241-
// create a mutex to capture if ctrl-c was pressed
242-
let ctrlc_pressed = Arc::new(Mutex::new(false));
243-
let ctrlc_pressed_clone = Arc::clone(&ctrlc_pressed);
244-
245-
ctrlc::set_handler(move || {
246-
let pressed = Arc::clone(&ctrlc_pressed);
247-
let mut pressed = pressed.lock().unwrap();
248-
*pressed = true;
249-
})
250-
.expect("Error setting Ctrl-C handler");
251-
252-
let mut initial_build = true;
253-
281+
let mut needs_compile_type = CompileType::None;
254282
loop {
255-
if *ctrlc_pressed_clone.lock().unwrap() {
256-
if show_progress {
257-
println!("\nExiting...");
258-
}
259-
build::with_build_lock(path, || clean::cleanup_after_build(&build_state));
283+
if ctrlc_pressed.load(Ordering::SeqCst) {
284+
cleanup_before_watch_exit(path, &build_state, show_progress, "\nExiting...");
260285
break Ok(());
261286
}
262287
let mut events: Vec<Event> = vec![];
@@ -278,10 +303,12 @@ async fn async_watch(
278303
.any(|path| path.ends_with(LockKind::Watch.file_name()))
279304
&& let EventKind::Remove(_) = event.kind
280305
{
281-
if show_progress {
282-
println!("\nExiting... (lockfile removed)");
283-
}
284-
build::with_build_lock(path, || clean::cleanup_after_build(&build_state));
306+
cleanup_before_watch_exit(
307+
path,
308+
&build_state,
309+
show_progress,
310+
"\nExiting... (lockfile removed)",
311+
);
285312
return Ok(());
286313
}
287314

@@ -413,7 +440,7 @@ async fn async_watch(
413440

414441
match needs_compile_type {
415442
CompileType::Incremental => {
416-
if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) {
443+
if should_clear_screen(clear_screen, show_progress, plain_output) {
417444
clear_terminal_screen();
418445
print_rebuild_header(CompileType::Incremental);
419446
}
@@ -422,47 +449,36 @@ async fn async_watch(
422449
let result = build::incremental_build(
423450
&mut build_state,
424451
None,
425-
initial_build,
452+
false,
426453
show_progress,
427-
!initial_build,
454+
true,
428455
create_sourcedirs,
429456
plain_output,
430457
);
431458

432459
match result {
433460
Ok(result) => {
434-
if let Some(a) = after_build.clone() {
435-
cmd::run(a)
436-
}
437-
let timing_total_elapsed = timing_total.elapsed();
438-
if show_progress {
439-
let compilation_type = if initial_build { "initial" } else { "incremental" };
440-
if plain_output {
441-
println!("Finished {compilation_type} compilation")
442-
} else {
443-
println!(
444-
"\n{}\n",
445-
build::format_finished_compilation_message(
446-
Some(compilation_type),
447-
result,
448-
timing_total_elapsed,
449-
)
450-
);
451-
}
452-
}
461+
finish_successful_watch_compile(
462+
after_build.clone(),
463+
timing_total,
464+
show_progress,
465+
plain_output,
466+
"Finished incremental compilation",
467+
Some("incremental"),
468+
result,
469+
);
453470
}
454471
Err(_) => {
455-
if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) {
472+
if should_clear_screen(clear_screen, show_progress, plain_output) {
456473
print_build_failed_footer();
457474
}
458475
}
459476
}
460477

461478
needs_compile_type = CompileType::None;
462-
initial_build = false;
463479
}
464480
CompileType::Full => {
465-
if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) {
481+
if should_clear_screen(clear_screen, show_progress, plain_output) {
466482
clear_terminal_screen();
467483
print_rebuild_header(CompileType::Full);
468484
}
@@ -497,7 +513,7 @@ async fn async_watch(
497513
let result = build::incremental_build_without_lock(
498514
&mut build_state,
499515
None,
500-
initial_build,
516+
false,
501517
show_progress,
502518
false,
503519
create_sourcedirs,
@@ -508,34 +524,23 @@ async fn async_watch(
508524
});
509525
match result {
510526
Ok(result) => {
511-
if let Some(a) = after_build.clone() {
512-
cmd::run(a)
513-
}
514-
515-
let timing_total_elapsed = timing_total.elapsed();
516-
if show_progress {
517-
if plain_output {
518-
println!("Finished compilation")
519-
} else {
520-
println!(
521-
"\n{}\n",
522-
build::format_finished_compilation_message(
523-
None,
524-
result,
525-
timing_total_elapsed,
526-
)
527-
);
528-
}
529-
}
527+
finish_successful_watch_compile(
528+
after_build.clone(),
529+
timing_total,
530+
show_progress,
531+
plain_output,
532+
"Finished compilation",
533+
None,
534+
result,
535+
);
530536
}
531537
Err(_) => {
532-
if should_clear_screen(clear_screen, show_progress, plain_output, initial_build) {
538+
if should_clear_screen(clear_screen, show_progress, plain_output) {
533539
print_build_failed_footer();
534540
}
535541
}
536542
}
537543
needs_compile_type = CompileType::None;
538-
initial_build = false;
539544
}
540545
CompileType::None => {
541546
// We want to sleep for a little while so the CPU can schedule other work. That way we end
@@ -569,31 +574,77 @@ pub fn start(
569574

570575
let path = Path::new(folder);
571576

572-
// Do an initial build to discover packages and source folders. Initialization can clean
573-
// previous build artifacts, so it has to be serialized with other build operations.
574-
let build_state: BuildCommandState = build::with_build_lock(path, || {
575-
build::initialize_build(
576-
None,
577-
filter,
577+
let ctrlc_pressed = Arc::new(AtomicBool::new(false));
578+
let ctrlc_pressed_for_handler = Arc::clone(&ctrlc_pressed);
579+
ctrlc::set_handler(move || {
580+
ctrlc_pressed_for_handler.store(true, Ordering::SeqCst);
581+
})
582+
.expect("Error setting Ctrl-C handler");
583+
584+
// Initialization can clean previous build artifacts, so it has to be serialized
585+
// with the initial compile too.
586+
let (build_state, current_watch_paths, initial_compile_result): StartupBuildResult =
587+
build::with_build_lock(path, || {
588+
let mut build_state = build::initialize_build(
589+
None,
590+
filter,
591+
show_progress,
592+
path,
593+
plain_output,
594+
warn_error.clone(),
595+
prod,
596+
features.clone(),
597+
)
598+
.with_context(|| "Could not initialize build")?;
599+
600+
// Compute and register targeted watches based on source folders.
601+
let current_watch_paths = compute_watch_paths(&build_state, path);
602+
register_watches(&mut watcher, &current_watch_paths);
603+
604+
let timing_total = Instant::now();
605+
let initial_compile_result = build::incremental_build_without_lock(
606+
&mut build_state,
607+
None,
608+
true,
609+
show_progress,
610+
false,
611+
create_sourcedirs,
612+
plain_output,
613+
)
614+
.ok()
615+
.map(|result| (timing_total, result));
616+
617+
Ok::<StartupBuildResult, anyhow::Error>((
618+
build_state,
619+
current_watch_paths,
620+
initial_compile_result,
621+
))
622+
})?;
623+
624+
if ctrlc_pressed.load(Ordering::SeqCst) {
625+
cleanup_before_watch_exit(path, &build_state, show_progress, "\nExiting...");
626+
return Ok(());
627+
}
628+
629+
// Run after-build outside build.lock. Hooks may invoke ReScript commands that need the same lock.
630+
if let Some((timing_total, result)) = initial_compile_result {
631+
finish_successful_watch_compile(
632+
after_build.clone(),
633+
timing_total,
578634
show_progress,
579-
path,
580635
plain_output,
581-
warn_error.clone(),
582-
prod,
583-
features.clone(),
584-
)
585-
.with_context(|| "Could not initialize build")
586-
})?;
587-
588-
// Compute and register targeted watches based on source folders
589-
let current_watch_paths = compute_watch_paths(&build_state, path);
590-
register_watches(&mut watcher, &current_watch_paths);
636+
"Finished initial compilation",
637+
Some("initial"),
638+
result,
639+
);
640+
}
591641

592642
async_watch(AsyncWatchArgs {
593643
watcher: &mut watcher,
594644
current_watch_paths,
595645
initial_build_state: build_state,
596646
q: consumer,
647+
ctrlc_pressed,
597648
path,
598649
show_progress,
599650
filter,
@@ -735,12 +786,11 @@ mod tests {
735786
}
736787

737788
#[test]
738-
fn clears_screen_only_for_non_initial_interactive_rebuilds() {
739-
assert!(should_clear_screen(true, true, false, false));
740-
assert!(!should_clear_screen(true, true, false, true));
741-
assert!(!should_clear_screen(true, true, true, false));
742-
assert!(!should_clear_screen(true, false, false, false));
743-
assert!(!should_clear_screen(false, true, false, false));
789+
fn clears_screen_only_for_interactive_rebuilds() {
790+
assert!(should_clear_screen(true, true, false));
791+
assert!(!should_clear_screen(true, true, true));
792+
assert!(!should_clear_screen(true, false, false));
793+
assert!(!should_clear_screen(false, true, false));
744794
}
745795

746796
#[test]

0 commit comments

Comments
 (0)