pispas_elevator/
service.rs

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, // run as System
65        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// ===== FUNCIONES PRINCIPALES =====
90
91#[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    // LaunchDaemon va en /Library/LaunchDaemons/ (requiere sudo)
99    let plist_path = std::path::PathBuf::from("/Library/LaunchDaemons")
100        .join(format!("{}.plist", service_name));
101
102    // Escribir el archivo plist (requiere sudo)
103    fs::write(&plist_path, plist_content)?;
104    tracing::info!("Created LaunchDaemon plist: {}", plist_path.display());
105
106    // Establecer permisos correctos
107    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    // Bootstrap como daemon del sistema
116    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    // Habilitar para auto-start
127    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    // Para instalación/registro usa la función de limpieza completa
143    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    // Parar el servicio si está ejecutándose
153    let _ = stop_service();
154
155    // Usar LaunchAgent path
156    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    // Descargar el servicio (sin sudo)
160    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    // También limpiar cualquier daemon residual (por si acaso)
175    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// ===== FUNCIONES DE CONTROL =====
191
192#[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);  // ✅ system domain
196
197    // Verificar si ya está corriendo
198    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    // Kickstart para forzar inicio
208    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    // Use `bootout` (newer API) or fallback to `unload` — these remove the
228    // daemon from launchd so it DOES NOT respawn. `launchctl kill SIGTERM`
229    // alone is not enough because our plist has KeepAlive=true, so launchd
230    // immediately respawns the service — and the new instance spawns a
231    // second pispas-modules that fights the old one for the remote WSS
232    // connection (connect → "closed by server" → reconnect loop).
233    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        // `bootout` may fail if the service isn't bootstrapped; try `unload`
240        // which is the older equivalent and tolerates that case.
241        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    // Belt-and-braces: kill any lingering child processes the daemon left
252    // behind (pispas-modules spawned as user via launchctl asuser + sudo).
253    // These don't die with the daemon because they're in a different
254    // bootstrap and owned by a different user.
255    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// ===== FUNCIONES DE INSTALACIÓN =====
276
277#[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/// On macOS Sonoma+ (14+) and especially macOS 15/26+, processes spawned
287/// by `launchd` cannot reach Local Network IPs (RFC1918) without explicit
288/// user consent. The consent dialog is only shown when the binary is
289/// inside an `.app` bundle that declares `NSLocalNetworkUsageDescription`
290/// in its `Info.plist` — a plain ELF/Mach-O file launched by launchd is
291/// silently denied with `EHOSTUNREACH`. This breaks Paytef terminals
292/// (172.16.x.x) and any other LAN integration.
293///
294/// Layout produced (idempotent — safe to run on every install/update):
295///
296/// ```text
297/// <install_dir>/
298/// ├── pispas-modules                                 (symlink)
299/// │       → pispas-modules.app/Contents/MacOS/pispas-modules
300/// └── pispas-modules.app/
301///     └── Contents/
302///         ├── Info.plist                             (with NSLocalNetworkUsageDescription)
303///         └── MacOS/
304///             └── pispas-modules                     (real binary)
305/// ```
306///
307/// The symlink keeps the existing service launch path
308/// (`<install_dir>/pispas-modules`) working without changes, while the
309/// real binary lives inside an `.app` bundle so macOS resolves the
310/// bundle's `Info.plist` for TCC checks.
311#[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    // Move the freshly-installed binary into the bundle. After
327    // copy_dir during update, modules_path is a regular file (the new
328    // binary just copied from the temp dir). On subsequent runs it's a
329    // symlink we put there, in which case the new binary is already
330    // inside the bundle from the previous step.
331    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        // Ensure executable.
338        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    // Always (re)write Info.plist — it's tiny and lets us update the
351    // description without manual fiddling on existing installs.
352    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    // (Re)create the symlink so external code that spawns
388    // <install_dir>/pispas-modules ends up exec'ing the bundled binary
389    // (and macOS resolves the surrounding bundle for TCC purposes).
390    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    // 1. MATAR todos los procesos pispas
407    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    // 2. ELIMINAR todos los plists existentes
416    let service_name = sharing::SERVICE_NAME;
417
418    // LaunchDaemons (nivel sistema)
419    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    // LaunchAgents nivel sistema
424    let system_agent_plist = format!("/Library/LaunchAgents/{}.plist", service_name);
425    let _ = Command::new("sudo").args(&["rm", "-f", &system_agent_plist]).output();
426
427    // LaunchAgents de usuario
428    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    // En el paso 3, cambia esto:
439    let _ = Command::new("sudo").args(&["launchctl", "remove", service_name]).output();
440    let _ = Command::new("launchctl").args(&["remove", service_name]).output();
441
442    // Por esto:
443    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    // 4. Crear el symlink del binario si no existe
450    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    // 5. Registrar el nuevo servicio como LaunchDaemon
463    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                // Construir la ruta dinámicamente
475                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// Para Linux (systemd)
524#[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    // Escribir el archivo de servicio
534    fs::write(&service_path, service_content)?;
535
536    // Establecer permisos correctos
537    let mut perms = fs::metadata(&service_path)?.permissions();
538    perms.set_mode(0o644);
539    fs::set_permissions(&service_path, perms)?;
540
541    // Recargar systemd y habilitar el servicio
542    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    // Parar y deshabilitar el servicio
566    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    // Eliminar el archivo de servicio
575    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    // Recargar systemd
581    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}