Skip to content
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

Reading from a nb PTY main wrapped in tokio::fs::File fails with EWOULDBLOCK #4488

Closed
flxo opened this issue Feb 10, 2022 · 2 comments
Closed
Labels
A-tokio Area: The main tokio crate C-bug Category: This is a bug. M-fs Module: tokio/fs

Comments

@flxo
Copy link
Contributor

flxo commented Feb 10, 2022

Version

1.16.1 9c688ec

Platform

5.10.0-8-amd64 #1 SMP Debian 5.10.46-4 (2021-08-03) x86_64 GNU/Linux

Description

Reading from a non blocking PTY main fd that is wrapped in a tokio::fs::File returns EAGAIN when calling any ready function, eg. read. The PTY is created with posix_openpt(OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK). The sub fd of the PTY is opened and some bytes are written to it.

The read fn returns EAGAIN immediately which should never happen on a correctly setup non blocking fd wrapped in File.

I tried this code:

[package]
name = "fdids"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.53"
bytes = "1.1.0"
nix = "0.23.1"
tokio = { path = "../tokio/tokio", features = ["full"] }
use std::os::unix::prelude::{AsRawFd, FromRawFd, IntoRawFd};

use nix::{
    fcntl::{self, OFlag},
    sys::{stat::Mode, termios::SetArg},
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Open a new PTY main
    let main_fd = nix::pty::posix_openpt(OFlag::O_RDWR | OFlag::O_NOCTTY | OFlag::O_NONBLOCK)?;

    // Ensure that the fd is non-blocking
    let flags = nix::fcntl::fcntl(main_fd.as_raw_fd(), fcntl::FcntlArg::F_GETFL)?;
    assert!(flags & fcntl::OFlag::O_NONBLOCK.bits() != 0);

    nix::pty::grantpt(&main_fd)?;
    nix::pty::unlockpt(&main_fd)?;

    // Set raw mode
    let mut termios = nix::sys::termios::tcgetattr(main_fd.as_raw_fd())?;
    nix::sys::termios::cfmakeraw(&mut termios);
    nix::sys::termios::tcsetattr(main_fd.as_raw_fd(), SetArg::TCSANOW, &termios)?;

    // Get the name of the sub e.g /dev/pts/4 and open it
    let sub_name = unsafe { nix::pty::ptsname(&main_fd) }.unwrap();
    let _ = fcntl::open(sub_name.as_str(), OFlag::O_RDWR, Mode::empty()).unwrap();

    // Write some bytes to the pty sub
    dbg!(nix::unistd::write(main_fd.as_raw_fd(), b"hello\n"))?;

    // Wrap the main fd in a File
    let mut file = unsafe { tokio::fs::File::from_raw_fd(main_fd.into_raw_fd()) };
    let buf = &mut [0u8; 16];
    loop {
        dbg!(file.read(buf).await)?;
    }

    Ok(())
}

Output:

   Compiling fdids v0.1.0 (/home/felix/fdids)
    Finished dev [unoptimized + debuginfo] target(s) in 1.11s
     Running `target/debug/fdids`
[src/main.rs:31] nix::unistd::write(main_fd.as_raw_fd(), b"hello\n") = Ok(
    6,
)
[src/main.rs:36] file.read(buf).await = Err(
    Os {
        code: 11,
        kind: WouldBlock,
        message: "Resource temporarily unavailable",
    },
)
Error: Resource temporarily unavailable (os error 11)

I expected to see this happen:

The read returns one or more times some Ok(n) with n >=1. Finally the program waits for more bytes to become available on main which never happens.

Instead, this happened:

The read.await resolves to EAGAIN.

I poked around and used AsyncFd for wrapping the RawFd. Using the exact same main fd and calling readable seems to work fine:

    <see above>
    // Write some bytes to the pty sub
    dbg!(nix::unistd::write(main_fd.as_raw_fd(), b"hello\n"))?;

    // let mut file = unsafe { tokio::fs::File::from_raw_fd(main_fd.into_raw_fd()) };
    // let buf = &mut [0u8; 16];
    // loop {
    //     dbg!(file.read(buf).await)?;
    // }

    let fd = tokio::io::unix::AsyncFd::new(main_fd.into_raw_fd())?;
    loop {
        let _ = dbg!(fd.readable().await?);
    }

Output:

    Finished dev [unoptimized + debuginfo] target(s) in 1.15s
     Running `target/debug/fdids`
[src/main.rs:31] nix::unistd::write(main_fd.as_raw_fd(), b"hello\n") = Ok(
    6,
)
<waits here forever>

In the application I'm writing I implemented the read with AsyncFd with try_io and unistd::read etc... which is a lot of nasty code with buffer index handling etc which I would like to avoid by wrapping the fd in something that implements AsyncRead.

Probably this is some speciality Unix/Linux has prepared with PTYs.

@flxo flxo added A-tokio Area: The main tokio crate C-bug Category: This is a bug. labels Feb 10, 2022
@Darksonn Darksonn added the M-fs Module: tokio/fs label Feb 10, 2022
@Darksonn
Copy link
Contributor

The tokio::fs::File utility is for reading actual files, which cannot be read in a non-blocking manner. Therefore, it offloads the blocking reads to a separate thread pool. You should not use it for FDs that are not files.

@flxo
Copy link
Contributor Author

flxo commented Feb 10, 2022

@Darksonn Right. I totally forgot that File is just a wrapper around blocking file fds. Sorry for the noise.

@flxo flxo closed this as completed Feb 10, 2022
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-bug Category: This is a bug. M-fs Module: tokio/fs
Projects
None yet
Development

No branches or pull requests

2 participants