1use crate::adapter::{DaemonHandle, PlatformAdapter, PlatformError, PlatformResult, TracerHandle};
6use crate::daemon::Daemon;
7use crate::platform::Platform;
8use crate::types::{DaemonStatus, FailureReason, Signal};
9
10use async_trait::async_trait;
11use std::path::PathBuf;
12use tokio::process::Command;
13
14pub struct SystemdAdapter {
33 unit_dir: PathBuf,
35 user_mode: bool,
37}
38
39impl SystemdAdapter {
40 #[must_use]
44 pub fn new() -> Self {
45 Self::user()
46 }
47
48 #[must_use]
52 pub fn system() -> Self {
53 Self {
54 unit_dir: PathBuf::from("/etc/systemd/system"),
55 user_mode: false,
56 }
57 }
58
59 #[must_use]
63 pub fn user() -> Self {
64 Self {
65 unit_dir: dirs_next::config_dir()
66 .map(|p| p.join("systemd/user"))
67 .unwrap_or_else(|| PathBuf::from("~/.config/systemd/user")),
68 user_mode: true,
69 }
70 }
71
72 #[must_use]
74 pub fn with_unit_dir(unit_dir: PathBuf, user_mode: bool) -> Self {
75 Self {
76 unit_dir,
77 user_mode,
78 }
79 }
80
81 #[must_use]
83 pub fn unit_dir(&self) -> &PathBuf {
84 &self.unit_dir
85 }
86
87 #[must_use]
89 pub const fn is_user_mode(&self) -> bool {
90 self.user_mode
91 }
92
93 fn unit_name(daemon_name: &str) -> String {
95 format!("duende-{}.service", daemon_name.replace(' ', "-"))
96 }
97
98 fn systemctl_cmd(&self) -> Command {
100 let mut cmd = Command::new("systemctl");
101 if self.user_mode {
102 cmd.arg("--user");
103 }
104 cmd
105 }
106
107 fn systemd_run_cmd(&self) -> Command {
109 let mut cmd = Command::new("systemd-run");
110 if self.user_mode {
111 cmd.arg("--user");
112 }
113 cmd
114 }
115
116 fn parse_status(output: &str, exit_code: i32) -> DaemonStatus {
118 if exit_code == 0 {
125 if output.contains("Active: active (running)") {
127 return DaemonStatus::Running;
128 }
129 if output.contains("Active: activating") {
130 return DaemonStatus::Starting;
131 }
132 }
133
134 if output.contains("Active: deactivating") {
135 return DaemonStatus::Stopping;
136 }
137
138 if output.contains("Active: inactive") {
139 return DaemonStatus::Stopped;
140 }
141
142 if output.contains("Active: failed") {
143 return DaemonStatus::Failed(FailureReason::ExitCode(1));
144 }
145
146 DaemonStatus::Stopped
148 }
149
150 fn signal_name(sig: Signal) -> &'static str {
152 match sig {
153 Signal::Term => "SIGTERM",
154 Signal::Kill => "SIGKILL",
155 Signal::Int => "SIGINT",
156 Signal::Quit => "SIGQUIT",
157 Signal::Hup => "SIGHUP",
158 Signal::Usr1 => "SIGUSR1",
159 Signal::Usr2 => "SIGUSR2",
160 Signal::Stop => "SIGSTOP",
161 Signal::Cont => "SIGCONT",
162 }
163 }
164}
165
166impl Default for SystemdAdapter {
167 fn default() -> Self {
168 Self::user()
170 }
171}
172
173#[async_trait]
174impl PlatformAdapter for SystemdAdapter {
175 fn platform(&self) -> Platform {
176 Platform::Linux
177 }
178
179 async fn spawn(&self, daemon: Box<dyn Daemon>) -> PlatformResult<DaemonHandle> {
180 let daemon_name = daemon.name().to_string();
181 let daemon_id = daemon.id();
182 let unit_name = Self::unit_name(&daemon_name);
183
184 let mut cmd = self.systemd_run_cmd();
188 cmd.arg("--unit")
189 .arg(&unit_name)
190 .arg("--description")
191 .arg(format!("Duende daemon: {}", daemon_name))
192 .arg("--remain-after-exit")
193 .arg("--collect");
194
195 cmd.arg("--property=Restart=on-failure")
197 .arg("--property=RestartSec=5");
198
199 cmd.arg("--").arg("/bin/true");
203
204 let output = cmd.output().await.map_err(|e| {
206 PlatformError::spawn_failed(format!("Failed to execute systemd-run: {}", e))
207 })?;
208
209 if !output.status.success() {
210 let stderr = String::from_utf8_lossy(&output.stderr);
211 return Err(PlatformError::spawn_failed(format!(
212 "systemd-run failed: {}",
213 stderr
214 )));
215 }
216
217 Ok(DaemonHandle::systemd(daemon_id, unit_name))
219 }
220
221 async fn signal(&self, handle: &DaemonHandle, sig: Signal) -> PlatformResult<()> {
222 let unit_name = handle.systemd_unit().ok_or_else(|| {
223 PlatformError::spawn_failed("Invalid handle type for systemd adapter")
224 })?;
225
226 let mut cmd = self.systemctl_cmd();
228 cmd.arg("kill")
229 .arg("--signal")
230 .arg(Self::signal_name(sig))
231 .arg(unit_name);
232
233 let output = cmd.output().await.map_err(|e| {
234 PlatformError::spawn_failed(format!("Failed to execute systemctl kill: {}", e))
235 })?;
236
237 if !output.status.success() {
238 let stderr = String::from_utf8_lossy(&output.stderr);
239 return Err(PlatformError::spawn_failed(format!(
240 "systemctl kill failed: {}",
241 stderr
242 )));
243 }
244
245 Ok(())
246 }
247
248 async fn status(&self, handle: &DaemonHandle) -> PlatformResult<DaemonStatus> {
249 let unit_name = handle.systemd_unit().ok_or_else(|| {
250 PlatformError::spawn_failed("Invalid handle type for systemd adapter")
251 })?;
252
253 let mut cmd = self.systemctl_cmd();
254 cmd.arg("status").arg(unit_name);
255
256 let output = cmd.output().await.map_err(|e| {
257 PlatformError::spawn_failed(format!("Failed to execute systemctl status: {}", e))
258 })?;
259
260 let stdout = String::from_utf8_lossy(&output.stdout);
261 let exit_code = output.status.code().unwrap_or(1);
262
263 Ok(Self::parse_status(&stdout, exit_code))
264 }
265
266 async fn attach_tracer(&self, handle: &DaemonHandle) -> PlatformResult<TracerHandle> {
267 let unit_name = handle.systemd_unit().ok_or_else(|| {
268 PlatformError::spawn_failed("Invalid handle type for systemd adapter")
269 })?;
270
271 let pid = self.get_main_pid(unit_name).await.ok_or_else(|| {
273 PlatformError::spawn_failed("Cannot attach tracer: failed to get PID")
274 })?;
275
276 if pid == 0 {
277 return Err(PlatformError::spawn_failed(
278 "Cannot attach tracer: PID unknown",
279 ));
280 }
281
282 Ok(TracerHandle::ptrace(handle.id()))
284 }
285}
286
287impl SystemdAdapter {
288 async fn get_main_pid(&self, unit_name: &str) -> Option<u64> {
290 let mut cmd = self.systemctl_cmd();
291 cmd.arg("show")
292 .arg("--property=MainPID")
293 .arg("--value")
294 .arg(unit_name);
295
296 let output = cmd.output().await.ok()?;
297 if !output.status.success() {
298 return None;
299 }
300
301 let stdout = String::from_utf8_lossy(&output.stdout);
302 stdout.trim().parse().ok()
303 }
304
305 pub async fn stop(&self, unit_name: &str) -> PlatformResult<()> {
307 let mut cmd = self.systemctl_cmd();
308 cmd.arg("stop").arg(unit_name);
309
310 let output = cmd.output().await.map_err(|e| {
311 PlatformError::spawn_failed(format!("Failed to execute systemctl stop: {}", e))
312 })?;
313
314 if !output.status.success() {
315 let stderr = String::from_utf8_lossy(&output.stderr);
316 return Err(PlatformError::spawn_failed(format!(
317 "systemctl stop failed: {}",
318 stderr
319 )));
320 }
321
322 Ok(())
323 }
324
325 pub async fn reset_failed(&self, unit_name: &str) -> PlatformResult<()> {
327 let mut cmd = self.systemctl_cmd();
328 cmd.arg("reset-failed").arg(unit_name);
329
330 let _ = cmd.output().await; Ok(())
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_systemd_adapter_system() {
341 let adapter = SystemdAdapter::system();
342 assert!(!adapter.is_user_mode());
343 assert_eq!(adapter.unit_dir(), &PathBuf::from("/etc/systemd/system"));
344 assert_eq!(adapter.platform(), Platform::Linux);
345 }
346
347 #[test]
348 fn test_systemd_adapter_user() {
349 let adapter = SystemdAdapter::user();
350 assert!(adapter.is_user_mode());
351 assert_eq!(adapter.platform(), Platform::Linux);
352 }
353
354 #[test]
355 fn test_systemd_adapter_default() {
356 let adapter = SystemdAdapter::default();
357 assert!(adapter.is_user_mode()); }
359
360 #[test]
361 fn test_unit_name_generation() {
362 assert_eq!(
363 SystemdAdapter::unit_name("my-daemon"),
364 "duende-my-daemon.service"
365 );
366 assert_eq!(
367 SystemdAdapter::unit_name("my daemon"),
368 "duende-my-daemon.service"
369 );
370 }
371
372 #[test]
373 fn test_signal_name() {
374 assert_eq!(SystemdAdapter::signal_name(Signal::Term), "SIGTERM");
375 assert_eq!(SystemdAdapter::signal_name(Signal::Kill), "SIGKILL");
376 assert_eq!(SystemdAdapter::signal_name(Signal::Hup), "SIGHUP");
377 }
378
379 #[test]
380 fn test_parse_status_running() {
381 let output = "● test.service - Test\n Active: active (running) since...";
382 assert_eq!(
383 SystemdAdapter::parse_status(output, 0),
384 DaemonStatus::Running
385 );
386 }
387
388 #[test]
389 fn test_parse_status_stopped() {
390 let output = "● test.service - Test\n Active: inactive (dead)";
391 assert_eq!(
392 SystemdAdapter::parse_status(output, 3),
393 DaemonStatus::Stopped
394 );
395 }
396
397 #[test]
398 fn test_parse_status_failed() {
399 let output = "● test.service - Test\n Active: failed";
400 assert!(matches!(
401 SystemdAdapter::parse_status(output, 1),
402 DaemonStatus::Failed(_)
403 ));
404 }
405
406 #[test]
407 fn test_parse_status_starting() {
408 let output = "● test.service - Test\n Active: activating (start)";
409 assert_eq!(
410 SystemdAdapter::parse_status(output, 0),
411 DaemonStatus::Starting
412 );
413 }
414
415 #[test]
416 fn test_with_unit_dir() {
417 let adapter = SystemdAdapter::with_unit_dir(PathBuf::from("/custom/path"), false);
418 assert_eq!(adapter.unit_dir(), &PathBuf::from("/custom/path"));
419 assert!(!adapter.is_user_mode());
420 }
421
422 #[test]
425 fn test_unit_name_special_characters() {
426 assert_eq!(
428 SystemdAdapter::unit_name("test"),
429 "duende-test.service"
430 );
431 assert_eq!(
432 SystemdAdapter::unit_name("test-daemon"),
433 "duende-test-daemon.service"
434 );
435 assert_eq!(
436 SystemdAdapter::unit_name("test daemon name"),
437 "duende-test-daemon-name.service"
438 );
439 assert_eq!(
440 SystemdAdapter::unit_name(""),
441 "duende-.service"
442 );
443 }
444
445 #[test]
446 fn test_signal_name_all_signals() {
447 assert_eq!(SystemdAdapter::signal_name(Signal::Int), "SIGINT");
448 assert_eq!(SystemdAdapter::signal_name(Signal::Quit), "SIGQUIT");
449 assert_eq!(SystemdAdapter::signal_name(Signal::Usr1), "SIGUSR1");
450 assert_eq!(SystemdAdapter::signal_name(Signal::Usr2), "SIGUSR2");
451 assert_eq!(SystemdAdapter::signal_name(Signal::Stop), "SIGSTOP");
452 assert_eq!(SystemdAdapter::signal_name(Signal::Cont), "SIGCONT");
453 }
454
455 #[test]
456 fn test_parse_status_deactivating() {
457 let output = "● test.service - Test\n Active: deactivating (stop-sigterm)";
458 assert_eq!(
459 SystemdAdapter::parse_status(output, 0),
460 DaemonStatus::Stopping
461 );
462 }
463
464 #[test]
465 fn test_parse_status_empty() {
466 assert_eq!(
467 SystemdAdapter::parse_status("", 4),
468 DaemonStatus::Stopped
469 );
470 }
471
472 #[test]
473 fn test_parse_status_unknown_output() {
474 let output = "Some random output without status";
475 assert_eq!(
476 SystemdAdapter::parse_status(output, 0),
477 DaemonStatus::Stopped
478 );
479 }
480
481 #[test]
482 fn test_parse_status_exit_codes() {
483 let output = "Some output";
485 assert_eq!(
486 SystemdAdapter::parse_status(output, 0),
487 DaemonStatus::Stopped
488 );
489
490 assert_eq!(
492 SystemdAdapter::parse_status(output, 1),
493 DaemonStatus::Stopped
494 );
495
496 assert_eq!(
498 SystemdAdapter::parse_status(output, 3),
499 DaemonStatus::Stopped
500 );
501
502 assert_eq!(
504 SystemdAdapter::parse_status(output, 4),
505 DaemonStatus::Stopped
506 );
507 }
508
509 #[test]
510 fn test_parse_status_inactive_variations() {
511 let output1 = "Active: inactive (dead)";
512 assert_eq!(
513 SystemdAdapter::parse_status(output1, 3),
514 DaemonStatus::Stopped
515 );
516
517 let output2 = " Active: inactive ";
518 assert_eq!(
519 SystemdAdapter::parse_status(output2, 3),
520 DaemonStatus::Stopped
521 );
522 }
523
524 #[test]
525 fn test_parse_status_activating_variations() {
526 let output1 = "Active: activating (auto-restart)";
527 assert_eq!(
528 SystemdAdapter::parse_status(output1, 0),
529 DaemonStatus::Starting
530 );
531
532 let output2 = "Active: activating (start-pre)";
533 assert_eq!(
534 SystemdAdapter::parse_status(output2, 0),
535 DaemonStatus::Starting
536 );
537 }
538
539 #[test]
540 fn test_systemd_adapter_new_alias() {
541 let adapter = SystemdAdapter::new();
542 assert!(adapter.is_user_mode());
544 }
545
546 #[test]
547 fn test_systemd_adapter_clone_path() {
548 let adapter = SystemdAdapter::with_unit_dir(PathBuf::from("/test"), true);
549 let path = adapter.unit_dir().clone();
550 assert_eq!(path, PathBuf::from("/test"));
551 }
552
553 #[test]
554 fn test_parse_status_failed_variations() {
555 let output1 = "Active: failed (Result: exit-code)";
557 assert!(matches!(
558 SystemdAdapter::parse_status(output1, 1),
559 DaemonStatus::Failed(_)
560 ));
561
562 let output2 = "Active: failed (Result: timeout)";
563 assert!(matches!(
564 SystemdAdapter::parse_status(output2, 1),
565 DaemonStatus::Failed(_)
566 ));
567 }
568
569 #[test]
570 fn test_systemctl_cmd_user_mode() {
571 let adapter = SystemdAdapter::user();
572 let cmd = adapter.systemctl_cmd();
573 let _ = cmd;
575 }
576
577 #[test]
578 fn test_systemctl_cmd_system_mode() {
579 let adapter = SystemdAdapter::system();
580 let cmd = adapter.systemctl_cmd();
581 let _ = cmd;
582 }
583
584 #[test]
585 fn test_systemd_run_cmd_user_mode() {
586 let adapter = SystemdAdapter::user();
587 let cmd = adapter.systemd_run_cmd();
588 let _ = cmd;
589 }
590
591 #[test]
592 fn test_systemd_run_cmd_system_mode() {
593 let adapter = SystemdAdapter::system();
594 let cmd = adapter.systemd_run_cmd();
595 let _ = cmd;
596 }
597}