fix: hard-stop studio preview to avoid teardown corruption

The disposable preview process was tripping MangoHud's shutdown path and
spamming allocator corruption messages on stop/restart. Use an immediate
kill for Studio preview teardown and give the preview child a cleaner socket
thread shutdown path so the live preview controls stay fast and quiet.
This commit is contained in:
2026-03-31 18:51:29 -04:00
parent fc840e9e98
commit 21ed77c74c
4 changed files with 76 additions and 18 deletions
+19 -3
View File
@@ -115,6 +115,15 @@ impl Default for SimState {
.and_then(|value| value.parse::<u32>().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
}
+20 -3
View File
@@ -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<Mutex<SimState>>) -> 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<Mutex<SimState>>) -> 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<Mutex<SimState>>) -> 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(())
+10
View File
@@ -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)?;
+27 -12
View File
@@ -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,