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!(
169 pid = pid,
170 "tracer attachment not supported on this platform"
171 );
172
173 Ok(TracerHandle {
177 platform: Platform::Native,
178 id: format!("renacer:{pid}"),
179 })
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_native_adapter_creation() {
189 let adapter = NativeAdapter::new();
190 assert_eq!(adapter.platform(), Platform::Native);
191 }
192
193 #[test]
194 fn test_native_adapter_default() {
195 let adapter = NativeAdapter::default();
196 assert_eq!(adapter.platform(), Platform::Native);
197 }
198
199 #[test]
200 fn test_daemon_handle_native() {
201 let handle = DaemonHandle::native(1234);
202 assert_eq!(handle.platform, Platform::Native);
203 assert_eq!(handle.pid, Some(1234));
204 }
205
206 #[tokio::test]
207 async fn test_signal_no_pid() {
208 let adapter = NativeAdapter::new();
209 let handle = DaemonHandle {
210 platform: Platform::Native,
211 pid: None,
212 id: "test".to_string(),
213 };
214
215 let result = adapter.signal(&handle, Signal::Term).await;
216 assert!(result.is_err());
217 }
218
219 #[tokio::test]
220 async fn test_status_no_pid() {
221 let adapter = NativeAdapter::new();
222 let handle = DaemonHandle {
223 platform: Platform::Native,
224 pid: None,
225 id: "test".to_string(),
226 };
227
228 let result = adapter.status(&handle).await;
229 assert!(result.is_err());
230 }
231
232 #[tokio::test]
233 async fn test_attach_tracer_no_pid() {
234 let adapter = NativeAdapter::new();
235 let handle = DaemonHandle {
236 platform: Platform::Native,
237 pid: None,
238 id: "test".to_string(),
239 };
240
241 let result = adapter.attach_tracer(&handle).await;
242 assert!(result.is_err());
243 }
244
245 #[tokio::test]
246 async fn test_attach_tracer_with_valid_pid() {
247 let adapter = NativeAdapter::new();
248 let pid = std::process::id();
250 let handle = DaemonHandle::native(pid);
251
252 let result = adapter.attach_tracer(&handle).await;
253 assert!(
254 result.is_ok(),
255 "attach_tracer should succeed for existing process"
256 );
257 let tracer = result.expect("tracer should succeed");
258 assert_eq!(tracer.platform, Platform::Native);
259 assert!(tracer.id.contains(&pid.to_string()));
260 }
261
262 #[cfg(unix)]
263 #[tokio::test]
264 async fn test_attach_tracer_nonexistent_pid() {
265 let adapter = NativeAdapter::new();
266 let handle = DaemonHandle::native(4000000);
268
269 let result = adapter.attach_tracer(&handle).await;
270 assert!(
271 result.is_err(),
272 "attach_tracer should fail for non-existent process"
273 );
274 }
275
276 #[cfg(unix)]
277 #[tokio::test]
278 async fn test_status_nonexistent_process() {
279 let adapter = NativeAdapter::new();
280 let handle = DaemonHandle::native(4000000);
282
283 let result = adapter.status(&handle).await;
284 assert!(result.is_ok());
285 assert_eq!(result.expect("status"), DaemonStatus::Stopped);
286 }
287
288 #[cfg(unix)]
289 #[tokio::test]
290 async fn test_status_current_process() {
291 let adapter = NativeAdapter::new();
292 let pid = std::process::id();
294 let handle = DaemonHandle::native(pid);
295
296 let result = adapter.status(&handle).await;
297 assert!(result.is_ok());
298 assert_eq!(result.expect("status"), DaemonStatus::Running);
299 }
300}