1
2use crate::{
4 pdf_manager::PDFManager,
5 send_message,
6 service::{Service, WebSocketWrite},
7};
8
9use async_trait::async_trait;
10use base64::{engine::general_purpose, Engine};
11use easy_trace::prelude::{debug, error, info};
12use futures_util::SinkExt;
13use lazy_static::lazy_static;
14use md5;
15use regex::{Regex, RegexBuilder};
16use serde::{Deserialize, Serialize};
17use serde_json::{json, Value};
18use std::{
19 collections::HashMap,
20 fs::File,
21 io::Write,
22 path::Path,
23 process::{Command, Stdio},
24 sync::Arc,
25};
26use printers::common::base::job::PrinterJobOptions;
27use tokio::sync::Mutex;
28use tokio::task;
29use tokio::time::timeout;
30use tokio::time::{sleep, Duration};
31use std::time::Instant;
32
33#[cfg(target_os = "windows")]
35use {
36 std::{
37 os::windows::{
38 process::CommandExt,
39 },
40 },
41};
42
43
44
45use printers::common::base::printer::Printer;
46use sharing::paths::WKHTMLTOPDF_PATH;
47use sharing::utils::ConfigEnv;
48
49type BoxError = Box<dyn std::error::Error + Send + Sync>;
50
51lazy_static! {
52 static ref PDF_MANAGER: Mutex<PDFManager> = Mutex::new(PDFManager::new());
54}
55
56pub const PRINT_VERSION: &str = "1.0.0";
58pub const BUFFER_OPEN_DRAWER: &[u8] = b"\x1B\x70\x00\x64\xC8";
60
61const PRINT_DEDUP_WINDOW: Duration = Duration::from_millis(2_500);
69
70lazy_static! {
71 static ref DEDUP_TIME_RE: Regex = Regex::new(r"\d{1,2}:\d{2}(?::\d{2})?").unwrap();
75
76 static ref DEDUP_STAGE_RE: Regex = Regex::new(r"(?i)(Nueva|Cambios? en) comanda").unwrap();
80}
81
82fn dedup_signature(html: &str) -> String {
88 let s1 = DEDUP_TIME_RE.replace_all(html, "");
89 let s2 = DEDUP_STAGE_RE.replace_all(&s1, "");
90 format!("{:x}", md5::compute(s2.as_bytes()))
91}
92
93pub struct PrintService {
95 printers: Arc<Mutex<Vec<Printer>>>, list_printers: Arc<Mutex<Vec<String>>>, config: ConfigEnv, command_viewer: bool, recent_prints: Arc<Mutex<HashMap<(String, String), Instant>>>,
101}
102
103impl Clone for PrintService {
104 fn clone(&self) -> Self {
105 PrintService {
106 printers: Arc::clone(&self.printers),
107 list_printers: Arc::clone(&self.list_printers),
108 config: self.config.clone(),
109 command_viewer: self.command_viewer,
110 recent_prints: Arc::clone(&self.recent_prints),
111 }
112 }
113}
114
115#[derive(Deserialize, Serialize, Debug, Clone)]
117#[serde(rename_all = "PascalCase")]
118pub enum PrintAction {
119 Print {
120 content: String,
121 printer_name: Option<String>,
122 copies: Option<u32>,
123 open: bool,
124 },
125 OpenDrawer {
126 printer_name: String,
127 },
128 Check,
129 ListPrinters,
130 Unknown,
131}
132
133impl PrintService {
134 pub async fn new(mut config: ConfigEnv) -> Self {
136 let devices = PrintService::list_printers().await;
137 println!("PrintService initialized with {} printers", devices.len());
138
139 let simplified_devices: Vec<String> = devices
141 .iter()
142 .map(|p| format!("name: {}, driver_name: {}", p.name, p.driver_name))
143 .collect();
144 debug!("PrintService initialized with printers: {:?}", simplified_devices);
145
146 let mut names: std::collections::HashSet<String> = devices.iter().map(|p| p.name.clone()).collect();
148
149 info!("modules configured: {:?}", config.modules);
151 let command_viewer = config.modules.iter().any(|m| m == "commandViewer");
152 if command_viewer {
153 names.insert("CommandViewer".to_string());
154 }
155 info!("CommandViewer enabled: {}", command_viewer);
156
157 if let Some(cfg_list) = &config.list_printers {
159 for p in cfg_list {
160 names.insert(p.clone());
161 }
162 } else {
163 info!("No printers configured in config, using detected ones");
164 }
165
166 let mut merged: Vec<String> = names.into_iter().collect();
168 merged.sort();
169
170 config.list_printers = Some(merged.clone());
172
173 info!("PRINTERS NAMES (merged): {:?}", merged);
174
175 config.save(); PrintService {
178 printers: Arc::new(Mutex::new(devices)),
179 list_printers: Arc::new(Mutex::new(merged)),
180 config,
181 command_viewer,
182 recent_prints: Arc::new(Mutex::new(HashMap::new())),
183 }
184 }
185
186 async fn add_printer_string(&mut self, printer_name: &str) {
187 info!("LIST_PRINTERS BEFORE: {:?}", self.config.list_printers);
188 let mut printers = self.list_printers.lock().await;
189 if !printers.contains(&printer_name.to_string()) {
190 printers.push(printer_name.to_string());
191 }
192 self.config.list_printers
193 .get_or_insert_with(Vec::new)
194 .push(printer_name.to_string());
195 info!("list_printers updated: {:?}", self.config.list_printers);
196 self.config.save();
197 }
198
199 async fn get_print_list(&self) -> Vec<String> {
200 let printers = self.list_printers.lock().await;
201 printers.clone()
202 }
203
204
205 fn extract_css_from_html(&self, html: &str) -> HashMap<String, String> {
210 let mut extracted_css = HashMap::new();
211
212 extracted_css.insert("margin-top".to_string(), "0mm".to_string());
213 extracted_css.insert("margin-right".to_string(), "0mm".to_string());
214 extracted_css.insert("margin-bottom".to_string(), "0mm".to_string());
215 extracted_css.insert("margin-left".to_string(), "0mm".to_string());
216 extracted_css.insert("page-width".to_string(), "72mm".to_string());
217 extracted_css.insert("page-height".to_string(), "297mm".to_string());
218
219 let page_re = RegexBuilder::new(r"@page\s*\{\s*([^}]*)\s*\}")
220 .dot_matches_new_line(true)
221 .build();
222
223 match page_re {
224 Ok(page_re) => {
225 if let Some(page_match) = page_re.captures(html) {
227 let page_css = page_match.get(1).map_or("", |m| m.as_str());
228
229 if let Ok(size_re) = Regex::new(r"size:\s*([\d.]+mm)\s+([\d.]+mm)(?:\s+\w+)?;")
231 {
232 if let Some(size_match) = size_re.captures(page_css) {
233 let page_width = size_match.get(1).map_or("", |m| m.as_str());
234 let page_height = size_match.get(2).map_or("", |m| m.as_str());
235 extracted_css.insert("page-width".to_string(), page_width.to_string());
236 extracted_css
237 .insert("page-height".to_string(), page_height.to_string());
238 }
239 }
240
241 if let Ok(margin_re) =
243 Regex::new(r"margin-(top|right|bottom|left):\s*([\d.]+[a-z]+);")
244 {
245 for margin_match in margin_re.captures_iter(page_css) {
246 let margin_name = margin_match.get(1).map_or("", |m| m.as_str());
247 let margin_value = margin_match.get(2).map_or("", |m| m.as_str());
248 extracted_css.insert(
249 format!("margin-{}", margin_name),
250 margin_value.to_string(),
251 );
252 }
253 }
254 }
255 }
256 Err(e) => {
257 error!("Error to create regex: {}", e);
258 }
259 }
260
261 extracted_css
262 }
263
264 async fn run_action(&mut self, action: PrintAction) -> (i32, String) {
266 match action {
267 PrintAction::Print {
268 content,
269 printer_name,
270 copies,
271 open,
272 } => {
273 info!("Printing content");
274
275 let print_result = self
277 .save_and_print_pdf(&content, printer_name.as_deref(), copies.unwrap_or(1))
278 .await;
279
280 match print_result {
281 Ok(_) => {
282 info!("Print job completed successfully.");
283
284 if open {
285 if let Some(printer_name) = printer_name {
286 info!("Opening drawer for printer: {}", printer_name);
287 let (status, message) = self.open_drawer(&printer_name).await;
288 if status != 0 {
289 error!("Failed to open drawer: {}", message);
290 } else {
291 info!("Drawer opened successfully");
292 }
293 } else {
294 error!("Printer name is not provided for opening drawer");
295 }
296 }
297
298 (0, "print ok".to_string())
299 }
300 Err(e) => {
301 error!("Failed to print: {}", e);
302 (1, "print failed".to_string())
303 }
304 }
305 }
306 PrintAction::OpenDrawer { printer_name } => {
307 info!("Opening drawer for printer: {}", printer_name);
308 self.open_drawer(&printer_name).await
309 }
310 PrintAction::Check => {
311 info!("Performing check action");
312 (0, "check ok".to_string())
313 }
314 PrintAction::ListPrinters => {
315 let printers = self.printers.lock();
316 let printer_names = printers
317 .await
318 .iter()
319 .map(|p| p.name.clone())
320 .collect::<Vec<String>>();
321 (0, serde_json::to_string(&printer_names).unwrap())
322 }
323 PrintAction::Unknown => {
324 error!("Unknown action");
325 (1, "unknown action".to_string())
326 }
327 }
328 }
329
330 async fn send_html_to_kitchen(
331 &self,
332 decoded_html: String,
333 _id: &str,
334 print_name: &str,
335 ) -> Result<(), BoxError> {
336 use serde_json::json;
337 use tokio_tungstenite::{connect_async, tungstenite::Message};
338
339 let url = "ws://127.0.0.1:9001"; let (mut socket, _) = connect_async(url).await?;
341 let uuid = uuid::Uuid::new_v4();
343 let command_message = json!({
345 "action": "addCommand",
346 "data": {
347 "id": format!("{}", uuid), "html": decoded_html,
349 "order_no": "",
350 "archived": false,
351 "printer": print_name,
352 }
353 });
354
355 socket
357 .send(Message::Text(command_message.to_string()))
358 .await?;
359 info!("HTML sent to kitchen");
361 Ok(())
362 }
363
364 #[cfg(not(target_os = "windows"))]
376 async fn print_pdf(
378 &self,
379 file_path: &Path,
380 printer_name: Option<&str>,
381 copies: u32,
382 ) -> Result<(), BoxError> {
383 info!("Printing with printers crate: {:?} printer {:?} copies {}",
384 file_path, printer_name, copies);
385
386 if !file_path.exists() {
388 error!("PDF file not found: {:?}", file_path);
389 return Err("PDF file not found".into());
390 }
391
392 let printer_name = printer_name.unwrap_or("Default");
393
394 if printer_name == "CommandViewer" {
396 info!("Skipping print for CommandViewer");
397 return Ok(());
398 }
399
400 let printer = if printer_name == "Default" {
402 printers::get_default_printer()
403 } else {
404 printers::get_printer_by_name(printer_name)
405 };
406
407 let printer = match printer {
408 Some(p) => p,
409 None => {
410 let new_printers = PrintService::list_printers().await;
412 let mut printers_cache = self.printers.lock().await;
413 *printers_cache = new_printers;
414
415 return Err(format!("Printer '{}' not found", printer_name).into());
416 }
417 };
418
419 for copy in 1..=copies {
421 let job_name = format!("Print Job {} (copy {}/{})",
422 file_path.file_name().unwrap_or_default().to_string_lossy(),
423 copy, copies);
424
425 let options = PrinterJobOptions {
426 name: Some(&job_name),
427 raw_properties: &[],
428 };
429
430 match printer.print_file(file_path.to_str().unwrap(), options) {
431 Ok(_) => {
432 info!("Print job {} sent successfully", job_name);
433 }
434 Err(e) => {
435 error!("Failed to print copy {}: {:?}", copy, e);
436 return Err(format!("Failed to print: {:?}", e).into());
437 }
438 }
439 }
440
441 info!("All {} copies printed successfully", copies);
442 Ok(())
443 }
444
445 pub async fn send_to_printer(&self, printer_name: &str, data: &[u8]) -> Result<(), BoxError> {
447 info!("Sending raw data to printer: {} ({} bytes)", printer_name, data.len());
448
449 let printers = self.printers.lock().await;
451 let printer = printers.iter()
452 .find(|p| p.name == printer_name)
453 .ok_or_else(|| format!("Printer '{}' not found", printer_name))?
454 .clone();
455
456 drop(printers); info!("Found printer: {} (driver: {})", printer.name, printer.driver_name);
459
460 let result = tokio::task::spawn_blocking({
462 let data = data.to_vec();
463 move || {
464 let options = PrinterJobOptions {
465 name: Some("Open Drawer Command"),
466 raw_properties: &[],
467 };
468
469 printer.print(&data, options)
470 }
471 }).await;
472
473 match result {
474 Ok(Ok(job)) => {
475 info!("Raw data sent successfully. Job: {:?}", job);
476 Ok(())
477 }
478 Ok(Err(e)) => {
479 error!("Failed to print raw data: {:?}", e);
480 Err(format!("Print failed: {:?}", e).into())
481 }
482 Err(e) => {
483 error!("Task spawn failed: {}", e);
484 Err(format!("Task spawn failed: {}", e).into())
485 }
486 }
487 }
488
489 pub async fn open_drawer(&self, printer_name: &str) -> (i32, String) {
491 info!("Opening drawer for printer: {}", printer_name);
492
493 match self.send_to_printer(printer_name, BUFFER_OPEN_DRAWER).await {
494 Ok(_) => {
495 info!("Drawer opened successfully");
496 (0, "drawer opened".to_string())
497 }
498 Err(e) => {
499 error!("Failed to open drawer: {}", e);
500 (1, "drawer failed".to_string())
501 }
502 }
503 }
504
505
506 async fn save_and_print_pdf(
507 &mut self,
508 content: &str,
509 printer_name: Option<&str>,
510 copies: u32,
511 ) -> Result<(), BoxError> {
512 let job_dir = sharing::paths::jobs_dir();
514 if !job_dir.exists() {
515 match std::fs::create_dir_all(&job_dir) {
516 Ok(_) => {
517 info!("Created jobs directory");
518 }
519 Err(e) => {
520 error!("Failed to create jobs directory: {}", e);
521 return Err(e.into());
522 }
523 }
524 }
525
526 let decoded_html = match general_purpose::STANDARD.decode(content) {
528 Ok(decoded) => decoded,
529 Err(e) => {
530 error!("Failed to decode content: {}", e);
531 return Err(e.into());
532 }
533 };
534
535 let decoded_html_str = String::from_utf8(decoded_html.clone())
537 .map_err(|e| format!("Failed to convert decoded content to string: {}", e))?;
538
539 let css_properties = self.extract_css_from_html(&decoded_html_str);
541
542 let page_width = css_properties
544 .get("page-width")
545 .unwrap_or(&"72mm".to_string())
546 .clone();
547 let page_height = css_properties
548 .get("page-height")
549 .unwrap_or(&"297mm".to_string())
550 .clone();
551 let margin_top = css_properties
552 .get("margin-top")
553 .unwrap_or(&"0mm".to_string())
554 .clone();
555 let margin_right = css_properties
556 .get("margin-right")
557 .unwrap_or(&"0mm".to_string())
558 .clone();
559 let margin_bottom = css_properties
560 .get("margin-bottom")
561 .unwrap_or(&"0mm".to_string())
562 .clone();
563 let margin_left = css_properties
564 .get("margin-left")
565 .unwrap_or(&"0mm".to_string())
566 .clone();
567
568 let file_md5 = format!("{:x}", md5::compute(decoded_html_str.as_bytes()));
569 let name = printer_name.unwrap_or("POS-80C");
570
571 {
581 let sig = dedup_signature(&decoded_html_str);
582 let key = (name.to_string(), sig.clone());
583 let mut recent = self.recent_prints.lock().await;
584 let now = Instant::now();
585
586 recent.retain(|_, ts| now.duration_since(*ts) <= PRINT_DEDUP_WINDOW);
587
588 if let Some(prev) = recent.get(&key) {
589 let age = now.duration_since(*prev);
590 info!(
591 "Skipping duplicate print: printer={} sig={} md5={} age={:?}",
592 name, sig, file_md5, age
593 );
594 return Ok(());
595 }
596
597 recent.insert(key, now);
598 }
599
600 if self.command_viewer{
601 if let Err(e) = self
602 .send_html_to_kitchen(decoded_html_str.clone(), &file_md5.clone(), name)
603 .await
604 {
605 error!("Failed to send HTML to kitchen: {}", e);
606 }
607 }
608
609 let pdf_filename = format!("{}_{}.pdf", self.config.service_name, file_md5);
611 let pdf_path = job_dir.join(pdf_filename);
612 info!("PDF path: {:?}", pdf_path);
613
614 if !pdf_path.exists() {
616 let html_filename = format!("{}_{}.html", self.config.service_name, file_md5);
618 let html_path = job_dir.join(html_filename);
619 let mut file = match File::create(&html_path) {
620 Ok(f) => f,
621 Err(e) => {
622 error!("Failed to create HTML file: {}", e);
623 return Err(e.into());
624 }
625 };
626 match file.write_all(&decoded_html.clone()) {
627 Ok(_) => {
628 info!("HTML content saved to file: {:?}", html_path);
629 }
630 Err(e) => {
631 error!("Failed to write HTML content to file: {}", e);
632 return Err(e.into());
633 }
634 }
635
636
637 if !Path::new(sharing::paths::WKHTMLTOPDF_PATH.as_str()).exists() {
639 info!("WKHTMLTOPDF_PATH: {:?}", sharing::paths::WKHTMLTOPDF_PATH);
640 return Err("wkhtmltopdf executable not found".into());
641 }
642 let wkhtmltopdf_path = WKHTMLTOPDF_PATH.as_str().to_string();
643 let pdf_path_clone = pdf_path.clone();
645 let html_path_clone = html_path.clone();
646
647 info!("Converting HTML to PDF");
648 let result = timeout(
649 Duration::from_secs(10),
650 task::spawn_blocking(move || {
651 let mut command = Command::new(&wkhtmltopdf_path);
652 command
653 .arg("--page-width")
654 .arg(page_width)
655 .arg("--page-height")
656 .arg(page_height)
657 .arg("--margin-top")
658 .arg(margin_top)
659 .arg("--margin-right")
660 .arg(margin_right)
661 .arg("--margin-bottom")
662 .arg(margin_bottom)
663 .arg("--margin-left")
664 .arg(margin_left)
665 .arg("--print-media-type")
666 .arg("--no-pdf-compression") .arg("--image-quality")
668 .arg("100")
669 .arg(&html_path_clone)
670 .arg(&pdf_path_clone)
671 .stdout(Stdio::null())
672 .stderr(Stdio::null());
673
674 #[cfg(target_os = "windows")]
676 command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
677
678 let status = command
679 .spawn()
680 .map_err(|e| format!("Failed to execute wkhtmltopdf: {}", e))?
681 .wait()
682 .map_err(|e| format!("Failed to wait for wkhtmltopdf: {}", e))?;
683
684 if status.success() {
685 Ok(())
686 } else {
687 Err(format!("wkhtmltopdf failed with exit code: {:?}", status))
688 }
689 }),
690 )
691 .await??;
692 match result {
693 Ok(_) => {
694 info!("HTML successfully converted to PDF.");
695 }
696 Err(e) => {
697 error!("Failed to convert HTML to PDF: {}", e);
698 return Err(e.into());
699 }
700 }
701
702
703 match std::fs::remove_file(&html_path) {
705 Ok(_) => {
706 info!("HTML file deleted: {:?}", html_path);
707 }
708 Err(e) => {
709 error!("Failed to delete HTML file: {}", e);
710 return Err(e.into());
711 }
712 }
713 }
714
715 info!("PDF file generated: {:?}", pdf_path);
716 if name.contains("CommandViewer") {
718 if !self.list_printers.lock().await.contains(&name.to_string()) {
720 info!("Adding CommandViewer to printer list");
721 self.add_printer_string(name).await;
722 }
723 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
724 return Ok(());
725 }
726 #[cfg(target_os = "windows")]
728 {
729 match self.print_with_sumatra(pdf_path.as_path(), printer_name, copies).await {
730 Ok(_) => {
731 info!("Print job sent successfully");
732 }
733 Err(e) => {
734 error!("Failed to send print job: {}", e);
735 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
736 return Err(e.into());
737 }
738 }
739 }
740 #[cfg(not(target_os = "windows"))]
741 {
742 match self.print_pdf(pdf_path.as_path(), printer_name, copies).await {
743 Ok(_) => {
744 info!("Print job sent successfully");
745 }
746 Err(e) => {
747 error!("Failed to send print job: {}", e);
748 crate::printsrvc::PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
749 return Err(e.into());
750 }
751 }
752 }
753
754 PDF_MANAGER.lock().await.add_pdf_file(pdf_path);
755 Ok(())
756 }
757
758
759 #[cfg(target_os = "windows")]
760 async fn print_with_sumatra(
771 &self,
772 file_path: &Path,
773 printer_name: Option<&str>,
774 copies: u32,
775 ) -> Result<(), BoxError> {
776 info!(
777 "Printing with SumatraPDF file {:?} printer {:?} copies {}",
778 file_path, printer_name, copies
779 );
780 if !Path::new(sharing::paths::SUMATRA_PATH.as_str()).exists() {
781 return Err("SumatraPDF executable not found".into());
782 }
783 let name = printer_name.unwrap_or("Default Printer");
784
785 let printer_exists = {
787 let printers = self.printers.lock().await; printers.iter().any(|p| p.name == name) || name == "CommandViewer"
789 };
790
791 if !printer_exists {
793 let new_printers = PrintService::list_printers().await;
794 let mut printers = self.printers.lock().await; if !new_printers.iter().any(|p| p.name == name) {
796 return Err("Printer not found".into());
797 }
798 *printers = new_printers;
799 }
800
801 for _ in 0..copies {
802 let file_path = file_path.to_path_buf(); let name = name.to_string(); let result = timeout(
806 Duration::from_secs(5),
807 task::spawn_blocking(move || {
808 Command::new(sharing::paths::SUMATRA_PATH.as_str())
809 .arg("-print-to")
810 .arg(name)
811 .arg("-print-settings")
812 .arg("noscale") .arg(file_path)
814 .stdout(Stdio::null())
815 .stderr(Stdio::null())
816 .status()
817 .expect("Failed to execute print command")
818 }),
819 )
820 .await;
821
822 match result {
823 Ok(Ok(status)) => {
824 if !status.success() {
825 error!("º command failed: {:?}", status);
826 return Err("Print command failed".into());
827 }
828 }
829 Ok(Err(e)) => {
830 error!("Task spawn failed: {:?}", e);
831 return Err("Task spawn failed".into());
832 }
833 Err(_) => {
834 error!("Print command timed out");
835 return Err("Print command timed out".into());
836 }
837 }
838 }
839
840 info!("Print job sent successfully");
841 Ok(())
842 }
843
844 pub async fn _find_printer(&self, printer_name: &str) -> Option<Printer> {
853 self.printers
854 .lock()
855 .await
856 .iter()
857 .find(|p| p.name == printer_name)
858 .cloned()
859 }
860
861
862
863 async fn list_printers() -> Vec<Printer> {
868 #[cfg(target_os = "linux")]
869 {
870 printers::get_printers()
871 }
872
873 #[cfg(target_os = "windows")]
874 {
875 printers::get_printers()
876 }
877
878 #[cfg(target_os = "macos")]
879 {
880 printers::get_printers()
881 }
882
883 #[cfg(target_os = "ios")]
884 {
885 Vec::new()
887 }
888 }
889
890 async fn process_action(&mut self, action: Value, write: WebSocketWrite) -> (i32, String) {
899 let action_type = match action.get("ACTION").and_then(Value::as_str) {
900 Some(action_str) => action_str,
901 None => {
902 error!("Missing or invalid ACTION field");
903 return (1, "Missing ACTION field".to_string());
904 }
905 };
906
907 if action_type == "getPrinters" {
908 info!("Processing getPrinters action");
909 let printers = self.get_print_list().await;
910
911 let uuid = match action.get("UUIDV4") {
912 Some(Value::String(uuid)) => uuid,
913 _ => {
914 error!("Missing or invalid MESSAGE_UUID field");
915 return (1, "Missing MESSAGE_UUID field".to_string());
916 }
917 };
918 let response = json!({
919 "SERVICE_NAME": "PRINT",
920 "SERVICE_VERS": PRINT_VERSION,
921 "MESSAGE_TYPE": "RESPONSE",
922 "MESSAGE_EXEC": "SUCCESS",
923 "MESSAGE_UUID": uuid,
924 "MESSAGE_DATA": printers,
925 }).to_string();
926
927 send_message(&write, response).await;
928 return (0, "Printers list sent".to_string());
929 }
930
931 let action_map = match action.get("ACTION") {
932 Some(Value::String(action_str)) => {
933 match serde_json::from_str::<serde_json::Map<String, Value>>(action_str) {
934 Ok(map) => map,
935 Err(e) => {
936 error!("Failed to parse ACTION as JSON object: {}", e);
937 return (1, format!("Invalid ACTION format: {}", e));
938 }
939 }
940 }
941 _ => {
942 error!("Missing or invalid ACTION field");
943 return (1, "Missing ACTION field".to_string());
944 }
945 };
946
947 for (device_name, device_actions) in action_map {
948 info!("Processing actions for device: {}", device_name);
949
950 if device_actions.is_null() {
951 return (0, "Device is reachable".to_string());
952 }
953
954 match &device_actions {
956 Value::Object(actions) => {
957 debug!("Device actions is object: {:?}", actions);
958
959 if let Some(Value::String(print_content)) = actions.get("print") {
961 info!("Found print content, length: {}", print_content.len());
962
963 if let Some(print_action) = self.parse_print_action(&device_name, &actions) {
964 let result = self.run_action(print_action).await;
965 if result.0 != 0 {
966 return result;
967 }
968 }
969 } else {
970 if let Some(open_action) = self.parse_open_action(&device_name, &actions) {
972 info!("open_action {:?}", open_action);
973 let result = self.run_action(open_action).await;
974 if result.0 != 0 {
975 return result;
976 }
977 }
978 }
979 }
980 Value::String(content) => {
981 info!("Device actions is string (assuming base64), length: {}", content.len());
983
984 let print_action = PrintAction::Print {
985 content: content.clone(),
986 printer_name: Some(device_name.clone()),
987 copies: Some(1),
988 open: false,
989 };
990
991 let result = self.run_action(print_action).await;
992 if result.0 != 0 {
993 return result;
994 }
995 }
996 _ => {
997 let error_msg = format!("Actions for '{}' must be a JSON object or string, got: {:?}",
998 device_name, device_actions);
999 error!("{}", error_msg);
1000 return (1, error_msg);
1001 }
1002 }
1003 }
1004
1005 (0, "All actions processed successfully".to_string())
1006 }
1007
1008 fn parse_print_action(
1018 &self,
1019 device: &str,
1020 actions: &serde_json::Map<String, Value>,
1021 ) -> Option<PrintAction> {
1022 if let Some(print_content) = actions.get("print") {
1023 let content = match print_content {
1024 Value::String(s) => {
1025 info!("Print content for device {}: {} chars", device, s.len());
1026 s.clone()
1027 }
1028 _ => {
1029 error!("Print content for device {} is not a string: {:?}", device, print_content);
1030 return None;
1031 }
1032 };
1033
1034 let copies = actions
1035 .get("copies")
1036 .and_then(Value::as_u64)
1037 .map(|c| c as u32)
1038 .unwrap_or(1);
1039
1040 let open = actions
1041 .get("open")
1042 .and_then(Value::as_bool)
1043 .unwrap_or(false);
1044
1045 info!("Parsed print action - device: {}, copies: {}, open: {}", device, copies, open);
1046
1047 Some(PrintAction::Print {
1048 content,
1049 printer_name: Some(device.to_string()),
1050 copies: Some(copies),
1051 open,
1052 })
1053 } else {
1054 debug!("No 'print' field found in actions for device: {}", device);
1055 None
1056 }
1057 }
1058
1059 fn parse_open_action(
1069 &self,
1070 device: &str,
1071 actions: &serde_json::Map<String, Value>,
1072 ) -> Option<PrintAction> {
1073 if let Some(open_drawer) = actions.get("open") {
1074 if open_drawer.as_bool().unwrap_or(false) {
1075 return Some(PrintAction::OpenDrawer {
1076 printer_name: device.to_string(),
1077 });
1078 }
1079 }
1080 None
1081 }
1082}
1083
1084#[async_trait]
1085impl Service for PrintService {
1086 async fn run(&self, action: Value, _write: WebSocketWrite) -> (i32, String) {
1099 let deserialized_action = if action.is_string() {
1100 match serde_json::from_str::<Value>(action.as_str().unwrap_or("")) {
1101 Ok(val) => val,
1102 Err(err) => {
1103 error!("Failed to parse string JSON: {}", err);
1104 return (1, format!("Invalid action format: {}", err));
1105 }
1106 }
1107 } else {
1108 action
1109 };
1110
1111 let result: Arc<Mutex<Option<(i32, String)>>> = Arc::new(Mutex::new(None)); let result_clone = Arc::clone(&result);
1113 let mut self_clone = self.clone();
1114
1115 tokio::spawn(async move {
1117 let process_result = self_clone.process_action(deserialized_action, _write).await;
1118 let mut lock = result_clone.lock().await;
1119 *lock = Some(process_result);
1120 });
1121
1122 let start = tokio::time::Instant::now();
1124 while start.elapsed() < Duration::from_secs(2) {
1125 {
1126 let lock = result.lock().await;
1127 if let Some((code, msg)) = &*lock {
1128 return (*code, msg.clone()); }
1130 }
1131 sleep(Duration::from_millis(100)).await; }
1133
1134 (0, "Action is being processed asynchronously".to_string())
1136 }
1137
1138 fn as_any(&self) -> &dyn std::any::Any {
1144 self
1145 }
1146
1147 fn stop_service(&self) {
1149 info!("Stopping PrintService...");
1150 }
1151
1152 fn get_version(&self) -> String {
1158 PRINT_VERSION.to_string()
1159 }
1160}