Merge pull request #2155 from safing/feature/s40-restart_ui_on_upgrade

(feat) Restart the UI process after automatic update

The Tauri (UI) process now automatically restarts after a successful update.
This commit is contained in:
Alexandr Stelnykovych
2026-04-10 16:54:30 +03:00
committed by GitHub
7 changed files with 225 additions and 3 deletions
+19
View File
@@ -19,6 +19,7 @@ mod portmaster;
mod traymenu;
mod window;
mod commands;
mod relaunch;
use log::{debug, error, info};
use portmaster::PortmasterExt;
@@ -61,6 +62,22 @@ impl portmaster::Handler for WsHandler {
fn on_connect(&mut self, cli: portapi::client::PortAPI) {
info!("connection established, creating main window");
// If an restart-ui-process was observed before disconnect (e.g. on upgrade),
// relaunch the UI process now that the core is reachable again.
if self.handle.portmaster().consume_restart_ui_proc_requested() {
info!("restart-ui-process pending, relaunching UI process");
match relaunch::request_ui_relaunch() {
Ok(()) => {
self.handle.exit(0);
return;
}
Err(err) => {
error!("failed to relaunch UI process after upgrade: {}", err);
error!("continuing with current UI process");
}
}
}
// we successfully connected to Portmaster. Set is_first_connect to false
// so we don't show the splash-screen when we loose connection.
self.is_first_connect = false;
@@ -141,6 +158,8 @@ fn show_webview_not_installed_dialog() -> i32 {
}
fn main() {
relaunch::run_relaunch_helper_if_requested();
if tauri::webview_version().is_err() {
std::process::exit(show_webview_not_installed_dialog());
}
@@ -75,6 +75,10 @@ pub struct PortmasterInterface<R: Runtime> {
// handle to the tray handler task so we can abort it when reconnecting
pub tray_handler_task: Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
// Marks that a UI process restart event was observed (e.g. on upgrade)
// and the UI process should relaunch after the next successful reconnect.
pending_restart: AtomicBool,
}
impl<R: Runtime> PortmasterInterface<R> {
@@ -261,6 +265,16 @@ impl<R: Runtime> PortmasterInterface<R> {
});
}
/// Marks that a UI process restart has been requested.
pub fn mark_restart_ui_proc_requested(&self) {
self.pending_restart.store(true, Ordering::Release);
}
/// Returns whether a UI process restart was requested and clears the flag.
pub fn consume_restart_ui_proc_requested(&self) -> bool {
self.pending_restart.swap(false, Ordering::AcqRel)
}
//// Internal functions
fn start_notification_handler(&self) {
if let Some(api) = self.get_api() {
@@ -346,6 +360,7 @@ pub fn setup(app: AppHandle) {
handle_prompts: AtomicBool::new(false),
should_show_after_bootstrap: AtomicBool::new(true),
tray_handler_task: Mutex::new(None),
pending_restart: AtomicBool::new(false),
};
app.manage(interface);
+152
View File
@@ -0,0 +1,152 @@
use std::{
ffi::OsString,
path::Path,
process::{Command, Stdio},
thread,
time::Duration,
};
use log::{debug, error, warn};
const UI_RELAUNCH_HELPER_ENV: &str = "PORTMASTER_UI_RELAUNCH_HELPER";
const RELAUNCH_RETRY_COUNT: usize = 40;
const RELAUNCH_RETRY_DELAY: Duration = Duration::from_millis(500);
fn current_process_argv0() -> Option<OsString> {
std::env::args_os().next()
}
fn is_usable_launch_program(program: &OsString) -> bool {
let path = Path::new(program);
// Fail closed for command-only values (for example, "portmaster"): we cannot
// verify where they resolve to, so do not use them for relaunch.
if !path.is_absolute() && path.components().count() <= 1 {
return false;
}
if !path.exists() || !path.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = std::fs::metadata(path) {
return meta.permissions().mode() & 0o111 != 0;
}
return false;
}
#[cfg(not(unix))]
{
true
}
}
fn resolve_launch_program() -> Result<OsString, String> {
let current_exe = std::env::current_exe().ok().map(|p| p.into_os_string());
let argv0 = current_process_argv0();
if let Some(program) = current_exe.as_ref().filter(|p| is_usable_launch_program(p)) {
return Ok(program.clone());
}
if let Some(program) = argv0.as_ref().filter(|p| is_usable_launch_program(p)) {
return Ok(program.clone());
}
Err("failed to determine relaunch executable: no verified launchable file path from current_exe or argv0".to_string())
}
fn current_process_args() -> Vec<OsString> {
std::env::args_os()
.skip(1)
.filter(|arg| {
// On upgrade-triggered relaunch we always want to show the UI,
// so do not propagate background startup flags.
arg != "--background" && arg != "-b"
})
.collect()
}
pub fn request_ui_relaunch() -> Result<(), String> {
let exe = resolve_launch_program()?;
let args = current_process_args();
let mut cmd = Command::new(&exe);
cmd.args(&args)
.env(UI_RELAUNCH_HELPER_ENV, "1")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.spawn()
.map_err(|err| format!("failed to spawn relaunch helper process: {}", err))?;
Ok(())
}
pub fn run_relaunch_helper_if_requested() {
if std::env::var(UI_RELAUNCH_HELPER_ENV).ok().as_deref() != Some("1") {
return;
}
if let Err(err) = run_relaunch_helper() {
error!("[tauri] relaunch helper failed: {}", err);
}
std::process::exit(0);
}
fn run_relaunch_helper() -> Result<(), String> {
let exe = resolve_launch_program()?;
let args = current_process_args();
debug!("[tauri] relaunch helper started");
for attempt in 1..=RELAUNCH_RETRY_COUNT {
let mut cmd = Command::new(&exe);
cmd.args(&args)
.env_remove(UI_RELAUNCH_HELPER_ENV)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = cmd
.spawn()
.map_err(|err| format!("failed to spawn replacement process: {}", err))?;
thread::sleep(RELAUNCH_RETRY_DELAY);
match child.try_wait() {
Ok(Some(status)) => {
// Most commonly means the single-instance guard still detected a running instance.
warn!(
"[tauri] replacement process exited quickly (attempt {}/{}; status={}), retrying",
attempt,
RELAUNCH_RETRY_COUNT,
status
);
thread::sleep(RELAUNCH_RETRY_DELAY);
}
Ok(None) => {
debug!(
"[tauri] replacement process is running (attempt {}/{})",
attempt,
RELAUNCH_RETRY_COUNT
);
return Ok(());
}
Err(err) => {
return Err(format!("failed to observe replacement process status: {}", err));
}
}
}
Err(format!(
"failed to relaunch UI process after {} attempts",
RELAUNCH_RETRY_COUNT
))
}
+30
View File
@@ -497,6 +497,22 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
}
};
let mut portmaster_restart_ui_proc_event_subscription = match cli
.request(Request::Subscribe(
"query runtime:modules/core/event/restart-ui-process".to_string(),
))
.await
{
Ok(rx) => rx,
Err(err) => {
error!(
"cancel try_handler: failed to subscribe to 'runtime:modules/core/event/restart-ui-process': {}",
err
);
return;
}
};
update_icon_color(&icon, IconColor::Blue);
let mut system_status = SystemStatus::default();
@@ -610,6 +626,20 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
},
_ => {},
}
},
msg = portmaster_restart_ui_proc_event_subscription.recv() => {
let msg = match msg {
Some(m) => m,
None => { break }
};
debug!("Upgrade restart event received: {:?}", msg);
match msg {
Response::Ok(_, _) | Response::New(_, _) | Response::Update(_, _) => {
app.portmaster().mark_restart_ui_proc_requested();
},
_ => {},
}
}
}
}
+6 -1
View File
@@ -180,9 +180,14 @@ func shutdown(_ *api.Request) (msg string, err error) {
}
// restart restarts the Portmaster.
func restart(_ *api.Request) (msg string, err error) {
func restart(ar *api.Request) (msg string, err error) {
log.Info("core: user requested restart via action")
// If the restart request came from an upgrade, also trigger a module event to restart the UI process, so that it can restart itself as well.
if ar != nil && ar.Request != nil && ar.Request.URL.Query().Get("source") == "upgrade" {
pushModuleEvent("core", "restart-ui-process", false, nil)
}
// Trigger restart
module.instance.Restart()
+2 -1
View File
@@ -377,7 +377,8 @@ func copyAndCheckSHA256Sum(src, dst, sha256sum string, filePermission utils.FSPe
// Check expected hash.
var expectedDigest []byte
if sha256sum != "" {
expectedDigest, err := hex.DecodeString(sha256sum)
var err error
expectedDigest, err = hex.DecodeString(sha256sum)
if err != nil {
return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err)
}
+1 -1
View File
@@ -423,7 +423,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
Type: notifications.ActionTypeWebhook,
Payload: notifications.ActionTypeWebhookPayload{
Method: "POST",
URL: "core/restart",
URL: "core/restart?source=upgrade",
},
},
},