diff options
Diffstat (limited to 'src/js/builtins/ProcessObjectInternals.ts')
-rw-r--r-- | src/js/builtins/ProcessObjectInternals.ts | 676 |
1 files changed, 676 insertions, 0 deletions
diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts new file mode 100644 index 000000000..8b24e68ba --- /dev/null +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -0,0 +1,676 @@ +/* + * Copyright 2023 Codeblog Corp. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +export function binding(bindingName) { + if (bindingName !== "constants") + throw new TypeError( + "process.binding() is not supported in Bun. If that breaks something, please file an issue and include a reproducible code sample.", + ); + + var cache = globalThis.Symbol.for("process.bindings.constants"); + var constants = globalThis[cache]; + if (!constants) { + // TODO: make this less hacky. + // This calls require("node:fs").constants + // except, outside an ESM module. + const { constants: fs } = globalThis[globalThis.Symbol.for("Bun.lazy")]("createImportMeta", "node:process").require( + "node:fs", + ); + constants = { + fs, + zlib: {}, + crypto: {}, + os: Bun._Os().constants, + }; + globalThis[cache] = constants; + } + return constants; +} + +export function getStdioWriteStream(fd_, rawRequire) { + var module = { path: "node:process", require: rawRequire }; + var require = path => module.require(path); + + function createStdioWriteStream(fd_) { + var { Duplex, eos, destroy } = require("node:stream"); + var StdioWriteStream = class StdioWriteStream extends Duplex { + #writeStream; + #readStream; + + #readable = true; + #writable = true; + #fdPath; + + #onClose; + #onDrain; + #onFinish; + #onReadable; + #isTTY; + + get isTTY() { + return (this.#isTTY ??= require("node:tty").isatty(fd_)); + } + + get fd() { + return fd_; + } + + constructor(fd) { + super({ readable: true, writable: true }); + this.#fdPath = `/dev/fd/${fd}`; + } + + #onFinished(err) { + const cb = this.#onClose; + this.#onClose = null; + + if (cb) { + cb(err); + } else if (err) { + this.destroy(err); + } else if (!this.#readable && !this.#writable) { + this.destroy(); + } + } + + _destroy(err, callback) { + if (!err && this.#onClose !== null) { + var AbortError = class AbortError extends Error { + code: string; + name: string; + constructor(message = "The operation was aborted", options = void 0) { + if (options !== void 0 && typeof options !== "object") { + throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`); + } + super(message, options); + this.code = "ABORT_ERR"; + this.name = "AbortError"; + } + }; + err = new AbortError(); + } + + this.#onDrain = null; + this.#onFinish = null; + if (this.#onClose === null) { + callback(err); + } else { + this.#onClose = callback; + if (this.#writeStream) destroy(this.#writeStream, err); + if (this.#readStream) destroy(this.#readStream, err); + } + } + + _write(chunk, encoding, callback) { + if (!this.#writeStream) { + var { createWriteStream } = require("node:fs"); + var stream = (this.#writeStream = createWriteStream(this.#fdPath)); + + stream.on("finish", () => { + if (this.#onFinish) { + const cb = this.#onFinish; + this.#onFinish = null; + cb(); + } + }); + + stream.on("drain", () => { + if (this.#onDrain) { + const cb = this.#onDrain; + this.#onDrain = null; + cb(); + } + }); + + eos(stream, err => { + this.#writable = false; + if (err) { + destroy(stream, err); + } + this.#onFinished(err); + }); + } + if (stream.write(chunk, encoding)) { + callback(); + } else { + this.#onDrain = callback; + } + } + + _final(callback) { + this.#writeStream && this.#writeStream.end(); + this.#onFinish = callback; + } + + #loadReadStream() { + var { createReadStream } = require("node:fs"); + + var readStream = (this.#readStream = createReadStream(this.#fdPath)); + + readStream.on("readable", () => { + if (this.#onReadable) { + const cb = this.#onReadable; + this.#onReadable = null; + cb(); + } else { + this.read(); + } + }); + + readStream.on("end", () => { + this.push(null); + }); + + eos(readStream, err => { + this.#readable = false; + if (err) { + destroy(readStream, err); + } + this.#onFinished(err); + }); + return readStream; + } + + _read() { + var stream = this.#readStream; + if (!stream) { + stream = this.#loadReadStream(); + } + + while (true) { + const buf = stream.read(); + if (buf === null || !this.push(buf)) { + return; + } + } + } + }; + return new StdioWriteStream(fd_); + } + + var { EventEmitter } = require("node:events"); + + function isFastEncoding(encoding) { + if (!encoding) return true; + + var normalied = encoding.toLowerCase(); + return normalied === "utf8" || normalied === "utf-8" || normalied === "buffer" || normalied === "binary"; + } + + var readline; + + var FastStdioWriteStream = class StdioWriteStream extends EventEmitter { + #fd; + #innerStream; + #writer; + #isTTY; + + bytesWritten = 0; + + setDefaultEncoding(encoding) { + if (this.#innerStream || !isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.setDefaultEncoding(encoding); + } + } + + #createWriter() { + switch (this.#fd) { + case 1: { + var writer = Bun.stdout.writer({ highWaterMark: 0 }); + writer.unref(); + return writer; + } + + case 2: { + var writer = Bun.stderr.writer({ highWaterMark: 0 }); + writer.unref(); + return writer; + } + default: { + throw new Error("Unsupported writer"); + } + } + } + + #getWriter() { + return (this.#writer ??= this.#createWriter()); + } + + constructor(fd_) { + super(); + this.#fd = fd_; + } + + get fd() { + return this.#fd; + } + + get isTTY() { + return (this.#isTTY ??= require("node:tty").isatty(this.#fd)); + } + + cursorTo(x, y, callback) { + return (readline ??= require("readline")).cursorTo(this, x, y, callback); + } + + moveCursor(dx, dy, callback) { + return (readline ??= require("readline")).moveCursor(this, dx, dy, callback); + } + + clearLine(dir, callback) { + return (readline ??= require("readline")).clearLine(this, dir, callback); + } + + clearScreenDown(callback) { + return (readline ??= require("readline")).clearScreenDown(this, callback); + } + + // TODO: once implemented this.columns and this.rows should be uncommented + // getWindowSize() { + // return [this.columns, this.rows]; + // } + + ref() { + this.#getWriter().ref(); + } + + unref() { + this.#getWriter().unref(); + } + + on(event, listener) { + if (event === "close" || event === "finish") { + this.#ensureInnerStream(); + return this.#innerStream.on(event, listener); + } + + if (event === "drain") { + return super.on("drain", listener); + } + + if (event === "error") { + return super.on("error", listener); + } + + return super.on(event, listener); + } + + get _writableState() { + this.#ensureInnerStream(); + return this.#innerStream._writableState; + } + + get _readableState() { + this.#ensureInnerStream(); + return this.#innerStream._readableState; + } + + pipe(destination) { + this.#ensureInnerStream(); + return this.#innerStream.pipe(destination); + } + + unpipe(destination) { + this.#ensureInnerStream(); + return this.#innerStream.unpipe(destination); + } + + #ensureInnerStream() { + if (this.#innerStream) return; + this.#innerStream = createStdioWriteStream(this.#fd); + const events = this.eventNames(); + for (const event of events) { + this.#innerStream.on(event, (...args) => { + this.emit(event, ...args); + }); + } + } + + #write1(chunk) { + var writer = this.#getWriter(); + const writeResult = writer.write(chunk); + this.bytesWritten += writeResult; + const flushResult = writer.flush(false); + return !!(writeResult || flushResult); + } + + #writeWithEncoding(chunk, encoding) { + if (!isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.write(chunk, encoding); + } + + return this.#write1(chunk); + } + + #performCallback(cb, err?: any) { + if (err) { + this.emit("error", err); + } + + try { + cb(err ? err : null); + } catch (err2) { + this.emit("error", err2); + } + } + + #writeWithCallbackAndEncoding(chunk, encoding, callback) { + if (!isFastEncoding(encoding)) { + this.#ensureInnerStream(); + return this.#innerStream.write(chunk, encoding, callback); + } + + var writer = this.#getWriter(); + const writeResult = writer.write(chunk); + const flushResult = writer.flush(true); + if (flushResult?.then) { + flushResult.then( + () => { + this.#performCallback(callback); + this.emit("drain"); + }, + err => this.#performCallback(callback, err), + ); + return false; + } + + queueMicrotask(() => { + this.#performCallback(callback); + }); + + return !!(writeResult || flushResult); + } + + write(chunk, encoding, callback) { + const result = this._write(chunk, encoding, callback); + + if (result) { + this.emit("drain"); + } + + return result; + } + + get hasColors() { + return Bun.tty[this.#fd].hasColors; + } + + _write(chunk, encoding, callback) { + var inner = this.#innerStream; + if (inner) { + return inner.write(chunk, encoding, callback); + } + + switch (arguments.length) { + case 0: { + var error = new Error("Invalid arguments"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } + case 1: { + return this.#write1(chunk); + } + case 2: { + if (typeof encoding === "function") { + return this.#writeWithCallbackAndEncoding(chunk, "", encoding); + } else if (typeof encoding === "string") { + return this.#writeWithEncoding(chunk, encoding); + } + } + default: { + if ( + (typeof encoding !== "undefined" && typeof encoding !== "string") || + (typeof callback !== "undefined" && typeof callback !== "function") + ) { + var error = new Error("Invalid arguments"); + error.code = "ERR_INVALID_ARG_TYPE"; + throw error; + } + + if (typeof callback === "undefined") { + return this.#writeWithEncoding(chunk, encoding); + } + + return this.#writeWithCallbackAndEncoding(chunk, encoding, callback); + } + } + } + + destroy() { + return this; + } + + end() { + return this; + } + }; + + return new FastStdioWriteStream(fd_); +} + +export function getStdinStream(fd_, rawRequire, Bun) { + var module = { path: "node:process", require: rawRequire }; + var require = path => module.require(path); + + var { Duplex, eos, destroy } = require("node:stream"); + + var StdinStream = class StdinStream extends Duplex { + #reader; + // TODO: investigate https://github.com/oven-sh/bun/issues/1607 + + #readRef; + #writeStream; + + #readable = true; + #unrefOnRead = false; + #writable = true; + + #onFinish; + #onClose; + #onDrain; + + get isTTY() { + return require("tty").isatty(fd_); + } + + get fd() { + return fd_; + } + + constructor() { + super({ readable: true, writable: true }); + } + + #onFinished(err?) { + const cb = this.#onClose; + this.#onClose = null; + + if (cb) { + cb(err); + } else if (err) { + this.destroy(err); + } else if (!this.#readable && !this.#writable) { + this.destroy(); + } + } + + _destroy(err, callback) { + if (!err && this.#onClose !== null) { + var AbortError = class AbortError extends Error { + constructor(message = "The operation was aborted", options = void 0) { + if (options !== void 0 && typeof options !== "object") { + throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`); + } + super(message, options); + this.code = "ABORT_ERR"; + this.name = "AbortError"; + } + }; + err = new AbortError(); + } + + if (this.#onClose === null) { + callback(err); + } else { + this.#onClose = callback; + if (this.#writeStream) destroy(this.#writeStream, err); + } + } + + setRawMode(mode) {} + + on(name, callback) { + // Streams don't generally required to present any data when only + // `readable` events are present, i.e. `readableFlowing === false` + // + // However, Node.js has a this quirk whereby `process.stdin.read()` + // blocks under TTY mode, thus looping `.read()` in this particular + // case would not result in truncation. + // + // Therefore the following hack is only specific to `process.stdin` + // and does not apply to the underlying Stream implementation. + if (name === "readable") { + this.ref(); + this.#unrefOnRead = true; + } + return super.on(name, callback); + } + + pause() { + this.unref(); + return super.pause(); + } + + resume() { + this.ref(); + return super.resume(); + } + + ref() { + this.#reader ??= Bun.stdin.stream().getReader(); + this.#readRef ??= setInterval(() => {}, 1 << 30); + } + + unref() { + if (this.#readRef) { + clearInterval(this.#readRef); + this.#readRef = null; + } + } + + async #readInternal() { + try { + var done, value; + const read = this.#reader.readMany(); + + // read same-tick if possible + if (!read?.then) { + ({ done, value } = read); + } else { + ({ done, value } = await read); + } + + if (!done) { + this.push(value[0]); + + // shouldn't actually happen, but just in case + const length = value.length; + for (let i = 1; i < length; i++) { + this.push(value[i]); + } + } else { + this.push(null); + this.pause(); + this.#readable = false; + this.#onFinished(); + } + } catch (err) { + this.#readable = false; + this.#onFinished(err); + } + } + + _read(size) { + if (this.#unrefOnRead) { + this.unref(); + this.#unrefOnRead = false; + } + this.#readInternal(); + } + + #constructWriteStream() { + var { createWriteStream } = require("node:fs"); + var writeStream = (this.#writeStream = createWriteStream("/dev/fd/0")); + + writeStream.on("finish", () => { + if (this.#onFinish) { + const cb = this.#onFinish; + this.#onFinish = null; + cb(); + } + }); + + writeStream.on("drain", () => { + if (this.#onDrain) { + const cb = this.#onDrain; + this.#onDrain = null; + cb(); + } + }); + + eos(writeStream, err => { + this.#writable = false; + if (err) { + destroy(writeStream, err); + } + this.#onFinished(err); + }); + + return writeStream; + } + + _write(chunk, encoding, callback) { + var writeStream = this.#writeStream; + if (!writeStream) { + writeStream = this.#constructWriteStream(); + } + + if (writeStream.write(chunk, encoding)) { + callback(); + } else { + this.#onDrain = callback; + } + } + + _final(callback) { + this.#writeStream.end(); + this.#onFinish = (...args) => callback(...args); + } + }; + + return new StdinStream(); +} |