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

fix: implement node:tty #20892

Merged
merged 28 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
}
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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for later work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code on Windows for TCP and UDP is TODO.

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