Skip to main content

dolang_vfs/process/
mod.rs

1use std::{
2    io::{self, IoSlice},
3    pin::Pin,
4    process::Stdio,
5    task::{Context, Poll},
6    time::Duration,
7};
8
9use dolang_rpc::handle::DefaultHandle;
10use serde::{Deserialize, Serialize};
11use tokio::{
12    fs::File,
13    io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadBuf},
14    task::JoinHandle,
15};
16
17use crate::{
18    STREAM_CHUNK_SIZE, SessionMode, Vfs, VfsInner, client, direct,
19    error::{Error, ErrorKind, Result},
20    path,
21    target::OperatingSystem,
22};
23
24mod foreign;
25
26pub(crate) use foreign::ProcessFamily;
27pub use foreign::{Process, ProcessExit, ProcessInfo, Processes, StartTime};
28
29/// Terminal status of a spawned process.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ProcessStatus {
32    Exited(i32),
33    Signaled(i32),
34}
35
36impl ProcessStatus {
37    /// Returns whether the process exited successfully.
38    pub const fn success(self) -> bool {
39        matches!(self, Self::Exited(0))
40    }
41
42    /// Returns the numeric exit code, if the process exited normally.
43    pub const fn code(self) -> Option<i32> {
44        match self {
45            Self::Exited(code) => Some(code),
46            Self::Signaled(_) => None,
47        }
48    }
49
50    /// Returns the native signal number, if a signal terminated the process.
51    pub const fn signal(self) -> Option<i32> {
52        match self {
53            Self::Exited(_) => None,
54            Self::Signaled(signal) => Some(signal),
55        }
56    }
57
58    pub(crate) fn from_native(status: std::process::ExitStatus) -> io::Result<Self> {
59        if let Some(code) = status.code() {
60            return Ok(Self::Exited(code));
61        }
62        #[cfg(unix)]
63        {
64            use std::os::unix::process::ExitStatusExt;
65            if let Some(signal) = status.signal() {
66                return Ok(Self::Signaled(signal));
67            }
68        }
69        Err(io::Error::new(
70            io::ErrorKind::InvalidData,
71            "process returned an unrepresentable terminal status",
72        ))
73    }
74}
75
76/// Whether a spawned process is attached to the foreground process group.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78pub enum ProcessControl {
79    /// Run in the foreground process group.
80    Foreground,
81    /// Run without foreground process-group control.
82    Background,
83}
84
85/// A Unix process signal.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[non_exhaustive]
88pub enum Signal {
89    Hup,
90    Int,
91    Quit,
92    Ill,
93    Trap,
94    Abrt,
95    Emt,
96    Fpe,
97    Kill,
98    Bus,
99    Segv,
100    Sys,
101    Pipe,
102    Alrm,
103    Term,
104    Urg,
105    Stop,
106    Tstp,
107    Cont,
108    Chld,
109    Ttin,
110    Ttou,
111    Io,
112    Xcpu,
113    Xfsz,
114    Vtalrm,
115    Prof,
116    Winch,
117    Info,
118    Usr1,
119    Usr2,
120    Stkflt,
121    Pwr,
122    Thr,
123    Librt,
124    Number(i32),
125}
126
127impl Signal {
128    /// Returns whether this signal exists on an operating system.
129    pub fn is_supported(self, operating_system: OperatingSystem) -> bool {
130        use OperatingSystem::{FreeBsd, Linux, Macos, Windows};
131        match self {
132            Self::Emt | Self::Info => matches!(operating_system, FreeBsd | Macos),
133            Self::Stkflt | Self::Pwr => operating_system == Linux,
134            Self::Thr | Self::Librt => operating_system == FreeBsd,
135            Self::Number(_) => operating_system != Windows,
136            _ => operating_system != Windows,
137        }
138    }
139}
140enum CommandInner<'a> {
141    Client(client::Command<'a>),
142    Direct(direct::Command<'a>),
143}
144
145/// Builds a process to spawn, relaying stdio across VFS domains when a
146/// given endpoint cannot be consumed directly by the spawn target.
147pub struct Command<'a> {
148    inner: CommandInner<'a>,
149    vfs: Vfs,
150    stdin: Option<StdioRecv>,
151    stdout: Option<StdioSend>,
152    stderr: Option<StdioSend>,
153}
154
155impl<'a> Command<'a> {
156    pub(crate) fn new(vfs: &'a Vfs, program: path::Path<'_>) -> Self {
157        let inner = match &vfs.inner {
158            VfsInner::Client(client) => CommandInner::Client(client.command(program)),
159            VfsInner::Direct(direct) => CommandInner::Direct(direct.command(program)),
160        };
161        Self {
162            inner,
163            vfs: vfs.clone(),
164            stdin: None,
165            stdout: None,
166            stderr: None,
167        }
168    }
169}
170
171enum ChildInner {
172    Client(client::Child),
173    Direct(Box<direct::Child>),
174}
175
176/// Tasks pumping bytes between a foreign-domain stdio endpoint and a pipe
177/// created in the spawn target's own domain, not yet started.
178///
179/// Relay tasks are only started once the underlying process has actually
180/// been spawned, so a failure setting up the spawn itself just drops the
181/// held endpoints (triggering their own best-effort cleanup) instead of
182/// leaking a running task.
183#[derive(Default)]
184struct PendingRelays {
185    inputs: Vec<(StdioRecv, StdioSend)>,
186    outputs: Vec<(StdioRecv, StdioSend)>,
187}
188
189impl PendingRelays {
190    fn start(self) -> ActiveRelays {
191        let spawn_all = |pairs: Vec<(StdioRecv, StdioSend)>| {
192            pairs
193                .into_iter()
194                .map(|(src, dst)| tokio::spawn(relay(src, dst)))
195                .collect()
196        };
197        ActiveRelays {
198            inputs: spawn_all(self.inputs),
199            outputs: spawn_all(self.outputs),
200        }
201    }
202}
203
204#[derive(Default)]
205struct ActiveRelays {
206    inputs: Vec<JoinHandle<()>>,
207    outputs: Vec<JoinHandle<()>>,
208}
209
210impl ActiveRelays {
211    fn abort_inputs(&mut self) {
212        for handle in self.inputs.drain(..) {
213            handle.abort();
214        }
215    }
216
217    /// Waits for output relays to finish flushing everything the child
218    /// already produced before it exited. Input relays are aborted instead
219    /// of awaited, since no further input can matter once the child is gone.
220    ///
221    /// Once the child has exited, its end of each output pipe is closed, so
222    /// the relay's copy loop reaches EOF and returns on its own; this just
223    /// ensures the caller can't observe a partially-relayed destination.
224    async fn finish(&mut self) {
225        self.abort_inputs();
226        for handle in self.outputs.drain(..) {
227            let _ = handle.await;
228        }
229    }
230
231    /// Aborts every relay task without waiting, for use when giving up on
232    /// the child without having confirmed it actually exited (e.g. `Drop`).
233    fn abandon(&mut self) {
234        self.abort_inputs();
235        for handle in self.outputs.drain(..) {
236            handle.abort();
237        }
238    }
239}
240
241impl Drop for ActiveRelays {
242    fn drop(&mut self) {
243        self.abandon();
244    }
245}
246
247/// A spawned process, possibly relaying stdio across VFS domains.
248pub struct Child {
249    inner: ChildInner,
250    relays: ActiveRelays,
251}
252
253impl Child {
254    /// Waits for the process to exit.
255    pub async fn wait(&mut self) -> Result<ProcessStatus> {
256        let status = match &mut self.inner {
257            ChildInner::Client(child) => child.wait().await,
258            ChildInner::Direct(child) => child.wait().await,
259        }?;
260        self.relays.finish().await;
261        Ok(status)
262    }
263
264    /// Terminates the process and returns its status when it has exited.
265    pub async fn terminate(mut self) -> Result<Option<ProcessStatus>> {
266        self.relays.abort_inputs();
267        let result = match self.inner {
268            ChildInner::Client(child) => child.terminate().await,
269            ChildInner::Direct(child) => child.terminate().await,
270        };
271        if result.as_ref().is_ok_and(Option::is_some) {
272            self.relays.finish().await;
273        } else {
274            self.relays.abandon();
275        }
276        result
277    }
278}
279
280/// Returns whether `stdio` can be handed directly to a process spawned in
281/// `target`'s domain without relaying.
282fn is_direct_recv(target: &Vfs, stdio: &StdioRecv) -> bool {
283    match (&target.inner, &stdio.0) {
284        (VfsInner::Direct(_), StdioRecvInner::Native(_)) => true,
285        (VfsInner::Client(client), StdioRecvInner::Remote(remote)) => {
286            client.is_same_vfs(remote.client())
287        }
288        (VfsInner::Client(client), StdioRecvInner::Native(_)) => {
289            client.mode() == SessionMode::Native
290        }
291        _ => false,
292    }
293}
294
295/// Returns whether `stdio` can be handed directly to a process spawned in
296/// `target`'s domain without relaying.
297fn is_direct_send(target: &Vfs, stdio: &StdioSend) -> bool {
298    match (&target.inner, &stdio.0) {
299        (VfsInner::Direct(_), StdioSendInner::Native(_)) => true,
300        (VfsInner::Client(client), StdioSendInner::Remote(remote)) => {
301            client.is_same_vfs(remote.client())
302        }
303        (VfsInner::Client(client), StdioSendInner::Native(_)) => {
304            client.mode() == SessionMode::Native
305        }
306        _ => false,
307    }
308}
309
310/// Classifies a stdin endpoint for a process about to be spawned in
311/// `target`'s domain, creating a relay pipe and queuing a pump task in
312/// `relays` if the endpoint cannot be consumed directly.
313async fn classify_recv(
314    target: &Vfs,
315    stdio: StdioRecv,
316    relays: &mut PendingRelays,
317) -> Result<StdioRecv> {
318    if is_direct_recv(target, &stdio) {
319        return Ok(stdio);
320    }
321    let (send, recv) = target.pipe(None).await?;
322    relays.inputs.push((stdio, send));
323    Ok(recv)
324}
325
326/// Classifies a stdout/stderr endpoint for a process about to be spawned in
327/// `target`'s domain, creating a relay pipe and queuing a pump task in
328/// `relays` if the endpoint cannot be consumed directly.
329async fn classify_send(
330    target: &Vfs,
331    stdio: StdioSend,
332    relays: &mut PendingRelays,
333) -> Result<StdioSend> {
334    if is_direct_send(target, &stdio) {
335        return Ok(stdio);
336    }
337    let (send, recv) = target.pipe(None).await?;
338    relays.outputs.push((recv, stdio));
339    Ok(send)
340}
341
342impl<'a> Command<'a> {
343    /// Appends an argument to the program invocation.
344    pub fn arg(&mut self, arg: &str) -> &mut Self {
345        match &mut self.inner {
346            CommandInner::Client(builder) => {
347                builder.arg(arg);
348            }
349            CommandInner::Direct(builder) => {
350                builder.arg(arg);
351            }
352        }
353        self
354    }
355
356    /// Sets an environment variable for the child.
357    pub fn env(&mut self, key: &str, val: &str) -> &mut Self {
358        match &mut self.inner {
359            CommandInner::Client(builder) => {
360                builder.env(key, val);
361            }
362            CommandInner::Direct(builder) => {
363                builder.env(key, val);
364            }
365        }
366        self
367    }
368
369    /// Removes an environment variable from the child.
370    pub fn env_remove(&mut self, key: &str) -> &mut Self {
371        match &mut self.inner {
372            CommandInner::Client(builder) => {
373                builder.env_remove(key);
374            }
375            CommandInner::Direct(builder) => {
376                builder.env_remove(key);
377            }
378        }
379        self
380    }
381
382    /// Sets the child's working directory.
383    pub fn current_dir(&mut self, dir: path::Path<'_>) -> &mut Self {
384        match &mut self.inner {
385            CommandInner::Client(builder) => {
386                builder.current_dir(dir);
387            }
388            CommandInner::Direct(builder) => {
389                builder.current_dir(dir);
390            }
391        }
392        self
393    }
394
395    /// Sets the child's standard input.
396    pub fn stdin(&mut self, stdio: StdioRecv) -> Result<&mut Self> {
397        self.stdin = Some(stdio);
398        Ok(self)
399    }
400
401    /// Sets the child's standard output.
402    pub fn stdout(&mut self, stdio: StdioSend) -> Result<&mut Self> {
403        self.stdout = Some(stdio);
404        Ok(self)
405    }
406
407    /// Inherit the host process's standard input.
408    ///
409    /// Opaque remote clients treat terminal input as null because Tokio cannot
410    /// cancel an outstanding terminal read. Redirected input is relayed to the
411    /// remote process.
412    pub fn stdin_inherit(&mut self) -> Result<&mut Self> {
413        self.stdin = None;
414        match &mut self.inner {
415            CommandInner::Client(builder) => {
416                builder.stdin_inherit()?;
417            }
418            CommandInner::Direct(builder) => {
419                builder.stdin_inherit()?;
420            }
421        }
422        Ok(self)
423    }
424
425    /// Inherits the host process's standard output.
426    pub fn stdout_inherit(&mut self) -> Result<&mut Self> {
427        self.stdout = None;
428        match &mut self.inner {
429            CommandInner::Client(builder) => {
430                builder.stdout_inherit()?;
431            }
432            CommandInner::Direct(builder) => {
433                builder.stdout_inherit()?;
434            }
435        }
436        Ok(self)
437    }
438
439    /// Connects the child's standard output to the parent process's standard
440    /// error.
441    pub fn stdout_inherit_stderr(&mut self) -> Result<&mut Self> {
442        self.stdout = None;
443        match &mut self.inner {
444            CommandInner::Client(builder) => {
445                builder.stdout_inherit_stderr()?;
446            }
447            CommandInner::Direct(builder) => {
448                builder.stdout_inherit_stderr()?;
449            }
450        }
451        Ok(self)
452    }
453
454    /// Connects the child's standard input to the null device.
455    pub fn stdin_null(&mut self) -> &mut Self {
456        self.stdin = None;
457        match &mut self.inner {
458            CommandInner::Client(builder) => {
459                builder.stdin_null();
460            }
461            CommandInner::Direct(builder) => {
462                builder.stdin_null();
463            }
464        }
465        self
466    }
467
468    /// Connects the child's standard output to the null device.
469    pub fn stdout_null(&mut self) -> &mut Self {
470        self.stdout = None;
471        match &mut self.inner {
472            CommandInner::Client(builder) => {
473                builder.stdout_null();
474            }
475            CommandInner::Direct(builder) => {
476                builder.stdout_null();
477            }
478        }
479        self
480    }
481
482    /// Sets the child's standard error.
483    pub fn stderr(&mut self, stdio: StdioSend) -> Result<&mut Self> {
484        self.stderr = Some(stdio);
485        Ok(self)
486    }
487
488    /// Inherits the host process's standard error.
489    pub fn stderr_inherit(&mut self) -> Result<&mut Self> {
490        self.stderr = None;
491        match &mut self.inner {
492            CommandInner::Client(builder) => {
493                builder.stderr_inherit()?;
494            }
495            CommandInner::Direct(builder) => {
496                builder.stderr_inherit()?;
497            }
498        }
499        Ok(self)
500    }
501
502    /// Connects the child's standard error to the same destination as its
503    /// configured standard output.
504    pub fn stderr_to_stdout(&mut self) -> Result<&mut Self> {
505        self.stderr = None;
506        match &mut self.inner {
507            CommandInner::Client(builder) => {
508                builder.stderr_to_stdout()?;
509            }
510            CommandInner::Direct(builder) => {
511                builder.stderr_to_stdout()?;
512            }
513        }
514        Ok(self)
515    }
516
517    /// Connects the child's standard error to the parent process's standard
518    /// output.
519    pub fn stderr_inherit_stdout(&mut self) -> Result<&mut Self> {
520        self.stderr = None;
521        match &mut self.inner {
522            CommandInner::Client(builder) => {
523                builder.stderr_inherit_stdout()?;
524            }
525            CommandInner::Direct(builder) => {
526                builder.stderr_inherit_stdout()?;
527            }
528        }
529        Ok(self)
530    }
531
532    /// Connects the child's standard error to the null device.
533    pub fn stderr_null(&mut self) -> &mut Self {
534        self.stderr = None;
535        match &mut self.inner {
536            CommandInner::Client(builder) => {
537                builder.stderr_null();
538            }
539            CommandInner::Direct(builder) => {
540                builder.stderr_null();
541            }
542        }
543        self
544    }
545
546    /// Sets foreground or background process control behavior.
547    pub fn process_control(&mut self, control: ProcessControl) -> &mut Self {
548        match &mut self.inner {
549            CommandInner::Client(builder) => {
550                builder.process_control(control);
551            }
552            CommandInner::Direct(builder) => {
553                builder.process_control(control);
554            }
555        }
556        self
557    }
558
559    /// Sets the policy used to terminate the child.
560    pub fn termination_policy(&mut self, policy: TerminationPolicy) -> &mut Self {
561        match &mut self.inner {
562            CommandInner::Client(builder) => {
563                builder.termination_policy(policy);
564            }
565            CommandInner::Direct(builder) => {
566                builder.termination_policy(policy);
567            }
568        }
569        self
570    }
571
572    /// Spawns the configured process.
573    pub async fn spawn(mut self) -> Result<Child> {
574        let mut relays = PendingRelays::default();
575        if let Some(stdio) = self.stdin.take() {
576            let stdio = classify_recv(&self.vfs, stdio, &mut relays).await?;
577            match &mut self.inner {
578                CommandInner::Client(builder) => {
579                    builder.stdin(stdio)?;
580                }
581                CommandInner::Direct(builder) => {
582                    builder.stdin(stdio)?;
583                }
584            }
585        }
586        if let Some(stdio) = self.stdout.take() {
587            let stdio = classify_send(&self.vfs, stdio, &mut relays).await?;
588            match &mut self.inner {
589                CommandInner::Client(builder) => {
590                    builder.stdout(stdio)?;
591                }
592                CommandInner::Direct(builder) => {
593                    builder.stdout(stdio)?;
594                }
595            }
596        }
597        if let Some(stdio) = self.stderr.take() {
598            let stdio = classify_send(&self.vfs, stdio, &mut relays).await?;
599            match &mut self.inner {
600                CommandInner::Client(builder) => {
601                    builder.stderr(stdio)?;
602                }
603                CommandInner::Direct(builder) => {
604                    builder.stderr(stdio)?;
605                }
606            }
607        }
608        let inner = match self.inner {
609            CommandInner::Client(builder) => builder.spawn().await.map(ChildInner::Client),
610            CommandInner::Direct(builder) => builder
611                .spawn()
612                .await
613                .map(|x| ChildInner::Direct(Box::new(x))),
614        }?;
615        Ok(Child {
616            inner,
617            relays: relays.start(),
618        })
619    }
620}
621
622/// Policy used to terminate a spawned process.
623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
624pub struct TerminationPolicy {
625    /// Signal sent to request graceful termination.
626    pub(crate) signal: Signal,
627    /// Time to wait before optional forced termination.
628    pub(crate) grace: Duration,
629    /// Whether to force termination after the grace period.
630    pub(crate) force: bool,
631}
632
633impl TerminationPolicy {
634    /// Creates a process-termination policy.
635    pub const fn new(signal: Signal, grace: Duration, force: bool) -> Self {
636        Self {
637            signal,
638            grace,
639            force,
640        }
641    }
642    /// Returns the signal used to request graceful termination.
643    pub const fn signal(self) -> Signal {
644        self.signal
645    }
646    /// Returns the grace period before forced termination.
647    pub const fn grace(self) -> Duration {
648        self.grace
649    }
650    /// Returns whether the process is forcibly terminated after the grace period.
651    pub const fn force(self) -> bool {
652        self.force
653    }
654    /// Sets the graceful-termination signal.
655    pub fn set_signal(&mut self, signal: Signal) -> &mut Self {
656        self.signal = signal;
657        self
658    }
659    /// Sets the grace period before forced termination.
660    pub fn set_grace(&mut self, grace: Duration) -> &mut Self {
661        self.grace = grace;
662        self
663    }
664    /// Selects whether to force termination after the grace period.
665    pub fn set_force(&mut self, force: bool) -> &mut Self {
666        self.force = force;
667        self
668    }
669}
670
671impl Default for TerminationPolicy {
672    fn default() -> Self {
673        Self {
674            signal: Signal::Term,
675            grace: Duration::from_secs(5),
676            force: true,
677        }
678    }
679}
680
681#[cfg(test)]
682mod policy_tests {
683    use super::{Signal, TerminationPolicy};
684    use std::time::Duration;
685
686    #[test]
687    fn termination_policy_construction_and_mutation() {
688        let mut policy = TerminationPolicy::new(Signal::Int, Duration::from_secs(2), false);
689        assert_eq!(policy.signal(), Signal::Int);
690        assert_eq!(policy.grace(), Duration::from_secs(2));
691        assert!(!policy.force());
692        policy
693            .set_signal(Signal::Term)
694            .set_grace(Duration::from_secs(3))
695            .set_force(true);
696        assert_eq!(
697            policy,
698            TerminationPolicy::new(Signal::Term, Duration::from_secs(3), true)
699        );
700    }
701}
702
703#[cfg(unix)]
704use std::os::fd::{AsFd, OwnedFd};
705#[cfg(windows)]
706use std::{
707    io::{PipeReader, PipeWriter, Read as _, Write as _},
708    os::windows::io::OwnedHandle,
709    sync::Arc,
710};
711/// A writable standard-I/O endpoint from either a local or remote VFS.
712#[derive(Debug)]
713pub struct StdioSend(pub(crate) StdioSendInner);
714
715#[derive(Debug)]
716pub(crate) enum StdioSendInner {
717    /// Endpoint backed by a local OS resource.
718    Native(NativeStdioSend),
719    /// Endpoint retained by a remote VFS session.
720    Remote(crate::client::RemoteStdioSend),
721}
722
723/// A readable standard-I/O endpoint from either a local or remote VFS.
724#[derive(Debug)]
725pub struct StdioRecv(pub(crate) StdioRecvInner);
726
727#[derive(Debug)]
728pub(crate) enum StdioRecvInner {
729    /// Endpoint backed by a local OS resource.
730    Native(NativeStdioRecv),
731    /// Endpoint retained by a remote VFS session.
732    Remote(crate::client::RemoteStdioRecv),
733}
734
735#[cfg(unix)]
736/// Local writable standard-I/O resource on Unix.
737#[derive(Debug)]
738pub(crate) enum NativeStdioSend {
739    /// Unix pipe sender.
740    Pipe(tokio::net::unix::pipe::Sender),
741    /// Asynchronous file.
742    File(File),
743}
744
745#[cfg(unix)]
746/// Local readable standard-I/O resource on Unix.
747#[derive(Debug)]
748pub(crate) enum NativeStdioRecv {
749    /// Unix pipe receiver.
750    Pipe(tokio::net::unix::pipe::Receiver),
751    /// Asynchronous file.
752    File(File),
753}
754
755#[cfg(windows)]
756#[derive(Debug)]
757pub(crate) enum NativeStdioSend {
758    Pipe {
759        inner: Arc<PipeWriter>,
760        pending: Option<JoinHandle<io::Result<usize>>>,
761    },
762    File(File),
763}
764
765#[cfg(windows)]
766#[derive(Debug)]
767pub(crate) enum NativeStdioRecv {
768    Pipe {
769        inner: Arc<PipeReader>,
770        pending: Option<JoinHandle<(Vec<u8>, io::Result<usize>)>>,
771        ready: Option<(Vec<u8>, usize)>,
772    },
773    File(File),
774}
775
776/// Pumps bytes from `src` to `dst` until EOF or error, then shuts `dst` down.
777pub(crate) async fn relay<R, W>(src: R, mut dst: W)
778where
779    R: AsyncRead + Unpin,
780    W: AsyncWrite + Unpin,
781{
782    let mut src = BufReader::with_capacity(STREAM_CHUNK_SIZE, src);
783    let _ = tokio::io::copy_buf(&mut src, &mut dst).await;
784    let _ = dst.shutdown().await;
785}
786
787/// Creates a native OS pipe, optionally hinting a kernel buffer size.
788///
789/// The hint is best-effort: on platforms/configurations where it can't be
790/// honored, the pipe is created with its default buffer size instead of
791/// failing outright.
792pub(crate) fn pipe(buf_size: Option<usize>) -> io::Result<(StdioSend, StdioRecv)> {
793    #[cfg(unix)]
794    {
795        let (send, recv) = tokio::net::unix::pipe::pipe()?;
796        if let Some(size) = buf_size {
797            use std::os::fd::AsRawFd;
798            set_pipe_buffer_size(send.as_raw_fd(), size);
799        }
800        Ok((
801            StdioSend(StdioSendInner::Native(NativeStdioSend::Pipe(send))),
802            StdioRecv(StdioRecvInner::Native(NativeStdioRecv::Pipe(recv))),
803        ))
804    }
805    #[cfg(windows)]
806    {
807        let (recv, send) = match buf_size {
808            Some(size) => create_pipe_sized(size)?,
809            None => std::io::pipe()?,
810        };
811        Ok((
812            StdioSend(StdioSendInner::Native(NativeStdioSend::Pipe {
813                inner: Arc::new(send),
814                pending: None,
815            })),
816            StdioRecv(StdioRecvInner::Native(NativeStdioRecv::Pipe {
817                inner: Arc::new(recv),
818                pending: None,
819                ready: None,
820            })),
821        ))
822    }
823}
824
825/// Resizes a pipe's kernel buffer via `fcntl(F_SETPIPE_SZ)`, applied to
826/// either end (the buffer is shared by both). No raw-handle bypass is
827/// needed: tokio's pipe types expose the fd directly via `AsRawFd`.
828#[cfg(target_os = "linux")]
829pub(crate) fn set_pipe_buffer_size(fd: std::os::fd::RawFd, size: usize) {
830    let size = i32::try_from(size).unwrap_or(i32::MAX);
831    // Best-effort: failure (e.g. exceeding /proc/sys/fs/pipe-max-size
832    // without CAP_SYS_RESOURCE) just leaves the default buffer size.
833    unsafe {
834        libc::fcntl(fd, libc::F_SETPIPE_SZ, size);
835    }
836}
837
838#[cfg(all(unix, not(target_os = "linux")))]
839pub(crate) fn set_pipe_buffer_size(_fd: std::os::fd::RawFd, _size: usize) {}
840
841/// Creates an anonymous pipe with a requested kernel buffer size.
842///
843/// `std::io::pipe` has no size parameter, so this bypasses it and calls
844/// `CreatePipe` directly with `bInheritHandle = FALSE`, matching the
845/// non-inheritable handles `std::io::pipe` itself produces.
846#[cfg(windows)]
847fn create_pipe_sized(size: usize) -> io::Result<(std::io::PipeReader, std::io::PipeWriter)> {
848    use std::os::windows::io::FromRawHandle;
849
850    use windows_sys::Win32::{
851        Foundation::HANDLE, Security::SECURITY_ATTRIBUTES, System::Pipes::CreatePipe,
852    };
853
854    let mut read_handle: HANDLE = std::ptr::null_mut();
855    let mut write_handle: HANDLE = std::ptr::null_mut();
856    let attrs = SECURITY_ATTRIBUTES {
857        nLength: size_of::<SECURITY_ATTRIBUTES>() as u32,
858        lpSecurityDescriptor: std::ptr::null_mut(),
859        bInheritHandle: 0,
860    };
861    let size = u32::try_from(size).unwrap_or(u32::MAX);
862    // SAFETY: `read_handle`/`write_handle` are valid out-params for the
863    // duration of this call; `attrs` lives on the stack until it returns.
864    let ok = unsafe { CreatePipe(&mut read_handle, &mut write_handle, &attrs, size) };
865    if ok == 0 {
866        return Err(io::Error::last_os_error());
867    }
868    // SAFETY: `read_handle`/`write_handle` are freshly created, uniquely
869    // owned handles from a successful `CreatePipe` call.
870    let reader = unsafe { std::io::PipeReader::from_raw_handle(read_handle as _) };
871    let writer = unsafe { std::io::PipeWriter::from_raw_handle(write_handle as _) };
872    Ok((reader, writer))
873}
874
875impl StdioSend {
876    /// Creates a writable child-stdio endpoint from a VFS file.
877    pub fn from_file(file: File) -> Self {
878        Self(StdioSendInner::Native(NativeStdioSend::File(file)))
879    }
880
881    pub(crate) fn remote(remote: crate::client::RemoteStdioSend) -> Self {
882        Self(StdioSendInner::Remote(remote))
883    }
884
885    /// Clones this writable child-stdio endpoint.
886    pub async fn try_clone(&self) -> Result<Self> {
887        match &self.0 {
888            #[cfg(unix)]
889            StdioSendInner::Native(NativeStdioSend::Pipe(pipe)) => {
890                let fd = pipe.as_fd().try_clone_to_owned()?;
891                Ok(Self(StdioSendInner::Native(NativeStdioSend::Pipe(
892                    tokio::net::unix::pipe::Sender::from_owned_fd_unchecked(fd)?,
893                ))))
894            }
895            #[cfg(windows)]
896            StdioSendInner::Native(NativeStdioSend::Pipe { inner, .. }) => {
897                Ok(Self(StdioSendInner::Native(NativeStdioSend::Pipe {
898                    inner: Arc::new(inner.try_clone()?),
899                    pending: None,
900                })))
901            }
902            StdioSendInner::Native(NativeStdioSend::File(file)) => Ok(Self(
903                StdioSendInner::Native(NativeStdioSend::File(file.try_clone().await?)),
904            )),
905            StdioSendInner::Remote(remote) => Ok(Self::remote(remote.try_clone().await?)),
906        }
907    }
908
909    /// Converts this endpoint into native process stdio.
910    ///
911    /// Remote endpoints cannot be converted.
912    pub async fn into_stdio(self) -> Result<Stdio> {
913        match self.0 {
914            StdioSendInner::Native(NativeStdioSend::File(file)) => {
915                Ok(Stdio::from(file.into_std().await))
916            }
917            #[cfg(unix)]
918            StdioSendInner::Native(NativeStdioSend::Pipe(pipe)) => {
919                let fd: OwnedFd = pipe.into_blocking_fd()?;
920                Ok(Stdio::from(fd))
921            }
922            #[cfg(windows)]
923            StdioSendInner::Native(NativeStdioSend::Pipe { inner, pending }) => {
924                if pending.is_some() {
925                    return Err(Error::new(
926                        ErrorKind::ResourceBusy,
927                        "cannot convert StdioSend while an async write is in flight",
928                    ));
929                }
930                Ok(Arc::try_unwrap(inner)
931                    .or_else(|inner| inner.try_clone())
932                    .map(Stdio::from)?)
933            }
934            StdioSendInner::Remote(_) => Err(Error::new(
935                ErrorKind::InvalidInput,
936                "remote stdio cannot be converted to a native handle",
937            )),
938        }
939    }
940
941    pub(crate) async fn into_blocking_handle(self) -> io::Result<DefaultHandle> {
942        match self.0 {
943            StdioSendInner::Native(NativeStdioSend::File(file)) => Ok(file.into_std().await.into()),
944            #[cfg(unix)]
945            StdioSendInner::Native(NativeStdioSend::Pipe(pipe)) => pipe.into_blocking_fd(),
946            #[cfg(windows)]
947            StdioSendInner::Native(NativeStdioSend::Pipe { inner, pending }) => {
948                if pending.is_some() {
949                    return Err(io::Error::other(
950                        "cannot convert StdioSend while an async write is in flight",
951                    ));
952                }
953                let pipe = Arc::try_unwrap(inner).or_else(|inner| inner.try_clone())?;
954                Ok(OwnedHandle::from(pipe))
955            }
956            StdioSendInner::Remote(_) => Err(io::Error::new(
957                io::ErrorKind::InvalidInput,
958                "remote stdio has no native handle",
959            )),
960        }
961    }
962}
963
964impl StdioRecv {
965    /// Creates a readable child-stdio endpoint from a VFS file.
966    pub fn from_file(file: File) -> Self {
967        Self(StdioRecvInner::Native(NativeStdioRecv::File(file)))
968    }
969
970    pub(crate) fn remote(remote: crate::client::RemoteStdioRecv) -> Self {
971        Self(StdioRecvInner::Remote(remote))
972    }
973
974    /// Clones this readable child-stdio endpoint.
975    pub async fn try_clone(&self) -> Result<Self> {
976        match &self.0 {
977            #[cfg(unix)]
978            StdioRecvInner::Native(NativeStdioRecv::Pipe(pipe)) => {
979                let fd = pipe.as_fd().try_clone_to_owned()?;
980                Ok(Self(StdioRecvInner::Native(NativeStdioRecv::Pipe(
981                    tokio::net::unix::pipe::Receiver::from_owned_fd_unchecked(fd)?,
982                ))))
983            }
984            #[cfg(windows)]
985            StdioRecvInner::Native(NativeStdioRecv::Pipe { inner, .. }) => {
986                Ok(Self(StdioRecvInner::Native(NativeStdioRecv::Pipe {
987                    inner: Arc::new(inner.try_clone()?),
988                    pending: None,
989                    ready: None,
990                })))
991            }
992            StdioRecvInner::Native(NativeStdioRecv::File(file)) => Ok(Self(
993                StdioRecvInner::Native(NativeStdioRecv::File(file.try_clone().await?)),
994            )),
995            StdioRecvInner::Remote(remote) => Ok(Self::remote(remote.try_clone().await?)),
996        }
997    }
998
999    /// Converts this endpoint into native process stdio.
1000    ///
1001    /// Remote endpoints cannot be converted.
1002    pub async fn into_stdio(self) -> Result<Stdio> {
1003        match self.0 {
1004            StdioRecvInner::Native(NativeStdioRecv::File(file)) => {
1005                Ok(Stdio::from(file.into_std().await))
1006            }
1007            #[cfg(unix)]
1008            StdioRecvInner::Native(NativeStdioRecv::Pipe(pipe)) => {
1009                let fd: OwnedFd = pipe.into_blocking_fd()?;
1010                Ok(Stdio::from(fd))
1011            }
1012            #[cfg(windows)]
1013            StdioRecvInner::Native(NativeStdioRecv::Pipe { inner, pending, .. }) => {
1014                if pending.is_some() {
1015                    return Err(Error::new(
1016                        ErrorKind::ResourceBusy,
1017                        "cannot convert StdioRecv while an async read is in flight",
1018                    ));
1019                }
1020                Ok(Arc::try_unwrap(inner)
1021                    .or_else(|inner| inner.try_clone())
1022                    .map(Stdio::from)?)
1023            }
1024            StdioRecvInner::Remote(_) => Err(Error::new(
1025                ErrorKind::InvalidInput,
1026                "remote stdio cannot be converted to a native handle",
1027            )),
1028        }
1029    }
1030
1031    pub(crate) async fn into_blocking_handle(self) -> io::Result<DefaultHandle> {
1032        match self.0 {
1033            StdioRecvInner::Native(NativeStdioRecv::File(file)) => Ok(file.into_std().await.into()),
1034            #[cfg(unix)]
1035            StdioRecvInner::Native(NativeStdioRecv::Pipe(pipe)) => pipe.into_blocking_fd(),
1036            #[cfg(windows)]
1037            StdioRecvInner::Native(NativeStdioRecv::Pipe { inner, pending, .. }) => {
1038                if pending.is_some() {
1039                    return Err(io::Error::other(
1040                        "cannot convert StdioRecv while an async read is in flight",
1041                    ));
1042                }
1043                let pipe = Arc::try_unwrap(inner).or_else(|inner| inner.try_clone())?;
1044                Ok(OwnedHandle::from(pipe))
1045            }
1046            StdioRecvInner::Remote(_) => Err(io::Error::new(
1047                io::ErrorKind::InvalidInput,
1048                "remote stdio has no native handle",
1049            )),
1050        }
1051    }
1052}
1053
1054impl AsyncWrite for StdioSend {
1055    fn poll_write(
1056        mut self: Pin<&mut Self>,
1057        cx: &mut Context<'_>,
1058        buf: &[u8],
1059    ) -> Poll<io::Result<usize>> {
1060        match &mut self.0 {
1061            StdioSendInner::Native(native) => Pin::new(native).poll_write(cx, buf),
1062            StdioSendInner::Remote(remote) => Pin::new(remote).poll_write(cx, buf),
1063        }
1064    }
1065
1066    fn poll_write_vectored(
1067        mut self: Pin<&mut Self>,
1068        cx: &mut Context<'_>,
1069        bufs: &[IoSlice<'_>],
1070    ) -> Poll<io::Result<usize>> {
1071        match &mut self.0 {
1072            StdioSendInner::Native(native) => Pin::new(native).poll_write_vectored(cx, bufs),
1073            StdioSendInner::Remote(remote) => Pin::new(remote).poll_write_vectored(cx, bufs),
1074        }
1075    }
1076
1077    fn is_write_vectored(&self) -> bool {
1078        match &self.0 {
1079            StdioSendInner::Native(native) => native.is_write_vectored(),
1080            StdioSendInner::Remote(remote) => remote.is_write_vectored(),
1081        }
1082    }
1083
1084    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1085        match &mut self.0 {
1086            StdioSendInner::Native(native) => Pin::new(native).poll_flush(cx),
1087            StdioSendInner::Remote(remote) => Pin::new(remote).poll_flush(cx),
1088        }
1089    }
1090
1091    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1092        match &mut self.0 {
1093            StdioSendInner::Native(native) => Pin::new(native).poll_shutdown(cx),
1094            StdioSendInner::Remote(remote) => Pin::new(remote).poll_shutdown(cx),
1095        }
1096    }
1097}
1098
1099impl AsyncRead for StdioRecv {
1100    fn poll_read(
1101        mut self: Pin<&mut Self>,
1102        cx: &mut Context<'_>,
1103        buf: &mut ReadBuf<'_>,
1104    ) -> Poll<io::Result<()>> {
1105        match &mut self.0 {
1106            StdioRecvInner::Native(native) => Pin::new(native).poll_read(cx, buf),
1107            StdioRecvInner::Remote(remote) => Pin::new(remote).poll_read(cx, buf),
1108        }
1109    }
1110}
1111
1112#[cfg(unix)]
1113impl AsyncWrite for NativeStdioSend {
1114    fn poll_write(
1115        mut self: Pin<&mut Self>,
1116        cx: &mut Context<'_>,
1117        buf: &[u8],
1118    ) -> Poll<io::Result<usize>> {
1119        match &mut *self {
1120            Self::Pipe(pipe) => Pin::new(pipe).poll_write(cx, buf),
1121            Self::File(file) => Pin::new(file).poll_write(cx, buf),
1122        }
1123    }
1124    fn poll_write_vectored(
1125        mut self: Pin<&mut Self>,
1126        cx: &mut Context<'_>,
1127        bufs: &[IoSlice<'_>],
1128    ) -> Poll<io::Result<usize>> {
1129        match &mut *self {
1130            Self::Pipe(pipe) => Pin::new(pipe).poll_write_vectored(cx, bufs),
1131            Self::File(file) => Pin::new(file).poll_write_vectored(cx, bufs),
1132        }
1133    }
1134    fn is_write_vectored(&self) -> bool {
1135        match self {
1136            Self::Pipe(pipe) => pipe.is_write_vectored(),
1137            Self::File(file) => file.is_write_vectored(),
1138        }
1139    }
1140    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1141        match &mut *self {
1142            Self::Pipe(pipe) => Pin::new(pipe).poll_flush(cx),
1143            Self::File(file) => Pin::new(file).poll_flush(cx),
1144        }
1145    }
1146    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1147        match &mut *self {
1148            Self::Pipe(pipe) => Pin::new(pipe).poll_shutdown(cx),
1149            Self::File(file) => Pin::new(file).poll_shutdown(cx),
1150        }
1151    }
1152}
1153
1154#[cfg(unix)]
1155impl AsyncRead for NativeStdioRecv {
1156    fn poll_read(
1157        mut self: Pin<&mut Self>,
1158        cx: &mut Context<'_>,
1159        buf: &mut ReadBuf<'_>,
1160    ) -> Poll<io::Result<()>> {
1161        match &mut *self {
1162            Self::Pipe(pipe) => Pin::new(pipe).poll_read(cx, buf),
1163            Self::File(file) => Pin::new(file).poll_read(cx, buf),
1164        }
1165    }
1166}
1167
1168#[cfg(windows)]
1169impl AsyncWrite for NativeStdioSend {
1170    fn poll_write(
1171        mut self: Pin<&mut Self>,
1172        cx: &mut Context<'_>,
1173        buf: &[u8],
1174    ) -> Poll<io::Result<usize>> {
1175        match &mut *self {
1176            Self::File(file) => Pin::new(file).poll_write(cx, buf),
1177            Self::Pipe { inner, pending } => {
1178                if let Some(task) = pending {
1179                    return match Pin::new(task).poll(cx) {
1180                        Poll::Pending => Poll::Pending,
1181                        Poll::Ready(Ok(result)) => {
1182                            *pending = None;
1183                            Poll::Ready(result)
1184                        }
1185                        Poll::Ready(Err(error)) => {
1186                            *pending = None;
1187                            Poll::Ready(Err(io::Error::other(error)))
1188                        }
1189                    };
1190                }
1191                let inner = Arc::clone(inner);
1192                let data = buf.to_vec();
1193                *pending = Some(tokio::task::spawn_blocking(move || (&*inner).write(&data)));
1194                self.poll_write(cx, &[])
1195            }
1196        }
1197    }
1198    fn poll_write_vectored(
1199        mut self: Pin<&mut Self>,
1200        cx: &mut Context<'_>,
1201        bufs: &[IoSlice<'_>],
1202    ) -> Poll<io::Result<usize>> {
1203        match &mut *self {
1204            Self::File(file) => Pin::new(file).poll_write_vectored(cx, bufs),
1205            Self::Pipe { inner, pending } => {
1206                if let Some(task) = pending {
1207                    return match Pin::new(task).poll(cx) {
1208                        Poll::Pending => Poll::Pending,
1209                        Poll::Ready(Ok(result)) => {
1210                            *pending = None;
1211                            Poll::Ready(result)
1212                        }
1213                        Poll::Ready(Err(error)) => {
1214                            *pending = None;
1215                            Poll::Ready(Err(io::Error::other(error)))
1216                        }
1217                    };
1218                }
1219                let mut data = Vec::new();
1220                for buf in bufs {
1221                    data.extend_from_slice(buf);
1222                }
1223                if data.is_empty() {
1224                    return Poll::Ready(Ok(0));
1225                }
1226                let inner = Arc::clone(inner);
1227                *pending = Some(tokio::task::spawn_blocking(move || (&*inner).write(&data)));
1228                self.poll_write_vectored(cx, &[])
1229            }
1230        }
1231    }
1232    fn is_write_vectored(&self) -> bool {
1233        true
1234    }
1235    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1236        match self.as_mut().poll_write(cx, &[]) {
1237            Poll::Pending => Poll::Pending,
1238            Poll::Ready(Ok(_)) => Poll::Ready(Ok(())),
1239            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
1240        }
1241    }
1242    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1243        self.poll_flush(cx)
1244    }
1245}
1246
1247#[cfg(windows)]
1248impl AsyncRead for NativeStdioRecv {
1249    fn poll_read(
1250        mut self: Pin<&mut Self>,
1251        cx: &mut Context<'_>,
1252        buf: &mut ReadBuf<'_>,
1253    ) -> Poll<io::Result<()>> {
1254        match &mut *self {
1255            Self::File(file) => Pin::new(file).poll_read(cx, buf),
1256            Self::Pipe {
1257                inner,
1258                pending,
1259                ready,
1260            } => {
1261                if let Some((data, len)) = ready {
1262                    let n = (*len).min(buf.remaining());
1263                    buf.put_slice(&data[..n]);
1264                    if n == *len {
1265                        *ready = None;
1266                    } else {
1267                        data.drain(..n);
1268                        *len -= n;
1269                    }
1270                    return Poll::Ready(Ok(()));
1271                }
1272                if pending.is_none() {
1273                    if buf.remaining() == 0 {
1274                        return Poll::Ready(Ok(()));
1275                    }
1276                    let inner = Arc::clone(inner);
1277                    let cap = buf.remaining();
1278                    *pending = Some(tokio::task::spawn_blocking(move || {
1279                        let mut data = vec![0; cap];
1280                        let result = (&*inner).read(&mut data);
1281                        (data, result)
1282                    }));
1283                }
1284                match Pin::new(pending.as_mut().unwrap()).poll(cx) {
1285                    Poll::Pending => Poll::Pending,
1286                    Poll::Ready(Ok((data, Ok(len)))) => {
1287                        *pending = None;
1288                        let n = len.min(buf.remaining());
1289                        buf.put_slice(&data[..n]);
1290                        if n < len {
1291                            *ready = Some((data[n..len].to_vec(), len - n));
1292                        }
1293                        Poll::Ready(Ok(()))
1294                    }
1295                    Poll::Ready(Ok((_, Err(error)))) => {
1296                        *pending = None;
1297                        Poll::Ready(Err(error))
1298                    }
1299                    Poll::Ready(Err(error)) => {
1300                        *pending = None;
1301                        Poll::Ready(Err(io::Error::other(error)))
1302                    }
1303                }
1304            }
1305        }
1306    }
1307}