Skip to content

Windows: no supported way to reopen console input (CONIN$) as a raw tty.ReadStream when stdin is a pipe #63852

Description

@edemaine

Version

v24.11.0

Platform

Microsoft Windows NT 10.0.26200.0 x64

Subsystem

node:tty or node:fs

What steps will reproduce the bug?

Run the following code in a CommonJS setting (or convert requires to imports):

const fs = require("node:fs");
const tty = require("node:tty");
const fd = fs.openSync("\\\\.\\CONIN$", "r");
const input = new tty.ReadStream(fd);
input.setRawMode(true);

How often does it reproduce? Is there a required condition?

Windows

What is the expected behavior? Why is that the expected behavior?

Successful open of console/TTY in raw mode

What do you see instead?

\\.\CONIN$ opens and new tty.ReadStream(fd) reports isTTY: true, but setRawMode(true) throws EPERM.

Additional information

Motivation

A terminal TUI/pager such as hunk needs to read application data from stdin, while still interacting via keyboard/mouse input from the controlling console. For example:

git diff | hunk pager -

In this process, fd 0 is the pipe containing the diff, not the keyboard. On Unix, this can be handled by opening /dev/tty and passing that fd to new tty.ReadStream(fd), then calling setRawMode(true). On Windows, I expected \\.\CONIN$ to be the equivalent, and Node can open it and reports the resulting tty.ReadStream as a TTY, but setRawMode(true) fails with EPERM.

I also tried a few other names for CONIN$:

const fs = require("node:fs");
const tty = require("node:tty");

for (const path of ["CON", "CONIN$", "\\\\.\\CONIN$", "//./CONIN$"]) {
  try {
    const fd = fs.openSync(path, "r");
    const stream = new tty.ReadStream(fd);

    let result;
    try {
      stream.setRawMode(true);
      result = "raw ok";
      stream.setRawMode(false);
    } catch (error) {
      result = `raw failed: ${error.code} ${error.message}`;
    }

    console.error(path, {
      fd,
      isTTY: stream.isTTY,
      result,
    });

    stream.destroy();
  } catch (error) {
    console.error(path, "open failed:", error.code, error.message);
  }
}

Output:

CON open failed: ENOENT ENOENT: no such file or directory, open '...\CON'
CONIN$ open failed: ENOENT ENOENT: no such file or directory, open '...\CONIN$'
\\.\CONIN$ { fd: 3, isTTY: true, result: 'raw failed: EPERM setRawMode EPERM' }
//./CONIN$ { fd: 3, isTTY: true, result: 'raw failed: EPERM setRawMode EPERM' }

There should be a supported way for a Windows CLI/TUI application to reopen the controlling console input while stdin is a pipe, then enable raw mode on that input stream. Otherwise, tools that consume piped stdin but need interactive terminal input cannot work as full-screen TUIs on Windows without native addons or platform-specific external helpers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions