Skip to content

Commit 056d408

Browse files
committed
Keep daemon tied to UI lease and stabilize launcher viewport
1 parent 4d98675 commit 056d408

7 files changed

Lines changed: 290 additions & 17 deletions

File tree

src/gui.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::fs;
22
use std::path::{Path, PathBuf};
3+
use std::sync::Arc;
4+
use std::sync::atomic::{AtomicBool, Ordering};
35
use std::sync::mpsc::{self, Receiver};
46
use std::thread;
57
use std::time::{Duration, Instant, SystemTime};
@@ -29,7 +31,7 @@ use crate::platform::spawn_detached;
2931
use crate::platform::{apply_app_window_icon, update_windows_shortcuts_for_exe};
3032
use crate::runtime_log::{append as append_runtime_log, read_recent_lines};
3133
use crate::service;
32-
use crate::state::ServiceState;
34+
use crate::state::{self, ServiceState};
3335

3436
const APP_WINDOW_TITLE: &str = "Linux.do Accelerator";
3537
const APP_ID: &str = "linuxdo-accelerator";
@@ -125,6 +127,8 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
125127
let show_id = show_item.id().clone();
126128
let quit_id = quit_item.id().clone();
127129
let config_for_timeout = config_path.clone();
130+
let tray_lease_stop = spawn_ui_lease_heartbeat(config_path.clone());
131+
let tray_lease_stop_in_loop = tray_lease_stop.clone();
128132

129133
let _menu_handler = MenuEvent::set_event_handler(Some(move |event: MenuEvent| {
130134
if event.id == show_id {
@@ -140,13 +144,15 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
140144
TrayCommand::Restore => {
141145
log_linux_tray_event("tray-shell restore clicked");
142146
let _ = tray_icon.set_visible(false);
147+
tray_lease_stop_in_loop.store(true, Ordering::Relaxed);
143148
let _ = spawn_ui_process(&config_for_timeout);
144149
gtk::main_quit();
145150
return ControlFlow::Break;
146151
}
147152
TrayCommand::Quit => {
148153
log_linux_tray_event("tray-shell quit clicked");
149154
let _ = tray_icon.set_visible(false);
155+
tray_lease_stop_in_loop.store(true, Ordering::Relaxed);
150156
gtk::main_quit();
151157
return ControlFlow::Break;
152158
}
@@ -157,6 +163,7 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
157163
});
158164

159165
gtk::main();
166+
tray_lease_stop.store(true, Ordering::Relaxed);
160167
log_linux_tray_event("tray-shell exit");
161168
Ok(())
162169
}
@@ -175,6 +182,7 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
175182

176183
struct TrayShellApp {
177184
config_path: PathBuf,
185+
lease_stop: Arc<AtomicBool>,
178186
tray_icon: Option<TrayIcon>,
179187
show_item: MenuItem,
180188
quit_item: MenuItem,
@@ -226,10 +234,12 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
226234
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: TrayShellEvent) {
227235
match event {
228236
TrayShellEvent::Restore => {
237+
self.lease_stop.store(true, Ordering::Relaxed);
229238
let _ = spawn_ui_process(&self.config_path);
230239
event_loop.exit();
231240
}
232241
TrayShellEvent::Quit => {
242+
self.lease_stop.store(true, Ordering::Relaxed);
233243
event_loop.exit();
234244
}
235245
}
@@ -240,6 +250,7 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
240250
let quit_item = MenuItem::with_id("tray-quit", "退出程序", true, None);
241251
let show_id = show_item.id().clone();
242252
let quit_id = quit_item.id().clone();
253+
let tray_lease_stop = spawn_ui_lease_heartbeat(config_path.clone());
243254

244255
let event_loop = EventLoop::<TrayShellEvent>::with_user_event()
245256
.build()
@@ -272,13 +283,15 @@ pub fn run_tray_shell(config_path: PathBuf) -> Result<()> {
272283

273284
let mut app = TrayShellApp {
274285
config_path,
286+
lease_stop: tray_lease_stop.clone(),
275287
tray_icon: None,
276288
show_item,
277289
quit_item,
278290
};
279291
let result = event_loop
280292
.run_app(&mut app)
281293
.map_err(|error| anyhow::anyhow!(error.to_string()));
294+
tray_lease_stop.store(true, Ordering::Relaxed);
282295
TrayIconEvent::set_event_handler::<fn(TrayIconEvent)>(None);
283296
MenuEvent::set_event_handler::<fn(MenuEvent)>(None);
284297
result
@@ -288,6 +301,8 @@ struct AcceleratorApp {
288301
config_path: PathBuf,
289302
config: AppConfig,
290303
edge_node_input: String,
304+
owns_ui_lease: bool,
305+
ui_lease_stop: Option<Arc<AtomicBool>>,
291306
status: ServiceState,
292307
recent_logs: Vec<String>,
293308
feedback: String,
@@ -365,6 +380,15 @@ impl AcceleratorApp {
365380
let edge_node_input = config.edge_node_override().unwrap_or_default().to_string();
366381
let config_modified_at = file_modified_at(&config_path);
367382
let status = service::status(Some(config_path.clone())).unwrap_or_default();
383+
let owns_ui_lease = service::resolve_paths(Some(config_path.clone()))
384+
.ok()
385+
.and_then(|paths| state::read_ui_lease(&paths).ok().flatten())
386+
.is_some();
387+
let ui_lease_stop = if owns_ui_lease {
388+
Some(spawn_ui_lease_heartbeat(config_path.clone()))
389+
} else {
390+
None
391+
};
368392
let recent_logs = load_recent_runtime_logs(&config_path);
369393
let runtime_log_modified_at = runtime_log_file_modified_at(&config_path);
370394
#[cfg(target_os = "windows")]
@@ -388,6 +412,8 @@ impl AcceleratorApp {
388412
config_path,
389413
config,
390414
edge_node_input,
415+
owns_ui_lease,
416+
ui_lease_stop,
391417
status,
392418
recent_logs,
393419
feedback: String::new(),
@@ -478,6 +504,16 @@ impl AcceleratorApp {
478504
return;
479505
}
480506

507+
if matches!(action, GuiAction::Start) {
508+
self.owns_ui_lease = self.touch_ui_lease().is_ok();
509+
if self.owns_ui_lease {
510+
if let Some(stop) = self.ui_lease_stop.take() {
511+
stop.store(true, Ordering::Relaxed);
512+
}
513+
self.ui_lease_stop = Some(spawn_ui_lease_heartbeat(self.config_path.clone()));
514+
}
515+
}
516+
481517
self.busy = true;
482518
self.feedback = action.pending_message().to_string();
483519

@@ -512,6 +548,11 @@ impl AcceleratorApp {
512548
self.status.running = false;
513549
self.status.status_text = "已停止".to_string();
514550
self.status.last_error = None;
551+
if let Some(stop) = self.ui_lease_stop.take() {
552+
stop.store(true, Ordering::Relaxed);
553+
}
554+
self.clear_ui_lease();
555+
self.owns_ui_lease = false;
515556
self.optimistic_running = Some((false, deadline));
516557
}
517558
None => {}
@@ -521,6 +562,11 @@ impl AcceleratorApp {
521562
self.optimistic_running = None;
522563
match self.pending_action {
523564
Some(GuiAction::Start) => {
565+
if let Some(stop) = self.ui_lease_stop.take() {
566+
stop.store(true, Ordering::Relaxed);
567+
}
568+
self.clear_ui_lease();
569+
self.owns_ui_lease = false;
524570
self.status.running = false;
525571
self.status.pid = None;
526572
self.status.status_text = "启动失败".to_string();
@@ -1567,6 +1613,18 @@ impl AcceleratorApp {
15671613
}
15681614
}
15691615

1616+
fn touch_ui_lease(&self) -> Result<()> {
1617+
let paths = service::resolve_paths(Some(self.config_path.clone()))?;
1618+
state::touch_ui_lease(&paths, std::process::id())
1619+
}
1620+
1621+
fn clear_ui_lease(&self) {
1622+
if let Ok(paths) = service::resolve_paths(Some(self.config_path.clone())) {
1623+
let _ = state::clear_ui_lease(&paths);
1624+
}
1625+
}
1626+
1627+
15701628
#[cfg(target_os = "windows")]
15711629
fn restore_from_tray(&mut self, ctx: &egui::Context) {
15721630
self.hidden_to_tray = false;
@@ -1645,8 +1703,8 @@ impl eframe::App for AcceleratorApp {
16451703
}
16461704

16471705
if self.current_page == UiPage::Launcher {
1706+
self.ensure_launcher_viewport(ctx);
16481707
if self.center_window_pending {
1649-
self.ensure_launcher_viewport(ctx);
16501708
if let Some(command) = egui::ViewportCommand::center_on_screen(ctx) {
16511709
ctx.send_viewport_cmd(command);
16521710
}
@@ -1727,6 +1785,16 @@ impl eframe::App for AcceleratorApp {
17271785

17281786
ctx.request_repaint_after(repaint_interval);
17291787
}
1788+
1789+
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
1790+
if let Some(stop) = self.ui_lease_stop.take() {
1791+
stop.store(true, Ordering::Relaxed);
1792+
}
1793+
if !self.status.running {
1794+
self.clear_ui_lease();
1795+
self.owns_ui_lease = false;
1796+
}
1797+
}
17301798
}
17311799

17321800
#[derive(Clone, Copy)]
@@ -1876,6 +1944,35 @@ fn file_modified_at(path: &Path) -> Option<SystemTime> {
18761944
fs::metadata(path).ok()?.modified().ok()
18771945
}
18781946

1947+
fn ui_lease_exists(config_path: &Path) -> bool {
1948+
service::resolve_paths(Some(config_path.to_path_buf()))
1949+
.ok()
1950+
.and_then(|paths| state::read_ui_lease(&paths).ok().flatten())
1951+
.is_some()
1952+
}
1953+
1954+
fn touch_ui_lease_for_config(config_path: &Path) {
1955+
if let Ok(paths) = service::resolve_paths(Some(config_path.to_path_buf())) {
1956+
let _ = state::touch_ui_lease(&paths, std::process::id());
1957+
}
1958+
}
1959+
1960+
fn spawn_ui_lease_heartbeat(config_path: PathBuf) -> Arc<AtomicBool> {
1961+
let stop = Arc::new(AtomicBool::new(false));
1962+
if !ui_lease_exists(&config_path) {
1963+
return stop;
1964+
}
1965+
1966+
let stop_flag = stop.clone();
1967+
thread::spawn(move || {
1968+
while !stop_flag.load(Ordering::Relaxed) {
1969+
touch_ui_lease_for_config(&config_path);
1970+
thread::sleep(Duration::from_secs(2));
1971+
}
1972+
});
1973+
stop
1974+
}
1975+
18791976
#[cfg(target_os = "macos")]
18801977
fn spawn_tray_shell(config_path: &Path) -> Result<()> {
18811978
let gui_binary = locate_gui_binary()?;

src/hosts_store.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ mod tests {
712712
cert_dir,
713713
state_path: runtime_dir.join("service-state.json"),
714714
pid_path: runtime_dir.join("linuxdo-accelerator.pid"),
715+
ui_lease_path: runtime_dir.join("ui-lease.json"),
715716
runtime_log_path: runtime_dir.join("operations.log"),
716717
hosts_backup_path: runtime_dir.join("hosts.backup"),
717718
hosts_backup_meta_path: runtime_dir.join("hosts.backup.json"),

src/paths.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct AppPaths {
1515
pub cert_dir: PathBuf,
1616
pub state_path: PathBuf,
1717
pub pid_path: PathBuf,
18+
pub ui_lease_path: PathBuf,
1819
pub runtime_log_path: PathBuf,
1920
pub hosts_backup_path: PathBuf,
2021
pub hosts_backup_meta_path: PathBuf,
@@ -52,6 +53,7 @@ impl AppPaths {
5253
let cert_dir = data_dir.join("certs");
5354
let state_path = runtime_dir.join("service-state.json");
5455
let pid_path = runtime_dir.join("linuxdo-accelerator.pid");
56+
let ui_lease_path = runtime_dir.join("ui-lease.json");
5557
let runtime_log_path = runtime_dir.join("operations.log");
5658
let hosts_backup_path = runtime_dir.join("hosts.backup");
5759
let hosts_backup_meta_path = runtime_dir.join("hosts.backup.json");
@@ -64,6 +66,7 @@ impl AppPaths {
6466
cert_dir,
6567
state_path,
6668
pid_path,
69+
ui_lease_path,
6770
runtime_log_path,
6871
hosts_backup_path,
6972
hosts_backup_meta_path,

src/proxy.rs

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use rustls::crypto::{aws_lc_rs, ring};
2525
use rustls::pki_types::{EchConfigListBytes, ServerName};
2626
use serde::Deserialize;
2727
use tokio::net::TcpListener;
28-
use tokio::sync::RwLock;
28+
use tokio::sync::{RwLock, watch};
2929
use tokio_rustls::TlsAcceptor;
3030
use tokio_rustls::TlsConnector;
3131
use tokio_rustls::client::TlsStream;
@@ -114,7 +114,12 @@ const DOH_CONNECT_TIMEOUT: Duration = Duration::from_secs(4);
114114
const DOH_REQUEST_TIMEOUT: Duration = Duration::from_secs(6);
115115
const UPSTREAM_CONNECT_TIMEOUT: Duration = Duration::from_secs(4);
116116

117-
pub async fn run_proxy(config: AppConfig, paths: AppPaths, bundle: CertificateBundle) -> Result<()> {
117+
pub async fn run_proxy(
118+
config: AppConfig,
119+
paths: AppPaths,
120+
bundle: CertificateBundle,
121+
shutdown_rx: watch::Receiver<bool>,
122+
) -> Result<()> {
118123
let doh_client = Client::builder()
119124
.connect_timeout(DOH_CONNECT_TIMEOUT)
120125
.timeout(DOH_REQUEST_TIMEOUT)
@@ -143,14 +148,14 @@ pub async fn run_proxy(config: AppConfig, paths: AppPaths, bundle: CertificateBu
143148
let https_state = state.clone();
144149

145150
tokio::try_join!(
146-
run_http_redirect(http_state),
147-
run_https_proxy(https_state, bundle)
151+
run_http_redirect(http_state, shutdown_rx.clone()),
152+
run_https_proxy(https_state, bundle, shutdown_rx)
148153
)?;
149154

150155
Ok(())
151156
}
152157

153-
async fn run_http_redirect(state: Arc<AppState>) -> Result<()> {
158+
async fn run_http_redirect(state: Arc<AppState>, mut shutdown_rx: watch::Receiver<bool>) -> Result<()> {
154159
let address = format!("{}:{}", state.config.listen_host, state.config.http_port);
155160
let listener = TcpListener::bind(&address)
156161
.await
@@ -159,10 +164,17 @@ async fn run_http_redirect(state: Arc<AppState>) -> Result<()> {
159164
println!("http redirect listening on http://{address}");
160165

161166
loop {
162-
let (stream, _) = listener
163-
.accept()
164-
.await
165-
.context("failed to accept HTTP socket")?;
167+
let (stream, _) = tokio::select! {
168+
_ = shutdown_rx.changed() => {
169+
if *shutdown_rx.borrow() {
170+
break;
171+
}
172+
continue;
173+
}
174+
result = listener.accept() => {
175+
result.context("failed to accept HTTP socket")?
176+
}
177+
};
166178
let state = state.clone();
167179

168180
tokio::spawn(async move {
@@ -175,9 +187,15 @@ async fn run_http_redirect(state: Arc<AppState>) -> Result<()> {
175187
}
176188
});
177189
}
190+
191+
Ok(())
178192
}
179193

180-
async fn run_https_proxy(state: Arc<AppState>, bundle: CertificateBundle) -> Result<()> {
194+
async fn run_https_proxy(
195+
state: Arc<AppState>,
196+
bundle: CertificateBundle,
197+
mut shutdown_rx: watch::Receiver<bool>,
198+
) -> Result<()> {
181199
let certs = load_certificate_chain(&bundle)?;
182200
let key = load_private_key(&bundle.server_key_path)?;
183201
let provider = ring::default_provider();
@@ -199,10 +217,17 @@ async fn run_https_proxy(state: Arc<AppState>, bundle: CertificateBundle) -> Res
199217
println!("https proxy listening on https://{address}");
200218

201219
loop {
202-
let (stream, _) = listener
203-
.accept()
204-
.await
205-
.context("failed to accept HTTPS socket")?;
220+
let (stream, _) = tokio::select! {
221+
_ = shutdown_rx.changed() => {
222+
if *shutdown_rx.borrow() {
223+
break;
224+
}
225+
continue;
226+
}
227+
result = listener.accept() => {
228+
result.context("failed to accept HTTPS socket")?
229+
}
230+
};
206231
let acceptor = acceptor.clone();
207232
let state = state.clone();
208233

@@ -228,6 +253,8 @@ async fn run_https_proxy(state: Arc<AppState>, bundle: CertificateBundle) -> Res
228253
}
229254
});
230255
}
256+
257+
Ok(())
231258
}
232259

233260
async fn redirect_handler(

0 commit comments

Comments
 (0)