1use std::collections::HashMap;
8use std::io::{self, Read, Write};
9use std::process::{Command, Stdio};
10use std::sync::{Arc, Mutex};
11use std::thread;
12use std::time::Duration;
13
14use rns_core::transport::types::{InterfaceId, InterfaceInfo};
15
16use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
17use crate::event::{Event, EventSender};
18use crate::hdlc;
19use crate::interface::{lock_or_recover, Writer};
20
21#[derive(Debug, Clone)]
23pub struct PipeConfig {
24 pub name: String,
25 pub command: String,
26 pub respawn_delay: Duration,
27 pub interface_id: InterfaceId,
28 pub runtime: Arc<Mutex<PipeRuntime>>,
29}
30
31#[derive(Debug, Clone)]
32pub struct PipeRuntime {
33 pub respawn_delay: Duration,
34}
35
36impl PipeRuntime {
37 pub fn from_config(config: &PipeConfig) -> Self {
38 Self {
39 respawn_delay: config.respawn_delay,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub struct PipeRuntimeConfigHandle {
46 pub interface_name: String,
47 pub runtime: Arc<Mutex<PipeRuntime>>,
48 pub startup: PipeRuntime,
49}
50
51impl Default for PipeConfig {
52 fn default() -> Self {
53 let mut config = PipeConfig {
54 name: String::new(),
55 command: String::new(),
56 respawn_delay: Duration::from_secs(5),
57 interface_id: InterfaceId(0),
58 runtime: Arc::new(Mutex::new(PipeRuntime {
59 respawn_delay: Duration::from_secs(5),
60 })),
61 };
62 let startup = PipeRuntime::from_config(&config);
63 config.runtime = Arc::new(Mutex::new(startup));
64 config
65 }
66}
67
68struct PipeWriter {
70 stdin: std::process::ChildStdin,
71}
72
73impl Writer for PipeWriter {
74 fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
75 self.stdin.write_all(&hdlc::frame(data))
76 }
77}
78
79pub fn start(config: PipeConfig, tx: EventSender) -> io::Result<Box<dyn Writer>> {
81 let id = config.interface_id;
82 {
83 let startup = PipeRuntime::from_config(&config);
84 *lock_or_recover(&config.runtime, "pipe runtime") = startup;
85 }
86
87 let mut child = spawn_child(&config.command)?;
88
89 let stdout = child
90 .stdout
91 .take()
92 .ok_or_else(|| io::Error::other("no stdout from child"))?;
93 let stdin = child
94 .stdin
95 .take()
96 .ok_or_else(|| io::Error::other("no stdin from child"))?;
97
98 log::info!(
99 "[{}] pipe interface started: {}",
100 config.name,
101 config.command
102 );
103
104 let _ = tx.send(Event::InterfaceUp(id, None, None));
106
107 thread::Builder::new()
109 .name(format!("pipe-reader-{}", id.0))
110 .spawn(move || {
111 reader_loop(stdout, child, id, config, tx);
112 })?;
113
114 Ok(Box::new(PipeWriter { stdin }))
115}
116
117fn spawn_child(command: &str) -> io::Result<std::process::Child> {
118 Command::new("sh")
119 .args(["-c", command])
120 .stdin(Stdio::piped())
121 .stdout(Stdio::piped())
122 .stderr(Stdio::null())
123 .spawn()
124}
125
126fn reader_loop(
129 mut stdout: std::process::ChildStdout,
130 mut child: std::process::Child,
131 id: InterfaceId,
132 config: PipeConfig,
133 tx: EventSender,
134) {
135 let mut decoder = hdlc::Decoder::new();
136 let mut buf = [0u8; 4096];
137
138 loop {
139 match stdout.read(&mut buf) {
140 Ok(0) => {
141 let _ = child.wait();
143 log::warn!("[{}] subprocess terminated", config.name);
144 let _ = tx.send(Event::InterfaceDown(id));
145 match respawn(&config, &tx) {
146 Some((new_stdout, new_child)) => {
147 stdout = new_stdout;
148 child = new_child;
149 decoder = hdlc::Decoder::new();
150 continue;
151 }
152 None => return,
153 }
154 }
155 Ok(n) => {
156 for frame in decoder.feed(&buf[..n]) {
157 if tx
158 .send(Event::Frame {
159 interface_id: id,
160 data: frame,
161 rssi: None,
162 snr: None,
163 })
164 .is_err()
165 {
166 let _ = child.kill();
168 return;
169 }
170 }
171 }
172 Err(e) => {
173 log::warn!("[{}] pipe read error: {}", config.name, e);
174 let _ = child.kill();
175 let _ = child.wait();
176 let _ = tx.send(Event::InterfaceDown(id));
177 match respawn(&config, &tx) {
178 Some((new_stdout, new_child)) => {
179 stdout = new_stdout;
180 child = new_child;
181 decoder = hdlc::Decoder::new();
182 continue;
183 }
184 None => return,
185 }
186 }
187 }
188 }
189}
190
191fn respawn(
193 config: &PipeConfig,
194 tx: &EventSender,
195) -> Option<(std::process::ChildStdout, std::process::Child)> {
196 loop {
197 let respawn_delay = lock_or_recover(&config.runtime, "pipe runtime").respawn_delay;
198 thread::sleep(respawn_delay);
199 log::info!(
200 "[{}] attempting to respawn subprocess: {}",
201 config.name,
202 config.command
203 );
204
205 match spawn_child(&config.command) {
206 Ok(mut child) => {
207 let stdout = match child.stdout.take() {
208 Some(s) => s,
209 None => {
210 let _ = child.kill();
211 let _ = child.wait();
212 continue;
213 }
214 };
215 let stdin = match child.stdin.take() {
216 Some(s) => s,
217 None => {
218 let _ = child.kill();
219 let _ = child.wait();
220 continue;
221 }
222 };
223
224 let new_writer: Box<dyn Writer> = Box::new(PipeWriter { stdin });
225 if tx
226 .send(Event::InterfaceUp(
227 config.interface_id,
228 Some(new_writer),
229 None,
230 ))
231 .is_err()
232 {
233 return None; }
235 log::info!("[{}] subprocess respawned", config.name);
236 return Some((stdout, child));
237 }
238 Err(e) => {
239 log::warn!("[{}] respawn failed: {}", config.name, e);
240 }
241 }
242 }
243}
244
245pub struct PipeFactory;
247
248impl InterfaceFactory for PipeFactory {
249 fn type_name(&self) -> &str {
250 "PipeInterface"
251 }
252
253 fn parse_config(
254 &self,
255 name: &str,
256 id: InterfaceId,
257 params: &HashMap<String, String>,
258 ) -> Result<Box<dyn InterfaceConfigData>, String> {
259 let command = params
260 .get("command")
261 .ok_or_else(|| "PipeInterface requires 'command'".to_string())?
262 .clone();
263
264 let respawn_delay = match params.get("respawn_delay") {
265 Some(v) => {
266 let ms: u64 = v
267 .parse()
268 .map_err(|_| format!("invalid respawn_delay: {}", v))?;
269 Duration::from_millis(ms)
270 }
271 None => Duration::from_secs(5),
272 };
273
274 Ok(Box::new(PipeConfig {
275 name: name.to_string(),
276 command,
277 respawn_delay,
278 interface_id: id,
279 runtime: Arc::new(Mutex::new(PipeRuntime { respawn_delay })),
280 }))
281 }
282
283 fn start(
284 &self,
285 config: Box<dyn InterfaceConfigData>,
286 ctx: StartContext,
287 ) -> io::Result<StartResult> {
288 let pipe_config = *config
289 .into_any()
290 .downcast::<PipeConfig>()
291 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
292
293 let id = pipe_config.interface_id;
294 let info = InterfaceInfo {
295 id,
296 name: pipe_config.name.clone(),
297 mode: ctx.mode,
298 gravity: ctx.gravity,
299 recursive_prs: ctx.recursive_prs,
300 announces_from_internal: ctx.announces_from_internal,
301 announces_to_internal: ctx.announces_to_internal,
302 out_capable: true,
303 in_capable: true,
304 bitrate: Some(1_000_000),
305 airtime_profile: None,
306 announce_rate_target: None,
307 announce_rate_grace: 0,
308 announce_rate_penalty: 0.0,
309 announce_cap: rns_core::constants::ANNOUNCE_CAP,
310 is_local_client: false,
311 wants_tunnel: false,
312 tunnel_id: None,
313 mtu: rns_core::constants::MTU as u32,
314 ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
315 ia_freq: 0.0,
316 ip_freq: 0.0,
317 op_freq: 0.0,
318 op_samples: 0,
319 started: crate::time::now(),
320 };
321
322 let writer = start(pipe_config, ctx.tx)?;
323
324 Ok(StartResult::Simple {
325 id,
326 info,
327 writer,
328 interface_type_name: "PipeInterface".to_string(),
329 })
330 }
331}
332
333pub(crate) fn pipe_runtime_handle_from_config(config: &PipeConfig) -> PipeRuntimeConfigHandle {
334 PipeRuntimeConfigHandle {
335 interface_name: config.name.clone(),
336 runtime: Arc::clone(&config.runtime),
337 startup: PipeRuntime::from_config(config),
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn pipe_start_and_receive() {
347 let (tx, rx) = crate::event::channel();
349 let config = PipeConfig {
350 name: "test-pipe".into(),
351 command: "cat".into(),
352 respawn_delay: Duration::from_secs(1),
353 interface_id: InterfaceId(100),
354 runtime: Arc::new(Mutex::new(PipeRuntime {
355 respawn_delay: Duration::from_secs(1),
356 })),
357 };
358
359 let mut writer = start(config, tx).unwrap();
360
361 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
363 assert!(matches!(
364 event,
365 Event::InterfaceUp(InterfaceId(100), None, None)
366 ));
367
368 let payload: Vec<u8> = (0..32).collect();
370 writer.send_frame(&payload).unwrap();
371
372 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
374 match event {
375 Event::Frame {
376 interface_id,
377 data,
378 rssi: _,
379 snr: _,
380 } => {
381 assert_eq!(interface_id, InterfaceId(100));
382 assert_eq!(data, payload);
383 }
384 other => panic!("expected Frame, got {:?}", other),
385 }
386 }
387
388 #[test]
389 fn pipe_writer_sends() {
390 let (tx, rx) = crate::event::channel();
392 let config = PipeConfig {
393 name: "test-pipe-writer".into(),
394 command: "cat".into(),
395 respawn_delay: Duration::from_secs(1),
396 interface_id: InterfaceId(101),
397 runtime: Arc::new(Mutex::new(PipeRuntime {
398 respawn_delay: Duration::from_secs(1),
399 })),
400 };
401
402 let mut writer = start(config, tx).unwrap();
403 let _ = rx.recv_timeout(Duration::from_secs(2)).unwrap(); let payload: Vec<u8> = (10..42).collect();
407 writer.send_frame(&payload).unwrap();
408
409 let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
410 match event {
411 Event::Frame { data, .. } => {
412 assert_eq!(data, payload);
413 }
414 other => panic!("expected Frame, got {:?}", other),
415 }
416 }
417
418 #[test]
419 fn pipe_subprocess_exit() {
420 let (tx, rx) = crate::event::channel();
422 let config = PipeConfig {
423 name: "test-pipe-exit".into(),
424 command: "true".into(), respawn_delay: Duration::from_secs(60), interface_id: InterfaceId(102),
427 runtime: Arc::new(Mutex::new(PipeRuntime {
428 respawn_delay: Duration::from_secs(60),
429 })),
430 };
431
432 let _writer = start(config, tx).unwrap();
433
434 let mut got_down = false;
436 for _ in 0..5 {
437 match rx.recv_timeout(Duration::from_secs(2)) {
438 Ok(Event::InterfaceDown(InterfaceId(102))) => {
439 got_down = true;
440 break;
441 }
442 Ok(_) => continue,
443 Err(_) => break,
444 }
445 }
446 assert!(
447 got_down,
448 "should receive InterfaceDown after subprocess exits"
449 );
450 }
451
452 #[test]
453 fn pipe_config_defaults() {
454 let config = PipeConfig::default();
455 assert_eq!(config.respawn_delay, Duration::from_secs(5));
456 assert_eq!(config.interface_id, InterfaceId(0));
457 assert!(config.command.is_empty());
458 }
459
460 #[test]
461 fn pipe_invalid_command() {
462 let (tx, _rx) = crate::event::channel();
463 let config = PipeConfig {
464 name: "test-pipe-bad".into(),
465 command: "/nonexistent_rns_test_binary_that_does_not_exist_xyz".into(),
466 respawn_delay: Duration::from_secs(60),
467 interface_id: InterfaceId(103),
468 runtime: Arc::new(Mutex::new(PipeRuntime {
469 respawn_delay: Duration::from_secs(60),
470 })),
471 };
472
473 let result = start(config, tx);
478 assert!(result.is_ok());
481 }
482}