1use std::fs::{File, OpenOptions};
5use std::io::Read;
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11
12use anyhow::{Context, Result, bail};
13use serde::{Deserialize, Serialize};
14
15use crate::proc::Quiet as _;
16use crate::talk::{self, Talks};
17
18pub const DEFAULT_TIMEOUT: u64 = 900;
20pub const MAX_TIMEOUT: u64 = 3600;
22pub const MAX_CONCURRENT: usize = 4;
24pub const OUTPUT_CAP: usize = 8192;
26
27#[derive(Debug, Serialize, Deserialize)]
28struct Job {
29 id: String,
30 talk: String,
31 cwd: PathBuf,
32 command: Vec<String>,
33 timeout: u64,
34 supervisor: Option<(u32, String)>,
35 child: Option<(u32, String)>,
36 result: Option<String>,
37 tail: String,
38 delivered: bool,
39}
40
41fn root(home: &Path) -> PathBuf {
42 home.join("chat-watches")
43}
44
45struct Lock(File);
46
47impl Drop for Lock {
48 fn drop(&mut self) {
49 unlock_file(&self.0);
50 }
51}
52
53fn open_lock(path: &Path) -> Result<File> {
54 Ok(OpenOptions::new()
55 .create(true)
56 .truncate(false)
57 .read(true)
58 .write(true)
59 .open(path)?)
60}
61
62fn lock(path: &Path) -> Result<Lock> {
63 let file = open_lock(path)?;
64 lock_file(&file, false)?;
65 Ok(Lock(file))
66}
67
68fn try_lock(path: &Path) -> Result<Option<Lock>> {
69 let file = open_lock(path)?;
70 if lock_file(&file, true)? {
71 Ok(Some(Lock(file)))
72 } else {
73 Ok(None)
74 }
75}
76
77#[cfg(unix)]
78fn lock_file(file: &File, immediate: bool) -> std::io::Result<bool> {
79 use std::os::fd::AsRawFd;
80 loop {
81 if unsafe {
82 libc::flock(
83 file.as_raw_fd(),
84 libc::LOCK_EX | if immediate { libc::LOCK_NB } else { 0 },
85 )
86 } == 0
87 {
88 return Ok(true);
89 }
90 let error = std::io::Error::last_os_error();
91 match error.kind() {
92 std::io::ErrorKind::Interrupted => continue,
93 std::io::ErrorKind::WouldBlock if immediate => return Ok(false),
94 _ => return Err(error),
95 }
96 }
97}
98
99#[cfg(unix)]
100fn unlock_file(file: &File) {
101 use std::os::fd::AsRawFd;
102 unsafe {
103 libc::flock(file.as_raw_fd(), libc::LOCK_UN);
104 }
105}
106
107#[cfg(windows)]
108fn lock_file(file: &File, immediate: bool) -> std::io::Result<bool> {
109 use std::os::windows::io::AsRawHandle;
110 windows::lock_file(file.as_raw_handle(), immediate)
111}
112
113#[cfg(windows)]
114fn unlock_file(file: &File) {
115 use std::os::windows::io::AsRawHandle;
116 windows::unlock_file(file.as_raw_handle());
117}
118
119fn load(dir: &Path) -> Result<Job> {
120 Ok(serde_json::from_slice(&std::fs::read(
121 dir.join("job.json"),
122 )?)?)
123}
124
125fn save(dir: &Path, job: &Job) -> Result<()> {
126 let tmp = dir.join("job.tmp");
127 std::fs::write(&tmp, serde_json::to_vec(job)?)?;
128 std::fs::rename(tmp, dir.join("job.json"))?;
129 Ok(())
130}
131
132fn directories(home: &Path) -> Result<Vec<PathBuf>> {
133 Ok(std::fs::read_dir(root(home))?
134 .filter_map(Result::ok)
135 .map(|e| e.path())
136 .filter(|p| {
137 p.is_dir()
138 && !p
139 .file_name()
140 .is_some_and(|n| n.to_string_lossy().starts_with('.'))
141 })
142 .collect())
143}
144
145pub fn authorize(talks: &Talks, run: &str, node: &str) -> Result<String> {
147 if node != crate::queue::CHAT_NODE || run.trim().is_empty() {
148 bail!("talk watch requires MAGI_NODE=chat and MAGI_RUN from a standing chat");
149 }
150 let t = talks.get(run)?;
151 if t.id != run || !t.status.open() {
152 bail!("talk watch requires the exact id of an open conversation");
153 }
154 Ok(t.id)
155}
156
157pub fn start(
160 home: &Path,
161 talks: &Talks,
162 run: &str,
163 node: &str,
164 command: Vec<String>,
165 timeout: u64,
166) -> Result<String> {
167 let talk = authorize(talks, run, node)?;
168 if command.is_empty() || !(1..=MAX_TIMEOUT).contains(&timeout) {
169 bail!("watch needs a command and a timeout between 1 and {MAX_TIMEOUT} seconds");
170 }
171 std::fs::create_dir_all(root(home))?;
172 let _store = lock(&root(home).join("store.lock"))?;
173 let active = directories(home)?
174 .iter()
175 .try_fold(0, |n, dir| -> Result<usize> {
176 Ok(n + usize::from(load(dir)?.result.is_none()))
177 })?;
178 if active >= MAX_CONCURRENT {
179 bail!("at most {MAX_CONCURRENT} chat watches may run concurrently");
180 }
181 let id = crate::rng::SplitMix64::new(crate::rng::entropy()).uuid_v4();
182 let dir = root(home).join(&id);
183 let staging = root(home).join(format!(".new-{id}"));
184 std::fs::create_dir(&staging)?;
185 let mut job = Job {
186 id: id.clone(),
187 talk,
188 cwd: std::env::current_dir()?,
189 command,
190 timeout,
191 supervisor: None,
192 child: None,
193 result: None,
194 tail: String::new(),
195 delivered: false,
196 };
197 save(&staging, &job)?;
198 std::fs::rename(&staging, &dir)?;
199 let mut cmd = Command::new(crate::updater::current_exe()?);
200 cmd.arg("talk")
201 .arg("watch-run")
202 .arg(home)
203 .arg(&id)
204 .env_remove("MAGI_RUN")
205 .env_remove("MAGI_NODE")
206 .stdin(Stdio::null())
207 .stdout(Stdio::null())
208 .stderr(Stdio::null());
209 #[cfg(unix)]
210 {
211 use std::os::unix::process::CommandExt;
212 unsafe {
214 cmd.pre_exec(|| {
215 if libc::setsid() < 0 {
216 return Err(std::io::Error::last_os_error());
217 }
218 Ok(())
219 });
220 }
221 }
222 #[cfg(windows)]
223 {
224 use std::os::windows::process::CommandExt;
225 cmd.creation_flags(0x0000_0008 | 0x0000_0200);
228 }
229 let mut supervisor = match cmd.spawn() {
230 Ok(child) => child,
231 Err(e) => {
232 job.result = Some(format!("supervisor startup failed: {e}"));
233 save(&dir, &job)?;
234 return Err(e.into());
235 }
236 };
237 let deadline = Instant::now() + Duration::from_secs(10);
238 loop {
239 let fresh = load(&dir)?;
240 if fresh.supervisor.is_some() && (fresh.child.is_some() || fresh.result.is_some()) {
241 std::thread::spawn(move || {
244 let _ = supervisor.wait();
245 });
246 return Ok(id);
247 }
248 if supervisor.try_wait()?.is_some() || Instant::now() >= deadline {
249 let _ = supervisor.kill();
250 let _ = supervisor.wait();
251 bail!(
252 "watch supervisor did not acknowledge startup; its durable reservation will be reconciled"
253 );
254 }
255 std::thread::sleep(Duration::from_millis(20));
256 }
257}
258
259fn identity(pid: u32) -> Result<(u32, String)> {
260 Ok((
261 pid,
262 crate::proc::process_started_at(pid).context("cannot identify watch process")?,
263 ))
264}
265
266fn append_tail(tail: &mut Vec<u8>, bytes: &[u8]) {
267 tail.extend_from_slice(bytes);
268 if tail.len() > OUTPUT_CAP {
269 tail.drain(..tail.len() - OUTPUT_CAP);
270 }
271}
272
273fn reader(
274 mut pipe: impl Pipe + Send + 'static,
275 tail: Arc<Mutex<Vec<u8>>>,
276 stopped: Arc<AtomicBool>,
277) -> std::thread::JoinHandle<()> {
278 std::thread::spawn(move || {
279 let mut chunk = [0; 4096];
280 if pipe.nonblocking().is_err() {
281 return;
282 }
283 let mut final_budget = None;
284 loop {
285 if stopped.load(Ordering::Acquire) {
288 let remaining = final_budget.get_or_insert(OUTPUT_CAP * 16);
289 if *remaining == 0 {
290 break;
291 }
292 }
293 match pipe.read_available(&mut chunk) {
294 Ok(0) => break,
295 Ok(n) => {
296 append_tail(
297 &mut tail.lock().unwrap_or_else(|e| e.into_inner()),
298 &chunk[..n],
299 );
300 if let Some(remaining) = final_budget.as_mut() {
301 *remaining = remaining.saturating_sub(n);
302 }
303 }
304 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
305 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
306 if stopped.load(Ordering::Acquire) {
307 break;
308 }
309 std::thread::sleep(Duration::from_millis(10));
310 }
311 Err(_) => break,
312 }
313 }
314 })
315}
316
317#[cfg(unix)]
318fn kill_group(pid: u32) {
319 if let Ok(pid) = libc::pid_t::try_from(pid) {
320 unsafe {
323 libc::kill(-pid, libc::SIGKILL);
324 }
325 }
326}
327
328#[cfg(not(unix))]
329fn kill_group(_pid: u32) {}
330
331fn watch_timeout(
334 startup_deadline: Instant,
335 execution_deadline: &mut Option<Instant>,
336 executing: bool,
337 timeout: u64,
338 now: Instant,
339) -> Option<String> {
340 if executing && execution_deadline.is_none() {
341 *execution_deadline = Some(now + Duration::from_secs(timeout));
342 }
343 match *execution_deadline {
344 Some(deadline) if now >= deadline => Some(format!(
345 "timed out after {timeout} second{}",
346 if timeout == 1 { "" } else { "s" }
347 )),
348 None if now >= startup_deadline => {
349 Some("startup failed: watch wrapper did not start the command".to_owned())
350 }
351 _ => None,
352 }
353}
354
355pub fn supervise(home: &Path, id: &str) -> Result<()> {
358 if id.contains(['/', '\\']) || id == "." || id == ".." {
359 bail!("invalid watch id");
360 }
361 let dir = root(home).join(id);
362 let _lease = lock(&dir.join("lease.lock"))?;
363 let mut job = load(&dir)?;
364 if job.result.is_some() || job.supervisor.is_some() {
365 return Ok(());
366 }
367 job.supervisor = Some(identity(std::process::id())?);
368 save(&dir, &job)?;
369 let mut cmd = Command::new(crate::updater::current_exe()?);
370 cmd.arg("talk")
371 .arg("watch-exec")
372 .arg(home)
373 .arg(id)
374 .current_dir(&job.cwd)
375 .env_remove("MAGI_RUN")
376 .env_remove("MAGI_NODE")
377 .stdin(Stdio::null())
378 .stdout(Stdio::piped())
379 .stderr(Stdio::piped());
380 cmd.quiet();
381 #[cfg(unix)]
382 {
383 use std::os::unix::process::CommandExt;
384 cmd.process_group(0);
385 }
386 let mut child = match cmd.spawn() {
387 Ok(c) => c,
388 Err(e) => {
389 job.result = Some(format!("startup failed: {e}"));
390 save(&dir, &job)?;
391 return Ok(());
392 }
393 };
394 #[cfg(windows)]
395 let containment = match windows::Containment::assign(&child) {
396 Ok(c) => c,
397 Err(e) => {
398 let _ = child.kill();
399 let _ = child.wait();
400 return Err(e.into());
401 }
402 };
403 let registered = identity(child.id()).and_then(|pid| {
405 job.child = Some(pid);
406 save(&dir, &job)
407 });
408 if let Err(e) = registered {
409 kill_group(child.id());
410 #[cfg(windows)]
411 drop(containment);
412 let _ = child.wait();
413 return Err(e);
414 }
415 std::fs::write(dir.join("armed"), b"ready")?;
419 let tail = Arc::new(Mutex::new(Vec::new()));
420 let stopped = Arc::new(AtomicBool::new(false));
421 let out = reader(
422 child.stdout.take().context("missing stdout")?,
423 tail.clone(),
424 stopped.clone(),
425 );
426 let err = reader(
427 child.stderr.take().context("missing stderr")?,
428 tail.clone(),
429 stopped.clone(),
430 );
431 let startup_deadline = Instant::now() + Duration::from_secs(10);
432 let mut execution_deadline = None;
433 let outcome = loop {
434 match child.try_wait() {
435 Ok(Some(status)) => {
436 #[cfg(unix)]
437 {
438 use std::os::unix::process::ExitStatusExt;
439 break status.code().map_or_else(
440 || format!("terminated by signal {}", status.signal().unwrap_or(0)),
441 |code| format!("exited with status {code}"),
442 );
443 }
444 #[cfg(not(unix))]
445 break status.code().map_or_else(
446 || "terminated without an exit code".to_owned(),
447 |code| format!("exited with status {code}"),
448 );
449 }
450 Err(e) => break format!("result unknown: {e}"),
451 Ok(None) => {
452 let executing = execution_deadline.is_some() || dir.join("executing").is_file();
453 if let Some(result) = watch_timeout(
454 startup_deadline,
455 &mut execution_deadline,
456 executing,
457 job.timeout,
458 Instant::now(),
459 ) {
460 break result;
461 }
462 std::thread::sleep(Duration::from_millis(20));
463 }
464 }
465 };
466 kill_group(child.id());
467 #[cfg(windows)]
468 drop(containment);
469 let _ = child.wait();
470 stopped.store(true, Ordering::Release);
471 let _ = out.join();
472 let _ = err.join();
473 job.tail =
474 String::from_utf8_lossy(&tail.lock().unwrap_or_else(|e| e.into_inner())).into_owned();
475 job.result = Some(match std::fs::read_to_string(dir.join("spawn-error")) {
476 Ok(error) => format!("startup failed: {error}"),
477 Err(_) => outcome,
478 });
479 save(&dir, &job)
480}
481
482pub fn sweep(home: &Path, talks: &Talks) {
485 if !root(home).exists() {
486 return;
487 }
488 let sweep = || -> Result<()> {
489 let _store = lock(&root(home).join("store.lock"))?;
490 for dir in directories(home)? {
491 let Some(_lease) = try_lock(&dir.join("lease.lock"))? else {
492 continue;
493 };
494 let result = reconcile(&dir, talks);
495 if let Err(e) = result {
496 tracing::warn!("watch completion not queued: {e:#}");
497 }
498 }
499 Ok(())
500 };
501 if let Err(e) = sweep() {
502 tracing::warn!("watch sweep: {e:#}");
503 }
504}
505
506fn reconcile(dir: &Path, talks: &Talks) -> Result<()> {
507 let mut job = load(dir)?;
508 if job.delivered {
509 return Ok(());
510 }
511 if job.result.is_none() {
512 if let Some((pid, started)) = &job.child {
513 match (
514 crate::proc::pid_status(*pid),
515 crate::proc::process_started_at(*pid),
516 ) {
517 (Some(false), _) => kill_group(*pid),
518 (Some(true), Some(current)) if current == *started => kill_group(*pid),
519 (Some(true), Some(_)) => {} _ => return Ok(()), }
522 }
523 job.result =
524 Some("result unknown: supervisor was lost; job will not be restarted".to_owned());
525 save(dir, &job)?;
526 }
527 let gone = || {
530 matches!(
531 std::fs::symlink_metadata(talks.path_of(&job.talk)),
532 Err(error) if error.kind() == std::io::ErrorKind::NotFound
533 )
534 };
535 let mut t = match talks.get(&job.talk) {
536 Ok(t) if t.status.open() => t,
537 Ok(_) => {
538 job.delivered = true;
539 return save(dir, &job);
540 }
541 Err(_) if gone() => {
542 job.delivered = true;
543 return save(dir, &job);
544 }
545 Err(e) => return Err(e),
546 };
547 let (text, display) = crate::prompt::chat_watch_report(
548 &job.id,
549 job.result.as_deref().unwrap_or("unknown"),
550 &job.tail,
551 );
552 if let Err(e) = talk::queue_notice(
553 &mut t,
554 talks,
555 &text,
556 &format!("watch:{}:finished", job.id),
557 &|_| false,
558 None,
559 display,
560 ) {
561 match talks.get(&job.talk) {
562 Ok(t) if !t.status.open() => {}
563 Err(_) if gone() => {}
564 _ => return Err(e),
565 }
566 }
567 job.delivered = true;
568 save(dir, &job)
569}
570
571pub fn execute(home: &Path, id: &str) -> Result<()> {
574 #[cfg(unix)]
575 use std::os::unix::process::CommandExt;
576 if id.contains(['/', '\\']) || id == "." || id == ".." {
577 bail!("invalid watch id");
578 }
579 let dir = root(home).join(id);
580 let deadline = Instant::now() + Duration::from_secs(10);
581 loop {
582 let job = load(&dir)?;
583 let (pid, started) = job.supervisor.as_ref().context("missing supervisor")?;
584 if job.result.is_some()
585 || crate::proc::pid_status(*pid) == Some(false)
586 || crate::proc::process_started_at(*pid).is_some_and(|s| s != *started)
587 {
588 bail!("watch supervisor was lost before launch");
589 }
590 if dir.join("armed").exists() {
591 if job.child.as_ref() != Some(&identity(std::process::id())?) {
592 bail!("watch launch belongs to a different process");
593 }
594 let mut command = Command::new(&job.command[0]);
595 command
596 .args(&job.command[1..])
597 .current_dir(&job.cwd)
598 .env_remove("MAGI_RUN")
599 .env_remove("MAGI_NODE");
600 command.quiet();
601 std::fs::write(dir.join("executing"), b"ready")?;
604 #[cfg(unix)]
605 let error = command.exec();
606 #[cfg(windows)]
607 let error = match command.status() {
608 Ok(status) => std::process::exit(status.code().unwrap_or(1)),
609 Err(error) => error,
610 };
611 std::fs::write(dir.join("spawn-error"), error.to_string())?;
612 return Err(error.into());
613 }
614 if Instant::now() >= deadline {
615 bail!("watch launch was not armed");
616 }
617 std::thread::sleep(Duration::from_millis(10));
618 }
619}
620
621trait Pipe: Read {
622 fn nonblocking(&self) -> std::io::Result<()>;
623 fn read_available(&mut self, bytes: &mut [u8]) -> std::io::Result<usize>;
624}
625
626#[cfg(unix)]
627impl<T: Read + std::os::fd::AsRawFd> Pipe for T {
628 fn nonblocking(&self) -> std::io::Result<()> {
629 let fd = self.as_raw_fd();
630 unsafe {
632 let flags = libc::fcntl(fd, libc::F_GETFL);
633 if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
634 return Err(std::io::Error::last_os_error());
635 }
636 }
637 Ok(())
638 }
639 fn read_available(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
640 self.read(bytes)
641 }
642}
643
644#[cfg(windows)]
645impl<T: Read + std::os::windows::io::AsRawHandle> Pipe for T {
646 fn nonblocking(&self) -> std::io::Result<()> {
647 Ok(())
648 }
649 fn read_available(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
650 let available = windows::available(self.as_raw_handle())?;
651 if available == 0 {
652 return Err(std::io::ErrorKind::WouldBlock.into());
653 }
654 let count = bytes.len().min(available as usize);
655 self.read(&mut bytes[..count])
656 }
657}
658
659#[cfg(windows)]
662pub(crate) mod windows {
663 use std::ffi::c_void;
664 use std::os::windows::io::AsRawHandle;
665 type Handle = *mut c_void;
666 #[repr(C)]
667 #[derive(Default)]
668 struct Overlapped {
669 internal: usize,
670 internal_high: usize,
671 offset: u32,
672 offset_high: u32,
673 event: Handle,
674 }
675 #[repr(C)]
676 #[derive(Default)]
677 struct BasicLimits {
678 process_time: i64,
679 job_time: i64,
680 flags: u32,
681 min_working_set: usize,
682 max_working_set: usize,
683 active_processes: u32,
684 affinity: usize,
685 priority: u32,
686 scheduling: u32,
687 }
688 #[repr(C)]
689 #[derive(Default)]
690 struct ExtendedLimits {
691 basic: BasicLimits,
692 io: [u64; 6],
693 process_memory: usize,
694 job_memory: usize,
695 peak_process_memory: usize,
696 peak_job_memory: usize,
697 }
698 #[link(name = "kernel32")]
699 unsafe extern "system" {
700 fn LockFileEx(
701 file: Handle,
702 flags: u32,
703 reserved: u32,
704 bytes: u32,
705 bytes_high: u32,
706 overlapped: *mut Overlapped,
707 ) -> i32;
708 fn UnlockFileEx(
709 file: Handle,
710 reserved: u32,
711 bytes: u32,
712 bytes_high: u32,
713 overlapped: *mut Overlapped,
714 ) -> i32;
715 fn CreateJobObjectW(attributes: Handle, name: *const u16) -> Handle;
716 fn SetInformationJobObject(job: Handle, class: i32, info: *const c_void, size: u32) -> i32;
717 fn AssignProcessToJobObject(job: Handle, process: Handle) -> i32;
718 fn CloseHandle(handle: Handle) -> i32;
719 fn GetCurrentProcess() -> Handle;
720 fn IsProcessInJob(process: Handle, job: Handle, result: *mut i32) -> i32;
721 fn QueryInformationJobObject(
722 job: Handle,
723 class: i32,
724 info: *mut c_void,
725 size: u32,
726 returned: *mut u32,
727 ) -> i32;
728 fn PeekNamedPipe(
729 pipe: Handle,
730 buffer: Handle,
731 size: u32,
732 read: *mut u32,
733 available: *mut u32,
734 left: *mut u32,
735 ) -> i32;
736 }
737 pub fn lock_file(file: Handle, immediate: bool) -> std::io::Result<bool> {
738 let mut overlapped = Overlapped::default();
739 if unsafe { LockFileEx(file, 2 | u32::from(immediate), 0, 1, 0, &mut overlapped) } != 0 {
740 return Ok(true);
741 }
742 let error = std::io::Error::last_os_error();
743 if immediate && error.raw_os_error() == Some(33) {
744 return Ok(false);
745 }
746 Err(error)
747 }
748 pub fn unlock_file(file: Handle) {
749 let mut overlapped = Overlapped::default();
750 unsafe {
751 UnlockFileEx(file, 0, 1, 0, &mut overlapped);
752 }
753 }
754 pub(crate) fn detached_flags() -> std::io::Result<u32> {
757 let mut in_job = 0;
758 if unsafe { IsProcessInJob(GetCurrentProcess(), std::ptr::null_mut(), &mut in_job) } == 0 {
759 return Err(std::io::Error::last_os_error());
760 }
761 if in_job == 0 {
762 return Ok(0x0000_0008 | 0x0000_0200);
763 }
764 let mut limits = BasicLimits::default();
765 if unsafe {
766 QueryInformationJobObject(
767 std::ptr::null_mut(),
768 2,
769 std::ptr::from_mut(&mut limits).cast(),
770 std::mem::size_of::<BasicLimits>() as u32,
771 std::ptr::null_mut(),
772 )
773 } == 0
774 {
775 return Err(std::io::Error::last_os_error());
776 }
777 let detached = 0x0000_0008 | 0x0000_0200;
778 if limits.flags & 0x1000 != 0 {
779 return Ok(detached);
780 } if limits.flags & 0x0800 != 0 {
782 return Ok(detached | 0x0100_0000);
783 } Ok(detached)
787 }
788
789 pub struct Containment(Handle);
790 impl Containment {
791 pub fn assign(child: &std::process::Child) -> std::io::Result<Self> {
792 unsafe {
793 let handle = CreateJobObjectW(std::ptr::null_mut(), std::ptr::null());
794 if handle.is_null() {
795 return Err(std::io::Error::last_os_error());
796 }
797 let job = Self(handle);
798 let mut limits = ExtendedLimits::default();
799 limits.basic.flags = 0x2000; if SetInformationJobObject(
801 handle,
802 9,
803 std::ptr::from_ref(&limits).cast(),
804 std::mem::size_of::<ExtendedLimits>() as u32,
805 ) == 0
806 || AssignProcessToJobObject(handle, child.as_raw_handle()) == 0
807 {
808 return Err(std::io::Error::last_os_error());
809 }
810 Ok(job)
811 }
812 }
813 }
814 impl Drop for Containment {
815 fn drop(&mut self) {
816 unsafe {
817 CloseHandle(self.0);
818 }
819 }
820 }
821 pub fn available(pipe: Handle) -> std::io::Result<u32> {
822 let mut available = 0;
823 if unsafe {
824 PeekNamedPipe(
825 pipe,
826 std::ptr::null_mut(),
827 0,
828 std::ptr::null_mut(),
829 &mut available,
830 std::ptr::null_mut(),
831 )
832 } == 0
833 {
834 return Err(std::io::Error::last_os_error());
835 }
836 Ok(available)
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use super::*;
843
844 struct EndlessOutput;
845 impl Read for EndlessOutput {
846 fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
847 bytes.fill(b'x');
848 Ok(bytes.len())
849 }
850 }
851 impl Pipe for EndlessOutput {
852 fn nonblocking(&self) -> std::io::Result<()> {
853 Ok(())
854 }
855 fn read_available(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
856 self.read(bytes)
857 }
858 }
859
860 #[test]
861 fn wrapper_startup_does_not_spend_the_command_timeout() {
862 let start = Instant::now();
863 let startup = start + Duration::from_secs(10);
864 let mut execution = None;
865 let ready = start + Duration::from_secs(9);
866 assert_eq!(
867 watch_timeout(startup, &mut execution, false, 1, ready),
868 None
869 );
870 assert_eq!(watch_timeout(startup, &mut execution, true, 1, ready), None);
871 assert_eq!(
872 watch_timeout(
873 startup,
874 &mut execution,
875 true,
876 1,
877 ready + Duration::from_millis(999)
878 ),
879 None
880 );
881 assert_eq!(
882 watch_timeout(
883 startup,
884 &mut execution,
885 false,
886 1,
887 ready + Duration::from_secs(1)
888 ),
889 Some("timed out after 1 second".into())
890 );
891 }
892
893 #[test]
894 fn an_unstarted_wrapper_has_a_bounded_startup_failure() {
895 let deadline = Instant::now();
896 let mut execution = None;
897 assert_eq!(
898 watch_timeout(deadline, &mut execution, false, 1, deadline),
899 Some("startup failed: watch wrapper did not start the command".into())
900 );
901 assert!(execution.is_none());
902 }
903
904 #[test]
905 fn stopped_readers_finish_even_if_an_inherited_pipe_never_stops_writing() {
906 let tail = Arc::new(Mutex::new(Vec::new()));
907 reader(EndlessOutput, tail.clone(), Arc::new(AtomicBool::new(true)))
908 .join()
909 .expect("reader finished");
910 assert_eq!(tail.lock().expect("tail").len(), OUTPUT_CAP);
911 }
912}