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;
|
Categories=System;PackageManager;GTK;
|
||||||
Keywords=AppImage;Application;Manager;Package;
|
Keywords=AppImage;Application;Manager;Package;
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
|
SingleMainWindow=true
|
||||||
|
|||||||
70
src/cli.rs
70
src/cli.rs
@@ -609,48 +609,36 @@ fn cmd_launch(db: &Database, path: &str) -> ExitCode {
|
|||||||
// Try to find in database for tracking
|
// Try to find in database for tracking
|
||||||
let record = db.get_appimage_by_path(path).ok().flatten();
|
let record = db.get_appimage_by_path(path).ok().flatten();
|
||||||
|
|
||||||
if let Some(ref record) = record {
|
let launch_result = if let Some(ref record) = record {
|
||||||
match launcher::launch_appimage(db, record.id, file_path, "cli", &[], &[]) {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Not in database - launch without tracking
|
launcher::launch_appimage_simple(file_path, &[])
|
||||||
match launcher::launch_appimage_simple(file_path, &[]) {
|
};
|
||||||
launcher::LaunchResult::Started { method, .. } => {
|
|
||||||
println!("Launched {} ({})", path, method.as_str());
|
match launch_result {
|
||||||
ExitCode::SUCCESS
|
launcher::LaunchResult::Started { pid, method } => {
|
||||||
}
|
let name = record.as_ref()
|
||||||
launcher::LaunchResult::Crashed { stderr, exit_code, .. } => {
|
.and_then(|r| r.app_name.as_deref())
|
||||||
eprintln!(
|
.unwrap_or(path);
|
||||||
"App crashed immediately (exit code: {})\n{}",
|
println!("Launched {} (PID: {}, {})", name, pid, method.as_str());
|
||||||
exit_code.map(|c| c.to_string()).unwrap_or_else(|| "unknown".into()),
|
ExitCode::SUCCESS
|
||||||
stderr,
|
}
|
||||||
);
|
launcher::LaunchResult::Crashed { exit_code, stderr, method } => {
|
||||||
ExitCode::FAILURE
|
let name = record.as_ref()
|
||||||
}
|
.and_then(|r| r.app_name.as_deref())
|
||||||
launcher::LaunchResult::Failed(msg) => {
|
.unwrap_or(path);
|
||||||
eprintln!("Error: {}", msg);
|
eprintln!(
|
||||||
ExitCode::FAILURE
|
"{} 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
|
||||||
|
}
|
||||||
|
launcher::LaunchResult::Failed(msg) => {
|
||||||
|
eprintln!("Error: {}", msg);
|
||||||
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,25 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub const APP_ID: &str = "app.driftwood.Driftwood";
|
pub const APP_ID: &str = "app.driftwood.Driftwood";
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
pub const GSETTINGS_SCHEMA_DIR: &str = env!("GSETTINGS_SCHEMA_DIR");
|
pub const GSETTINGS_SCHEMA_DIR: &str = env!("GSETTINGS_SCHEMA_DIR");
|
||||||
pub const SYSTEM_APPIMAGE_DIR: &str = "/opt/appimages";
|
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::path::PathBuf;
|
||||||
|
use std::sync::{Condvar, Mutex};
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
use crate::core::database::Database;
|
use crate::core::database::Database;
|
||||||
@@ -14,17 +15,21 @@ const MAX_CONCURRENT_ANALYSES: usize = 2;
|
|||||||
/// Counter for currently running analyses.
|
/// Counter for currently running analyses.
|
||||||
static RUNNING_ANALYSES: AtomicUsize = AtomicUsize::new(0);
|
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.
|
/// Returns the number of currently running background analyses.
|
||||||
pub fn running_count() -> usize {
|
pub fn running_count() -> usize {
|
||||||
RUNNING_ANALYSES.load(Ordering::Relaxed)
|
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;
|
struct AnalysisGuard;
|
||||||
|
|
||||||
impl Drop for AnalysisGuard {
|
impl Drop for AnalysisGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
RUNNING_ANALYSES.fetch_sub(1, Ordering::Release);
|
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.
|
/// 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) {
|
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 {
|
loop {
|
||||||
let current = RUNNING_ANALYSES.load(Ordering::Acquire);
|
let current = RUNNING_ANALYSES.load(Ordering::Acquire);
|
||||||
if current < MAX_CONCURRENT_ANALYSES {
|
if current < MAX_CONCURRENT_ANALYSES {
|
||||||
@@ -44,7 +49,8 @@ pub fn run_background_analysis(id: i64, path: PathBuf, appimage_type: AppImageTy
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
let _guard = AnalysisGuard;
|
||||||
|
|||||||
@@ -446,14 +446,14 @@ pub fn generate_catalog(db: &Database) -> Result<String, AppStreamError> {
|
|||||||
xml.push_str(" </categories>\n");
|
xml.push_str(" </categories>\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide hint about source
|
// Provide hint about source (AppStream spec uses <custom> for key-value data)
|
||||||
xml.push_str(" <metadata>\n");
|
xml.push_str(" <custom>\n");
|
||||||
xml.push_str(" <value key=\"managed-by\">driftwood</value>\n");
|
xml.push_str(" <value key=\"Driftwood::managed-by\">driftwood</value>\n");
|
||||||
xml.push_str(&format!(
|
xml.push_str(&format!(
|
||||||
" <value key=\"appimage-path\">{}</value>\n",
|
" <value key=\"Driftwood::appimage-path\">{}</value>\n",
|
||||||
xml_escape(&record.path),
|
xml_escape(&record.path),
|
||||||
));
|
));
|
||||||
xml.push_str(" </metadata>\n");
|
xml.push_str(" </custom>\n");
|
||||||
|
|
||||||
xml.push_str(" </component>\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> {
|
pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
||||||
let catalog_xml = generate_catalog(db)?;
|
let catalog_xml = generate_catalog(db)?;
|
||||||
|
|
||||||
let catalog_dir = dirs::data_dir()
|
let catalog_dir = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("swcatalog")
|
.join("swcatalog")
|
||||||
.join("xml");
|
.join("xml");
|
||||||
|
|
||||||
@@ -484,8 +483,7 @@ pub fn install_catalog(db: &Database) -> Result<PathBuf, AppStreamError> {
|
|||||||
|
|
||||||
/// Remove the AppStream catalog from the local swcatalog directory.
|
/// Remove the AppStream catalog from the local swcatalog directory.
|
||||||
pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
||||||
let catalog_path = dirs::data_dir()
|
let catalog_path = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("swcatalog")
|
.join("swcatalog")
|
||||||
.join("xml")
|
.join("xml")
|
||||||
.join("driftwood.xml");
|
.join("driftwood.xml");
|
||||||
@@ -500,8 +498,7 @@ pub fn uninstall_catalog() -> Result<(), AppStreamError> {
|
|||||||
|
|
||||||
/// Check if the AppStream catalog is currently installed.
|
/// Check if the AppStream catalog is currently installed.
|
||||||
pub fn is_catalog_installed() -> bool {
|
pub fn is_catalog_installed() -> bool {
|
||||||
let catalog_path = dirs::data_dir()
|
let catalog_path = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("swcatalog")
|
.join("swcatalog")
|
||||||
.join("xml")
|
.join("xml")
|
||||||
.join("driftwood.xml");
|
.join("driftwood.xml");
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ pub struct BackupPathEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn backups_dir() -> PathBuf {
|
fn backups_dir() -> PathBuf {
|
||||||
let dir = dirs::data_dir()
|
let dir = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("driftwood")
|
.join("driftwood")
|
||||||
.join("backups");
|
.join("backups");
|
||||||
fs::create_dir_all(&dir).ok();
|
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 {
|
for entry in &entries {
|
||||||
let source = Path::new(&entry.original_path);
|
let source = Path::new(&entry.original_path);
|
||||||
if source.exists() {
|
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) {
|
if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||||
tar_args.push("-C".to_string());
|
tar_args.push("-C".to_string());
|
||||||
tar_args.push(home_dir.to_string_lossy().to_string());
|
tar_args.push(home_dir.to_string_lossy().to_string());
|
||||||
tar_args.push(rel.to_string_lossy().to_string());
|
tar_args.push(rel.to_string_lossy().to_string());
|
||||||
} else {
|
} else {
|
||||||
tar_args.push("-C".to_string());
|
tar_args.push("-C".to_string());
|
||||||
|
tar_args.push("/".to_string());
|
||||||
tar_args.push(
|
tar_args.push(
|
||||||
source.parent().unwrap_or(Path::new("/")).to_string_lossy().to_string(),
|
source.strip_prefix("/").unwrap_or(source).to_string_lossy().to_string(),
|
||||||
);
|
|
||||||
tar_args.push(
|
|
||||||
source.file_name().unwrap_or_default().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) {
|
let extracted = if let Ok(rel) = source.strip_prefix(&home_dir) {
|
||||||
temp_dir.path().join(rel)
|
temp_dir.path().join(rel)
|
||||||
} else {
|
} else {
|
||||||
let source_name = source.file_name().unwrap_or_default();
|
// Non-home paths are archived with full path (leading / stripped)
|
||||||
temp_dir.path().join(source_name)
|
let abs_rel = source.strip_prefix("/").unwrap_or(source);
|
||||||
|
temp_dir.path().join(abs_rel)
|
||||||
};
|
};
|
||||||
let target = Path::new(&entry.original_path);
|
let target = Path::new(&entry.original_path);
|
||||||
|
|
||||||
|
|||||||
@@ -198,8 +198,7 @@ pub struct SandboxProfileRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn db_path() -> PathBuf {
|
fn db_path() -> PathBuf {
|
||||||
let data_dir = dirs::data_dir()
|
let data_dir = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("driftwood");
|
.join("driftwood");
|
||||||
std::fs::create_dir_all(&data_dir).ok();
|
std::fs::create_dir_all(&data_dir).ok();
|
||||||
data_dir.join("driftwood.db")
|
data_dir.join("driftwood.db")
|
||||||
@@ -753,7 +752,7 @@ impl Database {
|
|||||||
is_executable: bool,
|
is_executable: bool,
|
||||||
file_modified: Option<&str>,
|
file_modified: Option<&str>,
|
||||||
) -> SqlResult<i64> {
|
) -> 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)
|
"INSERT INTO appimages (path, filename, appimage_type, size_bytes, is_executable, file_modified)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||||
ON CONFLICT(path) DO UPDATE SET
|
ON CONFLICT(path) DO UPDATE SET
|
||||||
@@ -762,17 +761,9 @@ impl Database {
|
|||||||
size_bytes = excluded.size_bytes,
|
size_bytes = excluded.size_bytes,
|
||||||
is_executable = excluded.is_executable,
|
is_executable = excluded.is_executable,
|
||||||
file_modified = excluded.file_modified,
|
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],
|
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),
|
|row| row.get(0),
|
||||||
)?;
|
)?;
|
||||||
Ok(id)
|
Ok(id)
|
||||||
|
|||||||
@@ -329,6 +329,8 @@ fn build_name_group(name: &str, records: &[&AppImageRecord]) -> DuplicateGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compare two version strings for ordering.
|
/// 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 {
|
fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering {
|
||||||
use super::updater::{clean_version, version_is_newer};
|
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
|
std::cmp::Ordering::Equal
|
||||||
} else if version_is_newer(a, b) {
|
} else if version_is_newer(a, b) {
|
||||||
std::cmp::Ordering::Greater
|
std::cmp::Ordering::Greater
|
||||||
} else {
|
} else if version_is_newer(b, a) {
|
||||||
std::cmp::Ordering::Less
|
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 {
|
fn dir_size(path: &Path) -> u64 {
|
||||||
if path.is_file() {
|
// Use symlink_metadata to avoid following symlinks outside the tree
|
||||||
return path.metadata().map(|m| m.len()).unwrap_or(0);
|
if let Ok(meta) = path.symlink_metadata() {
|
||||||
|
if meta.is_file() {
|
||||||
|
return meta.len();
|
||||||
|
}
|
||||||
|
if meta.is_symlink() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut total = 0u64;
|
let mut total = 0u64;
|
||||||
if let Ok(entries) = std::fs::read_dir(path) {
|
if let Ok(entries) = std::fs::read_dir(path) {
|
||||||
@@ -406,6 +412,10 @@ fn dir_size(path: &Path) -> u64 {
|
|||||||
Ok(ft) => ft,
|
Ok(ft) => ft,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
// Skip symlinks to avoid counting external files or recursing out of tree
|
||||||
|
if ft.is_symlink() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ft.is_file() {
|
if ft.is_file() {
|
||||||
total += entry.metadata().map(|m| m.len()).unwrap_or(0);
|
total += entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||||
} else if ft.is_dir() {
|
} else if ft.is_dir() {
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ struct DesktopEntryFields {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn icons_cache_dir() -> PathBuf {
|
fn icons_cache_dir() -> PathBuf {
|
||||||
let dir = dirs::data_dir()
|
let dir = crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("driftwood")
|
.join("driftwood")
|
||||||
.join("icons");
|
.join("icons");
|
||||||
fs::create_dir_all(&dir).ok();
|
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 struct IntegrationResult {
|
||||||
pub desktop_file_path: PathBuf,
|
pub desktop_file_path: PathBuf,
|
||||||
pub icon_install_path: Option<PathBuf>,
|
pub icon_install_path: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn applications_dir() -> PathBuf {
|
fn applications_dir() -> PathBuf {
|
||||||
dirs::data_dir()
|
crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("applications")
|
.join("applications")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icons_dir() -> PathBuf {
|
fn icons_dir() -> PathBuf {
|
||||||
dirs::data_dir()
|
crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("icons/hicolor")
|
.join("icons/hicolor")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,21 +104,22 @@ pub fn integrate(record: &AppImageRecord) -> Result<IntegrationResult, Integrati
|
|||||||
|
|
||||||
let icon_id = format!("driftwood-{}", app_id);
|
let icon_id = format!("driftwood-{}", app_id);
|
||||||
|
|
||||||
let desktop_content = format!(
|
let desktop_content = format!("\
|
||||||
"[Desktop Entry]\n\
|
[Desktop Entry]
|
||||||
Type=Application\n\
|
Type=Application
|
||||||
Name={name}\n\
|
Name={name}
|
||||||
Exec=\"{exec}\" %U\n\
|
Exec=\"{exec}\" %U
|
||||||
Icon={icon}\n\
|
Icon={icon}
|
||||||
Categories={categories}\n\
|
Categories={categories}
|
||||||
Comment={comment}\n\
|
Comment={comment}
|
||||||
Terminal=false\n\
|
Terminal=false
|
||||||
X-AppImage-Path={path}\n\
|
X-AppImage-Path={path}
|
||||||
X-AppImage-Version={version}\n\
|
X-AppImage-Version={version}
|
||||||
X-AppImage-Managed-By=Driftwood\n\
|
X-AppImage-Managed-By=Driftwood
|
||||||
X-AppImage-Integrated-Date={date}\n",
|
X-AppImage-Integrated-Date={date}
|
||||||
|
",
|
||||||
name = app_name,
|
name = app_name,
|
||||||
exec = record.path,
|
exec = escape_exec_arg(&record.path),
|
||||||
icon = icon_id,
|
icon = icon_id,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
comment = comment,
|
comment = comment,
|
||||||
|
|||||||
@@ -58,18 +58,18 @@ impl LaunchMethod {
|
|||||||
/// Result of a launch attempt.
|
/// Result of a launch attempt.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LaunchResult {
|
pub enum LaunchResult {
|
||||||
/// Successfully spawned the process and it's still running.
|
/// Process spawned and survived the startup crash-check window.
|
||||||
Started {
|
Started {
|
||||||
child: Child,
|
pid: u32,
|
||||||
method: LaunchMethod,
|
method: LaunchMethod,
|
||||||
},
|
},
|
||||||
/// Process spawned but crashed immediately (within ~1 second).
|
/// Process spawned but exited during the startup crash-check window.
|
||||||
Crashed {
|
Crashed {
|
||||||
exit_code: Option<i32>,
|
exit_code: Option<i32>,
|
||||||
stderr: String,
|
stderr: String,
|
||||||
method: LaunchMethod,
|
method: LaunchMethod,
|
||||||
},
|
},
|
||||||
/// Failed to launch.
|
/// Failed to launch (binary not found, permission denied, etc.).
|
||||||
Failed(String),
|
Failed(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,34 +183,21 @@ fn execute_appimage(
|
|||||||
|
|
||||||
match cmd.spawn() {
|
match cmd.spawn() {
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
// Give the process a brief moment to fail on immediate errors
|
let pid = child.id();
|
||||||
// (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();
|
|
||||||
|
|
||||||
|
// 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 {
|
LaunchResult::Crashed {
|
||||||
exit_code: status.code(),
|
exit_code,
|
||||||
stderr: stderr_text,
|
stderr,
|
||||||
method: method.clone(),
|
method: method.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
None => {
|
||||||
// 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());
|
|
||||||
LaunchResult::Started {
|
LaunchResult::Started {
|
||||||
child,
|
pid,
|
||||||
method: method.clone(),
|
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.
|
/// Parse launch arguments with basic quote support.
|
||||||
/// Splits on whitespace, respecting double-quoted strings.
|
/// Splits on whitespace, respecting double-quoted strings.
|
||||||
/// Returns an empty Vec if the input is None or empty.
|
/// Returns an empty Vec if the input is None or empty.
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ pub struct CleanupSummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn applications_dir() -> PathBuf {
|
fn applications_dir() -> PathBuf {
|
||||||
dirs::data_dir()
|
crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("applications")
|
.join("applications")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icons_dir() -> PathBuf {
|
fn icons_dir() -> PathBuf {
|
||||||
dirs::data_dir()
|
crate::config::data_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.local/share"))
|
|
||||||
.join("icons/hicolor")
|
.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");
|
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 {
|
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!(
|
html.push_str(&format!(
|
||||||
"<tr><td>{}</td><td class=\"{}\">{}</td><td>{}</td><td>{} {}</td><td>{}</td><td>{}</td></tr>\n",
|
"<tr><td>{}</td><td class=\"{}\">{}</td><td>{}</td><td>{} {}</td><td>{}</td><td>{}</td></tr>\n",
|
||||||
html_escape(&f.cve_id),
|
html_escape(&f.cve_id),
|
||||||
|
|||||||
@@ -44,8 +44,7 @@ impl ProfileSource {
|
|||||||
|
|
||||||
/// Directory where local sandbox profiles are stored.
|
/// Directory where local sandbox profiles are stored.
|
||||||
fn profiles_dir() -> PathBuf {
|
fn profiles_dir() -> PathBuf {
|
||||||
let dir = dirs::config_dir()
|
let dir = crate::config::config_dir_fallback()
|
||||||
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
|
||||||
.join("driftwood")
|
.join("driftwood")
|
||||||
.join("sandbox");
|
.join("sandbox");
|
||||||
fs::create_dir_all(&dir).ok();
|
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)
|
// If all compared parts are equal, only consider newer if extra parts are non-zero
|
||||||
latest_parts.len() > current_parts.len()
|
// (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.
|
/// 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.
|
/// Check if AppImageUpdate tool is available on the system.
|
||||||
pub fn has_appimage_update_tool() -> bool {
|
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")
|
std::process::Command::new("AppImageUpdate")
|
||||||
.arg("--help")
|
.arg("--help")
|
||||||
.stdout(std::process::Stdio::null())
|
.stdout(std::process::Stdio::null())
|
||||||
.stderr(std::process::Stdio::null())
|
.stderr(std::process::Stdio::null())
|
||||||
.status()
|
.status()
|
||||||
.map(|s| s.success())
|
.is_ok()
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Batch check: read update info from an AppImage and check for updates.
|
/// 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
|
// Atomic rename temp -> target
|
||||||
if let Err(e) = fs::rename(&temp_path, appimage_path) {
|
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 {
|
if let Some(ref backup) = backup_path {
|
||||||
fs::rename(backup, appimage_path).ok();
|
fs::rename(backup, appimage_path).ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ use application::DriftwoodApplication;
|
|||||||
use config::{APP_ID, GSETTINGS_SCHEMA_DIR};
|
use config::{APP_ID, GSETTINGS_SCHEMA_DIR};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
// Point GSettings at our compiled schema directory (dev builds)
|
// Point GSettings at our compiled schema directory (dev builds).
|
||||||
std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR);
|
// SAFETY: Called before any threads are spawned, at program start.
|
||||||
|
unsafe { std::env::set_var("GSETTINGS_SCHEMA_DIR", GSETTINGS_SCHEMA_DIR); }
|
||||||
|
|
||||||
// Parse CLI arguments
|
// Parse CLI arguments
|
||||||
let parsed = cli::Cli::parse();
|
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
|
// Toast overlay for copy actions
|
||||||
let toast_overlay = adw::ToastOverlay::new();
|
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,
|
// vhomogeneous=false so the stack sizes to the visible child only,
|
||||||
// preventing shorter tabs from having excess scrollable empty space.
|
// preventing shorter tabs from having excess scrollable empty space.
|
||||||
let view_stack = adw::ViewStack::new();
|
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);
|
btn_ref.set_sensitive(true);
|
||||||
match result {
|
match result {
|
||||||
Ok(launcher::LaunchResult::Started { child, method }) => {
|
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||||
let pid = child.id();
|
|
||||||
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
|
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 db_wayland = db_launch.clone();
|
||||||
let path_clone = path.clone();
|
let path_clone = path.clone();
|
||||||
glib::spawn_future_local(async move {
|
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);
|
spinner_ref.set_visible(false);
|
||||||
if let Ok(Some(data)) = result {
|
if let Ok(Some(data)) = result {
|
||||||
let gbytes = glib::Bytes::from(&data);
|
let gbytes = glib::Bytes::from(&data);
|
||||||
let stream = gio::MemoryInputStream::from_bytes(&gbytes);
|
if let Ok(texture) = gtk::gdk::Texture::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);
|
|
||||||
picture_ref.set_paintable(Some(&texture));
|
picture_ref.set_paintable(Some(&texture));
|
||||||
if let Some(slot) = textures_load.borrow_mut().get_mut(idx) {
|
if let Some(slot) = textures_load.borrow_mut().get_mut(idx) {
|
||||||
*slot = Some(texture);
|
*slot = Some(texture);
|
||||||
@@ -708,8 +703,11 @@ fn build_overview_tab(record: &AppImageRecord, db: &Rc<Database>) -> gtk::Box {
|
|||||||
.margin_start(12)
|
.margin_start(12)
|
||||||
.margin_end(12)
|
.margin_end(12)
|
||||||
.build();
|
.build();
|
||||||
let label_row = adw::ActionRow::new();
|
let label_row = gtk::ListBoxRow::builder()
|
||||||
label_row.set_child(Some(&label));
|
.activatable(false)
|
||||||
|
.selectable(false)
|
||||||
|
.child(&label)
|
||||||
|
.build();
|
||||||
row.add_row(&label_row);
|
row.add_row(&label_row);
|
||||||
|
|
||||||
release_group.add(&row);
|
release_group.add(&row);
|
||||||
@@ -2080,12 +2078,7 @@ fn fetch_favicon_async(url: &str, image: >k::Image) {
|
|||||||
|
|
||||||
if let Ok(Some(data)) = result {
|
if let Ok(Some(data)) = result {
|
||||||
let gbytes = glib::Bytes::from(&data);
|
let gbytes = glib::Bytes::from(&data);
|
||||||
let stream = gio::MemoryInputStream::from_bytes(&gbytes);
|
if let Ok(texture) = gtk::gdk::Texture::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);
|
|
||||||
image_ref.set_paintable(Some(&texture));
|
image_ref.set_paintable(Some(&texture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,17 +90,15 @@ pub fn show_update_dialog(
|
|||||||
let db_update = db_ref.clone();
|
let db_update = db_ref.clone();
|
||||||
let record_path = record_clone.path.clone();
|
let record_path = record_clone.path.clone();
|
||||||
let new_version = check_result.latest_version.clone();
|
let new_version = check_result.latest_version.clone();
|
||||||
dialog_ref.connect_response(None, move |dlg, response| {
|
dialog_ref.connect_response(Some("update"), move |dlg, _response| {
|
||||||
if response == "update" {
|
start_update(
|
||||||
start_update(
|
dlg,
|
||||||
dlg,
|
&record_path,
|
||||||
&record_path,
|
&download_url,
|
||||||
&download_url,
|
record_id,
|
||||||
record_id,
|
new_version.as_deref(),
|
||||||
new_version.as_deref(),
|
&db_update,
|
||||||
&db_update,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -239,11 +237,9 @@ fn handle_old_version_cleanup(dialog: &adw::AlertDialog, old_path: PathBuf) {
|
|||||||
dialog.set_response_appearance("remove-old", adw::ResponseAppearance::Destructive);
|
dialog.set_response_appearance("remove-old", adw::ResponseAppearance::Destructive);
|
||||||
|
|
||||||
let path = old_path.clone();
|
let path = old_path.clone();
|
||||||
dialog.connect_response(None, move |_dlg, response| {
|
dialog.connect_response(Some("remove-old"), move |_dlg, _response| {
|
||||||
if response == "remove-old" {
|
if path.exists() {
|
||||||
if path.exists() {
|
std::fs::remove_file(&path).ok();
|
||||||
std::fs::remove_file(&path).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,12 +182,10 @@ pub fn copy_button(text_to_copy: &str, toast_overlay: Option<&adw::ToastOverlay>
|
|||||||
let text = text_to_copy.to_string();
|
let text = text_to_copy.to_string();
|
||||||
let toast = toast_overlay.cloned();
|
let toast = toast_overlay.cloned();
|
||||||
btn.connect_clicked(move |button| {
|
btn.connect_clicked(move |button| {
|
||||||
if let Some(display) = button.display().into() {
|
let clipboard = button.display().clipboard();
|
||||||
let clipboard = gtk::gdk::Display::clipboard(&display);
|
clipboard.set_text(&text);
|
||||||
clipboard.set_text(&text);
|
if let Some(ref overlay) = toast {
|
||||||
if let Some(ref overlay) = toast {
|
overlay.add_toast(adw::Toast::new("Copied to clipboard"));
|
||||||
overlay.add_toast(adw::Toast::new("Copied to clipboard"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
btn
|
btn
|
||||||
|
|||||||
@@ -614,8 +614,8 @@ impl DriftwoodWindow {
|
|||||||
launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &launch_args, &[])
|
launcher::launch_appimage(&bg_db, record_id, appimage_path, "gui_context", &launch_args, &[])
|
||||||
}).await;
|
}).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(launcher::LaunchResult::Started { child, method }) => {
|
Ok(launcher::LaunchResult::Started { pid, method }) => {
|
||||||
log::info!("Launched: {} (PID: {}, method: {})", path_str, child.id(), method.as_str());
|
log::info!("Launched: {} (PID: {}, method: {})", path_str, pid, method.as_str());
|
||||||
}
|
}
|
||||||
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
|
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
|
||||||
log::error!("App crashed (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
|
log::error!("App crashed (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
|
||||||
|
|||||||
Reference in New Issue
Block a user