singbox_driver/process/
command.rs1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum NativeConfigCheck {
13 Accepted,
15 Rejected,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum SingBoxChildPoll {
22 Running,
24 Exited,
26}
27
28#[async_trait]
30pub trait SingBoxChildProcess: Send {
31 fn pid(&self) -> u32;
33 async fn poll(&mut self) -> Result<SingBoxChildPoll, PortError>;
35 async fn terminate(&mut self) -> Result<(), PortError>;
37}
38
39#[async_trait]
41pub trait SingBoxCommandPort: Send + Sync {
42 async fn check_config(
44 &self,
45 binary_path: &Path,
46 config_path: &Path,
47 ) -> Result<NativeConfigCheck, PortError>;
48
49 async fn spawn_core(
51 &self,
52 binary_path: &Path,
53 config_path: &Path,
54 ) -> Result<Box<dyn SingBoxChildProcess>, PortError>;
55}
56
57#[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}