Skip to main content

singbox_driver/process/
command.rs

1//! Injectable command boundary for native sing-box checks and child startup.
2
3use async_trait::async_trait;
4use lurker_core_ports::error::PortError;
5use std::io::{self, ErrorKind};
6use std::path::Path;
7use std::process::Stdio;
8use tokio::process::{Child, Command};
9
10/// The verdict of one native configuration check.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum NativeConfigCheck {
13    /// The selected binary accepted the candidate configuration.
14    Accepted,
15    /// The selected binary rejected the candidate configuration.
16    Rejected,
17}
18
19/// The state of one polled child process.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum SingBoxChildPoll {
22    /// The child process is still running.
23    Running,
24    /// The child process has exited.
25    Exited,
26}
27
28/// The owned child process of a running sing-box core.
29#[async_trait]
30pub trait SingBoxChildProcess: Send {
31    /// Returns the process id of the child.
32    fn pid(&self) -> u32;
33    /// Polls whether the child is still running.
34    async fn poll(&mut self) -> Result<SingBoxChildPoll, PortError>;
35    /// Terminates the child and waits for its exit.
36    async fn terminate(&mut self) -> Result<(), PortError>;
37}
38
39/// The injectable command boundary of the sing-box driver.
40#[async_trait]
41pub trait SingBoxCommandPort: Send + Sync {
42    /// Runs the native `check` transaction of the selected binary.
43    async fn check_config(
44        &self,
45        binary_path: &Path,
46        config_path: &Path,
47    ) -> Result<NativeConfigCheck, PortError>;
48
49    /// Starts the sing-box core with the selected configuration.
50    async fn spawn_core(
51        &self,
52        binary_path: &Path,
53        config_path: &Path,
54    ) -> Result<Box<dyn SingBoxChildProcess>, PortError>;
55}
56
57/// The production command boundary of the sing-box driver.
58#[derive(Clone, Copy, Debug, Default)]
59pub struct TokioSingBoxCommandPort;
60
61#[async_trait]
62impl SingBoxCommandPort for TokioSingBoxCommandPort {
63    async fn check_config(
64        &self,
65        binary_path: &Path,
66        config_path: &Path,
67    ) -> Result<NativeConfigCheck, PortError> {
68        let output = Command::new(binary_path)
69            .arg("check")
70            .arg("-c")
71            .arg(config_path)
72            .stdin(Stdio::null())
73            .stdout(Stdio::null())
74            .stderr(Stdio::null())
75            .kill_on_drop(true)
76            .output()
77            .await
78            .map_err(|error| map_command_error(binary_path, error))?;
79
80        match output.status.success() {
81            true => Ok(NativeConfigCheck::Accepted),
82            false => Ok(NativeConfigCheck::Rejected),
83        }
84    }
85
86    async fn spawn_core(
87        &self,
88        binary_path: &Path,
89        config_path: &Path,
90    ) -> Result<Box<dyn SingBoxChildProcess>, PortError> {
91        let mut child = Command::new(binary_path)
92            .arg("run")
93            .arg("-c")
94            .arg(config_path)
95            .stdin(Stdio::null())
96            .stdout(Stdio::null())
97            .stderr(Stdio::null())
98            .kill_on_drop(true)
99            .spawn()
100            .map_err(|error| map_command_error(binary_path, error))?;
101        let pid = match child.id() {
102            Some(pid) => pid,
103            None => {
104                let _ = child.start_kill();
105                let _ = child.wait().await;
106                return Err(PortError::Failed(
107                    "sing-box child process did not expose a process ID".to_owned(),
108                ));
109            }
110        };
111
112        Ok(Box::new(TokioSingBoxChildProcess { child, pid }))
113    }
114}
115
116struct TokioSingBoxChildProcess {
117    child: Child,
118    pid: u32,
119}
120
121#[async_trait]
122impl SingBoxChildProcess for TokioSingBoxChildProcess {
123    fn pid(&self) -> u32 {
124        self.pid
125    }
126
127    async fn poll(&mut self) -> Result<SingBoxChildPoll, PortError> {
128        match self.child.try_wait() {
129            Ok(Some(_)) => Ok(SingBoxChildPoll::Exited),
130            Ok(None) => Ok(SingBoxChildPoll::Running),
131            Err(error) => Err(PortError::Io(format!(
132                "sing-box process poll failed: {error}"
133            ))),
134        }
135    }
136
137    async fn terminate(&mut self) -> Result<(), PortError> {
138        match self.child.try_wait() {
139            Ok(Some(_)) => Ok(()),
140            Ok(None) => {
141                self.child.start_kill().map_err(|error| {
142                    PortError::Io(format!("sing-box process termination failed: {error}"))
143                })?;
144                self.child.wait().await.map_err(|error| {
145                    PortError::Io(format!("sing-box process wait failed: {error}"))
146                })?;
147                Ok(())
148            }
149            Err(error) => Err(PortError::Io(format!(
150                "sing-box process status before termination failed: {error}"
151            ))),
152        }
153    }
154}
155
156fn map_command_error(binary_path: &Path, error: io::Error) -> PortError {
157    match error.kind() {
158        ErrorKind::NotFound => PortError::NotFound(format!(
159            "sing-box executable is unavailable at {}",
160            binary_path.display()
161        )),
162        ErrorKind::PermissionDenied => {
163            PortError::PermissionDenied("sing-box executable cannot be started".to_owned())
164        }
165        _ => PortError::Io(format!("sing-box command failed to start: {error}")),
166    }
167}