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:
lashman
2026-02-27 22:48:43 +02:00
parent e9343da249
commit 830c3cad9d
21 changed files with 228 additions and 181 deletions

View File

@@ -8,3 +8,4 @@ Type=Application
Categories=System;PackageManager;GTK;
Keywords=AppImage;Application;Manager;Package;
StartupNotify=true
SingleMainWindow=true

View File

@@ -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 {

View File

@@ -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"))
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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,

View File

@@ -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.

View File

@@ -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")
}

View File

@@ -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),

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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: &gtk::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));
}
}

View File

@@ -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();
}
}
});
}
}

View File

@@ -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
}

View File

@@ -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);