Skip to content

Commit ea887d5

Browse files
yozhgoorcecton
andauthored
feat: block HTTP requests during active rebuilds via WatchLock (#81)
## Summary - Obtain the shared `WatchLock` from the configured watcher before spawning the watch thread - Pass the lock into the HTTP serving loop - Acquire a read guard per request, after parsing the header, so file reads cannot race with rebuild writes - Re-export `WatchLock` and `WatchLockGuard` from `xtask-wasm` so consumers don't need a direct `xtask-watch` dependency to name the types - Document why `read_header` is intentionally called before acquiring the lock (connections are accepted and headers parsed during a build, reducing response latency once it finishes) ## Why Without coordination, a browser refresh mid-rebuild could receive incomplete or inconsistent dist artifacts. Using the shared watcher lock ensures the dev server only serves files once the current build has fully written its output. Closes #80 Requires rustminded/xtask-watch#34 --------- Co-authored-by: Cecile Tonglet <cecile.tonglet@cecton.com>
1 parent e9dac76 commit ea887d5

3 files changed

Lines changed: 24 additions & 4 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ log = "0.4.14"
3535
sass-rs = { version = "0.2.2", optional = true }
3636
walkdir = "2.3.2"
3737
wasm-bindgen-cli-support = "0.2.100"
38-
xtask-watch = "0.3.2"
38+
xtask-watch = "0.3.3"
3939

4040
[target.'cfg(unix)'.dependencies]
4141
libc = "0.2.112"

src/dev_server.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::{
1313
sync::Arc,
1414
thread,
1515
};
16+
use xtask_watch::WatchLock;
1617

1718
type RequestHandler = Arc<dyn Fn(Request) -> Result<()> + Send + Sync + 'static>;
1819

@@ -352,6 +353,8 @@ impl DevServer {
352353
}
353354
let dist_dir = self.dist_dir.clone().unwrap();
354355

356+
let watch_lock = self.watch.lock();
357+
355358
let watch_process = {
356359
// mem::take so we can pass &self to build_command while the fields are empty.
357360
let pre_hooks = std::mem::take(&mut self.pre_hooks);
@@ -373,7 +376,8 @@ impl DevServer {
373376
format!("cannot create dist directory `{}`", dist_dir.display())
374377
})?;
375378
let watch = self.watch.exclude_path(&dist_dir);
376-
let handle = std::thread::spawn(|| match watch.run(commands) {
379+
380+
let handle = std::thread::spawn(move || match watch.run(commands) {
377381
Ok(()) => log::trace!("Starting to watch"),
378382
Err(err) => log::error!("an error occurred when starting to watch: {err}"),
379383
});
@@ -385,15 +389,23 @@ impl DevServer {
385389
};
386390

387391
if let Some(handler) = self.request_handler {
388-
serve(self.ip, self.port, dist_dir, self.not_found_path, handler)
389-
.context("an error occurred when starting to serve")?;
392+
serve(
393+
self.ip,
394+
self.port,
395+
dist_dir,
396+
self.not_found_path,
397+
handler,
398+
watch_lock,
399+
)
400+
.context("an error occurred when starting to serve")?;
390401
} else {
391402
serve(
392403
self.ip,
393404
self.port,
394405
dist_dir,
395406
self.not_found_path,
396407
Arc::new(default_request_handler),
408+
watch_lock,
397409
)
398410
.context("an error occurred when starting to serve")?;
399411
}
@@ -428,6 +440,7 @@ fn serve(
428440
dist_dir: PathBuf,
429441
not_found_path: Option<PathBuf>,
430442
handler: RequestHandler,
443+
watch_lock: WatchLock,
431444
) -> Result<()> {
432445
let address = SocketAddr::new(ip, port);
433446
let listener = TcpListener::bind(address).context("cannot bind to the given address")?;
@@ -450,8 +463,14 @@ fn serve(
450463
let handler = handler.clone();
451464
let dist_dir = dist_dir.clone();
452465
let not_found_path = not_found_path.clone();
466+
let watch_lock = watch_lock.clone();
453467
thread::spawn(move || {
468+
// Read the request header *before* acquiring the watch lock so that connections
469+
// can be accepted and parsed while a rebuild is in progress. This reduces
470+
// perceived latency: the response is dispatched immediately once the build
471+
// finishes rather than having to re-parse the header afterward.
454472
let header = warn_not_fail!(read_header(&stream));
473+
let _guard = watch_lock.acquire();
455474
let request = Request {
456475
stream: &mut stream,
457476
header: header.as_ref(),

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::process::Command;
88
#[cfg(not(target_arch = "wasm32"))]
99
pub use xtask_watch::{
1010
anyhow, cargo_metadata, cargo_metadata::camino, clap, metadata, package, xtask_command, Watch,
11+
WatchLock, WatchLockGuard,
1112
};
1213

1314
#[cfg(not(target_arch = "wasm32"))]

0 commit comments

Comments
 (0)