Skip to main content

singbox_driver/
driver.rs

1//! Owner-local sing-box process lifecycle: spawn, stop, child polling and pid
2//! readback behind the neutral `CoreProcess` port.
3
4use crate::process::command::{
5    NativeConfigCheck, SingBoxChildPoll, SingBoxChildProcess, SingBoxCommandPort,
6    TokioSingBoxCommandPort,
7};
8use async_trait::async_trait;
9use lurker_core_contract::core_process::CoreProcessStartContext;
10use lurker_core_contract::error::{ErrorCode, Failure};
11use lurker_core_contract::identity::{CoreIdentity, CoreInstanceId, EngineId};
12use lurker_core_contract::lifecycle::CoreLifecycle;
13use lurker_core_ports::core_process::CoreProcess;
14use lurker_core_ports::error::PortError;
15use std::mem;
16use std::net::SocketAddr;
17use std::path::PathBuf;
18use std::sync::Arc;
19use tokio::sync::Mutex;
20use tokio::sync::OwnedMutexGuard;
21
22/// Serializes process activation with profile-file transactions that commit
23/// while the Core is inactive.
24#[derive(Default)]
25pub struct SingBoxCoreOperationGate {
26    lock: Arc<Mutex<()>>,
27}
28
29impl SingBoxCoreOperationGate {
30    /// Creates the gate.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Acquires the gate and returns its owned guard.
36    pub async fn acquire(&self) -> OwnedMutexGuard<()> {
37        Arc::clone(&self.lock).lock_owned().await
38    }
39}
40
41enum ProcessState {
42    Stopped,
43    Running(Box<dyn SingBoxChildProcess>),
44    Exited,
45}
46
47/// The owner-local sing-box process adapter.
48pub struct SingBoxProcess {
49    binary_path: PathBuf,
50    config_path: PathBuf,
51    identity: CoreIdentity,
52    controller_endpoint: String,
53    command: Arc<dyn SingBoxCommandPort>,
54    operation_gate: Arc<SingBoxCoreOperationGate>,
55    state: Mutex<ProcessState>,
56}
57
58impl SingBoxProcess {
59    /// Creates the process adapter that launches the binary through tokio.
60    ///
61    /// Fails with a `Failed` port error when the controller address is not a
62    /// fixed loopback address.
63    pub fn with_tokio(
64        binary_path: PathBuf,
65        config_path: PathBuf,
66        instance_id: CoreInstanceId,
67        controller_address: SocketAddr,
68    ) -> Result<Self, PortError> {
69        Self::new(
70            binary_path,
71            config_path,
72            instance_id,
73            controller_address,
74            Arc::new(TokioSingBoxCommandPort),
75        )
76    }
77
78    /// Creates the process adapter with an injected command boundary.
79    pub fn new(
80        binary_path: PathBuf,
81        config_path: PathBuf,
82        instance_id: CoreInstanceId,
83        controller_address: SocketAddr,
84        command: Arc<dyn SingBoxCommandPort>,
85    ) -> Result<Self, PortError> {
86        Self::new_with_operation_gate(
87            binary_path,
88            config_path,
89            instance_id,
90            controller_address,
91            command,
92            Arc::new(SingBoxCoreOperationGate::new()),
93        )
94    }
95
96    /// Creates the process adapter that shares one operation gate.
97    pub fn new_with_operation_gate(
98        binary_path: PathBuf,
99        config_path: PathBuf,
100        instance_id: CoreInstanceId,
101        controller_address: SocketAddr,
102        command: Arc<dyn SingBoxCommandPort>,
103        operation_gate: Arc<SingBoxCoreOperationGate>,
104    ) -> Result<Self, PortError> {
105        if !controller_address.ip().is_loopback() || controller_address.port() == 0 {
106            return Err(PortError::Failed(
107                "sing-box controller must bind to a fixed loopback address".to_owned(),
108            ));
109        }
110
111        Ok(Self {
112            binary_path,
113            config_path,
114            identity: CoreIdentity::new(EngineId::sing_box(), instance_id, None),
115            controller_endpoint: controller_address.to_string(),
116            command,
117            operation_gate,
118            state: Mutex::new(ProcessState::Stopped),
119        })
120    }
121
122    /// Returns the path of the sing-box binary the adapter launches.
123    pub fn binary_path(&self) -> &PathBuf {
124        &self.binary_path
125    }
126
127    /// Returns the path of the configuration file the core reads.
128    pub fn config_path(&self) -> &PathBuf {
129        &self.config_path
130    }
131
132    /// Returns the gate that serializes process activation with file commits.
133    pub fn operation_gate(&self) -> Arc<SingBoxCoreOperationGate> {
134        Arc::clone(&self.operation_gate)
135    }
136
137    async fn prepare_start(&self, state: &mut ProcessState) -> Result<(), PortError> {
138        match mem::replace(state, ProcessState::Stopped) {
139            ProcessState::Stopped | ProcessState::Exited => Ok(()),
140            ProcessState::Running(mut child) => match child.poll().await {
141                Ok(SingBoxChildPoll::Running) => {
142                    *state = ProcessState::Running(child);
143                    Err(PortError::Failed(
144                        "sing-box process is already running".to_owned(),
145                    ))
146                }
147                Ok(SingBoxChildPoll::Exited) => {
148                    *state = ProcessState::Exited;
149                    Ok(())
150                }
151                Err(error) => {
152                    *state = ProcessState::Running(child);
153                    Err(error)
154                }
155            },
156        }
157    }
158}
159
160#[async_trait]
161impl CoreProcess for SingBoxProcess {
162    fn core_identity(&self) -> Option<CoreIdentity> {
163        Some(self.identity.clone())
164    }
165
166    async fn start(&self, context: CoreProcessStartContext) -> Result<(), PortError> {
167        if !context.identity().same_instance(&self.identity) {
168            return Err(PortError::Failed(
169                "Core start context targets a different sing-box process".to_owned(),
170            ));
171        }
172        let _gate = self.operation_gate.acquire().await;
173        let mut state = self.state.lock().await;
174        self.prepare_start(&mut state).await?;
175
176        match self
177            .command
178            .check_config(&self.binary_path, &self.config_path)
179            .await?
180        {
181            NativeConfigCheck::Accepted => {}
182            NativeConfigCheck::Rejected => {
183                return Err(PortError::Rejected(Failure::new(
184                    ErrorCode::Configuration,
185                    "sing-box native config check rejected the active document",
186                    false,
187                )));
188            }
189        }
190
191        let child = self
192            .command
193            .spawn_core(&self.binary_path, &self.config_path)
194            .await?;
195        *state = ProcessState::Running(child);
196        Ok(())
197    }
198
199    async fn stop(&self) -> Result<(), PortError> {
200        let _gate = self.operation_gate.acquire().await;
201        let mut state = self.state.lock().await;
202        match mem::replace(&mut *state, ProcessState::Stopped) {
203            ProcessState::Stopped | ProcessState::Exited => Ok(()),
204            ProcessState::Running(mut child) => match child.terminate().await {
205                Ok(()) => Ok(()),
206                Err(error) => {
207                    *state = ProcessState::Running(child);
208                    Err(error)
209                }
210            },
211        }
212    }
213
214    async fn status(&self) -> Result<CoreLifecycle, PortError> {
215        let _gate = self.operation_gate.acquire().await;
216        let mut state = self.state.lock().await;
217        match mem::replace(&mut *state, ProcessState::Stopped) {
218            ProcessState::Stopped => Ok(CoreLifecycle::Stopped),
219            ProcessState::Exited => {
220                *state = ProcessState::Exited;
221                Ok(CoreLifecycle::Failed)
222            }
223            ProcessState::Running(mut child) => match child.poll().await {
224                Ok(SingBoxChildPoll::Running) => {
225                    *state = ProcessState::Running(child);
226                    Ok(CoreLifecycle::Running)
227                }
228                Ok(SingBoxChildPoll::Exited) => {
229                    *state = ProcessState::Exited;
230                    Ok(CoreLifecycle::Failed)
231                }
232                Err(error) => {
233                    *state = ProcessState::Running(child);
234                    Err(error)
235                }
236            },
237        }
238    }
239
240    fn controller_endpoint(&self) -> Option<String> {
241        Some(self.controller_endpoint.clone())
242    }
243
244    async fn pid(&self) -> Option<u32> {
245        match self.status().await {
246            Ok(CoreLifecycle::Running) => {
247                let state = self.state.lock().await;
248                match &*state {
249                    ProcessState::Running(child) => Some(child.pid()),
250                    ProcessState::Stopped | ProcessState::Exited => None,
251                }
252            }
253            Ok(
254                CoreLifecycle::Stopped
255                | CoreLifecycle::Starting
256                | CoreLifecycle::Ready
257                | CoreLifecycle::Stopping
258                | CoreLifecycle::Failed,
259            )
260            | Err(_) => None,
261        }
262    }
263}