duende_platform/
native.rs1use async_trait::async_trait;
6use std::process::Stdio;
7use tokio::process::Command;
8
9use duende_core::{Daemon, DaemonStatus, Signal};
10
11use crate::adapter::{DaemonHandle, PlatformAdapter, TracerHandle};
12use crate::detect::Platform;
13use crate::error::{PlatformError, Result};
14
15#[cfg(unix)]
16use nix::sys::signal::{Signal as NixSignal, kill as nix_kill};
17#[cfg(unix)]
18use nix::unistd::Pid;
19
20pub struct NativeAdapter {
25 }
27
28impl NativeAdapter {
29 #[must_use]
31 pub const fn new() -> Self {
32 Self {}
33 }
34}
35
36impl Default for NativeAdapter {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42#[async_trait]
43impl PlatformAdapter for NativeAdapter {
44 fn platform(&self) -> Platform {
45 Platform::Native
46 }
47
48 async fn spawn(&self, daemon: Box<dyn Daemon>) -> Result<DaemonHandle> {
49 let config = duende_core::DaemonConfig::new(daemon.name(), "/bin/sh");
50
51 let mut cmd = Command::new(&config.binary_path);
53 cmd.args(&config.args)
54 .envs(&config.env)
55 .stdin(Stdio::null())
56 .stdout(Stdio::null())
57 .stderr(Stdio::null());
58
59 if let Some(ref cwd) = config.working_dir {
60 cmd.current_dir(cwd);
61 }
62
63 let child = cmd
65 .spawn()
66 .map_err(|e| PlatformError::spawn(format!("failed to spawn process: {e}")))?;
67
68 let pid = child
69 .id()
70 .ok_or_else(|| PlatformError::spawn("process has no PID"))?;
71
72 tracing::info!(pid = pid, name = daemon.name(), "spawned native daemon");
73
74 Ok(DaemonHandle::native(pid))
75 }
76
77 async fn signal(&self, handle: &DaemonHandle, sig: Signal) -> Result<()> {
78 let pid = handle
79 .pid
80 .ok_or_else(|| PlatformError::signal("no PID available"))?;
81
82 #[cfg(unix)]
83 {
84 let nix_signal = match sig {
85 Signal::Hup => NixSignal::SIGHUP,
86 Signal::Int => NixSignal::SIGINT,
87 Signal::Quit => NixSignal::SIGQUIT,
88 Signal::Term => NixSignal::SIGTERM,
89 Signal::Kill => NixSignal::SIGKILL,
90 Signal::Usr1 => NixSignal::SIGUSR1,
91 Signal::Usr2 => NixSignal::SIGUSR2,
92 Signal::Stop => NixSignal::SIGSTOP,
93 Signal::Cont => NixSignal::SIGCONT,
94 };
95
96 #[allow(clippy::cast_possible_wrap)] nix_kill(Pid::from_raw(pid as i32), nix_signal)
98 .map_err(|e| PlatformError::signal(format!("kill({pid}, {sig:?}) failed: {e}")))?;
99 }
100
101 #[cfg(not(unix))]
102 {
103 let _ = (pid, sig); return Err(PlatformError::not_supported(
105 "signals not supported on this platform",
106 ));
107 }
108
109 tracing::debug!(pid = pid, signal = ?sig, "sent signal to native daemon");
110 Ok(())
111 }
112
113 async fn status(&self, handle: &DaemonHandle) -> Result<DaemonStatus> {
114 let pid = handle
115 .pid
116 .ok_or_else(|| PlatformError::Status("no PID available".to_string()))?;
117
118 #[cfg(unix)]
119 {
120 #[allow(clippy::cast_possible_wrap)] match nix_kill(Pid::from_raw(pid as i32), None) {
123 Ok(()) => Ok(DaemonStatus::Running),
124 Err(nix::errno::Errno::ESRCH) => Ok(DaemonStatus::Stopped),
125 Err(e) => Err(PlatformError::Status(format!(
126 "failed to check process: {e}"
127 ))),
128 }
129 }
130
131 #[cfg(not(unix))]
132 {
133 let _ = pid; Err(PlatformError::not_supported(
135 "process status not available on this platform",
136 ))
137 }
138 }
139
140 async fn attach_tracer(&self, handle: &DaemonHandle) -> Result<TracerHandle> {
141 let pid = handle
142 .pid
143 .ok_or_else(|| PlatformError::Tracer("no PID available".to_string()))?;
144
145 #[cfg(unix)]
147 {
148 #[allow(clippy::cast_possible_wrap)]
149 match nix_kill(Pid::from_raw(pid as i32), None) {
150 Ok(()) => {
151 tracing::info!(pid = pid, "tracer ready for attachment");
153 }
154 Err(nix::errno::Errno::ESRCH) => {
155 return Err(PlatformError::Tracer(format!(
156 "process {pid} does not exist"
157 )));
158 }
159 Err(e) => {
160 return Err(PlatformError::Tracer(format!(
161 "cannot verify process {pid}: {e}"
162 )));
163 }
164 }
165 }
166
167 #[cfg(not(unix))]
168 tracing::warn!(pid = pid, "tracer attachment not supported on this platform");
169
170 Ok(TracerHandle {
174 platform: Platform::Native,
175 id: format!("renacer:{pid}"),
176 })
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_native_adapter_creation() {
186 let adapter = NativeAdapter::new();
187 assert_eq!(adapter.platform(), Platform::Native);
188 }
189
190 #[test]
191 fn test_native_adapter_default() {
192 let adapter = NativeAdapter::default();
193 assert_eq!(adapter.platform(), Platform::Native);
194 }
195
196 #[test]
197 fn test_daemon_handle_native() {
198 let handle = DaemonHandle::native(1234);
199 assert_eq!(handle.platform, Platform::Native);
200 assert_eq!(handle.pid, Some(1234));
201 }
202
203 #[tokio::test]
204 async fn test_signal_no_pid() {
205 let adapter = NativeAdapter::new();
206 let handle = DaemonHandle {
207 platform: Platform::Native,
208 pid: None,
209 id: "test".to_string(),
210 };
211
212 let result = adapter.signal(&handle, Signal::Term).await;
213 assert!(result.is_err());
214 }
215
216 #[tokio::test]
217 async fn test_status_no_pid() {
218 let adapter = NativeAdapter::new();
219 let handle = DaemonHandle {
220 platform: Platform::Native,
221 pid: None,
222 id: "test".to_string(),
223 };
224
225 let result = adapter.status(&handle).await;
226 assert!(result.is_err());
227 }
228
229 #[tokio::test]
230 async fn test_attach_tracer_no_pid() {
231 let adapter = NativeAdapter::new();
232 let handle = DaemonHandle {
233 platform: Platform::Native,
234 pid: None,
235 id: "test".to_string(),
236 };
237
238 let result = adapter.attach_tracer(&handle).await;
239 assert!(result.is_err());
240 }
241
242 #[tokio::test]
243 async fn test_attach_tracer_with_valid_pid() {
244 let adapter = NativeAdapter::new();
245 let pid = std::process::id();
247 let handle = DaemonHandle::native(pid);
248
249 let result = adapter.attach_tracer(&handle).await;
250 assert!(result.is_ok(), "attach_tracer should succeed for existing process");
251 let tracer = result.expect("tracer should succeed");
252 assert_eq!(tracer.platform, Platform::Native);
253 assert!(tracer.id.contains(&pid.to_string()));
254 }
255
256 #[cfg(unix)]
257 #[tokio::test]
258 async fn test_attach_tracer_nonexistent_pid() {
259 let adapter = NativeAdapter::new();
260 let handle = DaemonHandle::native(4000000);
262
263 let result = adapter.attach_tracer(&handle).await;
264 assert!(result.is_err(), "attach_tracer should fail for non-existent process");
265 }
266
267 #[cfg(unix)]
268 #[tokio::test]
269 async fn test_status_nonexistent_process() {
270 let adapter = NativeAdapter::new();
271 let handle = DaemonHandle::native(4000000);
273
274 let result = adapter.status(&handle).await;
275 assert!(result.is_ok());
276 assert_eq!(result.expect("status"), DaemonStatus::Stopped);
277 }
278
279 #[cfg(unix)]
280 #[tokio::test]
281 async fn test_status_current_process() {
282 let adapter = NativeAdapter::new();
283 let pid = std::process::id();
285 let handle = DaemonHandle::native(pid);
286
287 let result = adapter.status(&handle).await;
288 assert!(result.is_ok());
289 assert_eq!(result.expect("status"), DaemonStatus::Running);
290 }
291}