sinelaw/fresh · error

io::Error::last_os_error()

Error message

io::Error::last_os_error()

What it means

On Unix, the editor duplicates the /dev/tty file descriptor onto stdin (fd 0) with libc::dup2. When dup2 returns -1 the raw OS errno is surfaced as an io::Error and bailed. This restores interactive terminal input after stdin was redirected or consumed (e.g. piped input).

Solutions

  1. Ensure the process has a controlling terminal (run from an interactive shell, not a daemon/CI context)
  2. Check that the /dev/tty open (which produced `tty`) succeeded before dup2
  3. Close leaked descriptors or raise the fd limit if file descriptors are exhausted
  4. If no tty exists, skip the reopen and use stdin as-is or exit with a clear message

Example fix

// before
let result = unsafe { libc::dup2(tty.as_raw_fd(), libc::STDIN_FILENO) };
if result == -1 { anyhow::bail!(io::Error::last_os_error()); }
// after
if !has_controlling_terminal() {
    eprintln!("No controlling terminal; skipping stdin reopen");
    return Ok(());
}
let result = unsafe { libc::dup2(tty.as_raw_fd(), libc::STDIN_FILENO) };
if result == -1 { anyhow::bail!(io::Error::last_os_error()); }
Defensive patterns

Strategy: validation

Validate before calling

fn has_controlling_terminal() -> bool {
    std::path::Path::new("/dev/tty").exists() && unsafe { libc::isatty(libc::STDERR_FILENO) } == 1
}

Type guard

fn tty_opened(tty: &std::fs::File) -> bool { tty.as_raw_fd() >= 0 }

Try / catch

if let Err(e) = reopen_stdin_from_tty() {
    eprintln!("stdin reopen failed: {e}");
}

Prevention

When it happens

Trigger: libc::dup2(tty_fd, STDIN_FILENO) returns -1: the tty fd is invalid/closed, the process has no controlling terminal, or fd 0 is in a state dup2 cannot replace.

Common situations: Running the editor without a controlling terminal (daemon, CI job, nohup with no tty); /dev/tty open failed earlier producing a bad fd; fd exhaustion (EMFILE-adjacent scenarios).

Understand the failure class

Background: "open() failed", "failed to open file", "cannot create file" — what a file open error means and how to fix it — this error's family across 42 libraries.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/320e2739430b149c. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/main.rs:802

}

/// Reopen stdin from /dev/tty after reading piped content.
/// This allows crossterm to use the terminal for keyboard input
/// even though the original stdin was a pipe.
#[cfg(unix)]
fn reopen_stdin_from_tty() -> AnyhowResult<()> {
    use std::fs::File;
    use std::os::unix::io::AsRawFd;

    // Open /dev/tty - the controlling terminal
    let tty = File::open("/dev/tty")?;

    // Duplicate /dev/tty to stdin (fd 0) using libc
    // SAFETY: dup2 is safe to call with valid file descriptors
    let result = unsafe { libc::dup2(tty.as_raw_fd(), libc::STDIN_FILENO) };

    if result == -1 {
        anyhow::bail!(io::Error::last_os_error());
    }

    Ok(())
}

/// Reopen stdin from CONIN$ on Windows.
/// This allows crossterm to receive keyboard events after stdin was a pipe.
#[cfg(windows)]
fn reopen_stdin_from_tty() -> AnyhowResult<()> {
    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
    use windows_sys::Win32::Storage::FileSystem::{
        CreateFileW, FILE_GENERIC_READ, FILE_SHARE_READ, OPEN_EXISTING,
    };
    use windows_sys::Win32::System::Console::{SetStdHandle, STD_INPUT_HANDLE};

    // "CONIN$" is the console input device on Windows
    // This is analogous to /dev/tty on Unix
    let conin: Vec<u16> = "CONIN$\0".encode_utf16().collect();

View on GitHub (pinned to 67894ca546)