1use std::io;
40use std::process::{Command, ExitStatus};
41
42pub enum DetachedStderr {
45 Null,
46 File(std::fs::File),
47}
48
49pub struct DetachedChild {
52 pid: u32,
53 #[cfg(not(windows))]
54 child: std::process::Child,
55 #[cfg(windows)]
56 process: win::Owned,
57}
58
59impl DetachedChild {
60 #[must_use]
61 pub fn id(&self) -> u32 {
62 self.pid
63 }
64
65 pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
67 #[cfg(not(windows))]
68 {
69 self.child.try_wait()
70 }
71 #[cfg(windows)]
72 {
73 win::try_wait(&self.process)
74 }
75 }
76
77 pub fn kill(&mut self) -> io::Result<()> {
79 #[cfg(not(windows))]
80 {
81 self.child.kill()?;
82 self.child.wait().map(|_| ())
83 }
84 #[cfg(windows)]
85 {
86 win::terminate(&self.process)
87 }
88 }
89}
90
91pub fn spawn_detached(cmd: &mut Command, stderr: DetachedStderr) -> io::Result<DetachedChild> {
95 #[cfg(not(windows))]
96 {
97 let stderr = match stderr {
98 DetachedStderr::Null => std::process::Stdio::null(),
99 DetachedStderr::File(file) => std::process::Stdio::from(file),
100 };
101 cmd.stdin(std::process::Stdio::null())
102 .stdout(std::process::Stdio::null())
103 .stderr(stderr);
104 #[cfg(unix)]
105 {
106 use std::os::unix::process::CommandExt;
107 cmd.process_group(0);
108 }
109 let child = cmd.spawn()?;
110 Ok(DetachedChild {
111 pid: child.id(),
112 child,
113 })
114 }
115 #[cfg(windows)]
116 {
117 win::spawn(cmd, stderr)
118 }
119}
120
121const JOB_OBJECT_LIMIT_BREAKAWAY_OK: u32 = 0x0800;
122const JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK: u32 = 0x1000;
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126#[cfg_attr(not(windows), allow(dead_code))]
127enum Detach {
128 Plain,
130 Breakaway,
132 Reparent,
134 StayInJob,
136}
137
138#[cfg_attr(not(windows), allow(dead_code))]
143fn detach_attempts(in_job: bool, limit_flags: u32, broker: bool) -> Vec<Detach> {
144 if !in_job {
145 return vec![Detach::Plain];
146 }
147 let mut attempts = Vec::with_capacity(3);
148 if limit_flags & (JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) != 0 {
149 attempts.push(Detach::Breakaway);
150 }
151 if broker {
152 attempts.push(Detach::Reparent);
153 }
154 attempts.push(Detach::StayInJob);
155 attempts
156}
157
158#[cfg_attr(not(windows), allow(dead_code))]
161fn append_windows_arg(out: &mut Vec<u16>, arg: &[u16]) {
162 const QUOTE: u16 = b'"' as u16;
163 const BACKSLASH: u16 = b'\\' as u16;
164 let needs_quotes = arg.is_empty()
165 || arg
166 .iter()
167 .any(|&c| matches!(c, 0x20 | 0x09 | 0x0a | 0x0b) || c == QUOTE);
168 if !needs_quotes {
169 out.extend_from_slice(arg);
170 return;
171 }
172 out.push(QUOTE);
173 let mut backslashes = 0_usize;
174 for &c in arg {
175 if c == BACKSLASH {
176 backslashes += 1;
177 continue;
178 }
179 if c == QUOTE {
180 out.extend(std::iter::repeat_n(BACKSLASH, backslashes * 2 + 1));
181 } else {
182 out.extend(std::iter::repeat_n(BACKSLASH, backslashes));
183 }
184 out.push(c);
185 backslashes = 0;
186 }
187 out.extend(std::iter::repeat_n(BACKSLASH, backslashes * 2));
188 out.push(QUOTE);
189}
190
191#[cfg_attr(not(windows), allow(dead_code))]
194fn windows_command_line(program: &[u16], args: &[Vec<u16>]) -> Vec<u16> {
195 let mut line = Vec::with_capacity(program.len() + 2 + args.iter().map(Vec::len).sum::<usize>());
196 line.push(u16::from(b'"'));
197 line.extend(program.iter().copied().filter(|&c| c != u16::from(b'"')));
198 line.push(u16::from(b'"'));
199 for arg in args {
200 line.push(u16::from(b' '));
201 append_windows_arg(&mut line, arg);
202 }
203 line.push(0);
204 line
205}
206
207#[cfg_attr(not(windows), allow(dead_code))]
211fn environment_block(
212 base: Vec<(Vec<u16>, Vec<u16>)>,
213 overrides: Vec<(Vec<u16>, Option<Vec<u16>>)>,
214) -> Vec<u16> {
215 let fold = |key: &[u16]| String::from_utf16_lossy(key).to_uppercase();
216 let mut vars: std::collections::BTreeMap<String, (Vec<u16>, Vec<u16>)> = base
217 .into_iter()
218 .filter(|(key, _)| !key.is_empty())
219 .map(|(key, value)| (fold(&key), (key, value)))
220 .collect();
221 for (key, value) in overrides {
222 match value {
223 Some(value) => {
224 vars.insert(fold(&key), (key, value));
225 }
226 None => {
227 vars.remove(&fold(&key));
228 }
229 }
230 }
231 let mut block = Vec::new();
232 for (key, value) in vars.into_values() {
233 block.extend_from_slice(&key);
234 block.push(u16::from(b'='));
235 block.extend_from_slice(&value);
236 block.push(0);
237 }
238 if block.is_empty() {
239 block.push(0);
240 }
241 block.push(0);
242 block
243}
244
245#[cfg(windows)]
246mod win {
247 use super::{Detach, DetachedChild, DetachedStderr};
248 use std::ffi::OsStr;
249 use std::io;
250 use std::os::windows::ffi::OsStrExt;
251 use std::os::windows::io::AsRawHandle;
252 use std::process::{Command, ExitStatus};
253 use windows_sys::Win32::Foundation::{
254 CloseHandle, DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE,
255 INVALID_HANDLE_VALUE, WAIT_OBJECT_0,
256 };
257 use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TokenSessionId};
258 use windows_sys::Win32::System::JobObjects::{
259 IsProcessInJob, JOBOBJECT_BASIC_LIMIT_INFORMATION, JobObjectBasicLimitInformation,
260 QueryInformationJobObject,
261 };
262 use windows_sys::Win32::System::Threading::{
263 CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_UNICODE_ENVIRONMENT,
264 CreateProcessW, DETACHED_PROCESS, DeleteProcThreadAttributeList,
265 EXTENDED_STARTUPINFO_PRESENT, GetCurrentProcess, GetExitCodeProcess,
266 InitializeProcThreadAttributeList, OpenProcess, OpenProcessToken,
267 PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
268 PROCESS_CREATE_PROCESS, PROCESS_DUP_HANDLE, PROCESS_INFORMATION,
269 PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, STARTF_USESTDHANDLES,
270 STARTUPINFOEXW, STARTUPINFOW, TerminateProcess, UpdateProcThreadAttribute,
271 WaitForSingleObject,
272 };
273
274 pub(in crate::ipc) struct Owned(HANDLE);
276
277 impl Drop for Owned {
278 fn drop(&mut self) {
279 if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE {
280 unsafe { CloseHandle(self.0) };
282 }
283 }
284 }
285
286 pub(super) fn try_wait(process: &Owned) -> io::Result<Option<ExitStatus>> {
287 use std::os::windows::process::ExitStatusExt;
288 if unsafe { WaitForSingleObject(process.0, 0) } != WAIT_OBJECT_0 {
290 return Ok(None);
291 }
292 let mut code = 0_u32;
293 if unsafe { GetExitCodeProcess(process.0, &raw mut code) } == 0 {
295 return Err(io::Error::last_os_error());
296 }
297 Ok(Some(ExitStatus::from_raw(code)))
298 }
299
300 pub(super) fn terminate(process: &Owned) -> io::Result<()> {
301 if unsafe { TerminateProcess(process.0, 1) } == 0 {
303 return Err(io::Error::last_os_error());
304 }
305 Ok(())
306 }
307
308 fn wide(s: &OsStr) -> Vec<u16> {
309 s.encode_wide().collect()
310 }
311
312 pub(super) fn spawn(cmd: &Command, stderr: DetachedStderr) -> io::Result<DetachedChild> {
313 let program = wide(cmd.get_program());
314 let args: Vec<Vec<u16>> = cmd.get_args().map(wide).collect();
315 let command_line = super::windows_command_line(&program, &args);
316 let env = super::environment_block(
317 std::env::vars_os()
318 .map(|(k, v)| (wide(&k), wide(&v)))
319 .collect(),
320 cmd.get_envs()
321 .map(|(k, v)| (wide(k), v.map(wide)))
322 .collect(),
323 );
324 let cwd = cmd.get_current_dir().map(|dir| {
325 let mut w = wide(dir.as_os_str());
326 w.push(0);
327 w
328 });
329 let null_device = || {
330 std::fs::OpenOptions::new()
331 .read(true)
332 .write(true)
333 .open("NUL")
334 };
335 let stdin = null_device()?;
336 let stdout = null_device()?;
337 let stderr = match stderr {
338 DetachedStderr::Null => null_device()?,
339 DetachedStderr::File(file) => file,
340 };
341 let std_handles: [HANDLE; 3] = [
342 stdin.as_raw_handle(),
343 stdout.as_raw_handle(),
344 stderr.as_raw_handle(),
345 ];
346
347 let (in_job, limit_flags) = job_limits();
348 let broker = if in_job { session_broker() } else { None };
349 let mut last_error = None;
350 for attempt in super::detach_attempts(in_job, limit_flags, broker.is_some()) {
351 let parent = if attempt == Detach::Reparent {
352 broker.as_ref()
353 } else {
354 None
355 };
356 match create(
357 &command_line,
358 &env,
359 cwd.as_deref(),
360 std_handles,
361 parent,
362 attempt,
363 ) {
364 Ok(child) => {
365 if attempt == Detach::StayInJob {
366 eprintln!(
367 "lean-ctx: started {} (PID {}) inside the caller's job object: the job forbids breakaway and this session has no Explorer to start it from, so it ends with that job (and `Start-Process -Wait` waits for it).",
368 cmd.get_program().to_string_lossy(),
369 child.pid
370 );
371 }
372 return Ok(child);
373 }
374 Err(error) => last_error = Some(error),
375 }
376 }
377 Err(last_error.unwrap_or_else(|| io::Error::other("no spawn attempt")))
378 }
379
380 fn job_limits() -> (bool, u32) {
383 let mut in_job = 0;
384 let ok =
387 unsafe { IsProcessInJob(GetCurrentProcess(), std::ptr::null_mut(), &raw mut in_job) };
388 if ok == 0 {
389 return (true, super::JOB_OBJECT_LIMIT_BREAKAWAY_OK);
391 }
392 if in_job == 0 {
393 return (false, 0);
394 }
395 let mut info: JOBOBJECT_BASIC_LIMIT_INFORMATION = unsafe { std::mem::zeroed() };
397 let size =
398 u32::try_from(std::mem::size_of::<JOBOBJECT_BASIC_LIMIT_INFORMATION>()).unwrap_or(0);
399 let ok = unsafe {
401 QueryInformationJobObject(
402 std::ptr::null_mut(),
403 JobObjectBasicLimitInformation,
404 (&raw mut info).cast(),
405 size,
406 std::ptr::null_mut(),
407 )
408 };
409 (true, if ok == 0 { 0 } else { info.LimitFlags })
410 }
411
412 fn token_session(process: HANDLE) -> Option<u32> {
413 let mut token = std::ptr::null_mut();
414 if unsafe { OpenProcessToken(process, TOKEN_QUERY, &raw mut token) } == 0 {
416 return None;
417 }
418 let token = Owned(token);
419 let mut session = 0_u32;
420 let mut len = 0_u32;
421 let ok = unsafe {
423 GetTokenInformation(
424 token.0,
425 TokenSessionId,
426 (&raw mut session).cast(),
427 4,
428 &raw mut len,
429 )
430 };
431 (ok != 0).then_some(session)
432 }
433
434 fn image_path(process: HANDLE) -> Option<String> {
435 let mut path = vec![0_u16; 32_768];
436 let mut len = u32::try_from(path.len()).ok()?;
437 if unsafe { QueryFullProcessImageNameW(process, 0, path.as_mut_ptr(), &raw mut len) } == 0 {
439 return None;
440 }
441 Some(String::from_utf16_lossy(path.get(..len as usize)?))
442 }
443
444 fn explorer_path() -> Option<String> {
445 use windows_sys::Win32::System::SystemInformation::GetSystemWindowsDirectoryW;
446 let mut buf = vec![0_u16; 1024];
447 let len =
449 unsafe { GetSystemWindowsDirectoryW(buf.as_mut_ptr(), u32::try_from(buf.len()).ok()?) }
450 as usize;
451 if len == 0 || len >= buf.len() {
452 return None;
453 }
454 let dir = String::from_utf16_lossy(&buf[..len]);
455 Some(format!("{}\\explorer.exe", dir.trim_end_matches('\\')))
456 }
457
458 fn session_broker() -> Option<Owned> {
461 use windows_sys::Win32::System::Diagnostics::ToolHelp::{
462 CreateToolhelp32Snapshot, PROCESSENTRY32W, Process32FirstW, Process32NextW,
463 TH32CS_SNAPPROCESS,
464 };
465 let me = unsafe { GetCurrentProcess() };
467 let my_session = token_session(me)?;
468 let my_sid = crate::ipc::process::windows_token_user_sid(me)?;
469 let expected = explorer_path()?;
470
471 let snapshot = Owned(unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) });
473 if snapshot.0.is_null() || snapshot.0 == INVALID_HANDLE_VALUE {
474 return None;
475 }
476 let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
478 entry.dwSize = u32::try_from(std::mem::size_of::<PROCESSENTRY32W>()).ok()?;
479 let mut more = unsafe { Process32FirstW(snapshot.0, &raw mut entry) } != 0;
481 while more {
482 let len = entry
483 .szExeFile
484 .iter()
485 .position(|&c| c == 0)
486 .unwrap_or(entry.szExeFile.len());
487 let name = String::from_utf16_lossy(&entry.szExeFile[..len]);
488 if name.eq_ignore_ascii_case("explorer.exe")
489 && let Some(broker) =
490 usable_broker(entry.th32ProcessID, my_session, &my_sid, &expected)
491 {
492 return Some(broker);
493 }
494 more = unsafe { Process32NextW(snapshot.0, &raw mut entry) } != 0;
496 }
497 None
498 }
499
500 fn usable_broker(pid: u32, session: u32, sid: &[u8], expected_path: &str) -> Option<Owned> {
501 let access =
502 PROCESS_CREATE_PROCESS | PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION;
503 let handle = unsafe { OpenProcess(access, 0, pid) };
505 if handle.is_null() {
506 return None;
507 }
508 let handle = Owned(handle);
509 let mut in_job = 1;
510 let job_ok =
512 unsafe { IsProcessInJob(handle.0, std::ptr::null_mut(), &raw mut in_job) } != 0;
513 let usable = job_ok
514 && in_job == 0
515 && token_session(handle.0) == Some(session)
516 && crate::ipc::process::windows_token_user_sid(handle.0).as_deref() == Some(sid)
517 && image_path(handle.0).is_some_and(|path| path.eq_ignore_ascii_case(expected_path));
518 usable.then_some(handle)
519 }
520
521 struct InheritedHandles {
524 owner: HANDLE,
525 handles: Vec<HANDLE>,
526 }
527
528 impl Drop for InheritedHandles {
529 fn drop(&mut self) {
530 for &handle in &self.handles {
531 unsafe {
534 DuplicateHandle(
535 self.owner,
536 handle,
537 std::ptr::null_mut(),
538 std::ptr::null_mut(),
539 0,
540 0,
541 DUPLICATE_CLOSE_SOURCE,
542 );
543 }
544 }
545 }
546 }
547
548 struct AttributeList {
550 buf: Vec<u64>,
551 }
552
553 impl AttributeList {
554 fn new(count: u32) -> io::Result<Self> {
555 let mut size = 0_usize;
556 unsafe {
558 InitializeProcThreadAttributeList(std::ptr::null_mut(), count, 0, &raw mut size)
559 };
560 if size == 0 {
561 return Err(io::Error::last_os_error());
562 }
563 let mut buf = vec![0_u64; size.div_ceil(8)];
564 if unsafe {
566 InitializeProcThreadAttributeList(buf.as_mut_ptr().cast(), count, 0, &raw mut size)
567 } == 0
568 {
569 return Err(io::Error::last_os_error());
570 }
571 Ok(Self { buf })
572 }
573
574 fn ptr(&mut self) -> *mut core::ffi::c_void {
575 self.buf.as_mut_ptr().cast()
576 }
577
578 unsafe fn set(
581 &mut self,
582 attribute: u32,
583 value: *const core::ffi::c_void,
584 size: usize,
585 ) -> io::Result<()> {
586 let ok = unsafe {
588 UpdateProcThreadAttribute(
589 self.ptr(),
590 0,
591 attribute as usize,
592 value,
593 size,
594 std::ptr::null_mut(),
595 std::ptr::null(),
596 )
597 };
598 if ok == 0 {
599 return Err(io::Error::last_os_error());
600 }
601 Ok(())
602 }
603 }
604
605 impl Drop for AttributeList {
606 fn drop(&mut self) {
607 unsafe { DeleteProcThreadAttributeList(self.ptr()) };
609 }
610 }
611
612 fn create(
613 command_line: &[u16],
614 env: &[u16],
615 cwd: Option<&[u16]>,
616 std_handles: [HANDLE; 3],
617 parent: Option<&Owned>,
618 attempt: Detach,
619 ) -> io::Result<DetachedChild> {
620 let me = unsafe { GetCurrentProcess() };
622 let owner = parent.map_or(me, |p| p.0);
625 let mut inherited = InheritedHandles {
626 owner,
627 handles: Vec::with_capacity(3),
628 };
629 for handle in std_handles {
630 let mut copy = std::ptr::null_mut();
631 let ok = unsafe {
633 DuplicateHandle(
634 me,
635 handle,
636 owner,
637 &raw mut copy,
638 0,
639 1,
640 DUPLICATE_SAME_ACCESS,
641 )
642 };
643 if ok == 0 {
644 return Err(io::Error::last_os_error());
645 }
646 inherited.handles.push(copy);
647 }
648
649 let mut attributes = AttributeList::new(if parent.is_some() { 2 } else { 1 })?;
650 unsafe {
654 attributes.set(
655 PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
656 inherited.handles.as_ptr().cast(),
657 std::mem::size_of_val(inherited.handles.as_slice()),
658 )?;
659 }
660 let parent_handle: HANDLE = parent.map_or(std::ptr::null_mut(), |p| p.0);
661 if parent.is_some() {
662 unsafe {
664 attributes.set(
665 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
666 (&raw const parent_handle).cast(),
667 std::mem::size_of::<HANDLE>(),
668 )?;
669 }
670 }
671
672 let mut startup: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
674 startup.StartupInfo.cb = u32::try_from(std::mem::size_of::<STARTUPINFOEXW>()).unwrap_or(0);
675 startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
676 startup.StartupInfo.hStdInput = inherited.handles[0];
677 startup.StartupInfo.hStdOutput = inherited.handles[1];
678 startup.StartupInfo.hStdError = inherited.handles[2];
679 startup.lpAttributeList = attributes.ptr();
680
681 let mut flags = DETACHED_PROCESS
682 | CREATE_NEW_PROCESS_GROUP
683 | CREATE_UNICODE_ENVIRONMENT
684 | EXTENDED_STARTUPINFO_PRESENT;
685 if attempt == Detach::Breakaway {
686 flags |= CREATE_BREAKAWAY_FROM_JOB;
687 }
688 let mut line = command_line.to_vec();
690 let mut info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
692 let ok = unsafe {
695 CreateProcessW(
696 std::ptr::null(),
697 line.as_mut_ptr(),
698 std::ptr::null(),
699 std::ptr::null(),
700 1,
701 flags,
702 env.as_ptr().cast(),
703 cwd.map_or(std::ptr::null(), <[u16]>::as_ptr),
704 (&raw const startup).cast::<STARTUPINFOW>(),
705 &raw mut info,
706 )
707 };
708 if ok == 0 {
709 return Err(io::Error::last_os_error());
710 }
711 drop(Owned(info.hThread));
712 Ok(DetachedChild {
713 pid: info.dwProcessId,
714 process: Owned(info.hProcess),
715 })
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 fn w(s: &str) -> Vec<u16> {
724 s.encode_utf16().collect()
725 }
726
727 fn arg(s: &str) -> String {
728 let mut out = Vec::new();
729 append_windows_arg(&mut out, &w(s));
730 String::from_utf16_lossy(&out)
731 }
732
733 #[test]
734 fn windows_args_round_trip_msvc_quoting() {
735 assert_eq!(arg("serve"), "serve");
736 assert_eq!(arg("--port=4444"), "--port=4444");
737 assert_eq!(arg(""), "\"\"");
738 assert_eq!(arg("a b"), "\"a b\"");
739 assert_eq!(
740 arg("C:\\Program Files\\x\\"),
741 "\"C:\\Program Files\\x\\\\\""
742 );
743 assert_eq!(arg("say \"hi\""), "\"say \\\"hi\\\"\"");
744 assert_eq!(arg("a\\\"b c"), "\"a\\\\\\\"b c\"");
745 assert_eq!(arg("C:\\no\\space"), "C:\\no\\space");
746 }
747
748 #[test]
749 fn command_line_quotes_the_program_and_ends_with_nul() {
750 let line = windows_command_line(
751 &w("C:\\Program Files\\lean-ctx.exe"),
752 &[w("proxy"), w("start")],
753 );
754 assert_eq!(line.last(), Some(&0));
755 assert_eq!(
756 String::from_utf16_lossy(&line[..line.len() - 1]),
757 "\"C:\\Program Files\\lean-ctx.exe\" proxy start"
758 );
759 }
760
761 #[test]
762 fn environment_block_applies_changes_case_insensitively() {
763 let block = environment_block(
764 vec![
765 (w("Path"), w("C:\\bin")),
766 (w("SECRET"), w("x")),
767 (w("b"), w("1")),
768 ],
769 vec![
770 (w("PATH"), Some(w("D:\\bin"))),
771 (w("secret"), None),
772 (w("LEAN_CTX_TCC_STANDALONE"), Some(w("1"))),
773 ],
774 );
775 let text = String::from_utf16_lossy(&block);
776 assert_eq!(text, "b=1\0LEAN_CTX_TCC_STANDALONE=1\0PATH=D:\\bin\0\0");
777 assert_eq!(environment_block(Vec::new(), Vec::new()), vec![0, 0]);
778 }
779
780 #[test]
781 fn job_without_breakaway_uses_the_broker_then_stays_announced() {
782 assert_eq!(detach_attempts(false, 0, true), vec![Detach::Plain]);
783 assert_eq!(
785 detach_attempts(true, 0, true),
786 vec![Detach::Reparent, Detach::StayInJob]
787 );
788 assert_eq!(detach_attempts(true, 0, false), vec![Detach::StayInJob]);
789 assert_eq!(
791 detach_attempts(true, 0x2800, true),
792 vec![Detach::Breakaway, Detach::Reparent, Detach::StayInJob]
793 );
794 assert_eq!(
795 detach_attempts(true, JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, false),
796 vec![Detach::Breakaway, Detach::StayInJob]
797 );
798 }
799
800 #[cfg(unix)]
802 #[test]
803 fn detached_spawn_does_not_hold_the_callers_stdout() {
804 let mut sleeper = Command::new("sleep");
805 sleeper.arg("30").stdout(std::process::Stdio::inherit());
806 let mut child = spawn_detached(&mut sleeper, DetachedStderr::Null).expect("spawn sleep");
807 let pid = child.id();
808 let out = Command::new("lsof")
809 .args(["-a", "-p", &pid.to_string(), "-d", "1", "-Fn"])
810 .output();
811 let _ = child.kill();
812 if let Ok(out) = out
813 && out.status.success()
814 {
815 let text = String::from_utf8_lossy(&out.stdout);
816 assert!(text.contains("/dev/null"), "fd 1 of the service: {text}");
817 }
818 }
819
820 #[cfg(unix)]
824 #[test]
825 fn detached_spawn_leads_its_own_process_group() {
826 let mut sleeper = Command::new("sleep");
827 sleeper.arg("30");
828 let mut child = spawn_detached(&mut sleeper, DetachedStderr::Null).expect("spawn sleep");
829 let pid = child.id();
830 let pid_t = libc::pid_t::try_from(pid).expect("pid fits pid_t");
831 let child_pgid = unsafe { libc::getpgid(pid_t) };
833 let own_pgid = unsafe { libc::getpgrp() };
835 let _ = child.kill();
836 assert_eq!(child_pgid, pid_t, "the service leads its own group");
837 assert_ne!(child_pgid, own_pgid, "the service left the caller's group");
838 }
839
840 #[cfg(windows)]
843 #[test]
844 fn windows_detached_spawn_runs_and_reports_exit() {
845 let mut cmd = Command::new("cmd.exe");
846 cmd.args(["/c", "if \"%LCX_T%\"==\"ok\" (exit 7) else (exit 3)"])
847 .env("LCX_T", "ok");
848 let mut child = spawn_detached(&mut cmd, DetachedStderr::Null).expect("spawn cmd");
849 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
850 loop {
851 if let Some(status) = child.try_wait().expect("try_wait") {
852 assert_eq!(status.code(), Some(7));
853 break;
854 }
855 assert!(std::time::Instant::now() < deadline, "cmd did not exit");
856 std::thread::sleep(std::time::Duration::from_millis(50));
857 }
858 }
859}