Add first-run permission summary dialog before launch

Shows what access the AppImage will have on first launch. Lists file,
network, and display server access. Mentions firejail if available.
Tracks prompted status in DB so the dialog only appears once per app.
This commit is contained in:
lashman
2026-02-28 00:07:49 +02:00
parent 8cf71ae858
commit 585320b363
4 changed files with 203 additions and 70 deletions

View File

@@ -101,79 +101,41 @@ pub fn build_detail_page(record: &AppImageRecord, db: &Rc<Database>) -> adw::Nav
let launch_args_raw = record.launch_args.clone();
let db_launch = db.clone();
let toast_launch = toast_overlay.clone();
let first_run_prompted = record.first_run_prompted;
let db_perm = db.clone();
launch_button.connect_clicked(move |btn| {
btn.set_sensitive(false);
let btn_ref = btn.clone();
let path = path.clone();
let app_name = app_name_launch.clone();
let db_launch = db_launch.clone();
let toast_ref = toast_launch.clone();
// Check if first-run permission dialog should be shown
if !first_run_prompted {
let btn_ref = btn.clone();
let path = path.clone();
let app_name = app_name_launch.clone();
let app_name_dialog = app_name.clone();
let db_launch = db_launch.clone();
let toast_ref = toast_launch.clone();
let launch_args_raw = launch_args_raw.clone();
let db_perm = db_perm.clone();
crate::ui::permission_dialog::show_permission_dialog(
btn,
&app_name_dialog,
record_id,
&db_perm,
move || {
do_launch(
&btn_ref, record_id, &path, &app_name,
launcher::parse_launch_args(launch_args_raw.as_deref()),
&db_launch, &toast_ref,
);
},
);
return;
}
let launch_args = launcher::parse_launch_args(launch_args_raw.as_deref());
glib::spawn_future_local(async move {
let path_bg = path.clone();
let result = gio::spawn_blocking(move || {
let appimage_path = std::path::Path::new(&path_bg);
launcher::launch_appimage(
&Database::open().expect("DB open"),
record_id,
appimage_path,
"gui_detail",
&launch_args,
&[],
)
}).await;
btn_ref.set_sensitive(true);
match result {
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 {
glib::timeout_future(std::time::Duration::from_secs(3)).await;
let analysis_result = gio::spawn_blocking(move || {
wayland::analyze_running_process(pid)
}).await;
if let Ok(Ok(analysis)) = analysis_result {
let status_str = analysis.as_status_str();
log::info!(
"Runtime Wayland: {} -> {} (pid={}, env: {:?})",
path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
);
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
}
});
}
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
if let Some(app) = gtk::gio::Application::default() {
notification::send_system_notification(
&app,
&format!("crash-{}", record_id),
&format!("{} crashed", app_name),
&stderr.chars().take(200).collect::<String>(),
gtk::gio::NotificationPriority::Urgent,
);
}
}
Ok(launcher::LaunchResult::Failed(msg)) => {
log::error!("Failed to launch: {}", msg);
let toast = adw::Toast::builder()
.title(&format!("Could not launch: {}", msg))
.timeout(5)
.build();
toast_ref.add_toast(toast);
}
Err(_) => {
log::error!("Launch task panicked");
}
}
});
do_launch(
btn, record_id, &path, &app_name_launch,
launch_args, &db_launch, &toast_launch,
);
});
header.pack_end(&launch_button);
// Check for Update button
let update_button = gtk::Button::builder()
@@ -2553,3 +2515,82 @@ fn show_uninstall_dialog(
dialog.present(Some(toast_overlay));
}
fn do_launch(
btn: &gtk::Button,
record_id: i64,
path: &str,
app_name: &str,
launch_args: Vec<String>,
db: &Rc<Database>,
toast_overlay: &adw::ToastOverlay,
) {
btn.set_sensitive(false);
let btn_ref = btn.clone();
let path = path.to_string();
let app_name = app_name.to_string();
let db_launch = db.clone();
let toast_ref = toast_overlay.clone();
glib::spawn_future_local(async move {
let path_bg = path.clone();
let result = gio::spawn_blocking(move || {
let appimage_path = std::path::Path::new(&path_bg);
launcher::launch_appimage(
&Database::open().expect("DB open"),
record_id,
appimage_path,
"gui_detail",
&launch_args,
&[],
)
}).await;
btn_ref.set_sensitive(true);
match result {
Ok(launcher::LaunchResult::Started { pid, method }) => {
log::info!("Launched: {} (PID: {}, method: {})", path, pid, method.as_str());
let db_wayland = db_launch.clone();
let path_clone = path.clone();
glib::spawn_future_local(async move {
glib::timeout_future(std::time::Duration::from_secs(3)).await;
let analysis_result = gio::spawn_blocking(move || {
wayland::analyze_running_process(pid)
}).await;
if let Ok(Ok(analysis)) = analysis_result {
let status_str = analysis.as_status_str();
log::info!(
"Runtime Wayland: {} -> {} (pid={}, env: {:?})",
path_clone, analysis.status_label(), analysis.pid, analysis.env_vars,
);
db_wayland.update_runtime_wayland_status(record_id, status_str).ok();
}
});
}
Ok(launcher::LaunchResult::Crashed { exit_code, stderr, method }) => {
log::error!("App crashed on launch (exit {}, method: {}): {}", exit_code.unwrap_or(-1), method.as_str(), stderr);
widgets::show_crash_dialog(&btn_ref, &app_name, exit_code, &stderr);
if let Some(app) = gtk::gio::Application::default() {
notification::send_system_notification(
&app,
&format!("crash-{}", record_id),
&format!("{} crashed", app_name),
&stderr.chars().take(200).collect::<String>(),
gtk::gio::NotificationPriority::Urgent,
);
}
}
Ok(launcher::LaunchResult::Failed(msg)) => {
log::error!("Failed to launch: {}", msg);
let toast = adw::Toast::builder()
.title(&format!("Could not launch: {}", msg))
.timeout(5)
.build();
toast_ref.add_toast(toast);
}
Err(_) => {
log::error!("Launch task panicked");
}
}
});
}