11use std:: fs;
22use std:: path:: { Path , PathBuf } ;
3+ use std:: sync:: Arc ;
4+ use std:: sync:: atomic:: { AtomicBool , Ordering } ;
35use std:: sync:: mpsc:: { self , Receiver } ;
46use std:: thread;
57use std:: time:: { Duration , Instant , SystemTime } ;
@@ -29,7 +31,7 @@ use crate::platform::spawn_detached;
2931use crate :: platform:: { apply_app_window_icon, update_windows_shortcuts_for_exe} ;
3032use crate :: runtime_log:: { append as append_runtime_log, read_recent_lines} ;
3133use crate :: service;
32- use crate :: state:: ServiceState ;
34+ use crate :: state:: { self , ServiceState } ;
3335
3436const APP_WINDOW_TITLE : & str = "Linux.do Accelerator" ;
3537const 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" ) ]
18801977fn spawn_tray_shell ( config_path : & Path ) -> Result < ( ) > {
18811978 let gui_binary = locate_gui_binary ( ) ?;
0 commit comments