From c4fffad0275284258034d19a670a1e3e10ec664b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 13:53:43 -0700 Subject: [PATCH] Fix permissions on demand instead of every launch Instead of chmod on every app start, catch EACCES (error 13) when spawning sidecar or ffmpeg, fix permissions, then retry once: - sidecar spawn: catches permission denied, runs set_executable_permissions on the sidecar dir, retries spawn - ffmpeg: catches permission denied, chmod +x ffmpeg and ffprobe, retries Zero overhead on normal launches. Only fixes permissions when actually needed. Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/commands/media.rs | 34 +++++++++++++++++++++++--- src-tauri/src/sidecar/mod.rs | 42 ++++++++++++++++++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/commands/media.rs b/src-tauri/src/commands/media.rs index c1f31f1..ff0017f 100644 --- a/src-tauri/src/commands/media.rs +++ b/src-tauri/src/commands/media.rs @@ -50,9 +50,37 @@ pub fn extract_audio(file_path: String, output_path: Option) -> Result s, + Err(e) if e.raw_os_error() == Some(13) => { + // Permission denied — fix permissions and retry + eprintln!("[media] Permission denied on ffmpeg, fixing permissions and retrying..."); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Ok(meta) = std::fs::metadata(&ffmpeg) { + let mut perms = meta.permissions(); + perms.set_mode(0o755); + let _ = std::fs::set_permissions(&ffmpeg, perms); + } + // Also fix ffprobe if it exists + let ffprobe = ffmpeg.replace("ffmpeg", "ffprobe"); + if let Ok(meta) = std::fs::metadata(&ffprobe) { + let mut perms = meta.permissions(); + perms.set_mode(0o755); + let _ = std::fs::set_permissions(&ffprobe, perms); + } + } + Command::new(&ffmpeg) + .args(["-y", "-i", &file_path, "-vn", "-acodec", "pcm_s16le", "-ar", "22050", "-ac", "1"]) + .arg(output.to_str().unwrap()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()) + .status() + .map_err(|e| format!("Failed to run ffmpeg after chmod: {e}"))? + } + Err(e) => return Err(format!("Failed to run ffmpeg: {e}")), + }; if !status.success() { return Err(format!("ffmpeg exited with status {status}")); diff --git a/src-tauri/src/sidecar/mod.rs b/src-tauri/src/sidecar/mod.rs index f892ce0..a4da97b 100644 --- a/src-tauri/src/sidecar/mod.rs +++ b/src-tauri/src/sidecar/mod.rs @@ -98,9 +98,6 @@ impl SidecarManager { // Already extracted — use it directly if binary_path.exists() { - // Ensure all binaries are executable (fixes previously extracted dirs) - #[cfg(unix)] - Self::set_executable_permissions(&extract_dir); Self::cleanup_old_sidecars(data_dir, ¤t_version); return Ok(binary_path); } @@ -334,12 +331,39 @@ impl SidecarManager { #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); - let child = cmd - .spawn() - .map_err(|e| format!("Failed to start sidecar binary: {e}"))?; - - self.attach(child)?; - self.wait_for_ready() + match cmd.spawn() { + Ok(child) => { + self.attach(child)?; + self.wait_for_ready() + } + Err(e) if e.raw_os_error() == Some(13) => { + // Permission denied — fix permissions and retry once + eprintln!("[sidecar-rs] Permission denied, fixing permissions and retrying..."); + if let Some(dir) = path.parent() { + Self::set_executable_permissions(dir); + } + let mut retry_cmd = Command::new(path); + retry_cmd + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(if let Some(data_dir) = DATA_DIR.get() { + let log_path = data_dir.join("sidecar.log"); + std::fs::File::create(&log_path) + .map(Stdio::from) + .unwrap_or_else(|_| Stdio::inherit()) + } else { + Stdio::inherit() + }); + #[cfg(target_os = "windows")] + retry_cmd.creation_flags(0x08000000); + let child = retry_cmd + .spawn() + .map_err(|e| format!("Failed to start sidecar binary after chmod: {e}"))?; + self.attach(child)?; + self.wait_for_ready() + } + Err(e) => Err(format!("Failed to start sidecar binary: {e}")), + } } /// Spawn the Python sidecar in dev mode (system Python).