Skip to content

Commit

Permalink
fix: implement node:tty (#20892)
Browse files Browse the repository at this point in the history
Fixes #21012
Closes #20855
Fixes #20890
Fixes #20611
Fixes #20336
Fixes `create-svelte` from #17248

Fixes more reports here:
- #6529 (comment)
- #6529 (comment)
- #6529 (comment)
  • Loading branch information
littledivy committed Oct 30, 2023
1 parent 1acef75 commit 0920410
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 177 deletions.
1 change: 1 addition & 0 deletions cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ pub(crate) fn unstable_warn_cb(feature: &str) {
pub fn main() {
setup_panic_hook();

util::unix::prepare_stdio();
util::unix::raise_fd_limit();
util::windows::ensure_stdio_open();
#[cfg(windows)]
Expand Down
2 changes: 2 additions & 0 deletions cli/tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"test-querystring.js",
"test-readline-interface.js",
"test-stdin-from-file-spawn.js",
"test-ttywrap-invalid-fd.js",
"test-url-urltooptions.js",
"test-util-format.js",
"test-util-inspect-namespace.js",
Expand Down Expand Up @@ -625,6 +626,7 @@
"test-timers-unref-throw-then-ref.js",
"test-timers-user-call.js",
"test-timers-zero-timeout.js",
"test-tty-stdin-end.js",
"test-url-domain-ascii-unicode.js",
"test-url-fileurltopath.js",
"test-url-format-invalid-input.js",
Expand Down
14 changes: 14 additions & 0 deletions cli/tests/node_compat/test/parallel/test-tty-stdin-end.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tools/node_compat/setup.ts`. Do not modify this file manually.

'use strict';
require('../common');

// This test ensures that Node.js doesn't crash on `process.stdin.emit("end")`.
// https://github.com/nodejs/node/issues/1068

process.stdin.emit('end');
74 changes: 74 additions & 0 deletions cli/tests/node_compat/test/parallel/test-ttywrap-invalid-fd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by `tools/node_compat/setup.ts`. Do not modify this file manually.

// Flags: --expose-internals
'use strict';

// const common = require('../common');
const tty = require('tty');
// const { internalBinding } = require('internal/test/binding');
// const {
// UV_EBADF,
// UV_EINVAL
// } = internalBinding('uv');
const assert = require('assert');

assert.throws(
() => new tty.WriteStream(-1),
{
code: 'ERR_INVALID_FD',
name: 'RangeError',
message: '"fd" must be a positive integer: -1'
}
);

// {
// const info = {
// code: common.isWindows ? 'EBADF' : 'EINVAL',
// message: common.isWindows ? 'bad file descriptor' : 'invalid argument',
// errno: common.isWindows ? UV_EBADF : UV_EINVAL,
// syscall: 'uv_tty_init'
// };

// const suffix = common.isWindows ?
// 'EBADF (bad file descriptor)' : 'EINVAL (invalid argument)';
// const message = `TTY initialization failed: uv_tty_init returned ${suffix}`;

// assert.throws(
// () => {
// common.runWithInvalidFD((fd) => {
// new tty.WriteStream(fd);
// });
// }, {
// code: 'ERR_TTY_INIT_FAILED',
// name: 'SystemError',
// message,
// info
// }
// );

// assert.throws(
// () => {
// common.runWithInvalidFD((fd) => {
// new tty.ReadStream(fd);
// });
// }, {
// code: 'ERR_TTY_INIT_FAILED',
// name: 'SystemError',
// message,
// info
// });
// }

assert.throws(
() => new tty.ReadStream(-1),
{
code: 'ERR_INVALID_FD',
name: 'RangeError',
message: '"fd" must be a positive integer: -1'
}
);
24 changes: 24 additions & 0 deletions cli/util/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,27 @@ pub fn raise_fd_limit() {
}
}
}

pub fn prepare_stdio() {
#[cfg(unix)]
// SAFETY: Save current state of stdio and restore it when we exit.
unsafe {
use libc::atexit;
use libc::tcgetattr;
use libc::tcsetattr;
use libc::termios;

let mut termios = std::mem::zeroed::<termios>();
if tcgetattr(libc::STDIN_FILENO, &mut termios) == 0 {
static mut ORIG_TERMIOS: Option<termios> = None;
ORIG_TERMIOS = Some(termios);

extern "C" fn reset_stdio() {
// SAFETY: Reset the stdio state.
unsafe { tcsetattr(libc::STDIN_FILENO, 0, &ORIG_TERMIOS.unwrap()) };
}

atexit(reset_stdio);
}
}
}
3 changes: 2 additions & 1 deletion ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ deno_core::extension!(deno_node,
ops::require::op_require_read_package_scope<P>,
ops::require::op_require_package_imports_resolve<P>,
ops::require::op_require_break_on_next_statement,
ops::util::op_node_guess_handle_type,
],
esm_entry_point = "ext:deno_node/02_init.js",
esm = [
Expand Down Expand Up @@ -490,7 +491,7 @@ deno_core::extension!(deno_node,
"timers.ts" with_specifier "node:timers",
"timers/promises.ts" with_specifier "node:timers/promises",
"tls.ts" with_specifier "node:tls",
"tty.ts" with_specifier "node:tty",
"tty.js" with_specifier "node:tty",
"url.ts" with_specifier "node:url",
"util.ts" with_specifier "node:util",
"util/types.ts" with_specifier "node:util/types",
Expand Down
1 change: 1 addition & 0 deletions ext/node/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod http2;
pub mod idna;
pub mod os;
pub mod require;
pub mod util;
pub mod v8;
pub mod winerror;
pub mod zlib;
83 changes: 83 additions & 0 deletions ext/node/ops/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

use deno_core::error::AnyError;
use deno_core::op2;
use deno_core::OpState;
use deno_core::ResourceHandle;
use deno_core::ResourceHandleFd;

#[repr(u32)]
enum HandleType {
#[allow(dead_code)]
Tcp = 0,
Tty,
#[allow(dead_code)]
Udp,
File,
Pipe,
Unknown,
}

#[op2(fast)]
pub fn op_node_guess_handle_type(
state: &mut OpState,
rid: u32,
) -> Result<u32, AnyError> {
let handle = state.resource_table.get_handle(rid)?;

let handle_type = match handle {
ResourceHandle::Fd(handle) => guess_handle_type(handle),
_ => HandleType::Unknown,
};

Ok(handle_type as u32)
}

#[cfg(windows)]
fn guess_handle_type(handle: ResourceHandleFd) -> HandleType {
use winapi::um::consoleapi::GetConsoleMode;
use winapi::um::fileapi::GetFileType;
use winapi::um::winbase::FILE_TYPE_CHAR;
use winapi::um::winbase::FILE_TYPE_DISK;
use winapi::um::winbase::FILE_TYPE_PIPE;

// SAFETY: Call to win32 fileapi. `handle` is a valid fd.
match unsafe { GetFileType(handle) } {
FILE_TYPE_DISK => HandleType::File,
FILE_TYPE_CHAR => {
let mut mode = 0;
// SAFETY: Call to win32 consoleapi. `handle` is a valid fd.
// `mode` is a valid pointer.
if unsafe { GetConsoleMode(handle, &mut mode) } == 1 {
HandleType::Tty
} else {
HandleType::File
}
}
FILE_TYPE_PIPE => HandleType::Pipe,
_ => HandleType::Unknown,
}
}

#[cfg(unix)]
fn guess_handle_type(handle: ResourceHandleFd) -> HandleType {
use std::io::IsTerminal;
// SAFETY: The resource remains open for the duration of borrow_raw.
if unsafe { std::os::fd::BorrowedFd::borrow_raw(handle).is_terminal() } {
return HandleType::Tty;
}

// SAFETY: It is safe to zero-initialize a `libc::stat` struct.
let mut s = unsafe { std::mem::zeroed() };
// SAFETY: Call to libc
if unsafe { libc::fstat(handle, &mut s) } == 1 {
return HandleType::Unknown;
}

match s.st_mode & 61440 {
libc::S_IFREG | libc::S_IFCHR => HandleType::File,
libc::S_IFIFO => HandleType::Pipe,
libc::S_IFSOCK => HandleType::Tcp,
_ => HandleType::Unknown,
}
}
74 changes: 15 additions & 59 deletions ext/node/polyfills/_process/streams.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
moveCursor,
} from "ext:deno_node/internal/readline/callbacks.mjs";
import { Duplex, Readable, Writable } from "node:stream";
import { isWindows } from "ext:deno_node/_util/os.ts";
import { fs as fsConstants } from "ext:deno_node/internal_binding/constants.ts";
import * as io from "ext:deno_io/12_io.js";
import * as tty from "node:tty";
import { guessHandleType } from "ext:deno_node/internal_binding/util.ts";

// https://github.com/nodejs/node/blob/00738314828074243c9a52a228ab4c68b04259ef/lib/internal/bootstrap/switches/is_main_thread.js#L41
export function createWritableStdioStream(writer, name) {
Expand Down Expand Up @@ -95,60 +95,21 @@ export function createWritableStdioStream(writer, name) {
return stream;
}

// TODO(PolarETech): This function should be replaced by
// `guessHandleType()` in "../internal_binding/util.ts".
// https://github.com/nodejs/node/blob/v18.12.1/src/node_util.cc#L257
function _guessStdinType(fd) {
if (typeof fd !== "number" || fd < 0) return "UNKNOWN";
if (Deno.isatty?.(fd)) return "TTY";

try {
const fileInfo = Deno.fstatSync?.(fd);

// https://github.com/nodejs/node/blob/v18.12.1/deps/uv/src/unix/tty.c#L333
if (!isWindows) {
switch (fileInfo.mode & fsConstants.S_IFMT) {
case fsConstants.S_IFREG:
case fsConstants.S_IFCHR:
return "FILE";
case fsConstants.S_IFIFO:
return "PIPE";
case fsConstants.S_IFSOCK:
// TODO(PolarETech): Need a better way to identify "TCP".
// Currently, unable to exclude UDP.
return "TCP";
default:
return "UNKNOWN";
}
}

// https://github.com/nodejs/node/blob/v18.12.1/deps/uv/src/win/handle.c#L31
if (fileInfo.isFile) {
// TODO(PolarETech): Need a better way to identify a piped stdin on Windows.
// On Windows, `Deno.fstatSync(rid).isFile` returns true even for a piped stdin.
// Therefore, a piped stdin cannot be distinguished from a file by this property.
// The mtime, atime, and birthtime of the file are "2339-01-01T00:00:00.000Z",
// so use the property as a workaround.
if (fileInfo.birthtime.valueOf() === 11644473600000) return "PIPE";
return "FILE";
}
} catch (e) {
// TODO(PolarETech): Need a better way to identify a character file on Windows.
// "EISDIR" error occurs when stdin is "null" on Windows,
// so use the error as a workaround.
if (isWindows && e.code === "EISDIR") return "FILE";
}

return "UNKNOWN";
return guessHandleType(fd);
}

const _read = function (size) {
const p = Buffer.alloc(size || 16 * 1024);
io.stdin?.read(p).then((length) => {
this.push(length === null ? null : p.slice(0, length));
}, (error) => {
this.destroy(error);
});
io.stdin?.read(p).then(
(length) => {
this.push(length === null ? null : p.slice(0, length));
},
(error) => {
this.destroy(error);
},
);
};

/** https://nodejs.org/api/process.html#process_process_stdin */
Expand All @@ -172,17 +133,12 @@ export const initStdin = () => {
});
break;
}
case "TTY":
case "TTY": {
stdin = new tty.ReadStream(fd);
break;
}
case "PIPE":
case "TCP": {
// TODO(PolarETech):
// For TTY, `new Duplex()` should be replaced `new tty.ReadStream()` if possible.
// There are two problems that need to be resolved.
// 1. Using them here introduces a circular dependency.
// 2. Creating a tty.ReadStream() is not currently supported.
// https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L194
// https://github.com/nodejs/node/blob/v18.12.1/lib/tty.js#L47

// For PIPE and TCP, `new Duplex()` should be replaced `new net.Socket()` if possible.
// There are two problems that need to be resolved.
// 1. Using them here introduces a circular dependency.
Expand Down
9 changes: 6 additions & 3 deletions ext/node/polyfills/internal_binding/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials

import { notImplemented } from "ext:deno_node/_utils.ts";
const core = globalThis.Deno.core;
const ops = core.ops;

export function guessHandleType(_fd: number): string {
notImplemented("util.guessHandleType");
const handleTypes = ["TCP", "TTY", "UDP", "FILE", "PIPE", "UNKNOWN"];
export function guessHandleType(fd: number): string {
const type = ops.op_node_guess_handle_type(fd);
return handleTypes[type];
}

export const ALL_PROPERTIES = 0;
Expand Down
Loading

0 comments on commit 0920410

Please sign in to comment.