diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index 3b1ca604bd7..f03dd41165b 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -751,7 +751,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E .and_then(|c| c.get_one::("server").ok().flatten()); let server_for_client = server_opt_client.as_deref().unwrap_or(resolved_server); let server_host_url = config.get_host_url(Some(server_for_client))?; - let _client_handle = if let Some(ref cmd) = client_command { + let mut client_handle = if let Some(ref cmd) = client_command { let mut child = start_client_process(cmd, &project_dir, db_name_for_client, &server_host_url)?; // Give the process a moment to fail fast (e.g., command not found, missing deps) @@ -802,40 +802,74 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E let mut debounce_timer; loop { - if rx.recv().is_ok() { - debounce_timer = std::time::Instant::now(); - while debounce_timer.elapsed() < Duration::from_millis(300) { - if rx.recv_timeout(Duration::from_millis(100)).is_ok() { - debounce_timer = std::time::Instant::now(); + // Use recv_timeout so we can periodically check if the client process exited + match rx.recv_timeout(Duration::from_secs(1)) { + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break Ok(()), + Ok(()) => { + debounce_timer = std::time::Instant::now(); + while debounce_timer.elapsed() < Duration::from_millis(300) { + if rx.recv_timeout(Duration::from_millis(100)).is_ok() { + debounce_timer = std::time::Instant::now(); + } } - } - println!("\n{}", "File change detected, rebuilding...".yellow()); - match generate_build_and_publish( - &config, - &project_dir, - loaded_config_dir.as_deref(), - &spacetimedb_dir, - &module_bindings_dir, - client_language, - clear_database, - &publish_configs, - &generate_configs_from_file, - using_spacetime_config, - server_from_cli, - force, - skip_publish, - skip_generate, - ) - .await - { - Ok(_) => {} - Err(e) => { - eprintln!("{} {}", "Error:".red().bold(), e); - println!("{}", "Waiting for next change...".dimmed()); + println!("\n{}", "File change detected, rebuilding...".yellow()); + match generate_build_and_publish( + &config, + &project_dir, + loaded_config_dir.as_deref(), + &spacetimedb_dir, + &module_bindings_dir, + client_language, + clear_database, + &publish_configs, + &generate_configs_from_file, + using_spacetime_config, + server_from_cli, + force, + skip_publish, + skip_generate, + ) + .await + { + Ok(_) => {} + Err(e) => { + eprintln!("{} {}", "Error:".red().bold(), e); + println!("{}", "Waiting for next change...".dimmed()); + } } } - } + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { + // No rebuild yet. Check if the client process has exited. + let Some(ref mut child) = client_handle else { + continue; + }; + match child.try_wait() { + Ok(None) => {} + Ok(Some(status)) => { + client_handle = None; + let code = status + .code() + .map(|c| c.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + println!( + "\n{} {}. {}", + "Client process exited with code".yellow(), + code, + "File watcher is still active.".dimmed() + ); + } + Err(e) => { + client_handle = None; + eprintln!( + "\n{} Failed to check client process status: {}", + "Warning:".yellow().bold(), + e + ); + } + } + } + }; } }