lean_ctx/shell/containment/
spawner.rs1use std::collections::HashMap;
13use std::io::PipeReader;
14use std::os::fd::{AsRawFd as _, OwnedFd};
15use std::os::unix::net::UnixStream;
16use std::process::{Child, Command, ExitStatus, Stdio};
17use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18use std::sync::{Arc, Mutex, PoisonError, mpsc};
19use std::time::Duration;
20
21use super::spawn_helper::{HELLO, exit_status};
22use super::wire::{Inbox, Reply, Request, SpawnSpec, Unsent, send_frame};
23
24const ANSWER_BUDGET: Duration = Duration::from_secs(5);
27
28static LINK: Mutex<Option<Arc<Link>>> = Mutex::new(None);
30
31struct Link {
33 socket: UnixStream,
34 sending: Mutex<()>,
36 calls: Mutex<HashMap<u64, mpsc::Sender<Reply>>>,
38 next_call: AtomicU64,
39 gone: AtomicBool,
41 helper: Mutex<Child>,
42}
43
44fn helper_command() -> std::io::Result<Command> {
46 #[cfg(any(test, feature = "test-support"))]
47 if super::harness_helper() {
48 return harness_helper_command();
49 }
50 let mut command = Command::new(std::env::current_exe()?);
51 command.arg(super::HELPER_MODE);
52 Ok(command)
53}
54
55#[cfg(any(test, feature = "test-support"))]
58fn harness_helper_command() -> std::io::Result<Command> {
59 if HELPER_BROKEN.load(Ordering::Relaxed) {
60 return Err(std::io::Error::from(std::io::ErrorKind::NotFound));
61 }
62 let mut command = Command::new(std::env::current_exe()?);
63 command
64 .args([
65 super::tests::HELPER_TEST,
66 "--exact",
67 "--ignored",
68 "--nocapture",
69 ])
70 .env(super::tests::HELPER_ENV, "spawn-helper")
71 .env_remove(super::tests::FAULT_ENV);
72 if let Some(fault) = *HELPER_FAULT.lock().unwrap_or_else(PoisonError::into_inner) {
73 command.env(super::tests::FAULT_ENV, fault);
74 }
75 Ok(command)
76}
77
78impl Link {
79 fn start() -> Option<Arc<Self>> {
81 let (ours, theirs) = UnixStream::pair().ok()?;
82 super::wire::quiet_socket(ours.as_raw_fd());
83 let mut command = helper_command().ok()?;
84 command
85 .stdin(OwnedFd::from(theirs))
86 .stdout(Stdio::null())
87 .stderr(Stdio::null());
88 let helper = command.spawn().ok()?;
89 drop(command);
92 let reading = ours.try_clone().ok();
93 let link = Arc::new(Self {
94 socket: ours,
95 sending: Mutex::new(()),
96 calls: Mutex::new(HashMap::new()),
97 next_call: AtomicU64::new(1),
98 gone: AtomicBool::new(false),
99 helper: Mutex::new(helper),
100 });
101 let (hello, greeted) = mpsc::channel();
102 let reader = reading.and_then(|reading| {
103 let link = Arc::clone(&link);
104 std::thread::Builder::new()
105 .name("spawn-helper-replies".into())
106 .spawn(move || link.read_replies(&reading, &hello))
107 .ok()
108 });
109 if reader.is_none() || greeted.recv_timeout(ANSWER_BUDGET).is_err() {
110 let mut helper = link.helper.lock().unwrap_or_else(PoisonError::into_inner);
113 let _ = helper.kill();
114 if reader.is_none() {
115 let _ = helper.wait();
116 }
117 drop(helper);
118 link.abandon();
119 return None;
120 }
121 Some(link)
122 }
123
124 fn read_replies(&self, socket: &UnixStream, hello: &mpsc::Sender<()>) {
127 let mut inbox = Inbox::default();
128 let mut greeted = false;
129 let mut understood = true;
130 'connection: while matches!(inbox.fill(socket.as_raw_fd()), Ok(true)) {
131 loop {
132 let frame = match inbox.next_frame() {
133 Ok(Some(frame)) => frame,
134 Ok(None) => break,
135 Err(_) => {
136 understood = false;
137 break 'connection;
138 }
139 };
140 if !greeted {
141 if frame != HELLO {
142 understood = false;
143 break 'connection;
144 }
145 greeted = true;
146 let _ = hello.send(());
147 continue;
148 }
149 let Some(reply) = Reply::decode(&frame) else {
150 understood = false;
151 break 'connection;
152 };
153 let (Reply::Spawned { call, .. }
154 | Reply::SpawnFailed { call, .. }
155 | Reply::Exited { call, .. }) = reply;
156 if let Some(sender) = self.lock_calls().get(&call) {
157 let _ = sender.send(reply);
158 }
159 }
160 }
161 self.gone.store(true, Ordering::SeqCst);
162 self.lock_calls().clear();
164 let mut helper = self.helper.lock().unwrap_or_else(PoisonError::into_inner);
165 if !understood {
166 let _ = helper.kill();
168 }
169 let _ = helper.wait();
172 }
173
174 fn lock_calls(&self) -> std::sync::MutexGuard<'_, HashMap<u64, mpsc::Sender<Reply>>> {
175 self.calls.lock().unwrap_or_else(PoisonError::into_inner)
176 }
177
178 fn abandon(&self) {
180 self.gone.store(true, Ordering::SeqCst);
181 let _ = self.socket.shutdown(std::net::Shutdown::Both);
182 }
183
184 fn send(&self, request: &Request, descriptors: &[std::os::fd::RawFd]) -> Result<(), Unsent> {
185 let _one_frame = self.sending.lock().unwrap_or_else(PoisonError::into_inner);
186 #[cfg(any(test, feature = "test-support"))]
187 if matches!(request, Request::Spawn { .. }) && SEND_FAILS_ONCE.swap(false, Ordering::SeqCst)
188 {
189 return Err(Unsent::Nothing);
190 }
191 send_frame(self.socket.as_raw_fd(), &request.encode(), descriptors)
192 }
193}
194
195fn link() -> Option<Arc<Link>> {
198 let mut slot = LINK.lock().unwrap_or_else(PoisonError::into_inner);
199 if let Some(link) = slot.as_ref()
200 && !link.gone.load(Ordering::SeqCst)
201 {
202 return Some(Arc::clone(link));
203 }
204 *slot = Link::start();
205 slot.clone()
206}
207
208pub fn shutdown_spawn_helper() {
211 let link = LINK.lock().unwrap_or_else(PoisonError::into_inner).take();
212 if let Some(link) = link {
213 let _ = link.send(&Request::Shutdown, &[]);
215 link.gone.store(true, Ordering::SeqCst);
216 }
217}
218
219pub enum HelperSpawn {
221 Helped {
223 child: HelpedChild,
224 stdout: PipeReader,
225 stderr: PipeReader,
226 },
227 Direct(Option<&'static str>),
231}
232
233pub fn spawn_shell(command: &Command) -> HelperSpawn {
237 if !super::guard_host() {
238 super::note_no_guard_host();
241 return HelperSpawn::Direct(None);
242 }
243 let spec = SpawnSpec {
244 program: command.get_program().to_os_string(),
245 args: command
246 .get_args()
247 .map(std::ffi::OsStr::to_os_string)
248 .collect(),
249 env: effective_environment(command),
250 cwd: command
251 .get_current_dir()
252 .map(|dir| dir.as_os_str().to_os_string()),
253 };
254 let mut request = Request::Spawn { call: 0, spec };
255 for _ in 0..2 {
260 let Some(link) = link() else {
261 continue;
262 };
263 match link.spawn(&mut request) {
264 Attempt::Done(spawned) => return spawned,
265 Attempt::NotStarted => {}
266 Attempt::Refused => break,
267 }
268 }
269 HelperSpawn::Direct(Some(super::HELPER_NOTICE))
270}
271
272fn effective_environment(command: &Command) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
275 let mut environment: std::collections::BTreeMap<_, _> = std::env::vars_os().collect();
276 for (name, value) in command.get_envs() {
277 match value {
278 Some(value) => environment.insert(name.to_os_string(), value.to_os_string()),
279 None => environment.remove(name),
280 };
281 }
282 environment.into_iter().collect()
283}
284
285enum Attempt {
286 Done(HelperSpawn),
287 NotStarted,
290 Refused,
293}
294
295impl Link {
296 fn spawn(self: &Arc<Self>, request: &mut Request) -> Attempt {
297 let (Ok((stdout, stdout_end)), Ok((stderr, stderr_end))) =
298 (std::io::pipe(), std::io::pipe())
299 else {
300 return Attempt::NotStarted;
301 };
302 let call = self.next_call.fetch_add(1, Ordering::Relaxed);
303 if let Request::Spawn { call: slot, .. } = request {
304 *slot = call;
305 }
306 let (sender, replies) = mpsc::channel();
307 self.lock_calls().insert(call, sender);
308 let mut child = HelpedChild {
309 call,
310 pid: 0,
311 link: Arc::clone(self),
312 replies,
313 status: None,
314 settled: false,
315 lost: false,
316 unclear: false,
317 };
318 let sent = if self.gone.load(Ordering::SeqCst) {
336 Err(Unsent::Nothing)
337 } else {
338 self.send(request, &[stdout_end.as_raw_fd(), stderr_end.as_raw_fd()])
339 };
340 let reply = match sent {
341 Ok(()) => child.replies.recv_timeout(ANSWER_BUDGET).ok(),
342 Err(Unsent::Nothing) => {
343 child.settled = true;
344 self.abandon();
345 return Attempt::NotStarted;
346 }
347 Err(Unsent::Part) => None,
348 };
349 drop((stdout_end, stderr_end));
352 match reply {
353 Some(Reply::Spawned { pid, .. }) => child.pid = pid,
354 Some(Reply::SpawnFailed { .. }) => {
355 child.settled = true;
356 return Attempt::Refused;
357 }
358 Some(Reply::Exited { .. }) | None => {
359 self.abandon();
362 child.settled = true;
363 child.unclear = true;
364 }
365 }
366 Attempt::Done(HelperSpawn::Helped {
367 child,
368 stdout,
369 stderr,
370 })
371 }
372}
373
374pub struct HelpedChild {
378 call: u64,
379 pid: u32,
380 link: Arc<Link>,
381 replies: mpsc::Receiver<Reply>,
382 status: Option<ExitStatus>,
383 settled: bool,
385 lost: bool,
387 unclear: bool,
389}
390
391impl HelpedChild {
392 pub(crate) fn id(&self) -> u32 {
394 self.pid
395 }
396
397 pub(crate) fn notice(&self) -> Option<&'static str> {
402 if self.unclear {
403 Some(super::UNCLEAR_NOTICE)
404 } else {
405 self.lost.then_some(super::END_NOTICE)
406 }
407 }
408
409 fn note_reply(&mut self, reply: &Reply) -> Option<ExitStatus> {
411 if let Reply::Exited { status, .. } = reply {
412 self.status = Some(exit_status(*status));
413 }
414 self.status
415 }
416
417 fn note_gone(&mut self) -> std::io::Error {
419 self.lost = true;
420 self.settled = true;
421 std::io::Error::other("the spawn helper is gone")
422 }
423
424 pub(crate) fn try_wait(&mut self) -> std::io::Result<Option<ExitStatus>> {
426 if self.status.is_some() {
427 return Ok(self.status);
428 }
429 if self.lost || self.unclear {
430 return Err(self.note_gone());
431 }
432 match self.replies.try_recv() {
433 Ok(reply) => Ok(self.note_reply(&reply)),
434 Err(mpsc::TryRecvError::Empty) => Ok(None),
435 Err(mpsc::TryRecvError::Disconnected) => Err(self.note_gone()),
436 }
437 }
438
439 pub(crate) fn kill(&mut self) {
442 if !self.settled {
443 self.settled = true;
444 let _ = self.link.send(&Request::Kill { call: self.call }, &[]);
448 }
449 }
450
451 pub(crate) fn wait(&mut self) {
453 while self.status.is_none() && !self.lost {
454 match self.replies.recv() {
455 Ok(reply) => {
456 self.note_reply(&reply);
457 }
458 Err(_) => {
459 let _ = self.note_gone();
460 }
461 }
462 }
463 }
464
465 pub(crate) fn release(mut self) -> bool {
474 if self.settled {
475 return true;
476 }
477 self.settled = true;
478 #[cfg(any(test, feature = "test-support"))]
479 if RELEASE_FAILS_ONCE.swap(false, Ordering::SeqCst) {
480 return false;
481 }
482 let sent = self
483 .link
484 .send(&Request::Release { call: self.call }, &[])
485 .is_ok();
486 if !sent {
487 tracing::warn!(
488 "spawn helper: a release could not be sent; what the command left running may be ended"
489 );
490 }
491 sent
492 }
493}
494
495impl Drop for HelpedChild {
498 fn drop(&mut self) {
499 self.kill();
500 self.link.lock_calls().remove(&self.call);
501 }
502}
503
504#[cfg(any(test, feature = "test-support"))]
505static SEND_FAILS_ONCE: AtomicBool = AtomicBool::new(false);
506
507#[cfg(any(test, feature = "test-support"))]
509pub fn fail_next_spawn_send_for_test() {
510 SEND_FAILS_ONCE.store(true, Ordering::SeqCst);
511}
512
513#[cfg(any(test, feature = "test-support"))]
514static RELEASE_FAILS_ONCE: AtomicBool = AtomicBool::new(false);
515
516#[cfg(any(test, feature = "test-support"))]
518pub fn fail_next_release_send_for_test() {
519 RELEASE_FAILS_ONCE.store(true, Ordering::SeqCst);
520}
521
522#[cfg(any(test, feature = "test-support"))]
523static HELPER_FAULT: Mutex<Option<&'static str>> = Mutex::new(None);
524
525#[cfg(any(test, feature = "test-support"))]
528pub fn set_helper_fault_for_test(fault: Option<&'static str>) {
529 *HELPER_FAULT.lock().unwrap_or_else(PoisonError::into_inner) = fault;
530}
531
532#[cfg(any(test, feature = "test-support"))]
533static HELPER_BROKEN: AtomicBool = AtomicBool::new(false);
534
535#[cfg(any(test, feature = "test-support"))]
537pub fn break_helper_for_test(broken: bool) {
538 HELPER_BROKEN.store(broken, Ordering::Relaxed);
539}
540
541#[cfg(any(test, feature = "test-support"))]
543pub fn helper_pid_for_test() -> Option<u32> {
544 let slot = LINK.lock().unwrap_or_else(PoisonError::into_inner);
545 let link = slot
546 .as_ref()
547 .filter(|link| !link.gone.load(Ordering::SeqCst))?;
548 Some(
549 link.helper
550 .lock()
551 .unwrap_or_else(PoisonError::into_inner)
552 .id(),
553 )
554}
555
556#[cfg(any(test, feature = "test-support"))]
559pub fn kill_helper_for_test() {
560 let link = LINK
561 .lock()
562 .unwrap_or_else(PoisonError::into_inner)
563 .clone()
564 .expect("a helper runs");
565 let _ = link
566 .helper
567 .lock()
568 .unwrap_or_else(PoisonError::into_inner)
569 .kill();
570 let deadline = std::time::Instant::now() + Duration::from_secs(5);
571 while !link.gone.load(Ordering::SeqCst) {
572 assert!(
573 std::time::Instant::now() < deadline,
574 "the helper's end is seen"
575 );
576 std::thread::sleep(Duration::from_millis(5));
577 }
578}