diff --git a/src/bin/mangotune-preview/main.rs b/src/bin/mangotune-preview/main.rs index 458394e..70bd644 100644 --- a/src/bin/mangotune-preview/main.rs +++ b/src/bin/mangotune-preview/main.rs @@ -115,6 +115,15 @@ impl Default for SimState { .and_then(|value| value.parse::().ok()) .unwrap_or(120); let present_mode = PreviewPresentMode::from_env(); + let paused = std::env::var("MANGOTUNE_PREVIEW_PAUSED") + .ok() + .map(|value| { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false); Self { gpu_load_target, @@ -125,7 +134,7 @@ impl Default for SimState { gpu_passes, interaction_steps, present_mode, - paused: false, + paused, should_quit: false, scene_preset, } @@ -152,11 +161,18 @@ fn main() -> Result<()> { let state = Arc::new(Mutex::new(SimState::default())); let state_for_socket = Arc::clone(&state); - std::thread::spawn(move || { + let socket_thread = std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); rt.block_on(socket_api::run(state_for_socket)) .expect("socket API crashed"); }); - renderer::run(state) + let result = renderer::run(Arc::clone(&state)); + + if let Ok(mut sim) = state.lock() { + sim.should_quit = true; + } + + let _ = socket_thread.join(); + result } diff --git a/src/bin/mangotune-preview/socket_api.rs b/src/bin/mangotune-preview/socket_api.rs index f50e8d8..70c9c8c 100644 --- a/src/bin/mangotune-preview/socket_api.rs +++ b/src/bin/mangotune-preview/socket_api.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixListener; +use tokio::time::{timeout, Duration}; pub fn socket_path() -> String { std::env::var("MANGOTUNE_SOCKET").unwrap_or_else(|_| "/tmp/mangotune_preview.sock".into()) @@ -71,8 +72,12 @@ pub async fn run(state: Arc>) -> Result<()> { log::info!("Socket API listening on {}", path); loop { - match listener.accept().await { - Ok((stream, _addr)) => { + if should_quit(&state) { + break; + } + + match timeout(Duration::from_millis(100), listener.accept()).await { + Ok(Ok((stream, _addr))) => { let state = Arc::clone(&state); tokio::spawn(async move { if let Err(err) = handle_connection(stream, state).await { @@ -80,11 +85,19 @@ pub async fn run(state: Arc>) -> Result<()> { } }); } - Err(err) => { + Ok(Err(err)) => { log::error!("Socket accept error: {}", err); } + Err(_) => {} } } + + let _ = tokio::fs::remove_file(&path).await; + Ok(()) +} + +fn should_quit(state: &Arc>) -> bool { + state.lock().map(|sim| sim.should_quit).unwrap_or(true) } async fn handle_connection( @@ -109,6 +122,10 @@ async fn handle_connection( let mut bytes = serde_json::to_vec(&response)?; bytes.push(b'\n'); write_half.write_all(&bytes).await?; + + if should_quit(&state) { + break; + } } Ok(()) diff --git a/src/launcher/runner.rs b/src/launcher/runner.rs index b644608..caf6bc2 100644 --- a/src/launcher/runner.rs +++ b/src/launcher/runner.rs @@ -87,6 +87,16 @@ impl Runner { Ok(()) } + /// Kill a running process immediately (SIGKILL, then wait). + pub fn kill(mut process: RunningProcess) -> Result<()> { + let _ = kill(Pid::from_raw(process.child.id() as i32), Signal::SIGKILL); + process + .child + .wait() + .map(|_| ()) + .map_err(|err| anyhow!("failed waiting for process exit: {err}")) + } + /// Send SIGUSR1 to reload config in a running MangoHud process. pub fn reload_config(pid: u32) -> Result<()> { kill(Pid::from_raw(pid as i32), Signal::SIGUSR1)?; diff --git a/src/preview/mod.rs b/src/preview/mod.rs index 0af95e5..820a24c 100644 --- a/src/preview/mod.rs +++ b/src/preview/mod.rs @@ -150,6 +150,10 @@ impl PreviewScene { "MANGOTUNE_PREVIEW_VSYNC".to_string(), if studio.vsync { "1" } else { "0" }.to_string(), ), + ( + "MANGOTUNE_PREVIEW_PAUSED".to_string(), + if studio.paused { "1" } else { "0" }.to_string(), + ), ], }) } @@ -379,13 +383,7 @@ impl PreviewController { }; if let Some(running) = running { - if running.request.scene == PreviewScene::Studio { - let _ = send_studio_command(&socket_path, r#"{"cmd":"quit"}"#); - std::thread::sleep(Duration::from_millis(120)); - } - Runner::stop(running.process)?; - let _ = fs::remove_file(socket_path); - let _ = fs::remove_file(temp_config_path); + stop_running_preview(running, &socket_path, &temp_config_path)?; return Ok(true); } @@ -461,10 +459,6 @@ impl PreviewController { state.current.take() }; - if let Some(running) = running { - let _ = Runner::stop(running.process); - } - let (temp_path, socket_path) = { let Ok(state) = self.inner.lock() else { return Err(anyhow!("could not access preview state")); @@ -475,6 +469,10 @@ impl PreviewController { ) }; + if let Some(running) = running { + let _ = stop_running_preview(running, &socket_path, &temp_path); + } + let layout_width = request.layout_width_override.unwrap_or(request.width); write_preview_config(config, &temp_path, layout_width, request.scene)?; if request.scene == PreviewScene::Studio { @@ -503,7 +501,6 @@ impl PreviewController { if request.scene == PreviewScene::Studio { wait_for_studio_socket(&socket_path, Duration::from_secs(3)) .context("studio preview socket did not appear")?; - apply_studio_runtime_settings(&socket_path, &request.studio)?; } let Ok(mut state) = self.inner.lock() else { @@ -516,6 +513,24 @@ impl PreviewController { } } +fn stop_running_preview( + running: RunningPreview, + socket_path: &Path, + temp_config_path: &Path, +) -> Result<()> { + if running.request.scene == PreviewScene::Studio { + Runner::kill(running.process)?; + let _ = fs::remove_file(socket_path); + let _ = fs::remove_file(temp_config_path); + return Ok(()); + } + + Runner::stop(running.process)?; + let _ = fs::remove_file(socket_path); + let _ = fs::remove_file(temp_config_path); + Ok(()) +} + fn write_preview_config( config: &AnnotatedConfig, path: &PathBuf,