1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-26 16:09:27 -05:00
denoland-deno/ext/node/polyfills/_process/streams.mjs
Matt Mastracci 08ec6e5831
perf: warm expensive init code at snapshot time (#22714)
Slightly different approach to similar changes in #22386

Note that this doesn't use a warmup script -- we are actually just doing
more work at snapshot time.
2024-03-22 20:49:07 +00:00

213 lines
6.3 KiB
JavaScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
import { Buffer } from "node:buffer";
import {
clearLine,
clearScreenDown,
cursorTo,
moveCursor,
} from "ext:deno_node/internal/readline/callbacks.mjs";
import { Duplex, Readable, Writable } from "node:stream";
import * as io from "ext:deno_io/12_io.js";
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, warmup = false) {
const stream = new Writable({
emitClose: false,
write(buf, enc, cb) {
if (!writer) {
this.destroy(
new Error(`Deno.${name} is not available in this environment`),
);
return;
}
writer.writeSync(buf instanceof Uint8Array ? buf : Buffer.from(buf, enc));
cb();
},
destroy(err, cb) {
cb(err);
this._undestroy();
if (!this._writableState.emitClose) {
nextTick(() => this.emit("close"));
}
},
});
let fd = -1;
if (writer instanceof io.Stdout) {
fd = io.STDOUT_RID;
} else if (writer instanceof io.Stderr) {
fd = io.STDERR_RID;
}
stream.fd = fd;
stream.destroySoon = stream.destroy;
stream._isStdio = true;
stream.once("close", () => writer?.close());
Object.defineProperties(stream, {
columns: {
enumerable: true,
configurable: true,
get: () =>
writer?.isTerminal() ? Deno.consoleSize?.().columns : undefined,
},
rows: {
enumerable: true,
configurable: true,
get: () => writer?.isTerminal() ? Deno.consoleSize?.().rows : undefined,
},
isTTY: {
enumerable: true,
configurable: true,
get: () => writer?.isTerminal(),
},
getWindowSize: {
enumerable: true,
configurable: true,
value: () =>
writer?.isTerminal() ? Object.values(Deno.consoleSize?.()) : undefined,
},
});
// If we're warming up, create a stdout/stderr stream that assumes a terminal (the most likely case).
// If we're wrong at boot time, we'll recreate it.
if (warmup || writer?.isTerminal()) {
// These belong on tty.WriteStream(), but the TTY streams currently have
// following problems:
// 1. Using them here introduces a circular dependency.
// 2. Creating a net.Socket() from a fd is not currently supported.
stream.cursorTo = function (x, y, callback) {
return cursorTo(this, x, y, callback);
};
stream.moveCursor = function (dx, dy, callback) {
return moveCursor(this, dx, dy, callback);
};
stream.clearLine = function (dir, callback) {
return clearLine(this, dir, callback);
};
stream.clearScreenDown = function (callback) {
return clearScreenDown(this, callback);
};
}
return stream;
}
function _guessStdinType(fd) {
if (typeof fd !== "number" || fd < 0) 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);
},
);
};
let readStream;
export function setReadStream(s) {
readStream = s;
}
/** https://nodejs.org/api/process.html#process_process_stdin */
// https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L189
/** Create process.stdin */
export const initStdin = (warmup = false) => {
const fd = io.stdin ? io.STDIN_RID : undefined;
let stdin;
// Warmup assumes a TTY for all stdio
const stdinType = warmup ? "TTY" : _guessStdinType(fd);
switch (stdinType) {
case "FILE": {
// Since `fs.ReadStream` cannot be imported before process initialization,
// use `Readable` instead.
// https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L200
// https://github.com/nodejs/node/blob/v18.12.1/lib/internal/fs/streams.js#L148
stdin = new Readable({
highWaterMark: 64 * 1024,
autoDestroy: false,
read: _read,
});
break;
}
case "TTY": {
// If it's a TTY, we know that the stdin we created during warmup is the correct one and
// just return null to re-use it.
if (!warmup) {
return null;
}
stdin = new readStream(fd);
break;
}
case "PIPE":
case "TCP": {
// 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.
// 2. Creating a net.Socket() from a fd is not currently supported.
// https://github.com/nodejs/node/blob/v18.12.1/lib/internal/bootstrap/switches/is_main_thread.js#L206
// https://github.com/nodejs/node/blob/v18.12.1/lib/net.js#L329
stdin = new Duplex({
readable: stdinType === "TTY" ? undefined : true,
writable: stdinType === "TTY" ? undefined : false,
readableHighWaterMark: stdinType === "TTY" ? 0 : undefined,
allowHalfOpen: false,
emitClose: false,
autoDestroy: true,
decodeStrings: false,
read: _read,
});
if (stdinType !== "TTY") {
// Make sure the stdin can't be `.end()`-ed
stdin._writableState.ended = true;
}
break;
}
default: {
// Provide a dummy contentless input for e.g. non-console
// Windows applications.
stdin = new Readable({ read() {} });
stdin.push(null);
}
}
stdin.on("close", () => io.stdin?.close());
stdin.fd = io.stdin ? io.STDIN_RID : -1;
Object.defineProperty(stdin, "isTTY", {
enumerable: true,
configurable: true,
get() {
return io.stdin.isTerminal();
},
});
stdin._isRawMode = false;
stdin.setRawMode = (enable) => {
io.stdin?.setRaw?.(enable);
stdin._isRawMode = enable;
return stdin;
};
Object.defineProperty(stdin, "isRaw", {
enumerable: true,
configurable: true,
get() {
return stdin._isRawMode;
},
});
return stdin;
};