1#[cfg(target_os = "windows")]
2use std::process::{Child, Command};
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use std::sync::{
7 atomic::{AtomicBool, Ordering},
8 Arc,
9};
10use tokio::sync::{Mutex, oneshot};
11use tokio::time::{Duration, timeout, sleep};
12use crate::service::{Service, WebSocketWrite};
13use easy_trace::prelude::{info, error, warn};
14use tokio_tungstenite::{connect_async, tungstenite::protocol::Message, MaybeTlsStream, WebSocketStream};
15use futures_util::{stream::SplitSink, SinkExt, StreamExt, stream::SplitStream};
16use tokio::net::TcpStream;
17
18#[cfg(target_os = "windows")]
20use {sharing::MYPOS_SERVER_FILE, sharing::proc::kill_process_by_name};
21
22pub const MYPOS_VERSION: &str = "1.1.0";
24
25const MAX_RETRIES: u32 = 3;
27
28const RETRY_DELAY_SECS: u64 = 2;
30
31const WATCHDOG_CHECK_INTERVAL_SECS: u64 = 5;
33
34const RESTART_DELAY_SECS: u64 = 3;
36
37const WEBSOCKET_CONNECT_TIMEOUT_SECS: u64 = 10;
39
40#[derive(Debug, Serialize, Deserialize)]
41struct ServerResponse {
42 status: String,
43 message: Option<String>,
44}
45
46type WsWriter = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>;
47type WsReader = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
48
49pub struct MyPOSService {
51 #[cfg(target_os = "windows")]
53 child_process: Arc<Mutex<Option<Child>>>,
54
55 running: Arc<AtomicBool>,
57
58 watchdog_enabled: Arc<AtomicBool>,
60
61 ws_writer: Arc<Mutex<Option<WsWriter>>>,
63
64 response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>,
66
67 restart_count: Arc<Mutex<u32>>,
69}
70
71impl MyPOSService {
72 pub fn new() -> Self {
74 MyPOSService {
75 #[cfg(target_os = "windows")]
76 child_process: Arc::new(Mutex::new(None)),
77 running: Arc::new(AtomicBool::new(false)),
78 watchdog_enabled: Arc::new(AtomicBool::new(false)),
79 ws_writer: Arc::new(Mutex::new(None)),
80 response_sender: Arc::new(Mutex::new(None)),
81 restart_count: Arc::new(Mutex::new(0)),
82 }
83 }
84
85 #[cfg(target_os = "windows")]
91 async fn launch_process(&self) -> Result<(), String> {
92 let exe_path = sharing::get_path_mypos_server();
93 info!("Launching MyPOS server at: {}", exe_path);
94
95 let mut guard = self.child_process.lock().await;
96
97 match Command::new(exe_path).spawn() {
98 Ok(child) => {
99 *guard = Some(child);
100 info!("MyPOS server process started successfully.");
101 Ok(())
102 }
103 Err(e) => {
104 error!("Failed to start MyPOS server: {:?}", e);
105 Err(format!("Failed to start process: {}", e))
106 }
107 }
108 }
109
110 async fn connect_websocket(&self) -> Result<(), String> {
112 let url = "ws://127.0.0.1:8765";
113 info!("Connecting to WebSocket at: {}", url);
114
115 let connect_future = connect_async(url);
116
117 let (ws_stream, _) = match timeout(
118 Duration::from_secs(WEBSOCKET_CONNECT_TIMEOUT_SECS),
119 connect_future
120 ).await {
121 Ok(Ok(result)) => result,
122 Ok(Err(e)) => return Err(format!("Failed to connect to WebSocket: {}", e)),
123 Err(_) => return Err("WebSocket connection timeout".to_string()),
124 };
125
126 let (write, read) = ws_stream.split();
127
128 *self.ws_writer.lock().await = Some(write);
130
131 let response_sender = self.response_sender.clone();
133 tokio::spawn(async move {
134 Self::handle_websocket_responses(read, response_sender).await;
135 });
136
137 info!("Connected to WebSocket successfully");
138 Ok(())
139 }
140
141 async fn handle_websocket_responses(
143 mut read: WsReader,
144 response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>
145 ) {
146 while let Some(msg_result) = read.next().await {
147 match msg_result {
148 Ok(Message::Text(response)) => {
149 if let Ok(server_response) = serde_json::from_str::<ServerResponse>(&response) {
150 info!("Received response: {:?}", server_response);
151
152 let mut sender_guard = response_sender.lock().await;
154 if let Some(sender) = sender_guard.take() {
155 if sender.send(server_response).is_err() {
156 warn!("Failed to send response to waiting transaction");
157 }
158 }
159 } else {
160 warn!("Received unexpected response: {}", response);
161 }
162 }
163 Ok(Message::Close(_)) => {
164 warn!("WebSocket connection closed");
165 break;
166 }
167 Err(e) => {
168 error!("WebSocket error: {:?}", e);
169 break;
170 }
171 _ => {}
172 }
173 }
174 info!("WebSocket response handler terminated");
175 }
176
177 #[cfg(target_os = "windows")]
183 async fn launch_and_monitor(&self) {
184 self.watchdog_enabled.store(true, Ordering::Relaxed);
186
187 if let Err(e) = self.launch_process().await {
189 error!("Failed to launch process: {}", e);
190 return;
191 }
192
193 self.running.store(true, Ordering::Relaxed);
194
195 sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
197
198 if let Err(e) = self.connect_websocket().await {
200 error!("Failed to connect to WebSocket: {:?}", e);
201 return;
202 }
203
204 self.start_watchdog().await;
206 }
207
208 #[cfg(target_os = "macos")]
210 async fn launch_and_monitor(&self) {
211 info!("macOS: Skipping process launch, connecting to existing WebSocket server...");
212
213 self.running.store(true, Ordering::Relaxed);
214 self.watchdog_enabled.store(true, Ordering::Relaxed);
215
216 if let Err(e) = self.connect_websocket().await {
218 error!("Failed to connect to WebSocket: {:?}", e);
219 self.running.store(false, Ordering::Relaxed);
220 return;
221 }
222
223 info!("macOS: Connected to existing WebSocket server");
224
225 self.start_watchdog().await;
227 }
228
229 async fn start_watchdog(&self) {
231 let running = self.running.clone();
232 let watchdog_enabled = self.watchdog_enabled.clone();
233 let ws_writer = self.ws_writer.clone();
234 let restart_count = self.restart_count.clone();
235 let response_sender = self.response_sender.clone();
236
237 #[cfg(target_os = "windows")]
238 let child_process = self.child_process.clone();
239
240 tokio::spawn(async move {
241 info!("Watchdog started");
242
243 while watchdog_enabled.load(Ordering::Relaxed) {
244 sleep(Duration::from_secs(WATCHDOG_CHECK_INTERVAL_SECS)).await;
245
246 if !watchdog_enabled.load(Ordering::Relaxed) {
247 info!("Watchdog disabled, stopping...");
248 break;
249 }
250
251 let mut needs_restart = false;
252 #[cfg(target_os = "windows")]
253 let mut process_died = false;
254
255 #[cfg(target_os = "windows")]
257 {
258 let mut child_guard = child_process.lock().await;
259
260 if let Some(child) = child_guard.as_mut() {
261 match child.try_wait() {
262 Ok(Some(status)) => {
263 error!("MyPOS server process exited with status: {:?}", status);
265 *child_guard = None;
266 drop(child_guard);
267 needs_restart = true;
268 process_died = true;
269 }
270 Ok(None) => {
271 drop(child_guard);
273 }
274 Err(e) => {
275 error!("Error checking process status: {:?}", e);
277 *child_guard = None;
278 drop(child_guard);
279 needs_restart = true;
280 process_died = true;
281 }
282 }
283 } else {
284 drop(child_guard);
286 if running.load(Ordering::Relaxed) {
287 warn!("Process handle lost, attempting restart");
288 needs_restart = true;
289 process_died = true;
290 }
291 }
292 }
293
294 if ws_writer.lock().await.is_none() && running.load(Ordering::Relaxed) {
296 warn!("WebSocket connection lost");
297 needs_restart = true;
298 }
299
300 if needs_restart {
302 let mut count = restart_count.lock().await;
303 *count += 1;
304 let attempt = *count;
305 drop(count);
306
307 warn!("🔄 Watchdog detected failure. Restart attempt #{}", attempt);
308
309 *ws_writer.lock().await = None;
311 running.store(false, Ordering::Relaxed);
312
313 #[cfg(target_os = "windows")]
315 {
316 info!("Watchdog: Attempting to restart MyPOS server...");
317
318 if process_died {
320 info!("Watchdog: Cleaning up residual processes...");
321 kill_process_by_name(MYPOS_SERVER_FILE);
322 sleep(Duration::from_millis(500)).await;
323 }
324
325 sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
327
328 let exe_path = sharing::get_path_mypos_server();
330 info!("Watchdog: Spawning new process at: {}", exe_path);
331
332 match Command::new(&exe_path).spawn() {
333 Ok(child) => {
334 *child_process.lock().await = Some(child);
335 info!("Watchdog: ✓ Process restarted successfully");
336
337 info!("Watchdog: Waiting {} seconds for server to start...", RESTART_DELAY_SECS);
339 sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
340
341 info!("Watchdog: Attempting to reconnect WebSocket...");
343 match Self::reconnect_websocket_static(ws_writer.clone(), response_sender.clone()).await {
344 Ok(_) => {
345 running.store(true, Ordering::Relaxed);
346 info!("Watchdog: ✓ WebSocket reconnected successfully");
347 info!("Watchdog: ✓ Full recovery completed on attempt #{}", attempt);
348 }
349 Err(e) => {
350 error!("Watchdog: ✗ Failed to reconnect WebSocket: {}", e);
351 }
352 }
353 }
354 Err(e) => {
355 error!("Watchdog: ✗ Failed to restart process: {:?}", e);
356 }
357 }
358 }
359
360 #[cfg(target_os = "macos")]
362 {
363 sleep(Duration::from_secs(RESTART_DELAY_SECS)).await;
364 info!("Watchdog: Attempting to reconnect WebSocket...");
365 match Self::reconnect_websocket_static(ws_writer.clone(), response_sender.clone()).await {
366 Ok(_) => {
367 running.store(true, Ordering::Relaxed);
368 info!("Watchdog: WebSocket reconnected successfully");
369 }
370 Err(e) => {
371 error!("Watchdog: Failed to reconnect: {}", e);
372 }
373 }
374 }
375 }
376 }
377
378 info!("Watchdog stopped");
379 });
380 }
381
382 async fn reconnect_websocket_static(
384 ws_writer: Arc<Mutex<Option<WsWriter>>>,
385 response_sender: Arc<Mutex<Option<oneshot::Sender<ServerResponse>>>>
386 ) -> Result<(), String> {
387 let url = "ws://127.0.0.1:8765";
388 info!("Reconnecting to WebSocket at: {}", url);
389
390 let connect_future = connect_async(url);
391 let (ws_stream, _) = match timeout(
392 Duration::from_secs(WEBSOCKET_CONNECT_TIMEOUT_SECS),
393 connect_future
394 ).await {
395 Ok(Ok(result)) => result,
396 Ok(Err(e)) => return Err(format!("Failed to reconnect: {}", e)),
397 Err(_) => return Err("Reconnection timeout".to_string()),
398 };
399
400 let (write, read) = ws_stream.split();
401 *ws_writer.lock().await = Some(write);
402
403 tokio::spawn(async move {
405 Self::handle_websocket_responses(read, response_sender).await;
406 });
407
408 info!("WebSocket reconnection completed");
409 Ok(())
410 }
411
412 #[cfg(target_os = "windows")]
418 async fn stop(&self) {
419 info!("Stopping MyPOS service...");
420
421 self.watchdog_enabled.store(false, Ordering::Relaxed);
423
424 sleep(Duration::from_millis(500)).await;
426
427 *self.ws_writer.lock().await = None;
429
430 let mut guard = self.child_process.lock().await;
432 if let Some(mut child) = guard.take() {
433 if let Err(e) = child.kill() {
434 error!("Error killing MyPOS server: {:?}", e);
435 } else {
436 kill_process_by_name(MYPOS_SERVER_FILE);
437 info!("MyPOS server stopped.");
438 }
439 }
440
441 self.running.store(false, Ordering::Relaxed);
442 *self.restart_count.lock().await = 0;
443 }
444
445 #[cfg(target_os = "macos")]
447 async fn stop(&self) {
448 info!("macOS: Closing WebSocket connection...");
449
450 self.watchdog_enabled.store(false, Ordering::Relaxed);
452
453 sleep(Duration::from_millis(500)).await;
455
456 *self.ws_writer.lock().await = None;
458
459 self.running.store(false, Ordering::Relaxed);
460 *self.restart_count.lock().await = 0;
461 info!("MyPOS WebSocket connection closed.");
462 }
463
464 async fn send_websocket_message(&self, message: Value) -> Result<ServerResponse, String> {
470 let (sender, receiver) = oneshot::channel();
472
473 {
475 let mut response_sender = self.response_sender.lock().await;
476 *response_sender = Some(sender);
477 }
478
479 {
481 let mut ws_writer = self.ws_writer.lock().await;
482 if let Some(writer) = ws_writer.as_mut() {
483 let msg = Message::Text(message.to_string());
484 if let Err(e) = writer.send(msg).await {
485 return Err(format!("Failed to send WebSocket message: {}", e));
486 }
487 } else {
488 return Err("WebSocket not connected".to_string());
489 }
490 }
491
492 match timeout(Duration::from_secs(60), receiver).await {
494 Ok(Ok(response)) => Ok(response),
495 Ok(Err(_)) => Err("Response channel closed".to_string()),
496 Err(_) => Err("Transaction timeout".to_string()),
497 }
498 }
499
500 async fn attempt_transaction_with_retries(
506 &self,
507 action: &str,
508 amount: f64,
509 ) -> Result<ServerResponse, String> {
510 for attempt in 1..=MAX_RETRIES {
511 info!("Attempt {}/{} for {} of {:.2} EUR", attempt, MAX_RETRIES, action, amount);
512
513 let message = json!({
514 "action": action,
515 "amount": amount
516 });
517
518 match self.send_websocket_message(message).await {
519 Ok(response) => {
520 let status_lower = response.status.to_lowercase();
521
522 if status_lower == "success" {
524 info!("Transaction successful on attempt {}", attempt);
525 return Ok(response);
526 }
527
528 if status_lower == "usercancel" {
530 warn!("Transaction cancelled by user, no retries");
531 return Ok(response);
532 }
533
534 if attempt < MAX_RETRIES {
536 warn!("Attempt {} failed with status: {}. Retrying in {} seconds...",
537 attempt, response.status, RETRY_DELAY_SECS);
538 sleep(Duration::from_secs(RETRY_DELAY_SECS)).await;
539 } else {
540 error!("All {} attempts failed. Last status: {}", MAX_RETRIES, response.status);
541 return Ok(response);
542 }
543 }
544 Err(e) => {
545 if attempt < MAX_RETRIES {
546 warn!("Attempt {} failed with error: {}. Retrying in {} seconds...",
547 attempt, e, RETRY_DELAY_SECS);
548 sleep(Duration::from_secs(RETRY_DELAY_SECS)).await;
549 } else {
550 error!("All {} attempts failed. Last error: {}", MAX_RETRIES, e);
551 return Err(e);
552 }
553 }
554 }
555 }
556
557 Err("Maximum retries exceeded".to_string())
558 }
559
560 async fn handle_purchase(&self, amount: f64, write: WebSocketWrite) -> (i32, String) {
566 info!("Handling purchase of: {:.2} EUR (with up to {} retries)", amount, MAX_RETRIES);
567
568 match self.attempt_transaction_with_retries("purchase", amount).await {
569 Ok(response) => {
570 let success = response.status.to_lowercase() == "success";
571 let message = response.message.unwrap_or_else(||
572 if success {
573 format!("Purchase of {:.2} EUR completed successfully", amount)
574 } else {
575 format!("Purchase of {:.2} EUR failed after {} attempts: {}",
576 amount, MAX_RETRIES, response.status)
577 }
578 );
579
580 if let Some(ws_lock) = &write {
582 let response_msg = json!({
583 "type": "purchase_result",
584 "success": success,
585 "amount": amount,
586 "message": message,
587 "status": response.status
588 });
589
590 let mut ws = ws_lock.write().await;
591 if let Err(e) = ws.send(Message::Text(response_msg.to_string())).await {
592 error!("Failed to send purchase result: {:?}", e);
593 }
594 }
595
596 if success { (0, message) } else { (1, message) }
597 }
598 Err(e) => {
599 let error_msg = format!("Purchase failed after {} attempts: {}", MAX_RETRIES, e);
600 error!("{}", error_msg);
601 (1, error_msg)
602 }
603 }
604 }
605
606 async fn handle_refund(&self, amount: f64, write: WebSocketWrite) -> (i32, String) {
608 info!("Handling refund of: {:.2} EUR (with up to {} retries)", amount, MAX_RETRIES);
609
610 match self.attempt_transaction_with_retries("refund", amount).await {
611 Ok(response) => {
612 let success = response.status.to_lowercase() == "success";
613 let message = response.message.unwrap_or_else(||
614 if success {
615 format!("Refund of {:.2} EUR completed successfully", amount)
616 } else {
617 format!("Refund of {:.2} EUR failed after {} attempts: {}",
618 amount, MAX_RETRIES, response.status)
619 }
620 );
621
622 if let Some(ws_lock) = &write {
624 let response_msg = json!({
625 "type": "refund_result",
626 "success": success,
627 "amount": amount,
628 "message": message,
629 "status": response.status
630 });
631
632 let mut ws = ws_lock.write().await;
633 if let Err(e) = ws.send(Message::Text(response_msg.to_string())).await {
634 error!("Failed to send refund result: {:?}", e);
635 }
636 }
637
638 if success { (0, message) } else { (1, message) }
639 }
640 Err(e) => {
641 let error_msg = format!("Refund failed after {} attempts: {}", MAX_RETRIES, e);
642 error!("{}", error_msg);
643 (1, error_msg)
644 }
645 }
646 }
647}
648
649#[async_trait]
654impl Service for MyPOSService {
655 async fn run(&self, action: Value, write: WebSocketWrite) -> (i32, String) {
657 info!("MyPOSService: Running action: {:?}", action);
658
659 let parsed_action = if let Some(action_str) = action.get("ACTION").and_then(|a| a.as_str()) {
661 serde_json::from_str::<Value>(action_str).unwrap_or_else(|_| {
663 json!({"command": action_str})
665 })
666 } else if let Some(action_obj) = action.get("ACTION") {
667 action_obj.clone()
669 } else {
670 action
672 };
673
674 let command = parsed_action
675 .get("command")
676 .and_then(|c| c.as_str())
677 .unwrap_or("UNKNOWN");
678
679 info!("MyPOS Command: {}", command);
680
681 match command {
682 "START" => {
683 self.launch_and_monitor().await;
684
685 #[cfg(target_os = "windows")]
686 let msg = "MyPOS server started with watchdog monitoring enabled.";
687
688 #[cfg(target_os = "macos")]
689 let msg = "MyPOS WebSocket connection established with watchdog monitoring (no process launched on macOS).";
690
691 (0, msg.to_string())
692 }
693 "STOP" => {
694 self.stop().await;
695 (0, "MyPOS stopped.".to_string())
696 }
697 "PURCHASE" => {
698 let amount = parsed_action.get("amount").and_then(|a| a.as_f64()).unwrap_or(0.0);
699 if amount > 0.0 {
700 self.handle_purchase(amount, write).await
701 } else {
702 (1, "Invalid purchase amount.".to_string())
703 }
704 }
705 "REFUND" => {
706 let amount = parsed_action.get("amount").and_then(|a| a.as_f64()).unwrap_or(0.0);
707 if amount > 0.0 {
708 self.handle_refund(amount, write).await
709 } else {
710 (1, "Invalid refund amount.".to_string())
711 }
712 }
713 _ => (1, format!("Unknown command: {}", command)),
714 }
715 }
716
717 fn as_any(&self) -> &dyn std::any::Any {
719 self
720 }
721
722 fn stop_service(&self) {
724 let running = self.running.clone();
725 let watchdog_enabled = self.watchdog_enabled.clone();
726
727 #[cfg(target_os = "windows")]
728 let child_process = self.child_process.clone();
729
730 let ws_writer = self.ws_writer.clone();
731
732 tokio::spawn(async move {
733 if running.load(Ordering::Relaxed) {
734 watchdog_enabled.store(false, Ordering::Relaxed);
736 sleep(Duration::from_millis(500)).await;
737
738 *ws_writer.lock().await = None;
740
741 #[cfg(target_os = "windows")]
743 {
744 if let Some(mut child) = child_process.lock().await.take() {
745 if let Err(e) = child.kill() {
746 error!("Error stopping MyPOS server: {:?}", e);
747 } else {
748 info!("MyPOS server stopped cleanly.");
749 }
750 }
751 }
752
753 #[cfg(target_os = "macos")]
754 info!("MyPOS WebSocket closed (no process to stop on macOS).");
755
756 running.store(false, Ordering::Relaxed);
757 }
758 });
759 }
760
761 fn get_version(&self) -> String {
763 MYPOS_VERSION.to_string()
764 }
765}