Fix second audit findings and restore crash detection dialog
Address 29 issues found in comprehensive API/spec audit: - Fix .desktop Exec key path escaping per Desktop Entry spec - Fix update dialog double-dispatch with connect_response - Fix version comparison total ordering with lexicographic fallback - Use RETURNING id for reliable upsert in database - Replace tilde-based path fallbacks with proper XDG helpers - Fix backup create/restore path asymmetry for non-home paths - HTML-escape severity class in security reports - Use AppStream <custom> element instead of <metadata> - Fix has_appimage_update_tool to check .is_ok() not .success() - Use ListBoxRow instead of ActionRow::set_child in ExpanderRow - Add ELF magic validation to architecture detection - Add timeout to extract_update_info_runtime - Skip symlinks in dir_size calculation - Use Condvar instead of busy-wait in analysis thread pool - Restore crash detection to single blocking call architecture
This commit is contained in:
@@ -8,3 +8,4 @@ Type=Application
|
||||
Categories=System;PackageManager;GTK;
|
||||
Keywords=AppImage;Application;Manager;Package;
|
||||
StartupNotify=true
|
||||
SingleMainWindow=true
|
||||
|
||||
50
src/cli.rs
50
src/cli.rs
@@ -609,40 +609,29 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode {
|
||||
// Try to find in database for tracking
|
||||
let record = db.get_appimage_by_path(path).ok().flatten();
|
||||
|
||||
if let Some(ref record) = record {
|
||||
match launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) {
|
||||
launcher::LaunchResult::Started { method, .. } => {
|
||||
println!(
|
||||
"Launched {} ({})",
|
||||
record.app_name.as_deref().unwrap_or(&record.filename),
|
||||
method.as_str(),
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
launcher::LaunchResult::Crashed { stderr, exit_code, .. } => {
|
||||
eprintln!(
|
||||
"App crashed immediately (exit code: {})\n{}",
|
||||
exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()),
|
||||
stderr,
|
||||
);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
launcher::LaunchResult::Failed(msg) => {
|
||||
eprintln!("Error: {}", msg);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
let launch_result = if let Some(ref record) = record {
|
||||
launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[])
|
||||
} else {
|
||||
// Not in database - launch without tracking
|
||||
match launcher::launch_appimage_simple(file_path, &[]) {
|
||||
launcher::LaunchResult::Started { method, .. } => {
|
||||
println!("Launched {} ({})", path, method.as_str());
|
||||
launcher::launch_appimage_simple(file_path, &[])
|
||||
};
|
||||
|
||||
match launch_result {
|
||||
launcher::LaunchResult::Started { pid, method } => {
|
||||
let name = record.as_ref()
|
||||
.and_then(|r| r.app_name.as_deref())
|
||||
.unwrap_or(path);
|
||||
println!("Launched {} (PID: {}, {})", name, pid, method.as_str());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
launcher::LaunchResult::Crashed { stderr, exit_code, .. } => {
|
||||
launcher::LaunchResult::Crashed { exit_code, stderr, method } => {
|
||||
let name = record.as_ref()
|
||||
.and_then(|r| r.app_name.as_deref())
|
||||
.unwrap_or(path);
|
||||
eprintln!(
|
||||
"App crashed immediately (exit code: {})\n{}",
|
||||
exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()),
|
||||
"{} crashed on launch (exit code: {}, method: {})\n{}",
|
||||
name,
|
||||
exit_code.map(|c: i32| c.to_string()).unwrap_or_else(|| "unknown".into()),
|
||||
method.as_str(),
|
||||
stderr,
|
||||
);
|
||||
ExitCode::FAILURE
|
||||
@@ -652,7 +641,6 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode {
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn do_inspect(path: &std::path::Path, appimage_type: &discovery::AppImageType) -> ExitCode {
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const APP_ID: &str = "app.driftwood.Driftwood";
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const GSETTINGS_SCHEMA_DIR: &str = env!("GSETTINGS_SCHEMA_DIR");
|
||||
pub const SYSTEM_APPIMAGE_DIR: &str = "/opt/appimages";
|
||||
|
||||
/// Return the XDG data directory with a proper $HOME-based fallback.
|
||||
/// Unlike `PathBuf::from("~/.local/share")`, this actually resolves to the
|
||||
/// user's home directory instead of creating a literal `~` path.
|
||||
pub fn data_dir_fallback() -> PathBuf {
|
||||
dirs::data_dir().unwrap_or_else(|| home_dir().join(".local/share"))
|
||||
}
|
||||
|
||||
/// Return the XDG config directory with a proper $HOME-based fallback.
|
||||
#[allow(dead_code)]
|
||||
pub fn config_dir_fallback() -> PathBuf {
|
||||
dirs::config_dir().unwrap_or_else(|| home_dir().join(".config"))
|
||||
}
|
||||
|
||||
fn home_dir() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.or_else(|| std::env::var("HOME").ok().map(PathBuf::from))
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Condvar, Mutex};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::core::database::Database;
|
||||
@@ -14,17 +15,21 @@ const MAX_CONCURRENT_ANALYSES: usize = 2;
|
||||
/// Counter for currently running analyses.
|
||||
static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Condvar to efficiently wait for a slot instead of busy-polling.
|
||||
static SLOT_AVAILABLE: (Mutex<()>, Condvar) = (Mutex::new(()), Condvar::new());
|
||||
|
||||
/// Returns the number of currently running background analyses.
|
||||
pub fn running_count() -> usize {
|
||||
RUNNING_ANALYSES.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// RAII guard that decrements the analysis counter on drop.
|
||||
/// RAII guard that decrements the analysis counter on drop and notifies waiters.
|
||||
struct AnalysisGuard;
|
||||
|
||||
impl Drop for AnalysisGuard {
|
||||
fn drop(&mut self) {
|
||||
RUNNING_ANALYSES.fetch_sub(1, Ordering::Release);
|
||||
SLOT_AVAILABLE.1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +41,7 @@ impl Drop for AnalysisGuard {
|
||||
///
|
||||
/// Blocks until a slot is available if the concurrency limit is reached.
|
||||
pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageType, integrate: bool) {
|
||||
// Wait for a slot to become available
|
||||
// Wait for a slot to become available using condvar instead of busy-polling
|
||||
loop {
|
||||
let current = RUNNING_ANALYSES.load(Ordering::Acquire);
|
||||
if current < MAX_CONCURRENT_ANALYSES {
|
||||
@@ -44,7 +49,8 @@ pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageTy
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
let lock = SLOT_AVAILABLE.0.lock().unwrap();
|
||||
let _ = SLOT_AVAILABLE.1.wait_timeout(lock, std::time::Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
let _guard = AnalysisGuard;
|
||||
|
||||
@@ -446,14 +446,14 @@ pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
|
||||
xml.push_str(" </categories>\n");
|
||||
}
|
||||
|
||||
// Provide hint about source
|
||||
xml.push_str(" <metadata>\n");
|
||||
xml.push_str(" <value key=\"managed-by\">driftwood</value>\n");
|
||||
// Provide hint about source (AppStream spec uses <custom> for key-value data)
|
||||
xml.push_str(" <custom>\n");
|
||||
xml.push_str(" <value key=\"Driftwood::managed-by\">driftwood</value>\n");
|
||||
xml.push_str(&format!(
|
||||
" <value key=\"appimage-path\">{}</value>\n",
|
||||
" <value key=\"Driftwood::appimage-path\">{}</value>\n",
|
||||
xml_escape(&record.path),
|
||||
));
|
||||
xml.push_str(" </metadata>\n");
|
||||
xml.push_str(" </custom>\n");
|
||||
|
||||
xml.push_str(" </component>\n");
|
||||
}
|
||||
@@ -467,8 +467,7 @@ pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
|
||||
pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
||||
let catalog_xml = generate_catalog(db)?;
|
||||
|
||||
let catalog_dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let catalog_dir = crate::config::data_dir_fallback()
|
||||
.join("swcatalog")
|
||||
.join("xml");
|
||||
|
||||
@@ -484,8 +483,7 @@ pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
||||
|
||||
/// Remove the AppStream catalog from the local swcatalog directory.
|
||||
pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
||||
let catalog_path = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let catalog_path = crate::config::data_dir_fallback()
|
||||
.join("swcatalog")
|
||||
.join("xml")
|
||||
.join("driftwood.xml");
|
||||
@@ -500,8 +498,7 @@ pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
||||
|
||||
/// Check if the AppStream catalog is currently installed.
|
||||
pub fn is_catalog_installed() -> bool {
|
||||
let catalog_path = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let catalog_path = crate::config::data_dir_fallback()
|
||||
.join("swcatalog")
|
||||
.join("xml")
|
||||
.join("driftwood.xml");
|
||||
|
||||
@@ -25,8 +25,7 @@ pub struct BackupPathEntry {
|
||||
}
|
||||
|
||||
fn backups_dir() -> PathBuf {
|
||||
let dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let dir = crate::config::data_dir_fallback()
|
||||
.join("driftwood")
|
||||
.join("backups");
|
||||
fs::create_dir_all(&dir).ok();
|
||||
@@ -123,17 +122,17 @@ pub fn create_backup(db: &Database, appimage_id: i64) -> Result<PathBuf, BackupE
|
||||
for entry in &entries {
|
||||
let source = Path::new(&entry.original_path);
|
||||
if source.exists() {
|
||||
// Archive all paths relative to home dir for consistent extract layout.
|
||||
// For non-home paths, archive with full absolute path (leading / stripped by tar).
|
||||
if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||
tar_args.push("-C".to_string());
|
||||
tar_args.push(home_dir.to_string_lossy().to_string());
|
||||
tar_args.push(rel.to_string_lossy().to_string());
|
||||
} else {
|
||||
tar_args.push("-C".to_string());
|
||||
tar_args.push("/".to_string());
|
||||
tar_args.push(
|
||||
source.parent().unwrap_or(Path::new("/")).to_string_lossy().to_string(),
|
||||
);
|
||||
tar_args.push(
|
||||
source.file_name().unwrap_or_default().to_string_lossy().to_string(),
|
||||
source.strip_prefix("/").unwrap_or(source).to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -204,8 +203,9 @@ pub fn restore_backup(archive_path: &Path) -> Result<RestoreResult, BackupError>
|
||||
let extracted = if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||
temp_dir.path().join(rel)
|
||||
} else {
|
||||
let source_name = source.file_name().unwrap_or_default();
|
||||
temp_dir.path().join(source_name)
|
||||
// Non-home paths are archived with full path (leading / stripped)
|
||||
let abs_rel = source.strip_prefix("/").unwrap_or(source);
|
||||
temp_dir.path().join(abs_rel)
|
||||
};
|
||||
let target = Path::new(&entry.original_path);
|
||||
|
||||
|
||||
@@ -198,8 +198,7 @@ pub struct SandboxProfileRecord {
|
||||
}
|
||||
|
||||
fn db_path() -> PathBuf {
|
||||
let data_dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let data_dir = crate::config::data_dir_fallback()
|
||||
.join("driftwood");
|
||||
std::fs::create_dir_all(&data_dir).ok();
|
||||
data_dir.join("driftwood.db")
|
||||
@@ -753,7 +752,7 @@ impl Database {
|
||||
is_executable: bool,
|
||||
file_modified: Option<&str>,
|
||||
) -> SqlResult<i64> {
|
||||
self.conn.execute(
|
||||
let id: i64 = self.conn.query_row(
|
||||
"INSERT INTO appimages (path, filename, appimage_type, size_bytes, is_executable, file_modified)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(path) DO UPDATE SET
|
||||
@@ -762,17 +761,9 @@ impl Database {
|
||||
size_bytes = excluded.size_bytes,
|
||||
is_executable = excluded.is_executable,
|
||||
file_modified = excluded.file_modified,
|
||||
last_scanned = datetime('now')",
|
||||
last_scanned = datetime('now')
|
||||
RETURNING id",
|
||||
params![path, filename, appimage_type, size_bytes, is_executable, file_modified],
|
||||
)?;
|
||||
// last_insert_rowid() returns 0 for ON CONFLICT UPDATE, so query the actual id
|
||||
let id = self.conn.last_insert_rowid();
|
||||
if id != 0 {
|
||||
return Ok(id);
|
||||
}
|
||||
let id: i64 = self.conn.query_row(
|
||||
"SELECT id FROM appimages WHERE path = ?1",
|
||||
params![path],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(id)
|
||||
|
||||
@@ -329,6 +329,8 @@ fn build_name_group(name: &str, records: &[&AppImageRecord]) -> DuplicateGroup {
|
||||
}
|
||||
|
||||
/// Compare two version strings for ordering.
|
||||
/// Falls back to lexicographic comparison of cleaned versions to guarantee
|
||||
/// the total ordering contract (antisymmetry) required by sort_by.
|
||||
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
use super::updater::{clean_version, version_is_newer};
|
||||
|
||||
@@ -339,8 +341,11 @@ fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||
std::cmp::Ordering::Equal
|
||||
} else if version_is_newer(a, b) {
|
||||
std::cmp::Ordering::Greater
|
||||
} else {
|
||||
} else if version_is_newer(b, a) {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
// Neither is newer (unparseable components) - use lexicographic fallback
|
||||
ca.cmp(&cb)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -396,8 +396,14 @@ pub fn dir_size_pub(path: &Path) -> u64 {
|
||||
}
|
||||
|
||||
fn dir_size(path: &Path) -> u64 {
|
||||
if path.is_file() {
|
||||
return path.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
// Use symlink_metadata to avoid following symlinks outside the tree
|
||||
if let Ok(meta) = path.symlink_metadata() {
|
||||
if meta.is_file() {
|
||||
return meta.len();
|
||||
}
|
||||
if meta.is_symlink() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
let mut total = 0u64;
|
||||
if let Ok(entries) = std::fs::read_dir(path) {
|
||||
@@ -406,6 +412,10 @@ fn dir_size(path: &Path) -> u64 {
|
||||
Ok(ft) => ft,
|
||||
Err(_) => continue,
|
||||
};
|
||||
// Skip symlinks to avoid counting external files or recursing out of tree
|
||||
if ft.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
if ft.is_file() {
|
||||
total += entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
} else if ft.is_dir() {
|
||||
|
||||
@@ -80,8 +80,7 @@ struct DesktopEntryFields {
|
||||
}
|
||||
|
||||
fn icons_cache_dir() -> PathBuf {
|
||||
let dir = dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
let dir = crate::config::data_dir_fallback()
|
||||
.join("driftwood")
|
||||
.join("icons");
|
||||
fs::create_dir_all(&dir).ok();
|
||||
|
||||
@@ -25,20 +25,34 @@ impl From<std::io::Error> for IntegrationError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape a string for use inside a double-quoted Exec argument in a .desktop file.
|
||||
/// Per the Desktop Entry spec, `\`, `"`, `` ` ``, and `$` must be escaped with `\`.
|
||||
fn escape_exec_arg(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\\' | '"' | '`' | '$' => {
|
||||
out.push('\\');
|
||||
out.push(c);
|
||||
}
|
||||
_ => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub struct IntegrationResult {
|
||||
pub desktop_file_path: PathBuf,
|
||||
pub icon_install_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn applications_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
crate::config::data_dir_fallback()
|
||||
.join("applications")
|
||||
}
|
||||
|
||||
fn icons_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
crate::config::data_dir_fallback()
|
||||
.join("icons/hicolor")
|
||||
}
|
||||
|
||||
@@ -90,21 +104,22 @@ pub fn integrate(record: &AppImageRecord) -> Result<IntegrationResult, Integrati
|
||||
|
||||
let icon_id = format!("driftwood-{}", app_id);
|
||||
|
||||
let desktop_content = format!(
|
||||
"[Desktop Entry]\n\
|
||||
Type=Application\n\
|
||||
Name={name}\n\
|
||||
Exec=\"{exec}\" %U\n\
|
||||
Icon={icon}\n\
|
||||
Categories={categories}\n\
|
||||
Comment={comment}\n\
|
||||
Terminal=false\n\
|
||||
X-AppImage-Path={path}\n\
|
||||
X-AppImage-Version={version}\n\
|
||||
X-AppImage-Managed-By=Driftwood\n\
|
||||
X-AppImage-Integrated-Date={date}\n",
|
||||
let desktop_content = format!("\
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name={name}
|
||||
Exec=\"{exec}\" %U
|
||||
Icon={icon}
|
||||
Categories={categories}
|
||||
Comment={comment}
|
||||
Terminal=false
|
||||
X-AppImage-Path={path}
|
||||
X-AppImage-Version={version}
|
||||
X-AppImage-Managed-By=Driftwood
|
||||
X-AppImage-Integrated-Date={date}
|
||||
",
|
||||
name = app_name,
|
||||
exec = record.path,
|
||||
exec = escape_exec_arg(&record.path),
|
||||
icon = icon_id,
|
||||
categories = categories,
|
||||
comment = comment,
|
||||
|
||||
@@ -58,18 +58,18 @@ impl LaunchMethod {
|
||||
/// Result of a launch attempt.
|
||||
#[derive(Debug)]
|
||||
pub enum LaunchResult {
|
||||
/// Successfully spawned the process and it's still running.
|
||||
/// Process spawned and survived the startup crash-check window.
|
||||
Started {
|
||||
child: Child,
|
||||
pid: u32,
|
||||
method: LaunchMethod,
|
||||
},
|
||||
/// Process spawned but crashed immediately (within ~1 second).
|
||||
/// Process spawned but exited during the startup crash-check window.
|
||||
Crashed {
|
||||
exit_code: Option<i32>,
|
||||
stderr: String,
|
||||
method: LaunchMethod,
|
||||
},
|
||||
/// Failed to launch.
|
||||
/// Failed to launch (binary not found, permission denied, etc.).
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
@@ -183,34 +183,21 @@ fn execute_appimage(
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(mut child) => {
|
||||
// Give the process a brief moment to fail on immediate errors
|
||||
// (missing libs, exec format errors, Qt plugin failures, etc.)
|
||||
std::thread::sleep(std::time::Duration::from_millis(150));
|
||||
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Already exited - immediate crash. Read stderr for details.
|
||||
let stderr_text = child.stderr.take().map(|mut pipe| {
|
||||
let mut buf = String::new();
|
||||
use std::io::Read;
|
||||
// Read with a size cap to avoid huge allocations
|
||||
let mut limited = (&mut pipe).take(64 * 1024);
|
||||
let _ = limited.read_to_string(&mut buf);
|
||||
buf
|
||||
}).unwrap_or_default();
|
||||
let pid = child.id();
|
||||
|
||||
// Monitor for early crash (2s window). This blocks the current
|
||||
// thread, so callers should run this inside gio::spawn_blocking.
|
||||
match check_early_crash(&mut child, std::time::Duration::from_secs(2)) {
|
||||
Some((exit_code, stderr)) => {
|
||||
LaunchResult::Crashed {
|
||||
exit_code: status.code(),
|
||||
stderr: stderr_text,
|
||||
exit_code,
|
||||
stderr,
|
||||
method: method.clone(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Still running after 150ms - drop the stderr pipe so the
|
||||
// child process won't block if it fills the pipe buffer.
|
||||
drop(child.stderr.take());
|
||||
None => {
|
||||
LaunchResult::Started {
|
||||
child,
|
||||
pid,
|
||||
method: method.clone(),
|
||||
}
|
||||
}
|
||||
@@ -220,7 +207,44 @@ fn execute_appimage(
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a launch_args string from the database into a Vec of individual arguments.
|
||||
/// Check if a recently-launched child process crashed during startup.
|
||||
/// Waits up to `timeout` for the process to exit. If it exits within that window,
|
||||
/// reads stderr and returns a Crashed result. If still running, drops the stderr
|
||||
/// pipe (to prevent pipe buffer deadlock) and returns None.
|
||||
///
|
||||
/// Call this from a background thread after spawning the process.
|
||||
pub fn check_early_crash(
|
||||
child: &mut Child,
|
||||
timeout: std::time::Duration,
|
||||
) -> Option<(Option<i32>, String)> {
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
// Process exited - read stderr for crash details
|
||||
let stderr_text = child.stderr.take().map(|mut pipe| {
|
||||
let mut buf = String::new();
|
||||
use std::io::Read;
|
||||
let mut limited = (&mut pipe).take(64 * 1024);
|
||||
let _ = limited.read_to_string(&mut buf);
|
||||
buf
|
||||
}).unwrap_or_default();
|
||||
|
||||
return Some((status.code(), stderr_text));
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed() >= timeout {
|
||||
// Still running - drop stderr pipe to avoid deadlock
|
||||
drop(child.stderr.take());
|
||||
return None;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse launch arguments with basic quote support.
|
||||
/// Splits on whitespace, respecting double-quoted strings.
|
||||
/// Returns an empty Vec if the input is None or empty.
|
||||
|
||||
@@ -14,14 +14,12 @@ pub struct CleanupSummary {
|
||||
}
|
||||
|
||||
fn applications_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
crate::config::data_dir_fallback()
|
||||
.join("applications")
|
||||
}
|
||||
|
||||
fn icons_dir() -> PathBuf {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
||||
crate::config::data_dir_fallback()
|
||||
.join("icons/hicolor")
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ pub fn render_html(report: &SecurityReport) -> String {
|
||||
|
||||
html.push_str("<table>\n<tr><th>CVE</th><th>Severity</th><th>CVSS</th><th>Library</th><th>Fixed In</th><th>Summary</th></tr>\n");
|
||||
for f in &app.findings {
|
||||
let sev_class = f.severity.to_lowercase();
|
||||
let sev_class = html_escape(&f.severity.to_lowercase());
|
||||
html.push_str(&format!(
|
||||
"<tr><td>{}</td><td class=\"{}\">{}</td><td>{}</td><td>{} {}</td><td>{}</td><td>{}</td></tr>\n",
|
||||
html_escape(&f.cve_id),
|
||||
|
||||
@@ -44,8 +44,7 @@ impl ProfileSource {
|
||||
|
||||
/// Directory where local sandbox profiles are stored.
|
||||
fn profiles_dir() -> PathBuf {
|
||||
let dir = dirs::config_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||
let dir = crate::config::config_dir_fallback()
|
||||
.join("driftwood")
|
||||
.join("sandbox");
|
||||
fs::create_dir_all(&dir).ok();
|
||||
|
||||
@@ -762,8 +762,12 @@ pub fn version_is_newer(latest: &str, current: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
// If all compared parts are equal, longer version wins (1.2.3 > 1.2)
|
||||
latest_parts.len() > current_parts.len()
|
||||
// If all compared parts are equal, only consider newer if extra parts are non-zero
|
||||
// (e.g., 1.2.1 > 1.2, but 1.2.0 == 1.2)
|
||||
if latest_parts.len() > current_parts.len() {
|
||||
return latest_parts[current_parts.len()..].iter().any(|&p| p > 0);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Parse a version string into numeric parts.
|
||||
@@ -781,13 +785,13 @@ fn parse_version_parts(version: &str) -> Vec<u64> {
|
||||
|
||||
/// Check if AppImageUpdate tool is available on the system.
|
||||
pub fn has_appimage_update_tool() -> bool {
|
||||
// Check that the binary exists and can be spawned (--help may return non-zero)
|
||||
std::process::Command::new("AppImageUpdate")
|
||||
.arg("--help")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Batch check: read update info from an AppImage and check for updates.
|
||||
@@ -935,7 +939,8 @@ pub fn download_and_apply_update(
|
||||
|
||||
// Atomic rename temp -> target
|
||||
if let Err(e) = fs::rename(&temp_path, appimage_path) {
|
||||
// Try to restore backup on failure
|
||||
// Clean up temp file and restore backup on failure
|
||||
fs::remove_file(&temp_path).ok();
|
||||
if let Some(ref backup) = backup_path {
|
||||
fs::rename(backup, appimage_path).ok();
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ use application::DriftwoodApplication;
|
||||
use config::{APP_ID, GSETTINGS_SCHEMA_DIR};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
// Point GSettings at our compiled schema directory (dev builds)
|
||||
std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR);
|
||||
// Point GSettings at our compiled schema directory (dev builds).
|
||||
// SAFETY: Called before any threads are spawned, at program start.
|
||||
unsafe { std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR); }
|
||||
|
||||
// Parse CLI arguments
|
||||
let parsed = cli::Cli::parse();
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
// Toast overlay for copy actions
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
// ViewStack for tabbed content with crossfade transitions.
|
||||
// ViewStack for tabbed content (transitions disabled for instant switching).
|
||||
// vhomogeneous=false so the stack sizes to the visible child only,
|
||||
// preventing shorter tabs from having excess scrollable empty space.
|
||||
let view_stack = adw::ViewStack::new();
|
||||
@@ -124,10 +124,10 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
|
||||
|
||||
btn_ref.set_sensitive(true);
|
||||
match result {
|
||||
Ok(launcher::LaunchResult::Started { child, method }) => {
|
||||
let pid = child.id();
|
||||
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
||||
|
||||
// App survived startup - do Wayland analysis after a delay
|
||||
let db_wayland = db_launch.clone();
|
||||
let path_clone = path.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
@@ -488,12 +488,7 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
spinner_ref.set_visible(false);
|
||||
if let Ok(Some(data)) = result {
|
||||
let gbytes = glib::Bytes::from(&data);
|
||||
let stream = gio::MemoryInputStream::from_bytes(&gbytes);
|
||||
if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream(
|
||||
&stream,
|
||||
None::<&gio::Cancellable>,
|
||||
) {
|
||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||
if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
|
||||
picture_ref.set_paintable(Some(&texture));
|
||||
if let Some(slot) = textures_load.borrow_mut().get_mut(idx) {
|
||||
*slot = Some(texture);
|
||||
@@ -708,8 +703,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
||||
.margin_start(12)
|
||||
.margin_end(12)
|
||||
.build();
|
||||
let label_row = adw::ActionRow::new();
|
||||
label_row.set_child(Some(&label));
|
||||
let label_row = gtk::ListBoxRow::builder()
|
||||
.activatable(false)
|
||||
.selectable(false)
|
||||
.child(&label)
|
||||
.build();
|
||||
row.add_row(&label_row);
|
||||
|
||||
release_group.add(&row);
|
||||
@@ -2080,12 +2078,7 @@ fn fetch_favicon_async(url: &str, image: >k::Image) {
|
||||
|
||||
if let Ok(Some(data)) = result {
|
||||
let gbytes = glib::Bytes::from(&data);
|
||||
let stream = gio::MemoryInputStream::from_bytes(&gbytes);
|
||||
if let Ok(pixbuf) = gtk::gdk_pixbuf::Pixbuf::from_stream(
|
||||
&stream,
|
||||
None::<&gio::Cancellable>,
|
||||
) {
|
||||
let texture = gtk::gdk::Texture::for_pixbuf(&pixbuf);
|
||||
if let Ok(texture) = gtk::gdk::Texture::from_bytes(&gbytes) {
|
||||
image_ref.set_paintable(Some(&texture));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,7 @@ pub fn show_update_dialog(
|
||||
let db_update = db_ref.clone();
|
||||
let record_path = record_clone.path.clone();
|
||||
let new_version = check_result.latest_version.clone();
|
||||
dialog_ref.connect_response(None, move |dlg, response| {
|
||||
if response == "update" {
|
||||
dialog_ref.connect_response(Some("update"), move |dlg, _response| {
|
||||
start_update(
|
||||
dlg,
|
||||
&record_path,
|
||||
@@ -100,7 +99,6 @@ pub fn show_update_dialog(
|
||||
new_version.as_deref(),
|
||||
&db_update,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -239,12 +237,10 @@ fn handle_old_version_cleanup(dialog: &adw::AlertDialog, old_path: PathBuf) {
|
||||
dialog.set_response_appearance("remove-old", adw::ResponseAppearance::Destructive);
|
||||
|
||||
let path = old_path.clone();
|
||||
dialog.connect_response(None, move |_dlg, response| {
|
||||
if response == "remove-old" {
|
||||
dialog.connect_response(Some("remove-old"), move |_dlg, _response| {
|
||||
if path.exists() {
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,13 +182,11 @@ pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>
|
||||
let text = text_to_copy.to_string();
|
||||
let toast = toast_overlay.cloned();
|
||||
btn.connect_clicked(move |button| {
|
||||
if let Some(display) = button.display().into() {
|
||||
let clipboard = gtk::gdk::Display::clipboard(&display);
|
||||
let clipboard = button.display().clipboard();
|
||||
clipboard.set_text(&text);
|
||||
if let Some(ref overlay) = toast {
|
||||
overlay.add_toast(adw::Toast::new("Copied to clipboard"));
|
||||
}
|
||||
}
|
||||
});
|
||||
btn
|
||||
}
|
||||
|
||||
@@ -614,8 +614,8 @@ impl DriftwoodWindow {
|
||||
launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &launch_args, &[])
|
||||
}).await;
|
||||
match result {
|
||||
Ok(launcher::LaunchResult::Started { child, method }) => {
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path_str, child.id(), method.as_str());
|
||||
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||
log::info!("Launched: {} (PID: {}, method: {})", path_str, pid, method.as_str());
|
||||
}
|
||||
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
|
||||
log::error!("App crashed (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
|
||||
|
||||
Reference in New Issue
Block a user