Skip to content

Difference in behavior between Stdio::piped() and tokio::net::unix::pipe #7376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
stepancheg opened this issue May 31, 2025 · 3 comments
Open
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-process Module: tokio/process

Comments

@stepancheg
Copy link
Contributor

Is your feature request related to a problem? Please describe.

Repro code does this:

  • create a process sh -c 'sleep 1000000000' (exec sleep 1 reproduces it too)
  • pipes stdout
  • kills the process (but does not kill child sleep process)
  • waits for process
  • waits for pipe

When using Stdio::piped() everything works fine.

When using tokio::net::unix::pipe, last step hangs.

I don't have explanation why it hangs.

    #[tokio::test]
    async fn repro_hanging_pipe() {
        let mut command = Command::new("sh");
        command.args(["-c", "sleep 10000000000"]);
        // Also reproduces with `sleep 1` and even `exec sleep 1`.

        command.stdin(Stdio::null());
        command.stderr(Stdio::inherit());

        let (mut child, mut stdout_rx): (_, Pin<Box<dyn AsyncRead>>) = if false {
            // Works with regular stdout pipe.
            command.stdout(Stdio::piped());
            let mut child = command.spawn().unwrap();
            let stdout_rx = mem::take(&mut child.stdout).unwrap();
            (child, Box::pin(stdout_rx))
        } else {
            // Hangs with tokio::net::unix::pipe.
            let (stdout_tx, stdout_rx) = tokio::net::unix::pipe::pipe().unwrap();
            command.stdout(stdout_tx.into_blocking_fd().unwrap());
            let child = command.spawn().unwrap();
            (child, Box::pin(stdout_rx))
        };

        let mut stdout = Vec::new();
        let stdout_fut = stdout_rx.read_to_end(&mut stdout);

        eprintln!("Sending SIGKILL");
        child.start_kill().unwrap();

        // To be safe.
        mem::take(&mut child.stdout);
        mem::take(&mut child.stderr);
        mem::take(&mut child.stdin);

        eprintln!("Calling wait");
        child.wait().await.unwrap();
        eprintln!("Waited; waiting for stdout");

        stdout_fut.await.unwrap();

        eprintln!("all good; this code is unreachable with tokio::net::unix::pipe");
    }

Describe the solution you'd like

Around this line

/// If you need to create a pipe for communication with a spawned process, you can
/// use [`Stdio::piped()`] instead.

explain the difference.

Describe alternatives you've considered

None.

Additional context

Reproduces on Mac and Linux.

@stepancheg stepancheg added A-tokio Area: The main tokio crate C-feature-request Category: A feature request. labels May 31, 2025
@Darksonn Darksonn added the M-process Module: tokio/process label Jun 4, 2025
@Darksonn
Copy link
Contributor

Darksonn commented Jun 4, 2025

My initial guess is that this is because tokio::net::unix::pipe sets O_CLOEXEC on the pipe, but I'm not sure.

@g2p
Copy link

g2p commented Jun 5, 2025

@stepancheg Does it work with tokio pinned to ~1.38?
I suspect 1.39 has cloexec changes (not sure yet if it's on the tokio or mio side)

@stepancheg
Copy link
Contributor Author

stepancheg commented Jun 5, 2025

@g2p no, in 1.38.2 behavior is the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate C-feature-request Category: A feature request. M-process Module: tokio/process
Projects
None yet
Development

No branches or pull requests

3 participants