Skip to content

Commit e54f2a2

Browse files
authored
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.
2 parents 67802f5 + ce67af8 commit e54f2a2

7 files changed

Lines changed: 225 additions & 3 deletions

File tree

desktop/tauri/src-tauri/src/main.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod portmaster;
1919
mod traymenu;
2020
mod window;
2121
mod commands;
22+
mod relaunch;
2223

2324
use log::{debug, error, info};
2425
use portmaster::PortmasterExt;
@@ -61,6 +62,22 @@ impl portmaster::Handler for WsHandler {
6162
fn on_connect(&mut self, cli: portapi::client::PortAPI) {
6263
info!("connection established, creating main window");
6364

65+
// If an restart-ui-process was observed before disconnect (e.g. on upgrade),
66+
// relaunch the UI process now that the core is reachable again.
67+
if self.handle.portmaster().consume_restart_ui_proc_requested() {
68+
info!("restart-ui-process pending, relaunching UI process");
69+
match relaunch::request_ui_relaunch() {
70+
Ok(()) => {
71+
self.handle.exit(0);
72+
return;
73+
}
74+
Err(err) => {
75+
error!("failed to relaunch UI process after upgrade: {}", err);
76+
error!("continuing with current UI process");
77+
}
78+
}
79+
}
80+
6481
// we successfully connected to Portmaster. Set is_first_connect to false
6582
// so we don't show the splash-screen when we loose connection.
6683
self.is_first_connect = false;
@@ -141,6 +158,8 @@ fn show_webview_not_installed_dialog() -> i32 {
141158
}
142159

143160
fn main() {
161+
relaunch::run_relaunch_helper_if_requested();
162+
144163
if tauri::webview_version().is_err() {
145164
std::process::exit(show_webview_not_installed_dialog());
146165
}

desktop/tauri/src-tauri/src/portmaster/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ pub struct PortmasterInterface<R: Runtime> {
7575

7676
// handle to the tray handler task so we can abort it when reconnecting
7777
pub tray_handler_task: Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
78+
79+
// Marks that a UI process restart event was observed (e.g. on upgrade)
80+
// and the UI process should relaunch after the next successful reconnect.
81+
pending_restart: AtomicBool,
7882
}
7983

8084
impl<R: Runtime> PortmasterInterface<R> {
@@ -261,6 +265,16 @@ impl<R: Runtime> PortmasterInterface<R> {
261265
});
262266
}
263267

268+
/// Marks that a UI process restart has been requested.
269+
pub fn mark_restart_ui_proc_requested(&self) {
270+
self.pending_restart.store(true, Ordering::Release);
271+
}
272+
273+
/// Returns whether a UI process restart was requested and clears the flag.
274+
pub fn consume_restart_ui_proc_requested(&self) -> bool {
275+
self.pending_restart.swap(false, Ordering::AcqRel)
276+
}
277+
264278
//// Internal functions
265279
fn start_notification_handler(&self) {
266280
if let Some(api) = self.get_api() {
@@ -346,6 +360,7 @@ pub fn setup(app: AppHandle) {
346360
handle_prompts: AtomicBool::new(false),
347361
should_show_after_bootstrap: AtomicBool::new(true),
348362
tray_handler_task: Mutex::new(None),
363+
pending_restart: AtomicBool::new(false),
349364
};
350365

351366
app.manage(interface);
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
use std::{
2+
ffi::OsString,
3+
path::Path,
4+
process::{Command, Stdio},
5+
thread,
6+
time::Duration,
7+
};
8+
9+
use log::{debug, error, warn};
10+
11+
const UI_RELAUNCH_HELPER_ENV: &str = "PORTMASTER_UI_RELAUNCH_HELPER";
12+
const RELAUNCH_RETRY_COUNT: usize = 40;
13+
const RELAUNCH_RETRY_DELAY: Duration = Duration::from_millis(500);
14+
15+
fn current_process_argv0() -> Option<OsString> {
16+
std::env::args_os().next()
17+
}
18+
19+
fn is_usable_launch_program(program: &OsString) -> bool {
20+
let path = Path::new(program);
21+
22+
// Fail closed for command-only values (for example, "portmaster"): we cannot
23+
// verify where they resolve to, so do not use them for relaunch.
24+
if !path.is_absolute() && path.components().count() <= 1 {
25+
return false;
26+
}
27+
28+
if !path.exists() || !path.is_file() {
29+
return false;
30+
}
31+
32+
#[cfg(unix)]
33+
{
34+
use std::os::unix::fs::PermissionsExt;
35+
36+
if let Ok(meta) = std::fs::metadata(path) {
37+
return meta.permissions().mode() & 0o111 != 0;
38+
}
39+
return false;
40+
}
41+
42+
#[cfg(not(unix))]
43+
{
44+
true
45+
}
46+
}
47+
48+
fn resolve_launch_program() -> Result<OsString, String> {
49+
let current_exe = std::env::current_exe().ok().map(|p| p.into_os_string());
50+
let argv0 = current_process_argv0();
51+
52+
if let Some(program) = current_exe.as_ref().filter(|p| is_usable_launch_program(p)) {
53+
return Ok(program.clone());
54+
}
55+
56+
if let Some(program) = argv0.as_ref().filter(|p| is_usable_launch_program(p)) {
57+
return Ok(program.clone());
58+
}
59+
60+
Err("failed to determine relaunch executable: no verified launchable file path from current_exe or argv0".to_string())
61+
}
62+
63+
fn current_process_args() -> Vec<OsString> {
64+
std::env::args_os()
65+
.skip(1)
66+
.filter(|arg| {
67+
// On upgrade-triggered relaunch we always want to show the UI,
68+
// so do not propagate background startup flags.
69+
arg != "--background" && arg != "-b"
70+
})
71+
.collect()
72+
}
73+
74+
pub fn request_ui_relaunch() -> Result<(), String> {
75+
let exe = resolve_launch_program()?;
76+
let args = current_process_args();
77+
78+
let mut cmd = Command::new(&exe);
79+
cmd.args(&args)
80+
.env(UI_RELAUNCH_HELPER_ENV, "1")
81+
.stdin(Stdio::null())
82+
.stdout(Stdio::null())
83+
.stderr(Stdio::null());
84+
85+
cmd.spawn()
86+
.map_err(|err| format!("failed to spawn relaunch helper process: {}", err))?;
87+
88+
Ok(())
89+
}
90+
91+
pub fn run_relaunch_helper_if_requested() {
92+
if std::env::var(UI_RELAUNCH_HELPER_ENV).ok().as_deref() != Some("1") {
93+
return;
94+
}
95+
96+
if let Err(err) = run_relaunch_helper() {
97+
error!("[tauri] relaunch helper failed: {}", err);
98+
}
99+
100+
std::process::exit(0);
101+
}
102+
103+
fn run_relaunch_helper() -> Result<(), String> {
104+
let exe = resolve_launch_program()?;
105+
let args = current_process_args();
106+
107+
debug!("[tauri] relaunch helper started");
108+
109+
for attempt in 1..=RELAUNCH_RETRY_COUNT {
110+
let mut cmd = Command::new(&exe);
111+
cmd.args(&args)
112+
.env_remove(UI_RELAUNCH_HELPER_ENV)
113+
.stdin(Stdio::null())
114+
.stdout(Stdio::null())
115+
.stderr(Stdio::null());
116+
117+
let mut child = cmd
118+
.spawn()
119+
.map_err(|err| format!("failed to spawn replacement process: {}", err))?;
120+
121+
thread::sleep(RELAUNCH_RETRY_DELAY);
122+
123+
match child.try_wait() {
124+
Ok(Some(status)) => {
125+
// Most commonly means the single-instance guard still detected a running instance.
126+
warn!(
127+
"[tauri] replacement process exited quickly (attempt {}/{}; status={}), retrying",
128+
attempt,
129+
RELAUNCH_RETRY_COUNT,
130+
status
131+
);
132+
thread::sleep(RELAUNCH_RETRY_DELAY);
133+
}
134+
Ok(None) => {
135+
debug!(
136+
"[tauri] replacement process is running (attempt {}/{})",
137+
attempt,
138+
RELAUNCH_RETRY_COUNT
139+
);
140+
return Ok(());
141+
}
142+
Err(err) => {
143+
return Err(format!("failed to observe replacement process status: {}", err));
144+
}
145+
}
146+
}
147+
148+
Err(format!(
149+
"failed to relaunch UI process after {} attempts",
150+
RELAUNCH_RETRY_COUNT
151+
))
152+
}

desktop/tauri/src-tauri/src/traymenu.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,22 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
497497
}
498498
};
499499

500+
let mut portmaster_restart_ui_proc_event_subscription = match cli
501+
.request(Request::Subscribe(
502+
"query runtime:modules/core/event/restart-ui-process".to_string(),
503+
))
504+
.await
505+
{
506+
Ok(rx) => rx,
507+
Err(err) => {
508+
error!(
509+
"cancel try_handler: failed to subscribe to 'runtime:modules/core/event/restart-ui-process': {}",
510+
err
511+
);
512+
return;
513+
}
514+
};
515+
500516
update_icon_color(&icon, IconColor::Blue);
501517

502518
let mut system_status = SystemStatus::default();
@@ -610,6 +626,20 @@ pub async fn tray_handler(cli: PortAPI, app: tauri::AppHandle) {
610626
},
611627
_ => {},
612628
}
629+
},
630+
msg = portmaster_restart_ui_proc_event_subscription.recv() => {
631+
let msg = match msg {
632+
Some(m) => m,
633+
None => { break }
634+
};
635+
636+
debug!("Upgrade restart event received: {:?}", msg);
637+
match msg {
638+
Response::Ok(_, _) | Response::New(_, _) | Response::Update(_, _) => {
639+
app.portmaster().mark_restart_ui_proc_requested();
640+
},
641+
_ => {},
642+
}
613643
}
614644
}
615645
}

service/core/api.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,14 @@ func shutdown(_ *api.Request) (msg string, err error) {
180180
}
181181

182182
// restart restarts the Portmaster.
183-
func restart(_ *api.Request) (msg string, err error) {
183+
func restart(ar *api.Request) (msg string, err error) {
184184
log.Info("core: user requested restart via action")
185185

186+
// 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.
187+
if ar != nil && ar.Request != nil && ar.Request.URL.Query().Get("source") == "upgrade" {
188+
pushModuleEvent("core", "restart-ui-process", false, nil)
189+
}
190+
186191
// Trigger restart
187192
module.instance.Restart()
188193

service/updates/index.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,8 @@ func copyAndCheckSHA256Sum(src, dst, sha256sum string, filePermission utils.FSPe
377377
// Check expected hash.
378378
var expectedDigest []byte
379379
if sha256sum != "" {
380-
expectedDigest, err := hex.DecodeString(sha256sum)
380+
var err error
381+
expectedDigest, err = hex.DecodeString(sha256sum)
381382
if err != nil {
382383
return fmt.Errorf("invalid hex encoding for expected hash %s: %w", sha256sum, err)
383384
}

service/updates/module.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ func (u *Updater) updateAndUpgrade(w *mgr.WorkerCtx, indexURLs []string, ignoreV
423423
Type: notifications.ActionTypeWebhook,
424424
Payload: notifications.ActionTypeWebhookPayload{
425425
Method: "POST",
426-
URL: "core/restart",
426+
URL: "core/restart?source=upgrade",
427427
},
428428
},
429429
},

0 commit comments

Comments
 (0)