From 8129c9bc557b9ce41735cbaf927dce7e3c766066 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Wed, 25 Feb 2026 20:02:31 -0500 Subject: [PATCH 1/2] Print feedback when client process exits during spacetime dev The main event loop blocked on file change events with no periodic check on the client process. If the client exited (e.g., user pressed Enter in the basic-rs template), spacetime dev gave no feedback. Now checks the client process status every second. On exit, prints: - 'Client process exited. File watcher is still active.' (success) - 'Warning: Client process exited with code N. File watcher is still active.' (failure) The file watcher continues running so module changes are still detected. --- crates/cli/src/subcommands/dev.rs | 50 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index 3b1ca604bd7..beded9b25d7 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) @@ -801,8 +801,54 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E } let mut debounce_timer; + let mut client_exited = false; loop { - if rx.recv().is_ok() { + // Use recv_timeout so we can periodically check if the client process exited + let got_event = match rx.recv_timeout(Duration::from_secs(1)) { + Ok(()) => true, + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => false, + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break Ok(()), + }; + + // Check if the client process has exited + if !client_exited { + if let Some(ref mut child) = client_handle { + match child.try_wait() { + Ok(Some(status)) => { + client_exited = true; + if status.success() { + println!( + "\n{} {}", + "Client process exited.".yellow(), + "File watcher is still active.".dimmed() + ); + } else { + let code = status + .code() + .map(|c| c.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + eprintln!( + "\n{} Client process exited with code {}. {}", + "Warning:".yellow().bold(), + code, + "File watcher is still active.".dimmed() + ); + } + } + Ok(None) => {} // Still running + Err(e) => { + client_exited = true; + eprintln!( + "\n{} Failed to check client process status: {}", + "Warning:".yellow().bold(), + e + ); + } + } + } + } + + if got_event { debounce_timer = std::time::Instant::now(); while debounce_timer.elapsed() < Duration::from_millis(300) { if rx.recv_timeout(Duration::from_millis(100)).is_ok() { From b0d0620758bf6a813fe4f808da649143023ce01c Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Wed, 25 Feb 2026 18:05:58 -0800 Subject: [PATCH 2/2] [bot/dev-client-exit-feedback]: review --- crates/cli/src/subcommands/dev.rs | 118 ++++++++++++++---------------- 1 file changed, 53 insertions(+), 65 deletions(-) diff --git a/crates/cli/src/subcommands/dev.rs b/crates/cli/src/subcommands/dev.rs index beded9b25d7..f03dd41165b 100644 --- a/crates/cli/src/subcommands/dev.rs +++ b/crates/cli/src/subcommands/dev.rs @@ -801,43 +801,66 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E } let mut debounce_timer; - let mut client_exited = false; loop { // Use recv_timeout so we can periodically check if the client process exited - let got_event = match rx.recv_timeout(Duration::from_secs(1)) { - Ok(()) => true, - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => false, + 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(); + } + } - // Check if the client process has exited - if !client_exited { - if let Some(ref mut child) = client_handle { + 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_exited = true; - if status.success() { - println!( - "\n{} {}", - "Client process exited.".yellow(), - "File watcher is still active.".dimmed() - ); - } else { - let code = status - .code() - .map(|c| c.to_string()) - .unwrap_or_else(|| "unknown".to_string()); - eprintln!( - "\n{} Client process exited with code {}. {}", - "Warning:".yellow().bold(), - code, - "File watcher is still active.".dimmed() - ); - } + 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() + ); } - Ok(None) => {} // Still running Err(e) => { - client_exited = true; + client_handle = None; eprintln!( "\n{} Failed to check client process status: {}", "Warning:".yellow().bold(), @@ -846,42 +869,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E } } } - } - - if got_event { - 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()); - } - } - } + }; } }