tauri-app: spawn record-daemon on startup if not present
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m14s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m14s
This commit is contained in:
@@ -26,5 +26,5 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
directories = "6"
|
||||
ffmpeg-sidecar = "2.4"
|
||||
tokio = { version = "1", features = ["net", "io-util", "time"] }
|
||||
tokio = { version = "1", features = ["net", "io-util", "time", "rt"] }
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! using the same JSON-over-newline-delimited protocol.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -236,6 +236,158 @@ impl IpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Daemon binary discovery & process spawning
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Find the record-daemon binary.
|
||||
///
|
||||
/// Search order:
|
||||
/// 1. Next to the current executable (bundled / installed mode)
|
||||
/// 2. In `<exe_dir>/../resources/` (macOS .app bundle)
|
||||
/// 3. Relative to the executable – walk up to the workspace root and look for
|
||||
/// `record-daemon/target/{release,debug}/` (development mode)
|
||||
/// 4. Relative to the current working directory (fallback for dev mode)
|
||||
fn find_daemon_binary() -> Option<PathBuf> {
|
||||
let exe_name = if cfg!(windows) {
|
||||
"record-daemon.exe"
|
||||
} else {
|
||||
"record-daemon"
|
||||
};
|
||||
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
// 1. Next to the current executable
|
||||
if let Some(exe_dir) = exe_path.parent() {
|
||||
let candidate = exe_dir.join(exe_name);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. In <exe_dir>/../resources/
|
||||
if let Some(exe_dir) = exe_path.parent().and_then(|p| p.parent()) {
|
||||
let candidate = exe_dir.join("resources").join(exe_name);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Development mode – relative to the exe location.
|
||||
// The exe is typically at <workspace>/tauri-app/src-tauri/target/debug/tauri-app.exe
|
||||
// so we walk up to find <workspace>/record-daemon/target/{release,debug}/
|
||||
if let Some(exe_dir) = exe_path.parent() {
|
||||
// Try walking up from the exe directory to find the daemon
|
||||
let dev_suffixes = [
|
||||
Path::new("record-daemon/target/release"),
|
||||
Path::new("record-daemon/target/debug"),
|
||||
];
|
||||
// Walk up at most 6 levels from the exe directory
|
||||
let mut dir = exe_dir.to_path_buf();
|
||||
for _ in 0..6 {
|
||||
for suffix in &dev_suffixes {
|
||||
let candidate = dir.join(suffix).join(exe_name);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback – look relative to the current working directory
|
||||
let cwd_dev_paths = [
|
||||
Path::new("../../record-daemon/target/release"),
|
||||
Path::new("../../record-daemon/target/debug"),
|
||||
Path::new("../record-daemon/target/release"),
|
||||
Path::new("../record-daemon/target/debug"),
|
||||
];
|
||||
for dir in &cwd_dev_paths {
|
||||
let candidate = dir.join(exe_name);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Spawn the record-daemon as a detached background process.
|
||||
///
|
||||
/// Returns `Ok(())` if the process was successfully spawned.
|
||||
fn spawn_daemon() -> Result<(), String> {
|
||||
let binary = find_daemon_binary()
|
||||
.ok_or_else(|| "record-daemon binary not found".to_string())?;
|
||||
|
||||
eprintln!("[daemon] Spawning record-daemon: {}", binary.display());
|
||||
|
||||
let mut cmd = std::process::Command::new(&binary);
|
||||
cmd.arg("--foreground");
|
||||
|
||||
// Set the working directory to the daemon binary's directory so it can
|
||||
// find its bundled resources (OBS plugins, etc.) relative to itself.
|
||||
if let Some(bin_dir) = binary.parent() {
|
||||
cmd.current_dir(bin_dir);
|
||||
}
|
||||
|
||||
// Detach from the parent process group so the daemon survives if the
|
||||
// Tauri app is restarted.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
// CREATE_NEW_PROCESS_GROUP (0x200) | DETACHED_PROCESS (0x8)
|
||||
cmd.creation_flags(0x00000200 | 0x00000008);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
// Start in a new session so the daemon gets its own process group
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
// Redirect stdio so the daemon doesn't inherit our handles
|
||||
cmd.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null());
|
||||
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to spawn record-daemon: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the record-daemon is running.
|
||||
///
|
||||
/// If the daemon is already reachable via IPC, returns immediately.
|
||||
/// Otherwise, spawns the daemon binary and waits up to `timeout_secs` seconds
|
||||
/// for it to become ready.
|
||||
pub async fn ensure_daemon_running(timeout_secs: u64) -> Result<bool, String> {
|
||||
let client = IpcClient::new();
|
||||
|
||||
// Fast path – already running
|
||||
if client.is_daemon_running().await {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Spawn the daemon
|
||||
spawn_daemon()?;
|
||||
|
||||
// Poll until the daemon responds or we time out
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
|
||||
loop {
|
||||
if client.is_daemon_running().await {
|
||||
return Ok(true);
|
||||
}
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Ok(false);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tauri commands
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -247,6 +399,15 @@ pub async fn daemon_is_running() -> bool {
|
||||
client.is_daemon_running().await
|
||||
}
|
||||
|
||||
/// Ensure the record-daemon is running, starting it if necessary.
|
||||
///
|
||||
/// Returns `true` if the daemon is running (either already was or just started),
|
||||
/// `false` if it could not be started within the timeout.
|
||||
#[tauri::command]
|
||||
pub async fn daemon_ensure_running() -> Result<bool, String> {
|
||||
ensure_daemon_running(30).await
|
||||
}
|
||||
|
||||
/// Get the daemon status.
|
||||
#[tauri::command]
|
||||
pub async fn daemon_get_status() -> Result<serde_json::Value, String> {
|
||||
|
||||
@@ -395,6 +395,28 @@ fn get_video_metadata(video_path: String) -> Result<Value, String> {
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
// Auto-start the record-daemon on app launch.
|
||||
// We spawn a background task so the UI isn't blocked while the
|
||||
// daemon boots (it can take a few seconds to initialise OBS).
|
||||
let handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match daemon_ipc::ensure_daemon_running(30).await {
|
||||
Ok(true) => {
|
||||
eprintln!("[tauri] record-daemon is running");
|
||||
}
|
||||
Ok(false) => {
|
||||
eprintln!("[tauri] record-daemon did not become ready within the timeout");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[tauri] failed to start record-daemon: {}", e);
|
||||
}
|
||||
}
|
||||
// Drop the handle – no further use
|
||||
drop(handle);
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_game_history,
|
||||
get_timeline,
|
||||
@@ -408,6 +430,7 @@ pub fn run() {
|
||||
get_video_metadata,
|
||||
// Daemon IPC commands
|
||||
daemon_ipc::daemon_is_running,
|
||||
daemon_ipc::daemon_ensure_running,
|
||||
daemon_ipc::daemon_get_status,
|
||||
daemon_ipc::daemon_get_settings,
|
||||
daemon_ipc::daemon_update_settings,
|
||||
|
||||
Reference in New Issue
Block a user