Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ engines/colmap/**
engines/brush/**
!engines/brush/README.md
engines/linux/brush/**
!engines/linux/brush/README.md
engines/macos/arm64/**
!engines/macos/arm64/README.md
7 changes: 7 additions & 0 deletions engines/linux/brush/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Brush v0.3.0 Ubuntu x86_64 runtime. `npm run setup:engines:linux` downloads and
verifies `brush_app` into this directory; the binary itself is never committed.

This README is tracked so the directory exists in a fresh clone.
`src-tauri/tauri.linux.conf.json` declares the directory as a bundle resource,
and the Tauri build script aborts when a declared resource path is missing --
which would otherwise make `cargo test` fail before compiling any test.
8 changes: 8 additions & 0 deletions engines/macos/arm64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Apple Silicon macOS runtime. `npm run setup:engines:macos` downloads and verifies
the arm64 FFmpeg/FFprobe/COLMAP/Brush closure into `bin/` and `lib/`; the
binaries themselves are never committed.

This README is tracked so the directory exists in a fresh clone.
`src-tauri/tauri.macos.conf.json` declares the directory as a bundle resource,
and the Tauri build script aborts when a declared resource path is missing --
which would otherwise make `cargo test` fail before compiling any test.
4 changes: 2 additions & 2 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,9 @@ pub async fn begin_gaussian_video_export(
let (root, _, _) = catalog::registered_final_ply_for_project(project_id).await?;

let active = state.active.lock().await;
if !active
if active
.as_ref()
.is_some_and(|session| session.project_id == project_id)
.is_none_or(|session| session.project_id != project_id)
{
return Err(SplatError::Process(
"该项目当前未在 OOOSplat 预览中打开,无法导出视频。".into(),
Expand Down
10 changes: 9 additions & 1 deletion src-tauri/src/engines/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,15 @@ mod tests {
let paths = EnginePaths::from_root("/opt/ooosplat-engines");
assert_eq!(paths.colmap, PathBuf::from("/opt/ooosplat-engines/colmap"));
let discovered = EnginePaths::from_candidates(PathBuf::from("/missing/engines"));
assert!(discovered.ffmpeg.is_file());
// FFmpeg is not installed on every contributor machine or CI runner, so assert
// the resolver contract instead of requiring the binary: an explicit override
// wins, then PATH, and otherwise the managed path is kept so engine health can
// report the exact file it expected.
let expected = std::env::var_os("OOOSPLAT_FFMPEG")
.map(PathBuf::from)
.or_else(|| find_on_path("ffmpeg"))
.unwrap_or_else(|| PathBuf::from("/missing/engines/ffmpeg"));
assert_eq!(discovered.ffmpeg, expected);
}

#[cfg(target_os = "macos")]
Expand Down
41 changes: 39 additions & 2 deletions src-tauri/src/process/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,44 @@ mod tests {
let pid = descendant_pid.lock().unwrap().expect("descendant PID");
manager.cancel();
assert!(matches!(run.await.unwrap(), Err(SplatError::Cancelled)));
tokio::time::sleep(Duration::from_millis(50)).await;
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());

let mut terminated = false;
for _ in 0..150 {
if descendant_is_terminated(pid) {
terminated = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
terminated,
"descendant {pid} was still running after cancellation"
);
}

/// A killed descendant is reparented to PID 1, and PID 1 does not reap in every
/// container, so the process can linger as a zombie with its `/proc` entry intact.
/// The entry being gone and the entry being a zombie both mean the process stopped
/// running, which is what cancellation has to guarantee.
#[cfg(target_os = "linux")]
fn descendant_is_terminated(pid: u32) -> bool {
let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) else {
return true;
};
status
.lines()
.find_map(|line| line.strip_prefix("State:"))
.map(|state| state.trim_start().starts_with('Z'))
.unwrap_or(true)
}

/// Unix targets without `/proc` reap orphans through PID 1, so a terminated
/// descendant disappears outright rather than lingering as a visible zombie.
#[cfg(all(unix, not(target_os = "linux")))]
fn descendant_is_terminated(pid: u32) -> bool {
// SAFETY: signal 0 only runs the existence and permission checks for the PID
// and delivers nothing, so no process state is observed or modified.
let alive = unsafe { libc::kill(pid as libc::pid_t, 0) } == 0;
!alive
}
}