pispas_tray_app/
pispas_tray.rs

1/*
2 * Authors: Jorge.A Duran & Mario Gónzalez
3 * Company: pispas Technologies SL
4 * Date: April 23, 2023
5 * Description: tray definition and implementation for tray-icon.
6 */
7use tray_icon::{menu::{Menu, MenuItem, PredefinedMenuItem, MenuEvent, MenuId}, TrayIconBuilder,TrayIconEventReceiver };
8use std::path::PathBuf;
9use tray_icon::menu::MenuEventReceiver;
10use sharing::VERSION;
11use winit::event_loop::{EventLoopBuilder, EventLoop};
12use parking_lot::RwLock;
13use std::io::{Read, Write};
14use std::net::{TcpStream};
15use std::sync::Arc;
16use easy_trace::instruments::tracing::{error, info};
17use sharing::service::{Action, DriverStatus};
18use crate::tray_utils::{load_icon, open_window, ICON_ON_BYTES, ICON_OFF_BYTES, ICON_ALERT_BYTES};
19use std::str::FromStr;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::thread::sleep;
22use std::time::Duration;
23use std::sync::mpsc;
24
25pub type TrayIconResult<T> = sharing::PisPasResult<T>;
26use tray_icon::TrayIconEvent;
27#[derive(Clone)]
28pub struct PisPasTrayIcon {
29    try_icon_on: tray_icon::Icon,
30    try_icon_off: tray_icon::Icon,
31    try_icon_alert: tray_icon::Icon,
32    tray_icon: tray_icon::TrayIcon,
33    configurator: MenuItem,
34    about: MenuItem,
35    start: MenuItem,
36    restart: MenuItem,
37    stop: MenuItem,
38    check_update: MenuItem,
39    menu_status: MenuItem,
40    quit: MenuItem,
41    tray_status: Arc<RwLock<DriverStatus>>,
42    stream: Option<Arc<RwLock<TcpStream>>>,
43}
44
45
46unsafe impl Send for PisPasTrayIcon {}
47unsafe impl Sync for PisPasTrayIcon {}
48impl PisPasTrayIcon {
49    pub fn get_id_configurator(&self) -> &MenuId  { self.configurator.id() }
50    pub fn get_id_about(&self) -> &MenuId {
51        self.about.id()
52    }
53    pub fn get_id_start(&self) -> &MenuId {
54        self.start.id()
55    }
56    pub fn get_id_stop(&self) -> &MenuId {
57        self.stop.id()
58    }
59    pub fn get_id_quit(&self) -> &MenuId {
60        self.quit.id()
61    }
62    pub fn get_id_check_update(&self) -> &MenuId {
63        self.check_update.id()
64    }
65    pub fn get_id_restart(&self) -> &MenuId {
66        self.restart.id()
67    }
68    pub fn build_event_loop() -> (&'static MenuEventReceiver, &'static TrayIconEventReceiver, EventLoop<()>) {
69        let menu_channel = MenuEvent::receiver();
70        let tray_channel = TrayIconEvent::receiver();
71        match EventLoopBuilder::new().build() {
72            Ok(event_loop) => {
73                info!("Event loop created");
74                return (menu_channel, tray_channel, event_loop);
75            }
76            Err(e) => {
77                panic!("Error creating event loop: {}", e);
78            }
79        }
80    }
81    pub fn get_stream(&self) -> Option<Arc<RwLock<TcpStream>>> {
82        self.stream.clone()
83    }
84
85    pub fn tray_connect(&mut self) -> TrayIconResult<()>{
86        match TcpStream::connect(sharing::CHANNEL_NAME) {
87            #[allow(unused_mut)]
88            Ok(mut stream) => {
89                info!("Connected to pispas-channel on INIT");
90                match stream.set_nonblocking(true) {
91                    Ok(_) => {
92                        info!("Socket set to nonblocking");
93                    }
94                    Err(e) => {
95                        error!("Error setting socket to nonblocking: {}", e);
96                    }
97                }
98                self.stream = Some(Arc::new(RwLock::new(stream)));
99                return Ok(());
100            }
101            Err(e) => {
102                error!("Error connecting to pispas-channel: {}", e);
103            }
104        }
105
106        Err(anyhow::anyhow!("Error connecting to pispas-channel"))
107    }
108
109    fn new(try_icon_on: tray_icon::Icon,
110           try_icon_off: tray_icon::Icon,
111           try_icon_alert: tray_icon::Icon,
112           tray_icon: tray_icon::TrayIcon,
113           configurator: MenuItem,
114           about: MenuItem,
115           start: MenuItem,
116           restart: MenuItem,
117           stop: MenuItem,
118           check_update: MenuItem,
119           menu_status: MenuItem,
120           quit: MenuItem
121
122    ) -> Self {
123        Self {
124            try_icon_on,
125            try_icon_off,
126            try_icon_alert,
127            tray_icon,
128            configurator,
129            about,
130            start,
131            restart,
132            stop,
133            check_update,
134            menu_status,
135            quit,
136            tray_status: Arc::new(RwLock::new(DriverStatus::Stopped)),
137            stream: None,
138        }
139    }
140
141    pub fn build() -> TrayIconResult<Self>
142    {
143        let icon = load_icon(ICON_ON_BYTES)?;
144        let icon_off = load_icon(ICON_OFF_BYTES)?;
145        let icon_alert = load_icon(ICON_ALERT_BYTES)?;
146        let menu = Menu::new();
147        let configurator = MenuItem::new("Configurator", true, None);
148        let about = MenuItem::new("About", true, None);
149        let start= MenuItem::new("Start", true, None);
150        let restart= MenuItem::new("Restart", true, None);
151        let stop = MenuItem::new("Stop", true, None);
152        let check_update = MenuItem::new("Check Updates", true, None);
153        let menu_status = MenuItem::new("STATUS: OFF", false, None);
154        let quit = MenuItem::new("Quit", true, None);
155        let separator = &PredefinedMenuItem::separator();
156        let _ = menu.append_items(&[
157            &configurator,
158            &about,
159            separator,
160            &start,
161            &stop,
162            &check_update,
163            &restart,
164            separator,
165            &menu_status,
166            separator,
167            &quit,
168        ]);
169        let box_icon = Box::new(menu);
170        let tray_builder = TrayIconBuilder::new()
171            .with_menu(box_icon)
172            .with_tooltip("pispas service")
173            .with_icon(icon_off.clone())
174            .build()?;
175
176        Ok(Self::new(icon, icon_off, icon_alert, tray_builder, configurator, about, start, restart, stop, check_update, menu_status, quit))
177    }
178
179    pub fn run_suscriber(&mut self, cancel: Arc<AtomicBool>) {
180        loop {
181            sleep(Duration::from_millis(300));
182            if let Some(stream) = self.get_stream() {
183                let message = self.read_channel(stream.clone(), cancel.clone());
184                info!("Message received from Subscription: {}", message);
185                if !message.is_empty() {
186                    if message.starts_with("MessageBox:") {
187                        let msg = message.trim_start_matches("MessageBox:").trim();
188                        if !msg.is_empty() {
189                            sharing::natives::api::show_message_box_non_blocking(msg);
190                        } else {
191                            info!("No message provided for MessageBox.");
192                        }
193                    } else {
194                        match self.change_status(DriverStatus::from_str(message.as_str()).unwrap_or(DriverStatus::Stopped)) {
195                            Ok(_) => {
196                                info!("Status changed to: {}", message);
197                            }
198                            Err(error) => {
199                                error!("Error changing status: {}", error);
200                            }
201                        }
202                    }
203                }
204            } else {
205                match self.tray_connect() {
206                    Ok(_) => {
207                        info!("Connected to tray");
208                        self.subscribe_service();
209                    }
210                    Err(error) => {
211                        error!("Error connecting to tray: {}", error);
212                        sleep(Duration::from_millis(1000));
213                    }
214                }
215            }
216            if cancel.load(Ordering::Relaxed) {
217                if let Some(stream) = self.get_stream() {
218                    match stream.write().write_all(Action::Close.to_string().as_bytes()) {
219                        Ok(_) => {
220                            info!("run_suscriber Message sent: {} CLOSE", Action::Close.to_string());
221                        }
222                        Err(e) => {
223                            error!("Error writing to stream CLOSE: {:?}", e);
224                        }
225                    }
226                }
227                break;
228            }
229        }
230    }
231
232    pub fn change_status(&mut self, status: DriverStatus) -> TrayIconResult<()>{
233        match status {
234            DriverStatus::Running => self.running()?,
235            DriverStatus::Stopped => self.stopped()?,
236            DriverStatus::Launched => self.launched()?,
237            _ => self.warning()?,
238        }
239        let mut write_status = self.tray_status.write();
240        *write_status = status;
241        Ok(())
242    }
243    pub fn change_status_main_thread(&mut self, status: DriverStatus) -> TrayIconResult<()> {
244        self.change_status(status)
245    }
246    fn read_channel_background(&self,
247                               stream: Arc<RwLock<TcpStream>>,
248                               cancel: Arc<AtomicBool>) -> String {
249        let mut buffer = Vec::new();
250
251        loop {
252            sleep(Duration::from_millis(100));
253            let mut partial_buffer = [0; 1024];
254
255            match stream.write().read(&mut partial_buffer) {
256                Ok(size) => {
257                    if size == 0 {
258                        if cancel.load(Ordering::Relaxed) {
259                            info!("Closing socket");
260                            break;
261                        }
262                        continue;
263                    }
264                    buffer.extend_from_slice(&partial_buffer[..size]);
265                    if size < partial_buffer.len() {
266                        break;
267                    }
268                }
269                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
270                    continue;
271                }
272                Err(e) => {
273                    error!("Failed to read from socket in background thread: {}", e);
274                    break;
275                }
276            }
277
278            if cancel.load(Ordering::Relaxed) {
279                break;
280            }
281        }
282        let message = String::from_utf8_lossy(&buffer).to_string();
283        info!("Message received: {} on read_channel_background", message);
284        message
285    }
286    pub fn launched(&mut self) -> TrayIconResult<()> {
287        self.start.set_enabled(false);
288        self.stop.set_enabled(true);
289        self.menu_status.set_text("STATUS: STARTING");
290        self.tray_icon.set_icon(Some(self.try_icon_alert.clone()))?;
291        Ok(())
292    }
293    fn subscribe_service_background(&self, stream: &Arc<RwLock<TcpStream>>) {
294        match stream.write().write_all(Action::Subscribe.to_string().as_bytes()) {
295            Ok(_) => {
296                info!("Message sent: {} SUBSCRIBE (background)", Action::Subscribe.to_string());
297            }
298            Err(e) => {
299                error!("Error writing to stream SUBSCRIBE: {:?}", e);
300            }
301        }
302    }
303    // ✅ NUEVO MÉTODO: Para usar en background thread (SIN modificar GUI)
304    pub fn run_subscriber_background(&self,
305                                     cancel: Arc<AtomicBool>,
306                                     status_sender: mpsc::Sender<DriverStatus>) {
307        let mut stream_holder: Option<Arc<RwLock<TcpStream>>> = None;
308
309        loop {
310            sleep(Duration::from_millis(300));
311
312            if let Some(stream) = &stream_holder {
313                let message = self.read_channel_background(stream.clone(), cancel.clone());
314                info!("Message received from Subscription: {}", message);
315
316                if !message.is_empty() {
317                    if message.starts_with("MessageBox:") {
318                        let msg = message.trim_start_matches("MessageBox:").trim();
319                        if !msg.is_empty() {
320                            sharing::natives::api::show_message_box_non_blocking(msg);
321                        }
322                    } else {
323                        let new_status = DriverStatus::from_str(message.as_str())
324                            .unwrap_or(DriverStatus::Stopped);
325
326                        // ✅ ENVIAR STATUS POR CANAL EN LUGAR DE MODIFICAR GUI
327                        if let Err(e) = status_sender.send(new_status) {
328                            error!("Error sending status update: {}", e);
329                            break; // Main thread probablemente cerrado
330                        }
331                    }
332                }
333            } else {
334                // Intentar conectar
335                match self.tray_connect_background() {
336                    Ok(stream) => {
337                        info!("Connected to tray in background");
338                        stream_holder = Some(stream.clone());
339                        self.subscribe_service_background(&stream);
340                    }
341                    Err(error) => {
342                        error!("Error connecting to tray: {}", error);
343                        sleep(Duration::from_millis(1000));
344                    }
345                }
346            }
347
348            if cancel.load(Ordering::Relaxed) {
349                if let Some(stream) = &stream_holder {
350                    let _ = self.send_close_background(stream);
351                }
352                break;
353            }
354        }
355    }
356    // ✅ NUEVO MÉTODO: Send close sin modificar self
357    fn send_close_background(&self, stream: &Arc<RwLock<TcpStream>>) -> TrayIconResult<()> {
358        match stream.write().write_all(Action::Close.to_string().as_bytes()) {
359            Ok(_) => {
360                info!("Background thread sent CLOSE message");
361                Ok(())
362            }
363            Err(e) => {
364                error!("Error writing CLOSE to stream: {:?}", e);
365                Err(anyhow::anyhow!("Error writing CLOSE: {}", e))
366            }
367        }
368    }
369    fn tray_connect_background(&self) -> TrayIconResult<Arc<RwLock<TcpStream>>> {
370        match TcpStream::connect(sharing::CHANNEL_NAME) {
371            Ok(stream) => {
372                info!("Connected to pispas-channel on INIT (background)");
373                match stream.set_nonblocking(true) {
374                    Ok(_) => {
375                        info!("Socket set to nonblocking");
376                    }
377                    Err(e) => {
378                        error!("Error setting socket to nonblocking: {}", e);
379                    }
380                }
381                Ok(Arc::new(RwLock::new(stream)))
382            }
383            Err(e) => {
384                error!("Error connecting to pispas-channel: {}", e);
385                Err(anyhow::anyhow!("Error connecting to pispas-channel"))
386            }
387        }
388    }
389    pub fn running(&mut self) -> TrayIconResult<()>{
390        self.start.set_enabled(false);
391        self.stop.set_enabled(true);
392        self.menu_status.set_text("STATUS: RUNNING");
393        self.tray_icon.set_icon(Some(self.try_icon_on.clone()))?;
394        Ok(())
395    }
396
397    pub fn stopped(&mut self) ->TrayIconResult<()>{
398        self.start.set_enabled(true);
399        self.stop.set_enabled(false);
400        self.menu_status.set_text("STATUS: STOPPED");
401        self.tray_icon.set_icon(Some(self.try_icon_off.clone()))?;
402        Ok(())
403    }
404
405    pub fn warning(&mut self) -> TrayIconResult<()>{
406        self.start.set_enabled(false);
407        self.stop.set_enabled(true);
408        self.menu_status.set_text(self.tray_status.read().to_string());
409        self.tray_icon.set_icon(Some(self.try_icon_alert.clone()))?;
410        Ok(())
411    }
412
413    pub fn log_dir() -> PathBuf {
414        sharing::paths::HOME_DIR.join(crate::TRY_ICON_LOG_FILE)
415    }
416
417    pub fn about(&self) {
418        open_window("About", format!("pispas service\nService Versión {VERSION}\n\nAuthors: GEKKOTECH S.L.").as_str());
419    }
420    pub fn open_configurator(&mut self)  {
421        let p = sharing::paths::configurator_path().display().to_string();
422        info!("Opening configurator: {}", p);
423        std::thread::spawn(move || {
424            match std::process::Command::new(&p).status() {
425                Ok(status) => {
426                    info!("Configurator exited with status: {:?}", status);
427                },
428                Err(e) => {
429                    error!("Error spawning configurator: {:?}", e);
430                }
431            }
432        });
433    }
434
435    pub fn download_drivers(&mut self)  {
436        let path = sharing::paths::elevator_path().display().to_string();
437        let output = std::process::Command::new("cmd")
438            .args(&["/C", &path])
439            .output()
440            .expect("Failed to execute command");
441
442        if !output.status.success() {
443            error!("Command failed with output: {:?}", output);
444        }
445
446    }
447    pub fn subscribe_service(&mut self){
448        if let Some(stream) = &self.stream {
449            match stream.write().write_all(Action::Subscribe.to_string().as_bytes()){
450                Ok(_) => {
451                    info!("Message sent: {} SUSCRIBE", Action::Subscribe.to_string());
452                }
453                Err(e) => {
454                    error!("Error writing to stream SUSCRIBE: {:?}", e);
455                }
456            }
457        }
458    }
459
460    // pub fn restart_service(&mut self) -> TrayIconResult<()> {
461    //     self.stop_service()?;
462    //     sleep(Duration::from_millis(500));
463    //     self.start_service()?;
464    //     Ok(())
465    // }
466    pub fn send_action(&mut self, action: Action) -> sharing::PisPasResult<()> {
467        match self.send_string(action.to_string().as_str()) {
468            Ok(_) => {
469                info!("Message sent: {}", action.to_string());
470            }
471            Err(e) => {
472                error!("Error sending stop command: {}", e);
473            }
474        }
475        Ok(())
476    }
477    pub fn check_updates(&mut self) -> sharing::PisPasResult<()>{
478        self.send_action(Action::Update)
479    }
480    pub fn stop_service(&mut self) -> sharing::PisPasResult<()> {
481        self.send_action(Action::Stop)
482    }
483    pub fn start_service(&mut self) -> sharing::PisPasResult<()>  {
484        self.send_action(Action::Start)
485    }
486    pub fn restart(&mut self) -> sharing::PisPasResult<()> {
487        self.send_action(Action::Restart)
488    }
489    pub fn close_socket(&mut self) -> sharing::PisPasResult<()> {
490        self.send_action(Action::Close)
491    }
492    pub fn read_channel(&mut self, stream: Arc<RwLock<TcpStream>>, cancel: Arc<AtomicBool>) -> String {
493        let mut buffer = Vec::new(); // Use a dynamic Vec to store the incoming message
494
495        loop {
496            sleep(Duration::from_millis(100));
497            let mut partial_buffer = [0; 1024];
498
499            match stream.write().read(&mut partial_buffer) {
500                Ok(size) => {
501                    if size == 0 {
502                        if cancel.load(Ordering::Relaxed) {
503                            info!("Closing socket");
504                            break;
505                        }
506                        continue;
507                    }
508                    buffer.extend_from_slice(&partial_buffer[..size]);
509                    if size < partial_buffer.len() {
510                        break; // Finished reading the complete message
511                    }
512                }
513                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
514                    // Operation would block, wait a bit and try again
515                    continue;
516                }
517                Err(e) => {
518                    error!("Failed to read from socket in dedicated thread: {}", e);
519                    self.stream = None;
520                    break;
521                }
522            }
523
524            if cancel.load(Ordering::Relaxed) {
525                break;
526            }
527        }
528
529        let message = String::from_utf8_lossy(&buffer).to_string();
530        info!("Message received: {} on read_channel", message);
531
532        message
533    }
534
535    fn send_string(&mut self, message: &str) -> sharing::PisPasResult<()> {
536        match TcpStream::connect(sharing::CHANNEL_NAME) {
537            Ok(mut streamsocket) => {
538                info!("Connected to pispas-channel, message send: {}", message);
539                // let stream = Arc::new(RwLock::new(streamsocket));
540                match streamsocket.write_all(message.as_bytes()) {
541                    Ok(_) => {
542                        info!("Message sent: {}", message);
543                        return Ok(());
544                    }
545                    Err(e) => {
546                        error!("Failed to write to socket in dedicated thread {}", e);
547                        return Err(anyhow::anyhow!("Failed to write to socket in dedicated thread {}", e));
548                    }
549                }
550
551            }
552            Err(e) => {
553                error!("Error connecting to pispas-channel: {}", e);
554            }
555        }
556
557        Err(anyhow::anyhow!("No socket available"))
558    }
559
560}
561
562