1#[cfg(target_os = "windows")]
2use {
3 windows_service::{
4 service::{
5 ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceType,
6 },
7 service_manager::{ServiceManager, ServiceManagerAccess},
8 },std::{
9 ffi::OsString,
10 thread::sleep
11 }
12};
13
14use easy_trace::instruments::tracing;
15#[cfg(not(target_os = "windows"))]
16use
17{ users::get_user_by_uid,
18 users::os::unix::UserExt
19};
20#[cfg(target_os = "windows")]
21pub fn start_service() -> sharing::PisPasResult<()> {
22 let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
23 let service = manager.open_service(sharing::SERVICE_NAME, ServiceAccess::START)?;
24 service.start(&[""])?;
25 Ok(())
26}
27
28#[cfg(target_os = "windows")]
29pub fn stop_service() -> sharing::PisPasResult<()> {
30 let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
31 let service = manager.open_service(sharing::SERVICE_NAME, ServiceAccess::STOP)?;
32 service.stop()?;
33 Ok(())
34}
35
36#[cfg(target_os = "windows")]
37pub fn unregister_service() -> sharing::PisPasResult<()> {
38 match sharing::natives::api::stop_pispas_modules() {
39 Ok(_) => tracing::info!("Pispas modules stopped successfully"),
40 Err(e) => tracing::error!("Error stopping pispas modules: {}", e),
41 }
42 sleep(std::time::Duration::from_secs(1));
43 let manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT)?;
44 let service = manager.open_service(sharing::SERVICE_NAME, ServiceAccess::DELETE)?;
45 service.delete()?;
46 Ok(())
47}
48#[cfg(windows)]
49pub fn register_service() -> sharing::PisPasResult<()> {
50 let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
51 let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)?;
52
53 let service_binary_path = sharing::paths::bin_dir().join(sharing::SERVICE_NAME_EXE);
54
55 let service_info = ServiceInfo {
56 name: OsString::from(sharing::SERVICE_NAME),
57 display_name: OsString::from(sharing::SERVICE_NAME),
58 service_type: ServiceType::OWN_PROCESS,
59 start_type: ServiceStartType::AutoStart,
60 error_control: ServiceErrorControl::Normal,
61 executable_path: service_binary_path,
62 launch_arguments: vec![],
63 dependencies: vec![],
64 account_name: None, account_password: None,
66 };
67 match service_manager.open_service(
68 sharing::SERVICE_NAME,
69 ServiceAccess::CHANGE_CONFIG,
70 ) {
71 Ok(service) => {
72 service.change_config(&service_info)?;
73 tracing::info!("Service {} update", sharing::SERVICE_NAME);
74 sleep(std::time::Duration::from_secs(1));
75 }
76 Err(e) => {
77 let service = service_manager.create_service(&service_info, ServiceAccess::CHANGE_CONFIG)?;
78 service.set_description(sharing::SERVICE_NAME)?;
79 sleep(std::time::Duration::from_secs(1));
80 tracing::info!("Service {} does not exist error {}", sharing::SERVICE_NAME, e);
81 match service.start(&[""]) {
82 Ok(_) => tracing::info!("Service {} started", sharing::SERVICE_NAME),
83 Err(e) => tracing::error!("Service {} start error {}", sharing::SERVICE_NAME, e),
84 }
85 }
86 }
87 Ok(())
88}
89#[cfg(target_os = "macos")]
92pub fn register_service_as_daemon() -> sharing::PisPasResult<()> {
93 use std::fs;
94
95 let service_name = sharing::SERVICE_NAME;
96 let plist_content = create_launch_daemon_plist()?;
97
98 let plist_path = std::path::PathBuf::from("/Library/LaunchDaemons")
100 .join(format!("{}.plist", service_name));
101
102 fs::write(&plist_path, plist_content)?;
104 tracing::info!("Created LaunchDaemon plist: {}", plist_path.display());
105
106 std::process::Command::new("chown")
108 .args(&["root:wheel", &plist_path.to_string_lossy()])
109 .output()?;
110
111 std::process::Command::new("chmod")
112 .args(&["644", &plist_path.to_string_lossy()])
113 .output()?;
114
115 let bootstrap_output = std::process::Command::new("launchctl")
117 .args(&["bootstrap", "system", &plist_path.to_string_lossy()])
118 .output()?;
119
120 if !bootstrap_output.status.success() {
121 let error = String::from_utf8_lossy(&bootstrap_output.stderr);
122 tracing::error!("Failed to bootstrap daemon: {}", error);
123 return Err(anyhow::anyhow!("Failed to bootstrap daemon: {}", error));
124 }
125
126 let service_target = format!("system/{}", service_name);
128 let enable_output = std::process::Command::new("launchctl")
129 .args(&["enable", &service_target])
130 .output()?;
131
132 if !enable_output.status.success() {
133 let error = String::from_utf8_lossy(&enable_output.stderr);
134 tracing::warn!("Failed to enable daemon: {}", error);
135 }
136
137 tracing::info!("Service {} bootstrapped and enabled as daemon", service_name);
138 Ok(())
139}
140#[cfg(target_os = "macos")]
141pub fn register_service() -> sharing::PisPasResult<()> {
142 register_service_as_daemon()
144}
145
146#[cfg(target_os = "macos")]
147pub fn unregister_service() -> sharing::PisPasResult<()> {
148 let service_name = sharing::SERVICE_NAME;
149
150 tracing::info!("Unregistering service: {}", service_name);
151
152 let _ = stop_service();
154
155 let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot get home directory"))?;
157 let plist_path = home_dir.join("Library/LaunchAgents").join(format!("{}.plist", service_name));
158
159 if plist_path.exists() {
161 let output = std::process::Command::new("launchctl")
162 .args(&["unload", &plist_path.to_string_lossy()])
163 .output()?;
164
165 if !output.status.success() {
166 let error = String::from_utf8_lossy(&output.stderr);
167 tracing::warn!("Failed to unload service: {}", error);
168 }
169
170 std::fs::remove_file(&plist_path)?;
171 tracing::info!("Service plist removed: {}", plist_path.display());
172 }
173
174 let daemon_plist = format!("/Library/LaunchDaemons/{}.plist", service_name);
176 if std::path::Path::new(&daemon_plist).exists() {
177 let _ = std::process::Command::new("sudo")
178 .args(&["launchctl", "unload", &daemon_plist])
179 .output();
180 let _ = std::process::Command::new("sudo")
181 .args(&["rm", "-f", &daemon_plist])
182 .output();
183 tracing::info!("Cleaned up legacy daemon plist");
184 }
185
186 tracing::info!("Service {} unregistered successfully", service_name);
187 Ok(())
188}
189
190#[cfg(target_os = "macos")]
193pub fn start_service() -> sharing::PisPasResult<()> {
194 let service_name = sharing::SERVICE_NAME;
195 let service_target = format!("system/{}", service_name); let print_output = std::process::Command::new("launchctl")
199 .args(&["print", &service_target])
200 .output()?;
201
202 if print_output.status.success() {
203 tracing::info!("Service {} is already loaded", service_name);
204 return Ok(());
205 }
206
207 let output = std::process::Command::new("launchctl")
209 .args(&["kickstart", &service_target])
210 .output()?;
211
212 if !output.status.success() {
213 let error = String::from_utf8_lossy(&output.stderr);
214 tracing::error!("Failed to start service: {}", error);
215 return Err(anyhow::anyhow!("Failed to start service: {}", error));
216 }
217
218 tracing::info!("Service {} started successfully", service_name);
219 Ok(())
220}
221
222#[cfg(target_os = "macos")]
223pub fn stop_service() -> sharing::PisPasResult<()> {
224 let service_name = sharing::SERVICE_NAME;
225 let plist_path = format!("/Library/LaunchDaemons/{}.plist", service_name);
226
227 let bootout = std::process::Command::new("launchctl")
234 .args(&["bootout", &format!("system/{}", service_name)])
235 .output()?;
236
237 if !bootout.status.success() {
238 let err = String::from_utf8_lossy(&bootout.stderr);
239 tracing::warn!("bootout failed ({}), trying unload", err.trim());
242 let unload = std::process::Command::new("launchctl")
243 .args(&["unload", &plist_path])
244 .output()?;
245 if !unload.status.success() {
246 let err = String::from_utf8_lossy(&unload.stderr);
247 tracing::warn!("unload also failed: {}", err.trim());
248 }
249 }
250
251 sharing::proc::kill_process_by_name(sharing::PRINT_SERVICE);
256 sharing::proc::kill_process_by_name(sharing::TRAY_ICON_NAME);
257
258 tracing::info!("Service {} unloaded and children killed", service_name);
259 Ok(())
260}
261
262
263#[cfg(target_os = "macos")]
264pub fn restart_service() -> sharing::PisPasResult<()> {
265 tracing::info!("Restarting service...");
266
267 let _ = stop_service();
268 std::thread::sleep(std::time::Duration::from_secs(1));
269 start_service()?;
270
271 tracing::info!("Service restarted successfully");
272 Ok(())
273}
274
275#[cfg(target_os = "macos")]
278pub fn install_service() -> sharing::PisPasResult<()> {
279 tracing::info!("Installing pispas service on macOS...");
280 if let Err(e) = setup_modules_app_bundle() {
281 tracing::error!("Failed to set up pispas-modules.app bundle: {}", e);
282 }
283 clean_and_register_service()
284}
285
286#[cfg(target_os = "macos")]
312pub fn setup_modules_app_bundle() -> sharing::PisPasResult<()> {
313 use std::fs;
314 use std::path::Path;
315
316 let install_dir = sharing::paths::get_install_dir();
317 let modules_path = install_dir.join(sharing::PRINT_SERVICE);
318 let bundle_dir = install_dir.join("pispas-modules.app");
319 let contents_dir = bundle_dir.join("Contents");
320 let macos_dir = contents_dir.join("MacOS");
321 let bundle_binary = macos_dir.join("pispas-modules");
322 let info_plist = contents_dir.join("Info.plist");
323
324 fs::create_dir_all(&macos_dir)?;
325
326 let was_real_file = match fs::symlink_metadata(&modules_path) {
332 Ok(meta) => meta.file_type().is_file(),
333 Err(_) => false,
334 };
335 if was_real_file {
336 fs::rename(&modules_path, &bundle_binary)?;
337 use std::os::unix::fs::PermissionsExt;
339 let mut perms = fs::metadata(&bundle_binary)?.permissions();
340 perms.set_mode(0o755);
341 fs::set_permissions(&bundle_binary, perms)?;
342 } else if !bundle_binary.exists() {
343 return Err(anyhow::anyhow!(
344 "pispas-modules missing at {} and bundle binary not found at {}",
345 modules_path.display(),
346 bundle_binary.display(),
347 ));
348 }
349
350 let plist = r#"<?xml version="1.0" encoding="UTF-8"?>
353<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
354<plist version="1.0">
355<dict>
356 <key>CFBundleDevelopmentRegion</key>
357 <string>en</string>
358 <key>CFBundleExecutable</key>
359 <string>pispas-modules</string>
360 <key>CFBundleIdentifier</key>
361 <string>com.pispas.modules</string>
362 <key>CFBundleInfoDictionaryVersion</key>
363 <string>6.0</string>
364 <key>CFBundleName</key>
365 <string>Pispas Modules</string>
366 <key>CFBundlePackageType</key>
367 <string>APPL</string>
368 <key>CFBundleShortVersionString</key>
369 <string>1.0.0</string>
370 <key>CFBundleVersion</key>
371 <string>1</string>
372 <key>LSMinimumSystemVersion</key>
373 <string>11.0</string>
374 <key>LSUIElement</key>
375 <true/>
376 <key>NSLocalNetworkUsageDescription</key>
377 <string>Pispas Modules necesita acceder al terminal de pago en la red local para procesar transacciones.</string>
378 <key>NSBonjourServices</key>
379 <array>
380 <string>_http._tcp</string>
381 </array>
382</dict>
383</plist>
384"#;
385 fs::write(&info_plist, plist)?;
386
387 if Path::new(&modules_path).exists() || fs::symlink_metadata(&modules_path).is_ok() {
391 let _ = fs::remove_file(&modules_path);
392 }
393 std::os::unix::fs::symlink("pispas-modules.app/Contents/MacOS/pispas-modules", &modules_path)?;
394
395 tracing::info!("pispas-modules.app bundle ready at {}", bundle_dir.display());
396 Ok(())
397}
398
399#[cfg(target_os = "macos")]
400pub fn clean_and_register_service() -> sharing::PisPasResult<()> {
401 use std::fs;
402 use std::process::Command;
403
404 tracing::info!("Cleaning existing pispas services...");
405
406 let _ = Command::new("sudo").args(&["pkill", "-f", "pispas"]).output();
408 let _ = Command::new("sudo").args(&["pkill", "-f", "pispas-modules"]).output();
409 let _ = Command::new("sudo").args(&["pkill", "-f", "pispas-service"]).output();
410 let _ = Command::new("sudo").args(&["pkill", "-f", "pispas-tray-app"]).output();
411
412
413 std::thread::sleep(std::time::Duration::from_secs(2));
414
415 let service_name = sharing::SERVICE_NAME;
417
418 let daemon_plist = format!("/Library/LaunchDaemons/{}.plist", service_name);
420 let _ = Command::new("sudo").args(&["launchctl", "unload", &daemon_plist]).output();
421 let _ = Command::new("sudo").args(&["rm", "-f", &daemon_plist]).output();
422
423 let system_agent_plist = format!("/Library/LaunchAgents/{}.plist", service_name);
425 let _ = Command::new("sudo").args(&["rm", "-f", &system_agent_plist]).output();
426
427 let home_dir = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Cannot get home directory"))?;
429 let user_agent_plist = home_dir.join("Library/LaunchAgents").join(format!("{}.plist", service_name));
430
431 if user_agent_plist.exists() {
432 let _ = Command::new("launchctl")
433 .args(&["unload", &user_agent_plist.to_string_lossy()])
434 .output();
435 let _ = fs::remove_file(&user_agent_plist);
436 }
437
438 let _ = Command::new("sudo").args(&["launchctl", "remove", service_name]).output();
440 let _ = Command::new("launchctl").args(&["remove", service_name]).output();
441
442 let uid = unsafe { libc::getuid() };
444 let domain = format!("gui/{}", uid);
445 let _ = Command::new("launchctl").args(&["bootout", &domain, &user_agent_plist.to_string_lossy()]).output();
446
447 tracing::info!("Cleanup completed. Registering new service...");
448
449 let service_binary = sharing::paths::bin_dir().join(sharing::SERVICE_NAME_EXE);
451 if !service_binary.exists() {
452 let modules_binary = sharing::paths::bin_dir().join("pispas-modules");
453 if modules_binary.exists() {
454 std::os::unix::fs::symlink(&modules_binary, &service_binary)?;
455 tracing::info!("Created symlink: {} -> {}",
456 service_binary.display(), modules_binary.display());
457 } else {
458 return Err(anyhow::anyhow!("Neither pispas-service nor pispas-modules binary found"));
459 }
460 }
461
462 register_service_as_daemon()?;
464
465 Ok(())
466}
467
468#[cfg(target_os = "macos")]
469fn create_launch_daemon_plist() -> sharing::PisPasResult<String> {
470 match sharing::natives::api::get_first_logged_user() {
471 Ok((_username, uid)) => {
472 if let Some(user) = get_user_by_uid(uid) {
473
474 let service_binary_path = format!(
476 "{}/.config/pispas/{}",
477 user.home_dir().display(),
478 sharing::SERVICE_NAME_EXE
479 );
480
481 let working_directory = format!(
482 "{}/.config/pispas",
483 user.home_dir().display()
484 );
485
486 let plist_content = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
487 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
488 <plist version="1.0">
489 <dict>
490 <key>Label</key>
491 <string>{}</string>
492 <key>ProgramArguments</key>
493 <array>
494 <string>{}</string>
495 </array>
496 <key>RunAtLoad</key>
497 <true/>
498 <key>KeepAlive</key>
499 <true/>
500 <key>StandardOutPath</key>
501 <string>/var/log/pispas/pispas-service.out.log</string>
502 <key>StandardErrorPath</key>
503 <string>/var/log/pispas/pispas-service.err.log</string>
504 <key>WorkingDirectory</key>
505 <string>{}</string>
506 </dict>
507 </plist>"#,
508 sharing::SERVICE_NAME,
509 service_binary_path,
510 working_directory
511 );
512
513 return Ok(plist_content);
514 }
515 }
516 Err(e) => {
517 tracing::error!("Error getting first logged user: {}", e);
518 return Err(anyhow::anyhow!("Failed to get first logged user"));
519 }
520 }
521 Err(anyhow::anyhow!("Failed to create LaunchDaemon plist"))
522}
523#[cfg(target_os = "linux")]
525pub fn register_service() -> sharing::PisPasResult<()> {
526 use std::fs;
527 use std::os::unix::fs::PermissionsExt;
528
529 let service_name = sharing::SERVICE_NAME;
530 let service_content = create_systemd_service()?;
531 let service_path = format!("/etc/systemd/system/{}.service", service_name);
532
533 fs::write(&service_path, service_content)?;
535
536 let mut perms = fs::metadata(&service_path)?.permissions();
538 perms.set_mode(0o644);
539 fs::set_permissions(&service_path, perms)?;
540
541 std::process::Command::new("systemctl")
543 .args(&["daemon-reload"])
544 .output()?;
545
546 let output = std::process::Command::new("systemctl")
547 .args(&["enable", service_name])
548 .output()?;
549
550 if !output.status.success() {
551 let error = String::from_utf8_lossy(&output.stderr);
552 tracing::error!("Failed to enable service: {}", error);
553 return Err(anyhow::anyhow!("Failed to enable service: {}", error));
554 }
555
556 tracing::info!("Service {} registered successfully", service_name);
557 Ok(())
558}
559
560#[cfg(target_os = "linux")]
561pub fn unregister_service() -> sharing::PisPasResult<()> {
562 let service_name = sharing::SERVICE_NAME;
563 let service_path = format!("/etc/systemd/system/{}.service", service_name);
564
565 std::process::Command::new("systemctl")
567 .args(&["stop", service_name])
568 .output()?;
569
570 std::process::Command::new("systemctl")
571 .args(&["disable", service_name])
572 .output()?;
573
574 if std::path::Path::new(&service_path).exists() {
576 std::fs::remove_file(&service_path)?;
577 tracing::info!("Service file removed: {}", service_path);
578 }
579
580 std::process::Command::new("systemctl")
582 .args(&["daemon-reload"])
583 .output()?;
584
585 tracing::info!("Service {} unregistered successfully", service_name);
586 Ok(())
587}
588
589#[cfg(target_os = "linux")]
590pub fn start_service() -> sharing::PisPasResult<()> {
591 let service_name = sharing::SERVICE_NAME;
592
593 let output = std::process::Command::new("systemctl")
594 .args(&["start", service_name])
595 .output()?;
596
597 if !output.status.success() {
598 let error = String::from_utf8_lossy(&output.stderr);
599 tracing::error!("Failed to start service: {}", error);
600 return Err(anyhow::anyhow!("Failed to start service: {}", error));
601 }
602
603 tracing::info!("Service {} started successfully", service_name);
604 Ok(())
605}
606
607#[cfg(target_os = "linux")]
608pub fn stop_service() -> sharing::PisPasResult<()> {
609 let service_name = sharing::SERVICE_NAME;
610
611 let output = std::process::Command::new("systemctl")
612 .args(&["stop", service_name])
613 .output()?;
614
615 if !output.status.success() {
616 let error = String::from_utf8_lossy(&output.stderr);
617 tracing::warn!("Failed to stop service (might not be running): {}", error);
618 }
619
620 tracing::info!("Service {} stopped", service_name);
621 Ok(())
622}
623
624#[cfg(target_os = "linux")]
625fn create_systemd_service() -> sharing::PisPasResult<String> {
626 let service_binary_path = sharing::paths::bin_dir().join(sharing::SERVICE_NAME_EXE);
627 let service_name = sharing::SERVICE_NAME;
628
629 let service_content = format!(r#"[Unit]
630Description={}
631After=network.target
632
633[Service]
634Type=simple
635ExecStart={} run
636Restart=always
637RestartSec=5
638User=root
639WorkingDirectory={}
640
641[Install]
642WantedBy=multi-user.target
643"#,
644 service_name,
645 service_binary_path.display(),
646 sharing::paths::bin_dir().display()
647 );
648
649 Ok(service_content)
650}