diff --git a/cli/main.rs b/cli/main.rs index 0817c0984ca667..dbd3b470b4e565 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -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)] diff --git a/cli/tests/node_compat/config.jsonc b/cli/tests/node_compat/config.jsonc index 8a31f4dc264ace..93a51b671383e2 100644 --- a/cli/tests/node_compat/config.jsonc +++ b/cli/tests/node_compat/config.jsonc @@ -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", @@ -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", diff --git a/cli/tests/node_compat/test/parallel/test-tty-stdin-end.js b/cli/tests/node_compat/test/parallel/test-tty-stdin-end.js new file mode 100644 index 00000000000000..ee38cbd2cfc480 --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-tty-stdin-end.js @@ -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'); diff --git a/cli/tests/node_compat/test/parallel/test-ttywrap-invalid-fd.js b/cli/tests/node_compat/test/parallel/test-ttywrap-invalid-fd.js new file mode 100644 index 00000000000000..95b9bffe6ab49f --- /dev/null +++ b/cli/tests/node_compat/test/parallel/test-ttywrap-invalid-fd.js @@ -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' + } +); diff --git a/cli/util/unix.rs b/cli/util/unix.rs index fd0c94ea68d530..2fa3c206360b9d 100644 --- a/cli/util/unix.rs +++ b/cli/util/unix.rs @@ -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::(); + if tcgetattr(libc::STDIN_FILENO, &mut termios) == 0 { + static mut ORIG_TERMIOS: Option = 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); + } + } +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 6d7e85ec4614bd..730554f2d84d32 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -270,6 +270,7 @@ deno_core::extension!(deno_node, ops::require::op_require_read_package_scope

, ops::require::op_require_package_imports_resolve

, 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 = [ @@ -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", diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index cf4abf3ddc5773..d1bb4b7f4802d9 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -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; diff --git a/ext/node/ops/util.rs b/ext/node/ops/util.rs new file mode 100644 index 00000000000000..1cb80e0e3bf14a --- /dev/null +++ b/ext/node/ops/util.rs @@ -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 { + 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, + } +} diff --git a/ext/node/polyfills/_process/streams.mjs b/ext/node/polyfills/_process/streams.mjs index b6efef65ea6f73..39ee89a8224138 100644 --- a/ext/node/polyfills/_process/streams.mjs +++ b/ext/node/polyfills/_process/streams.mjs @@ -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) { @@ -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 */ @@ -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. diff --git a/ext/node/polyfills/internal_binding/util.ts b/ext/node/polyfills/internal_binding/util.ts index a2d355c1ebdf4c..38eeebee007624 100644 --- a/ext/node/polyfills/internal_binding/util.ts +++ b/ext/node/polyfills/internal_binding/util.ts @@ -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; diff --git a/ext/node/polyfills/process.ts b/ext/node/polyfills/process.ts index 618f92d3f23a47..a4fc3317d2a752 100644 --- a/ext/node/polyfills/process.ts +++ b/ext/node/polyfills/process.ts @@ -33,8 +33,6 @@ export { _nextTick as nextTick, chdir, cwd, env, version, versions }; import { createWritableStdioStream, initStdin, - Readable, - Writable, } from "ext:deno_node/_process/streams.mjs"; import { enableNextTick, @@ -57,41 +55,9 @@ export let platform = ""; // TODO(kt3k): This should be set at start up time export let pid = 0; -// We want streams to be as lazy as possible, but we cannot export a getter in a module. To -// work around this we make these proxies that eagerly instantiate the underlying object on -// first access of any property/method. -function makeLazyStream(objectFactory: () => T): T { - return new Proxy({}, { - get: function (_, prop, receiver) { - // deno-lint-ignore no-explicit-any - return Reflect.get(objectFactory() as any, prop, receiver); - }, - has: function (_, prop) { - // deno-lint-ignore no-explicit-any - return Reflect.has(objectFactory() as any, prop); - }, - ownKeys: function (_) { - // deno-lint-ignore no-explicit-any - return Reflect.ownKeys(objectFactory() as any); - }, - set: function (_, prop, value, receiver) { - // deno-lint-ignore no-explicit-any - return Reflect.set(objectFactory() as any, prop, value, receiver); - }, - getPrototypeOf: function (_) { - // deno-lint-ignore no-explicit-any - return Reflect.getPrototypeOf(objectFactory() as any); - }, - getOwnPropertyDescriptor(_, prop) { - // deno-lint-ignore no-explicit-any - return Reflect.getOwnPropertyDescriptor(objectFactory() as any, prop); - }, - }) as T; -} +let stdin, stdout, stderr; -export let stderr = makeLazyStream(getStderr); -export let stdin = makeLazyStream(getStdin); -export let stdout = makeLazyStream(getStdout); +export { stderr, stdin, stdout }; import { getBinding } from "ext:deno_node/internal_binding/mod.ts"; import * as constants from "ext:deno_node/internal_binding/constants.ts"; @@ -646,19 +612,13 @@ class Process extends EventEmitter { memoryUsage = memoryUsage; /** https://nodejs.org/api/process.html#process_process_stderr */ - get stderr(): Writable { - return getStderr(); - } + stderr = stderr; /** https://nodejs.org/api/process.html#process_process_stdin */ - get stdin(): Readable { - return getStdin(); - } + stdin = stdin; /** https://nodejs.org/api/process.html#process_process_stdout */ - get stdout(): Writable { - return getStdout(); - } + stdout = stdout; /** https://nodejs.org/api/process.html#process_process_version */ version = version; @@ -906,52 +866,24 @@ internals.__bootstrapNodeProcess = function ( core.setMacrotaskCallback(runNextTicks); enableNextTick(); + stdin = process.stdin = initStdin(); + /** https://nodejs.org/api/process.html#process_process_stdout */ + stdout = process.stdout = createWritableStdioStream( + io.stdout, + "stdout", + ); + + /** https://nodejs.org/api/process.html#process_process_stderr */ + stderr = process.stderr = createWritableStdioStream( + io.stderr, + "stderr", + ); + process.setStartTime(Date.now()); + // @ts-ignore Remove setStartTime and #startTime is not modifiable delete process.setStartTime; delete internals.__bootstrapNodeProcess; }; -// deno-lint-ignore no-explicit-any -let stderr_ = null as any; -// deno-lint-ignore no-explicit-any -let stdin_ = null as any; -// deno-lint-ignore no-explicit-any -let stdout_ = null as any; - -function getStdin(): Readable { - if (!stdin_) { - stdin_ = initStdin(); - stdin = stdin_; - Object.defineProperty(process, "stdin", { get: () => stdin_ }); - } - return stdin_; -} - -/** https://nodejs.org/api/process.html#process_process_stdout */ -function getStdout(): Writable { - if (!stdout_) { - stdout_ = createWritableStdioStream( - io.stdout, - "stdout", - ); - stdout = stdout_; - Object.defineProperty(process, "stdout", { get: () => stdout_ }); - } - return stdout_; -} - -/** https://nodejs.org/api/process.html#process_process_stderr */ -function getStderr(): Writable { - if (!stderr_) { - stderr_ = createWritableStdioStream( - io.stderr, - "stderr", - ); - stderr = stderr_; - Object.defineProperty(process, "stderr", { get: () => stderr_ }); - } - return stderr_; -} - export default process; diff --git a/ext/node/polyfills/tty.js b/ext/node/polyfills/tty.js new file mode 100644 index 00000000000000..54f8f6eae62098 --- /dev/null +++ b/ext/node/polyfills/tty.js @@ -0,0 +1,83 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { ERR_INVALID_FD } from "ext:deno_node/internal/errors.ts"; +import { LibuvStreamWrap } from "ext:deno_node/internal_binding/stream_wrap.ts"; +import { providerType } from "ext:deno_node/internal_binding/async_wrap.ts"; +import { Duplex } from "node:stream"; +const { Error } = globalThis.__bootstrap.primordials; + +// Returns true when the given numeric fd is associated with a TTY and false otherwise. +function isatty(fd) { + if (typeof fd !== "number") { + return false; + } + try { + return Deno.isatty(fd); + } catch (_) { + return false; + } +} + +class TTY extends LibuvStreamWrap { + constructor(handle) { + super(providerType.TTYWRAP, handle); + } +} + +export class ReadStream extends Duplex { + constructor(fd, options) { + if (fd >> 0 !== fd || fd < 0) { + throw new ERR_INVALID_FD(fd); + } + + // We only support `stdin`. + if (fd != 0) throw new Error("Only fd 0 is supported."); + + const tty = new TTY(Deno.stdin); + super({ + readableHighWaterMark: 0, + handle: tty, + manualStart: true, + ...options, + }); + + this.isRaw = false; + this.isTTY = true; + } + + setRawMode(flag) { + flag = !!flag; + this._handle.setRaw(flag); + + this.isRaw = flag; + return this; + } +} + +export class WriteStream extends Duplex { + constructor(fd) { + if (fd >> 0 !== fd || fd < 0) { + throw new ERR_INVALID_FD(fd); + } + + // We only support `stdin`, `stdout` and `stderr`. + if (fd > 2) throw new Error("Only fd 0, 1 and 2 are supported."); + + const tty = new TTY( + fd === 0 ? Deno.stdin : fd === 1 ? Deno.stdout : Deno.stderr, + ); + + super({ + readableHighWaterMark: 0, + handle: tty, + manualStart: true, + }); + + const { columns, rows } = Deno.consoleSize(); + this.columns = columns; + this.rows = rows; + } +} + +export { isatty }; +export default { isatty, WriteStream, ReadStream }; diff --git a/ext/node/polyfills/tty.ts b/ext/node/polyfills/tty.ts deleted file mode 100644 index d33f779caaa328..00000000000000 --- a/ext/node/polyfills/tty.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -import { Socket } from "node:net"; - -// Returns true when the given numeric fd is associated with a TTY and false otherwise. -function isatty(fd: number) { - if (typeof fd !== "number") { - return false; - } - try { - return Deno.isatty(fd); - } catch (_) { - return false; - } -} - -// TODO(kt3k): Implement tty.ReadStream class -export class ReadStream extends Socket { -} -// TODO(kt3k): Implement tty.WriteStream class -export class WriteStream extends Socket { -} - -export { isatty }; -export default { isatty, WriteStream, ReadStream }; diff --git a/tools/node_compat/TODO.md b/tools/node_compat/TODO.md index 41dfb70b0e788d..b4c971d8918100 100644 --- a/tools/node_compat/TODO.md +++ b/tools/node_compat/TODO.md @@ -3,7 +3,7 @@ NOTE: This file should not be manually edited. Please edit `cli/tests/node_compat/config.json` and run `deno task setup` in `tools/node_compat` dir instead. -Total: 2924 +Total: 2923 - [abort/test-abort-backtrace.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-backtrace.js) - [abort/test-abort-fatal-error.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-fatal-error.js) @@ -2356,7 +2356,6 @@ Total: 2924 - [parallel/test-trace-exit.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-trace-exit.js) - [parallel/test-tracing-no-crash.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-tracing-no-crash.js) - [parallel/test-tty-backwards-api.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-tty-backwards-api.js) -- [parallel/test-tty-stdin-end.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-tty-stdin-end.js) - [parallel/test-tty-stdin-pipe.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-tty-stdin-pipe.js) - [parallel/test-ttywrap-invalid-fd.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-ttywrap-invalid-fd.js) - [parallel/test-ttywrap-stack.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-ttywrap-stack.js)