Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions rewatch/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::build::compile::{mark_modules_with_deleted_deps_dirty, mark_modules_w
use crate::build::compiler_info::{CompilerCheckResult, verify_compiler_info, write_compiler_info};
use crate::helpers::emojis::*;
use crate::helpers::{self};
use crate::lock::{LockKind, drop_lock, get_lock_or_exit};
use crate::project_context::ProjectContext;
use crate::sourcedirs;
use anyhow::{Context, Result, anyhow};
Expand Down Expand Up @@ -146,7 +147,13 @@ pub fn initialize_build(
return Err(anyhow!("Failed to validate package dependencies"));
}

let mut build_state = BuildCommandState::new(project_context, packages, compiler, warn_error);
let mut build_state = BuildCommandState::new(
path.to_path_buf(),
project_context,
packages,
compiler,
warn_error,
);
packages::parse_packages(&mut build_state)?;

let compile_assets_state = read_compile_state::read(&mut build_state)?;
Expand Down Expand Up @@ -238,6 +245,10 @@ pub fn incremental_build(
create_sourcedirs: bool,
plain_output: bool,
) -> Result<(), IncrementalBuildError> {
let build_folder = build_state.root_folder.to_string_lossy().to_string();

let _lock = get_lock_or_exit(LockKind::Build, &build_folder);

logs::initialize(&build_state.packages);
let num_dirty_modules = build_state.modules.values().filter(|m| is_dirty(m)).count() as u64;
let pb = if !plain_output && show_progress {
Expand Down Expand Up @@ -281,6 +292,8 @@ pub fn incremental_build(
}

eprintln!("{}", &err);
let _lock = drop_lock(LockKind::Build, &build_folder);

return Err(IncrementalBuildError {
kind: IncrementalBuildErrorKind::SourceFileParseError,
plain_output,
Expand Down Expand Up @@ -343,9 +356,13 @@ pub fn incremental_build(
|| pb.inc(1),
|size| pb.set_length(size),
)
.map_err(|e| IncrementalBuildError {
kind: IncrementalBuildErrorKind::CompileError(Some(e.to_string())),
plain_output,
.map_err(|e| {
let _lock = drop_lock(LockKind::Build, &build_folder);

IncrementalBuildError {
kind: IncrementalBuildErrorKind::CompileError(Some(e.to_string())),
plain_output,
}
})?;

let compile_duration = start_compiling.elapsed();
Expand Down Expand Up @@ -379,6 +396,9 @@ pub fn incremental_build(
if helpers::contains_ascii_characters(&compile_errors) {
eprintln!("{}", &compile_errors);
}

let _lock = drop_lock(LockKind::Build, &build_folder);

Err(IncrementalBuildError {
kind: IncrementalBuildErrorKind::CompileError(None),
plain_output,
Expand Down Expand Up @@ -409,6 +429,7 @@ pub fn incremental_build(
// Write per-package compiler metadata to `lib/bs/compiler-info.json` (idempotent)
write_compiler_info(build_state);

let _lock = drop_lock(LockKind::Build, &build_folder);
Ok(())
}
}
Expand Down
3 changes: 3 additions & 0 deletions rewatch/src/build/build_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub struct BuildState {
/// - This prevents the "code smell" of optional fields that are None for some commands
#[derive(Debug)]
pub struct BuildCommandState {
pub root_folder: PathBuf,
pub build_state: BuildState,
// Command-line --warn-error flag override (takes precedence over rescript.json config)
pub warn_error_override: Option<String>,
Expand Down Expand Up @@ -171,12 +172,14 @@ impl BuildState {

impl BuildCommandState {
pub fn new(
root_folder: PathBuf,
project_context: ProjectContext,
packages: AHashMap<String, Package>,
compiler: CompilerInfo,
warn_error_override: Option<String>,
) -> Self {
Self {
root_folder,
build_state: BuildState::new(project_context, packages, compiler),
warn_error_override,
}
Expand Down
171 changes: 150 additions & 21 deletions rewatch/src/lock.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
use anyhow::Result;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::process;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use sysinfo::{PidExt, ProcessExt, System, SystemExt};

use crate::queue::FifoQueue;
use crate::queue::*;

/* This locking mechanism is meant to never be deleted. Instead, it stores the PID of the process
* that's running, when trying to aquire a lock, it checks wether that process is still running. If
* not, it rewrites the lockfile to have its own PID instead. */

pub static LOCKFILE: &str = "rescript.lock";
pub enum AwaitLockError {
Watcher(notify::Error),
Timeout(String),
}

impl std::fmt::Display for AwaitLockError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let msg = match self {
AwaitLockError::Watcher(error) => format!("Error starting file watcher {}", error),
AwaitLockError::Timeout(path) => format!("Timeout awaiting lockfile {}", path),
};
write!(f, "{msg}")
}
}

pub enum Error {
Locked(u32),
AwaitingLockFile(AwaitLockError),
ParsingLockfile(std::num::ParseIntError),
ReadingLockfile(std::io::Error),
ReadingLockfile(LockKind, std::io::Error),
WritingLockfile(std::io::Error),
ProjectFolderMissing(std::path::PathBuf),
}
Expand All @@ -28,14 +49,20 @@ impl std::fmt::Display for Error {
Error::ParsingLockfile(e) => format!(
"Could not parse lockfile: \n {e} \n (try removing it and running the command again)"
),
Error::ReadingLockfile(e) => {
format!("Could not read lockfile: \n {e} \n (try removing it and running the command again)")
Error::ReadingLockfile(kind, e) => {
format!(
"Could not read lockfile: {}, \n {e} \n (try removing it and running the command again)",
kind.file_name()
)
}
Error::WritingLockfile(e) => format!("Could not write lockfile: \n {e}"),
Error::ProjectFolderMissing(path) => format!(
"Could not write lockfile because the specified project folder does not exist: {}",
path.to_string_lossy()
),
Error::AwaitingLockFile(await_lock_error) => {
format!("Error awaiting lockfile: {await_lock_error}")
}
};
write!(f, "{msg}")
}
Expand Down Expand Up @@ -72,28 +99,93 @@ fn pid_matches_current_process(to_check_pid: u32) -> bool {
})
}

pub fn get(folder: &str) -> Lock {
#[derive(Clone, Copy)]
pub enum LockKind {
Watch,
Build,
}

impl LockKind {
pub fn file_name(&self) -> String {
String::from(match self {
LockKind::Watch => "watch.lock",
LockKind::Build => "build.lock",
})
}
}

pub const TIMEOUT_SECONDS: u64 = 60;

pub fn await_lock_deletion(location: &Path, kind: LockKind) -> Result<(), Error> {
let now = SystemTime::now();
let queue = Arc::new(FifoQueue::<Result<Event, notify::Error>>::new());
let producer = queue.clone();

let mut watcher = RecommendedWatcher::new(move |res| producer.push(res), Config::default())
.map_err(|e| Error::AwaitingLockFile(AwaitLockError::Watcher(e)))?;

watcher
.watch(location, RecursiveMode::NonRecursive)
.map_err(|e| Error::AwaitingLockFile(AwaitLockError::Watcher(e)))?;

loop {
while !queue.is_empty() {
match queue.pop() {
Ok(Event {
Comment thread
rolandpeelen marked this conversation as resolved.
kind: EventKind::Remove(_),
paths,
..
}) if paths.iter().find(|p| p.ends_with(kind.file_name())).is_some() => return Ok(()),
Ok(_) | Err(_) => (),
}
}

match now.elapsed() {
Ok(elapsed) if elapsed < Duration::from_secs(TIMEOUT_SECONDS) => {
std::thread::sleep(Duration::from_millis(50));
}
Ok(_) | Err(_) => {
return Err(Error::AwaitingLockFile(AwaitLockError::Timeout(
location.to_string_lossy().to_string(),
)));
}
}
}
}

pub fn get(kind: LockKind, folder: &str) -> Lock {
let project_folder = Path::new(folder);
if !project_folder.exists() {
return Lock::Error(Error::ProjectFolderMissing(project_folder.to_path_buf()));
}

let lib_dir = project_folder.join("lib");
let location = lib_dir.join(LOCKFILE);
let location = lib_dir.join(kind.file_name());
let pid = process::id();

// When a lockfile already exists we parse its PID: if the process is still alive we refuse to
// proceed, otherwise we will overwrite the stale lock with our own PID.
match fs::read_to_string(&location) {
Ok(contents) => match contents.parse::<u32>() {
Ok(parsed_pid) if pid_matches_current_process(parsed_pid) => {
return Lock::Error(Error::Locked(parsed_pid));
}
Ok(_) => (),
Err(e) => return Lock::Error(Error::ParsingLockfile(e)),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => (),
Err(e) => return Lock::Error(Error::ReadingLockfile(e)),
loop {
match fs::read_to_string(&location) {
Ok(contents) => match contents.parse::<u32>() {
Ok(parsed_pid) if pid_matches_current_process(parsed_pid) => match kind {
LockKind::Build => {
println!("Awaiting lockfile");
match await_lock_deletion(&lib_dir, kind) {
Ok(_) => {
continue;
}
Err(_) => return Lock::Error(Error::Locked(parsed_pid)),
};
}
LockKind::Watch => return Lock::Error(Error::Locked(parsed_pid)),
},
Ok(_) => break,
Err(e) => return Lock::Error(Error::ParsingLockfile(e)),
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => break,
Err(e) => return Lock::Error(Error::ReadingLockfile(kind, e)),
}
}

if let Err(e) = fs::create_dir_all(&lib_dir) {
Expand All @@ -110,6 +202,31 @@ pub fn get(folder: &str) -> Lock {
}
}

pub fn get_lock_or_exit(kind: LockKind, folder: &str) -> Lock {
match get(kind, folder) {
Lock::Error(error) => {
eprintln!("Could not start Rescript build: {error}");
std::process::exit(1);
}

acquired_lock => acquired_lock,
}
}

pub fn drop_lock(kind: LockKind, folder: &str) -> Result<()> {
let project_folder = Path::new(folder);
if !project_folder.exists() {
return Ok(());
}

let lib_dir = project_folder.join("lib");
let location = lib_dir.join(kind.file_name());

fs::remove_file(&location)?;

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -121,7 +238,10 @@ mod tests {
let temp_dir = TempDir::new().expect("temp dir should be created");
let missing_folder = temp_dir.path().join("missing_project");

match get(missing_folder.to_str().expect("path should be valid")) {
match get(
LockKind::Watch,
missing_folder.to_str().expect("path should be valid"),
) {
Lock::Error(Error::ProjectFolderMissing(path)) => {
assert_eq!(path, missing_folder);
}
Expand All @@ -140,7 +260,10 @@ mod tests {
let project_folder = temp_dir.path().join("project");
fs::create_dir(&project_folder).expect("project folder should be created");

match get(project_folder.to_str().expect("path should be valid")) {
match get(
LockKind::Watch,
project_folder.to_str().expect("path should be valid"),
) {
Lock::Aquired(_) => {}
_ => panic!("expected lock to be acquired"),
}
Expand All @@ -150,7 +273,10 @@ mod tests {
"lib directory should be created"
);
assert!(
project_folder.join("lib").join(LOCKFILE).exists(),
project_folder
.join("lib")
.join(LockKind::Watch.file_name())
.exists(),
"lockfile should be created"
);
}
Expand All @@ -161,9 +287,12 @@ mod tests {
let project_folder = temp_dir.path().join("project");
let lib_dir = project_folder.join("lib");
fs::create_dir_all(&lib_dir).expect("lib directory should be created");
fs::write(lib_dir.join(LOCKFILE), "1").expect("lockfile should be written");
fs::write(lib_dir.join(LockKind::Watch.file_name()), "1").expect("lockfile should be written");

match get(project_folder.to_str().expect("path should be valid")) {
match get(
LockKind::Watch,
project_folder.to_str().expect("path should be valid"),
) {
Lock::Aquired(_) => {}
_ => panic!("expected stale lock from unrelated process to be ignored"),
}
Expand Down
Loading
Loading