mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix(macos): unify bundled yt-dlp detection
This commit is contained in:
@@ -179,7 +179,7 @@ console.log('\n─── 6. yt-dlp packaging ───');
|
||||
// ───── Check 7, 8 & 9: Engine version self-tests ─────
|
||||
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
|
||||
|
||||
function runEngine(label, engine, args) {
|
||||
function runEngine(label, engine, args, timeout = 30000) {
|
||||
const p = binPath(engine);
|
||||
if (!fs.existsSync(p)) {
|
||||
fail(`${label} binary not found at ${p}`);
|
||||
@@ -189,7 +189,7 @@ function runEngine(label, engine, args) {
|
||||
try {
|
||||
const stdout = execFileSync(p, args, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
timeout,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const firstLine = stdout.trim().split('\n')[0];
|
||||
@@ -209,7 +209,7 @@ function runEngine(label, engine, args) {
|
||||
}
|
||||
}
|
||||
|
||||
runEngine('yt-dlp', 'yt-dlp', ['--version']);
|
||||
runEngine('yt-dlp', 'yt-dlp', ['--version'], 45000);
|
||||
runEngine('ffmpeg', 'ffmpeg', ['-version']);
|
||||
runEngine('deno', 'deno', ['--version']);
|
||||
runEngine('aria2c', 'aria2c', ['--version']);
|
||||
|
||||
+166
-56
@@ -257,7 +257,9 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
||||
let path_env = format!("{}:/usr/bin:/bin", bin_dir_str);
|
||||
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| format!("Failed to create sidecar yt-dlp: {}", e))?;
|
||||
let ytdlp_path = resolve_bundled_binary_path(&app_handle, "yt-dlp")
|
||||
.map_err(|e| format!("failed to find bundled yt-dlp: {e}"))?;
|
||||
let mut cmd = app_handle.shell().command(&ytdlp_path);
|
||||
cmd = cmd.env("PATH", &path_env)
|
||||
.arg("--dump-json")
|
||||
.arg("--no-warnings")
|
||||
@@ -330,27 +332,11 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
||||
#[tauri::command]
|
||||
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_ytdlp called!");
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = app_handle.shell().sidecar("yt-dlp")
|
||||
.map_err(|e| e.to_string())?
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
println!("Failed to execute: {}", e);
|
||||
format!("Failed to execute yt-dlp: {}", e)
|
||||
})?;
|
||||
|
||||
println!("yt-dlp execution finished with status: {:?}", output.status);
|
||||
if output.status.success() {
|
||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
println!("yt-dlp output: {}", text);
|
||||
Ok(text)
|
||||
} else {
|
||||
let err = String::from_utf8_lossy(&output.stderr);
|
||||
println!("yt-dlp error output: {}", err);
|
||||
Err(format!("yt-dlp error: {}", err))
|
||||
let (version, error, _) = run_sidecar_version(&app_handle, "yt-dlp", &["--version"]).await;
|
||||
match (version, error) {
|
||||
(Some(version), None) => Ok(version),
|
||||
(_, Some(error)) => Err(error),
|
||||
_ => Err("yt-dlp returned no version output".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,8 +345,8 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_ffmpeg called!");
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = app_handle.shell().sidecar("ffmpeg")
|
||||
.map_err(|e| e.to_string())?
|
||||
let binary_path = resolve_bundled_binary_path(&app_handle, "ffmpeg")?;
|
||||
let output = app_handle.shell().command(&binary_path)
|
||||
.arg("-version")
|
||||
.output()
|
||||
.await
|
||||
@@ -395,8 +381,8 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||
println!("test_deno called!");
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
let output = app_handle.shell().sidecar("deno")
|
||||
.map_err(|e| e.to_string())?
|
||||
let binary_path = resolve_bundled_binary_path(&app_handle, "deno")?;
|
||||
let output = app_handle.shell().command(&binary_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.await
|
||||
@@ -737,28 +723,64 @@ async fn run_sidecar_version(
|
||||
) -> (Option<String>, Option<String>, Option<String>) {
|
||||
let binary_path = match resolve_bundled_binary_path(app_handle, sidecar_name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return (None, Some(format!("Cannot find '{}': {}", sidecar_name, e)), None),
|
||||
Err(e) => return (None, Some(format!("Missing bundled binary '{}': {}", sidecar_name, e)), None),
|
||||
};
|
||||
|
||||
let bin = binary_path.clone();
|
||||
let arg_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
|
||||
if let Err(error) = validate_bundled_binary(&binary_path) {
|
||||
return (None, Some(error), None);
|
||||
}
|
||||
|
||||
let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
tokio::process::Command::new(&bin)
|
||||
.args(&arg_owned)
|
||||
.output().await
|
||||
}).await;
|
||||
let cache_key = version_cache_key(&binary_path, args);
|
||||
if let Ok(cache) = version_check_cache().lock() {
|
||||
if let Some(cached) = cache.get(&cache_key) {
|
||||
return cached.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let mut command = tokio::process::Command::new(&binary_path);
|
||||
command.args(args).kill_on_drop(true);
|
||||
let output_future = command.output();
|
||||
tokio::pin!(output_future);
|
||||
|
||||
const NORMAL_VERSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
const YTDLP_FIRST_RUN_GRACE: std::time::Duration = std::time::Duration::from_secs(40);
|
||||
|
||||
let result = match tokio::time::timeout(NORMAL_VERSION_TIMEOUT, &mut output_future).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) if sidecar_name == "yt-dlp" && !ytdlp_expects_internal_dir(&binary_path) => {
|
||||
log::info!(
|
||||
"Bundled yt-dlp version check exceeded {} seconds; allowing {} seconds for standalone first-run initialization",
|
||||
NORMAL_VERSION_TIMEOUT.as_secs(),
|
||||
YTDLP_FIRST_RUN_GRACE.as_secs()
|
||||
);
|
||||
tokio::time::timeout(YTDLP_FIRST_RUN_GRACE, &mut output_future)
|
||||
.await
|
||||
.map_err(|_| NORMAL_VERSION_TIMEOUT + YTDLP_FIRST_RUN_GRACE)
|
||||
}
|
||||
Err(_) => Err(NORMAL_VERSION_TIMEOUT),
|
||||
};
|
||||
|
||||
let output = match result {
|
||||
Ok(Ok(o)) => o,
|
||||
Ok(Ok(output)) => output,
|
||||
Ok(Err(e)) => return (None, Some(format!("Failed to execute '{}': {}", sidecar_name, e)), None),
|
||||
Err(_) => return (None, Some(format!("'{}' version check timed out after 5 seconds", sidecar_name)), None),
|
||||
Err(timeout) => {
|
||||
return (
|
||||
None,
|
||||
Some(format!(
|
||||
"'{}' version check timed out after {} seconds at '{}'",
|
||||
sidecar_name,
|
||||
timeout.as_secs(),
|
||||
binary_path.display()
|
||||
)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let stderr_tail = if stderr.is_empty() { None } else { Some(stderr.clone()) };
|
||||
|
||||
if output.status.success() {
|
||||
let result = if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
(Some(stdout), None, stderr_tail)
|
||||
} else {
|
||||
@@ -769,7 +791,95 @@ async fn run_sidecar_version(
|
||||
format!("Exited with code {:?}", output.status.code())
|
||||
};
|
||||
(if stdout.is_empty() { None } else { Some(stdout) }, Some(err), stderr_tail)
|
||||
};
|
||||
|
||||
if result.1.is_none() {
|
||||
if let Ok(mut cache) = version_check_cache().lock() {
|
||||
cache.insert(cache_key, result.clone());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
type VersionCheckResult = (Option<String>, Option<String>, Option<String>);
|
||||
|
||||
fn version_check_cache(
|
||||
) -> &'static std::sync::Mutex<std::collections::HashMap<String, VersionCheckResult>> {
|
||||
static CACHE: std::sync::OnceLock<
|
||||
std::sync::Mutex<std::collections::HashMap<String, VersionCheckResult>>,
|
||||
> = std::sync::OnceLock::new();
|
||||
CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
|
||||
}
|
||||
|
||||
fn version_cache_key(binary_path: &std::path::Path, args: &[&str]) -> String {
|
||||
let modified = std::fs::metadata(binary_path)
|
||||
.and_then(|metadata| metadata.modified())
|
||||
.ok()
|
||||
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or_default();
|
||||
format!("{}:{modified}:{}", binary_path.display(), args.join("\u{1f}"))
|
||||
}
|
||||
|
||||
fn ytdlp_expects_internal_dir(binary_path: &std::path::Path) -> bool {
|
||||
binary_path
|
||||
.parent()
|
||||
.is_some_and(|parent| parent.join("_internal").is_dir())
|
||||
}
|
||||
|
||||
fn validate_bundled_binary(binary_path: &std::path::Path) -> Result<(), String> {
|
||||
let metadata = std::fs::metadata(binary_path).map_err(|error| {
|
||||
format!(
|
||||
"Missing bundled binary at '{}': {error}",
|
||||
binary_path.display()
|
||||
)
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(format!(
|
||||
"Bundled binary path is not a file: '{}'",
|
||||
binary_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if metadata.permissions().mode() & 0o111 == 0 {
|
||||
return Err(format!(
|
||||
"Bundled binary is not executable: '{}'",
|
||||
binary_path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let expected_arch = if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"x86_64"
|
||||
};
|
||||
if let Ok(output) = std::process::Command::new("/usr/bin/lipo")
|
||||
.arg("-archs")
|
||||
.arg(binary_path)
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let archs = String::from_utf8_lossy(&output.stdout);
|
||||
if !archs.split_whitespace().any(|arch| arch == expected_arch) {
|
||||
return Err(format!(
|
||||
"Wrong architecture for '{}': expected {}, found {}",
|
||||
binary_path.display(),
|
||||
expected_arch,
|
||||
archs.trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_remediation_hint(error: &str, _kind: &str) -> Option<String> {
|
||||
@@ -896,9 +1006,9 @@ async fn check_ytdlp(app_handle: &tauri::AppHandle) -> EngineStatusItem {
|
||||
daemon_alive: None,
|
||||
rpc_ready: None,
|
||||
last_stderr_tail: None,
|
||||
expects_internal_dir: Some(has_internal_dir),
|
||||
has_internal_dir: Some(has_internal_dir),
|
||||
has_python_framework: Some(has_python_framework),
|
||||
expects_internal_dir: has_internal_dir.then_some(true),
|
||||
has_internal_dir: has_internal_dir.then_some(true),
|
||||
has_python_framework: has_internal_dir.then_some(has_python_framework),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1009,10 +1119,12 @@ fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str)
|
||||
if cfg!(target_arch = "aarch64") { "aarch64" } else { "x86_64" },
|
||||
);
|
||||
|
||||
// Production: sidecar sits next to the main executable inside the .app bundle
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
if let Some(exe_dir) = exe_path.parent() {
|
||||
let candidate = exe_dir.join(&full_name);
|
||||
// Production: use the resource copy as the canonical engine location.
|
||||
if let Ok(resource_dir) = app_handle.path().resource_dir() {
|
||||
for candidate in [
|
||||
resource_dir.join("binaries").join(&full_name),
|
||||
resource_dir.join(&full_name),
|
||||
] {
|
||||
if candidate.is_file() {
|
||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate);
|
||||
return Ok(candidate);
|
||||
@@ -1038,17 +1150,14 @@ fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Tauri resource directory (some configs place sidecars there)
|
||||
if let Ok(resource_dir) = app_handle.path().resource_dir() {
|
||||
let candidate = resource_dir.join(&full_name);
|
||||
if candidate.is_file() {
|
||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate);
|
||||
return Ok(candidate);
|
||||
}
|
||||
let candidate2 = resource_dir.join("binaries").join(&full_name);
|
||||
if candidate2.is_file() {
|
||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate2);
|
||||
return Ok(candidate2);
|
||||
// Compatibility fallback for older bundles produced with externalBin.
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
if let Some(exe_dir) = exe_path.parent() {
|
||||
let candidate = exe_dir.join(binary_name);
|
||||
if candidate.is_file() {
|
||||
log::info!("Resolved legacy bundled '{}' at: {:?}", binary_name, candidate);
|
||||
return Ok(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,7 +1288,8 @@ pub(crate) async fn start_media_download_internal(
|
||||
let mut processing_started = false;
|
||||
|
||||
while strike <= MAX_RETRIES {
|
||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| e.to_string())?
|
||||
let ytdlp_path = resolve_bundled_binary_path(&app_handle, "yt-dlp")?;
|
||||
let mut cmd = app_handle.shell().command(&ytdlp_path)
|
||||
.arg("--newline")
|
||||
.arg("--no-check-formats")
|
||||
.arg("--socket-timeout").arg("20")
|
||||
|
||||
@@ -38,12 +38,6 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"externalBin": [
|
||||
"binaries/yt-dlp",
|
||||
"binaries/aria2c",
|
||||
"binaries/ffmpeg",
|
||||
"binaries/deno"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
@@ -112,8 +112,8 @@ export default function SettingsView() {
|
||||
{item.rpc_ready != null && <p>RPC ready: {String(item.rpc_ready)}</p>}
|
||||
{item.rpc_port != null && <p>RPC port: {item.rpc_port}</p>}
|
||||
{item.last_stderr_tail && <details><summary className="cursor-pointer text-text-muted">daemon stderr</summary><pre className="mt-1 whitespace-pre-wrap">{item.last_stderr_tail}</pre></details>}
|
||||
{item.expects_internal_dir != null && <p>Expects _internal layout: {String(item.expects_internal_dir)}</p>}
|
||||
{item.has_internal_dir != null && <p>_internal directory found: {String(item.has_internal_dir)}</p>}
|
||||
{item.expects_internal_dir === true && <p>Packaging: PyInstaller onedir (_internal required)</p>}
|
||||
{item.has_internal_dir === true && <p>_internal directory found: true</p>}
|
||||
{item.has_python_framework != null && <p>Python runtime found: {String(item.has_python_framework)}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user