node child process maxbuf support (#18293)

This commit is contained in:
pfg
2025-04-03 17:03:26 -07:00
committed by GitHub
parent 04a432f54f
commit d9c77be90d
24 changed files with 1199 additions and 120 deletions

6
.vscode/launch.json generated vendored
View File

@@ -1118,7 +1118,11 @@
"request": "attach",
"name": "rr",
"trace": "Off",
"setupCommands": ["handle SIGPWR nostop noprint pass"],
"setupCommands": [
"handle SIGPWR nostop noprint pass",
"source ${workspaceFolder}/misctools/gdb/std_gdb_pretty_printers.py",
"source ${workspaceFolder}/misctools/gdb/zig_gdb_pretty_printers.py",
],
},
],
"inputs": [

View File

@@ -253,6 +253,19 @@ const proc = Bun.spawn({
The `killSignal` option also controls which signal is sent when an AbortSignal is aborted.
## Using maxBuffer
For spawnSync, you can limit the maximum number of bytes of output before the process is killed:
```ts
// KIll 'yes' after it emits over 100 bytes of output
const result = Bun.spawnSync({
cmd: ["yes"], // or ["bun", "exec", "yes"] on windows
maxBuffer: 100,
});
// process exits
```
## Inter-process communication (IPC)
Bun supports direct inter-process communication channel between two `bun` processes. To receive messages from a spawned Bun subprocess, specify an `ipc` handler.
@@ -423,6 +436,7 @@ namespace SpawnOptions {
signal?: AbortSignal;
timeout?: number;
killSignal?: string | number;
maxBuffer?: number;
}
type Readable =

View File

@@ -0,0 +1,142 @@
# pretty printing for the standard library.
# put "source /path/to/stage2_gdb_pretty_printers.py" in ~/.gdbinit to load it automatically.
import re
import gdb.printing
# Handles both ArrayList and ArrayListUnmanaged.
class ArrayListPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
type = self.val.type.name[len('std.array_list.'):]
type = re.sub(r'^ArrayListAligned(Unmanaged)?\((.*),null\)$', r'ArrayList\1(\2)', type)
return '%s of length %s, capacity %s' % (type, self.val['items']['len'], self.val['capacity'])
def children(self):
for i in range(self.val['items']['len']):
item = self.val['items']['ptr'] + i
yield ('[%d]' % i, item.dereference())
def display_hint(self):
return 'array'
class MultiArrayListPrinter:
def __init__(self, val):
self.val = val
def child_type(self):
(helper_fn, _) = gdb.lookup_symbol('%s.dbHelper' % self.val.type.name)
return helper_fn.type.fields()[1].type.target()
def to_string(self):
type = self.val.type.name[len('std.multi_array_list.'):]
return '%s of length %s, capacity %s' % (type, self.val['len'], self.val['capacity'])
def slice(self):
fields = self.child_type().fields()
base = self.val['bytes']
cap = self.val['capacity']
len = self.val['len']
if len == 0:
return
fields = sorted(fields, key=lambda field: field.type.alignof, reverse=True)
for field in fields:
ptr = base.cast(field.type.pointer()).dereference().cast(field.type.array(len - 1))
base += field.type.sizeof * cap
yield (field.name, ptr)
def children(self):
for i, (name, ptr) in enumerate(self.slice()):
yield ('[%d]' % i, name)
yield ('[%d]' % i, ptr)
def display_hint(self):
return 'map'
# Handles both HashMap and HashMapUnmanaged.
class HashMapPrinter:
def __init__(self, val):
self.type = val.type
is_managed = re.search(r'^std\.hash_map\.HashMap\(', self.type.name)
self.val = val['unmanaged'] if is_managed else val
def header_ptr_type(self):
(helper_fn, _) = gdb.lookup_symbol('%s.dbHelper' % self.val.type.name)
return helper_fn.type.fields()[1].type
def header(self):
if self.val['metadata'] == 0:
return None
return (self.val['metadata'].cast(self.header_ptr_type()) - 1).dereference()
def to_string(self):
type = self.type.name[len('std.hash_map.'):]
type = re.sub(r'^HashMap(Unmanaged)?\((.*),std.hash_map.AutoContext\(.*$', r'AutoHashMap\1(\2)', type)
hdr = self.header()
if hdr is not None:
cap = hdr['capacity']
else:
cap = 0
return '%s of length %s, capacity %s' % (type, self.val['size'], cap)
def children(self):
hdr = self.header()
if hdr is None:
return
is_map = self.display_hint() == 'map'
for i in range(hdr['capacity']):
metadata = self.val['metadata'] + i
if metadata.dereference()['used'] == 1:
yield ('[%d]' % i, (hdr['keys'] + i).dereference())
if is_map:
yield ('[%d]' % i, (hdr['values'] + i).dereference())
def display_hint(self):
for field in self.header_ptr_type().target().fields():
if field.name == 'values':
return 'map'
return 'array'
# Handles both ArrayHashMap and ArrayHashMapUnmanaged.
class ArrayHashMapPrinter:
def __init__(self, val):
self.type = val.type
is_managed = re.search(r'^std\.array_hash_map\.ArrayHashMap\(', self.type.name)
self.val = val['unmanaged'] if is_managed else val
def to_string(self):
type = self.type.name[len('std.array_hash_map.'):]
type = re.sub(r'^ArrayHashMap(Unmanaged)?\((.*),std.array_hash_map.AutoContext\(.*$', r'AutoArrayHashMap\1(\2)', type)
return '%s of length %s' % (type, self.val['entries']['len'])
def children(self):
entries = MultiArrayListPrinter(self.val['entries'])
len = self.val['entries']['len']
fields = {}
for name, ptr in entries.slice():
fields[str(name)] = ptr
for i in range(len):
if 'key' in fields:
yield ('[%d]' % i, fields['key'][i])
else:
yield ('[%d]' % i, '{}')
if 'value' in fields:
yield ('[%d]' % i, fields['value'][i])
def display_hint(self):
for name, ptr in MultiArrayListPrinter(self.val['entries']).slice():
if name == 'value':
return 'map'
return 'array'
pp = gdb.printing.RegexpCollectionPrettyPrinter('Zig standard library')
pp.add_printer('ArrayList', r'^std\.array_list\.ArrayListAligned(Unmanaged)?\(.*\)$', ArrayListPrinter)
pp.add_printer('MultiArrayList', r'^std\.multi_array_list\.MultiArrayList\(.*\)$', MultiArrayListPrinter)
pp.add_printer('HashMap', r'^std\.hash_map\.HashMap(Unmanaged)?\(.*\)$', HashMapPrinter)
pp.add_printer('ArrayHashMap', r'^std\.array_hash_map\.ArrayHashMap(Unmanaged)?\(.*\)$', ArrayHashMapPrinter)
gdb.printing.register_pretty_printer(gdb.current_objfile(), pp)

View File

@@ -0,0 +1,63 @@
# pretty printing for the language.
# put "source /path/to/zig_gdb_pretty_printers.py" in ~/.gdbinit to load it automatically.
import gdb.printing
class ZigPrettyPrinter(gdb.printing.PrettyPrinter):
def __init__(self):
super().__init__('Zig')
def __call__(self, val):
tag = val.type.tag
if tag is None:
return None
if tag == '[]u8':
return StringPrinter(val)
if tag.startswith('[]'):
return SlicePrinter(val)
if tag.startswith('?'):
return OptionalPrinter(val)
return None
class SlicePrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return f"{self.val['len']} items at {self.val['ptr']}"
def children(self):
def it(val):
for i in range(int(val['len'])):
item = val['ptr'] + i
yield (f'[{i}]', item.dereference())
return it(self.val)
def display_hint(self):
return 'array'
class StringPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
return self.val['ptr'].string(length=int(self.val['len']))
def display_hint(self):
return 'string'
class OptionalPrinter:
def __init__(self, val):
self.val = val
def to_string(self):
if self.val['some']:
return self.val['data']
else:
return 'null'
gdb.printing.register_pretty_printer(gdb.current_objfile(), ZigPrettyPrinter())

View File

@@ -6587,7 +6587,8 @@ declare module "bun" {
timeout?: number;
/**
* The signal to use when killing the process after a timeout or when the AbortSignal is aborted.
* The signal to use when killing the process after a timeout, when the AbortSignal is aborted,
* or when the process goes over the `maxBuffer` limit.
*
* @default "SIGTERM" (signal 15)
*
@@ -6602,6 +6603,14 @@ declare module "bun" {
* ```
*/
killSignal?: string | number;
/**
* The maximum number of bytes the process may output. If the process goes over this limit,
* it is killed with signal `killSignal` (defaults to SIGTERM).
*
* @default undefined (no limit)
*/
maxBuffer?: number;
}
type OptionsToSubprocess<Opts extends OptionsObject> =
@@ -6847,7 +6856,8 @@ declare module "bun" {
resourceUsage: ResourceUsage;
signalCode?: string;
exitedDueToTimeout?: true;
exitedDueToTimeout?: boolean;
exitedDueToMaxBuffer?: boolean;
pid: number;
}

84
src/bun.js/MaxBuf.zig Normal file
View File

@@ -0,0 +1,84 @@
const Subprocess = @import("api/bun/subprocess.zig");
const MaxBuf = @This();
const bun = @import("root").bun;
const std = @import("std");
pub const Kind = enum {
stdout,
stderr,
};
// null after subprocess finalize
owned_by_subprocess: ?*Subprocess,
// null after pipereader finalize
owned_by_reader: bool,
// if this goes negative, onMaxBuffer is called on the subprocess
remaining_bytes: i64,
// (once both are null, it is freed)
pub fn createForSubprocess(owner: *Subprocess, ptr: *?*MaxBuf, initial: ?i64) void {
if (initial == null) {
ptr.* = null;
return;
}
const maxbuf = bun.default_allocator.create(MaxBuf) catch bun.outOfMemory();
maxbuf.* = .{
.owned_by_subprocess = owner,
.owned_by_reader = false,
.remaining_bytes = initial.?,
};
ptr.* = maxbuf;
}
fn disowned(this: *MaxBuf) bool {
return this.owned_by_subprocess != null and this.owned_by_reader == false;
}
fn destroy(this: *MaxBuf) void {
bun.assert(this.disowned());
bun.default_allocator.destroy(this);
}
pub fn removeFromSubprocess(ptr: *?*MaxBuf) void {
if (ptr.* == null) return;
const this = ptr.*.?;
bun.assert(this.owned_by_subprocess != null);
this.owned_by_subprocess = null;
ptr.* = null;
if (this.disowned()) {
this.destroy();
}
}
pub fn addToPipereader(value: ?*MaxBuf, ptr: *?*MaxBuf) void {
if (value == null) return;
bun.assert(ptr.* == null);
ptr.* = value;
bun.assert(!value.?.owned_by_reader);
value.?.owned_by_reader = true;
}
pub fn removeFromPipereader(ptr: *?*MaxBuf) void {
if (ptr.* == null) return;
const this = ptr.*.?;
bun.assert(this.owned_by_reader);
this.owned_by_reader = false;
ptr.* = null;
if (this.disowned()) {
this.destroy();
}
}
pub fn transferToPipereader(prev: *?*MaxBuf, next: *?*MaxBuf) void {
if (prev.* == null) return;
next.* = prev.*;
prev.* = null;
}
pub fn onReadBytes(this: *MaxBuf, bytes: u64) void {
this.remaining_bytes = std.math.sub(i64, this.remaining_bytes, std.math.cast(i64, bytes) orelse 0) catch -1;
if (this.remaining_bytes < 0 and this.owned_by_subprocess != null) {
const owned_by = this.owned_by_subprocess.?;
if (owned_by.stderr_maxbuf == this) {
MaxBuf.removeFromSubprocess(&owned_by.stderr_maxbuf);
owned_by.onMaxBuffer(.stderr);
} else if (owned_by.stdout_maxbuf == this) {
MaxBuf.removeFromSubprocess(&owned_by.stdout_maxbuf);
owned_by.onMaxBuffer(.stdout);
} else {
bun.assert(false);
}
}
}

View File

@@ -87,7 +87,7 @@ pub const Stdio = union(enum) {
}
}
pub fn canUseMemfd(this: *const @This(), is_sync: bool) bool {
pub fn canUseMemfd(this: *const @This(), is_sync: bool, has_max_buffer: bool) bool {
if (comptime !Environment.isLinux) {
return false;
}
@@ -95,7 +95,7 @@ pub const Stdio = union(enum) {
return switch (this.*) {
.blob => !this.blob.needsToReadFile(),
.memfd, .array_buffer => true,
.pipe => is_sync,
.pipe => is_sync and !has_max_buffer,
else => false,
};
}

View File

@@ -1,6 +1,7 @@
//! The Subprocess object is returned by `Bun.spawn`. This file also holds the
//! code for `Bun.spawnSync`
const Subprocess = @This();
const MaxBuf = @import("../../MaxBuf.zig");
pub usingnamespace JSC.Codegen.JSSubprocess;
pub usingnamespace bun.NewRefCounted(@This(), deinit, null);
@@ -40,6 +41,10 @@ event_loop_timer: JSC.API.Bun.Timer.EventLoopTimer = .{
},
killSignal: SignalCode,
stdout_maxbuf: ?*MaxBuf = null,
stderr_maxbuf: ?*MaxBuf = null,
exited_due_to_maxbuf: ?MaxBuf.Kind = null,
pub const Flags = packed struct {
is_sync: bool = false,
killed: bool = false,
@@ -177,7 +182,6 @@ pub fn appendEnvpFromJS(globalThis: *JSC.JSGlobalObject, object: *JSC.JSObject,
}
const log = Output.scoped(.Subprocess, false);
const default_max_buffer_size = 1024 * 1024 * 4;
pub const StdioKind = enum {
stdin,
stdout,
@@ -412,24 +416,11 @@ const Readable = union(enum) {
}
}
pub fn init(stdio: Stdio, event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: u32, is_sync: bool) Readable {
pub fn init(stdio: Stdio, event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, allocator: std.mem.Allocator, max_size: ?*MaxBuf, is_sync: bool) Readable {
_ = allocator; // autofix
_ = max_size; // autofix
_ = is_sync; // autofix
assertStdioResult(result);
if (Environment.isWindows) {
return switch (stdio) {
.inherit => Readable{ .inherit = {} },
.ignore, .ipc, .path, .memfd => Readable{ .ignore = {} },
.fd => |fd| Readable{ .fd = fd },
.dup2 => |dup2| Readable{ .fd = dup2.out.toFd() },
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) },
.array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}),
.capture => Output.panic("TODO: implement capture support in Stdio readable", .{}),
};
}
if (comptime Environment.isPosix) {
if (stdio == .pipe) {
_ = bun.sys.setNonblocking(result.?);
@@ -439,12 +430,12 @@ const Readable = union(enum) {
return switch (stdio) {
.inherit => Readable{ .inherit = {} },
.ignore, .ipc, .path => Readable{ .ignore = {} },
.fd => Readable{ .fd = result.? },
.memfd => Readable{ .memfd = stdio.memfd },
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result) },
.fd => |fd| if (Environment.isPosix) Readable{ .fd = result.? } else Readable{ .fd = fd },
.memfd => if (Environment.isPosix) Readable{ .memfd = stdio.memfd } else Readable{ .ignore = {} },
.dup2 => |dup2| if (Environment.isPosix) Output.panic("TODO: implement dup2 support in Stdio readable", .{}) else Readable{ .fd = dup2.out.toFd() },
.pipe => Readable{ .pipe = PipeReader.create(event_loop, process, result, max_size) },
.array_buffer, .blob => Output.panic("TODO: implement ArrayBuffer & Blob support in Stdio readable", .{}),
.capture => Output.panic("TODO: implement capture support in Stdio readable", .{}),
.dup2 => Output.panic("TODO: implement dup2 support in Stdio readable", .{}),
};
}
@@ -637,6 +628,11 @@ pub fn timeoutCallback(this: *Subprocess) JSC.API.Bun.Timer.EventLoopTimer.Arm {
return .disarm;
}
pub fn onMaxBuffer(this: *Subprocess, kind: MaxBuf.Kind) void {
this.exited_due_to_maxbuf = kind;
_ = this.tryKill(this.killSignal);
}
fn parseSignal(arg: JSC.JSValue, globalThis: *JSC.JSGlobalObject) !SignalCode {
if (arg.getNumber()) |sig64| {
// Node does this:
@@ -1039,7 +1035,6 @@ pub const PipeReader = struct {
err: bun.sys.Error,
} = .{ .pending = {} },
stdio_result: StdioResult,
pub const IOReader = bun.io.BufferedReader;
pub const Poll = IOReader;
@@ -1061,13 +1056,14 @@ pub const PipeReader = struct {
this.deref();
}
pub fn create(event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult) *PipeReader {
pub fn create(event_loop: *JSC.EventLoop, process: *Subprocess, result: StdioResult, limit: ?*MaxBuf) *PipeReader {
var this = PipeReader.new(.{
.process = process,
.reader = IOReader.init(@This()),
.event_loop = event_loop,
.stdio_result = result,
});
MaxBuf.addToPipereader(limit, &this.reader.maxbuf);
if (Environment.isWindows) {
this.reader.source = .{ .pipe = this.stdio_result.buffer };
}
@@ -1754,6 +1750,9 @@ pub fn finalize(this: *Subprocess) callconv(.C) void {
}
this.setEventLoopTimerRefd(false);
MaxBuf.removeFromSubprocess(&this.stdout_maxbuf);
MaxBuf.removeFromSubprocess(&this.stderr_maxbuf);
this.flags.finalized = true;
this.deref();
}
@@ -1947,6 +1946,7 @@ pub fn spawnMaybeSync(
var ipc_channel: i32 = -1;
var timeout: ?i32 = null;
var killSignal: SignalCode = SignalCode.default;
var maxBuffer: ?i64 = null;
var windows_hide: bool = false;
var windows_verbatim_arguments: bool = false;
@@ -2139,7 +2139,7 @@ pub fn spawnMaybeSync(
}
if (try args.get(globalThis, "timeout")) |val| {
if (val.isNumber()) {
if (val.isNumber() and val.isFinite()) {
timeout = @max(val.coerce(i32, globalThis), 1);
}
}
@@ -2147,18 +2147,26 @@ pub fn spawnMaybeSync(
if (try args.get(globalThis, "killSignal")) |val| {
killSignal = try parseSignal(val, globalThis);
}
if (try args.get(globalThis, "maxBuffer")) |val| {
if (val.isNumber() and val.isFinite()) { // 'Infinity' does not set maxBuffer
maxBuffer = val.coerce(i64, globalThis);
}
}
} else {
try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv);
}
}
log("spawn maxBuffer: {?d}", .{maxBuffer});
if (!override_env and env_array.items.len == 0) {
env_array.items = jsc_vm.transpiler.env.map.createNullDelimitedEnvMap(allocator) catch |err| return globalThis.throwError(err, "in Bun.spawn") catch return .zero;
env_array.capacity = env_array.items.len;
}
inline for (0..stdio.len) |fd_index| {
if (stdio[fd_index].canUseMemfd(is_sync)) {
if (stdio[fd_index].canUseMemfd(is_sync, fd_index > 0 and maxBuffer != null)) {
stdio[fd_index].useMemfd(fd_index);
}
}
@@ -2310,6 +2318,9 @@ pub fn spawnMaybeSync(
else
bun.invalid_fd;
MaxBuf.createForSubprocess(subprocess, &subprocess.stderr_maxbuf, maxBuffer);
MaxBuf.createForSubprocess(subprocess, &subprocess.stdout_maxbuf, maxBuffer);
// When run synchronously, subprocess isn't garbage collected
subprocess.* = Subprocess{
.globalThis = globalThis,
@@ -2330,7 +2341,7 @@ pub fn spawnMaybeSync(
subprocess,
spawned.stdout,
jsc_vm.allocator,
default_max_buffer_size,
subprocess.stdout_maxbuf,
is_sync,
),
.stderr = Readable.init(
@@ -2339,7 +2350,7 @@ pub fn spawnMaybeSync(
subprocess,
spawned.stderr,
jsc_vm.allocator,
default_max_buffer_size,
subprocess.stderr_maxbuf,
is_sync,
),
// 1. JavaScript.
@@ -2357,6 +2368,8 @@ pub fn spawnMaybeSync(
.is_sync = is_sync,
},
.killSignal = killSignal,
.stderr_maxbuf = subprocess.stderr_maxbuf,
.stdout_maxbuf = subprocess.stdout_maxbuf,
};
subprocess.process.setExitHandler(subprocess);
@@ -2544,6 +2557,7 @@ pub fn spawnMaybeSync(
const stderr = try subprocess.stderr.toBufferedValue(globalThis);
const resource_usage: JSValue = if (!globalThis.hasException()) subprocess.createResourceUsageObject(globalThis) else .zero;
const exitedDueToTimeout = subprocess.event_loop_timer.state == .FIRED;
const exitedDueToMaxBuffer = subprocess.exited_due_to_maxbuf;
const resultPid = JSC.JSValue.jsNumberFromInt32(subprocess.pid());
subprocess.finalize();
@@ -2561,7 +2575,8 @@ pub fn spawnMaybeSync(
sync_value.put(globalThis, JSC.ZigString.static("stderr"), stderr);
sync_value.put(globalThis, JSC.ZigString.static("success"), JSValue.jsBoolean(exitCode.isInt32() and exitCode.asInt32() == 0));
sync_value.put(globalThis, JSC.ZigString.static("resourceUsage"), resource_usage);
if (exitedDueToTimeout) sync_value.put(globalThis, JSC.ZigString.static("exitedDueToTimeout"), JSC.JSValue.true);
if (timeout != null) sync_value.put(globalThis, JSC.ZigString.static("exitedDueToTimeout"), if (exitedDueToTimeout) JSC.JSValue.true else JSC.JSValue.false);
if (maxBuffer != null) sync_value.put(globalThis, JSC.ZigString.static("exitedDueToMaxBuffer"), if (exitedDueToMaxBuffer != null) JSC.JSValue.true else JSC.JSValue.false);
sync_value.put(globalThis, JSC.ZigString.static("pid"), resultPid);
return sync_value;

View File

@@ -2001,6 +2001,14 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_AMBIGUOUS_ARGUMENT, message));
}
case Bun::ErrorCode::ERR_CHILD_PROCESS_STDIO_MAXBUFFER: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto message = makeString(str0, " maxBuffer length exceeded"_s);
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_CHILD_PROCESS_STDIO_MAXBUFFER, message));
}
case ErrorCode::ERR_IPC_DISCONNECTED:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_IPC_DISCONNECTED, "IPC channel is already disconnected"_s));
case ErrorCode::ERR_SERVER_NOT_RUNNING:

View File

@@ -26,6 +26,7 @@ const errors: ErrorCodeMapping = [
["ERR_BUFFER_OUT_OF_BOUNDS", RangeError],
["ERR_BUFFER_TOO_LARGE", RangeError],
["ERR_CHILD_PROCESS_IPC_REQUIRED", Error],
["ERR_CHILD_PROCESS_STDIO_MAXBUFFER", RangeError],
["ERR_CLOSED_MESSAGE_PORT", Error],
["ERR_CONSOLE_WRITABLE_STREAM", TypeError, "TypeError"],
["ERR_CONSTRUCT_CALL_INVALID", TypeError],

View File

@@ -113,6 +113,10 @@ pub fn etimedoutErrorCode(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError
return JSC.JSValue.jsNumberFromInt32(-bun.C.UV_ETIMEDOUT);
}
pub fn enobufsErrorCode(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue {
return JSC.JSValue.jsNumberFromInt32(-bun.C.UV_ENOBUFS);
}
/// `extractedSplitNewLines` for ASCII/Latin1 strings. Panics if passed a non-string.
/// Returns `undefined` if param is utf8 or utf16 and not fully ascii.
///

View File

@@ -5,6 +5,7 @@ const Source = @import("./source.zig").Source;
const ReadState = @import("./pipes.zig").ReadState;
const FileType = @import("./pipes.zig").FileType;
const MaxBuf = @import("../bun.js/MaxBuf.zig");
const PollOrFd = @import("./pipes.zig").PollOrFd;
@@ -92,6 +93,8 @@ const PosixBufferedReader = struct {
_offset: usize = 0,
vtable: BufferedReaderVTable,
flags: Flags = .{},
count: usize = 0,
maxbuf: ?*MaxBuf = null,
const Flags = packed struct {
is_done: bool = false,
@@ -139,6 +142,7 @@ const PosixBufferedReader = struct {
other.flags.is_done = true;
other.handle = .{ .closed = {} };
other._offset = 0;
MaxBuf.transferToPipereader(&other.maxbuf, &to.maxbuf);
to.handle.setOwner(to);
// note: the caller is supposed to drain the buffer themselves
@@ -184,14 +188,6 @@ const PosixBufferedReader = struct {
}
}
fn _onReadChunk(this: *PosixBufferedReader, chunk: []u8, hasMore: ReadState) bool {
if (hasMore == .eof) {
this.flags.received_eof = true;
}
return this.vtable.onReadChunk(chunk, hasMore);
}
pub fn getFd(this: *PosixBufferedReader) bun.FileDescriptor {
return this.handle.getFd();
}
@@ -266,6 +262,7 @@ const PosixBufferedReader = struct {
}
pub fn deinit(this: *PosixBufferedReader) void {
MaxBuf.removeFromPipereader(&this.maxbuf);
this.buffer().clearAndFree();
this.closeWithoutReporting();
}
@@ -468,6 +465,7 @@ const PosixBufferedReader = struct {
parent._offset,
)) {
.result => |bytes_read| {
if (parent.maxbuf) |l| l.onReadBytes(bytes_read);
parent._offset += bytes_read;
buf = stack_buffer_head[0..bytes_read];
stack_buffer_head = stack_buffer_head[bytes_read..];
@@ -560,6 +558,7 @@ const PosixBufferedReader = struct {
switch (sys_fn(fd, buf, parent._offset)) {
.result => |bytes_read| {
if (parent.maxbuf) |l| l.onReadBytes(bytes_read);
parent._offset += bytes_read;
buf = buf[0..bytes_read];
resizable_buffer.items.len += bytes_read;
@@ -678,6 +677,7 @@ pub const WindowsBufferedReader = struct {
_buffer: std.ArrayList(u8) = std.ArrayList(u8).init(bun.default_allocator),
// for compatibility with Linux
flags: Flags = .{},
maxbuf: ?*MaxBuf = null,
parent: *anyopaque = undefined,
vtable: WindowsOutputReaderVTable = undefined,
@@ -741,6 +741,7 @@ pub const WindowsBufferedReader = struct {
other._offset = 0;
other.buffer().* = std.ArrayList(u8).init(bun.default_allocator);
other.source = null;
MaxBuf.transferToPipereader(&other.maxbuf, &to.maxbuf);
to.setParent(parent);
}
@@ -802,6 +803,7 @@ pub const WindowsBufferedReader = struct {
}
fn _onReadChunk(this: *WindowsOutputReader, buf: []u8, hasMore: ReadState) bool {
if (this.maxbuf) |m| m.onReadBytes(buf.len);
this.flags.has_inflight_read = false;
if (hasMore == .eof) {
this.flags.received_eof = true;
@@ -867,6 +869,7 @@ pub const WindowsBufferedReader = struct {
}
pub fn deinit(this: *WindowsOutputReader) void {
MaxBuf.removeFromPipereader(&this.maxbuf);
this.buffer().deinit();
const source = this.source orelse return;
if (!source.isClosed()) {

View File

@@ -488,7 +488,7 @@ declare function $createCommonJSModule(
): JSCommonJSModule;
declare function $evaluateCommonJSModule(
moduleToEvaluate: JSCommonJSModule,
sourceModule: JSCommonJSModule
sourceModule: JSCommonJSModule,
): JSCommonJSModule[];
declare function $overridableRequire(this: JSCommonJSModule, id: string): any;
@@ -644,6 +644,7 @@ declare function $ERR_BUFFER_OUT_OF_BOUNDS(name?: string): RangeError;
declare function $ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(value, expected): TypeError;
declare function $ERR_CRYPTO_INCOMPATIBLE_KEY(name, value): Error;
declare function $ERR_CHILD_PROCESS_IPC_REQUIRED(where): Error;
declare function $ERR_CHILD_PROCESS_STDIO_MAXBUFFER(message): Error;
declare function $ERR_INVALID_ASYNC_ID(name, value): RangeError;
declare function $ERR_ASYNC_TYPE(name): TypeError;
declare function $ERR_ASYNC_CALLBACK(name): TypeError;

View File

@@ -38,6 +38,7 @@ cluster._setupWorker = function () {
// before calling, check if the channel is refd. if it isn't, then unref it after calling process.once();
$newZigFunction("node_cluster_binding.zig", "channelIgnoreOneDisconnectEventListener", 0)();
process.once("disconnect", () => {
process.channel = null;
worker.emit("disconnect");
if (!worker.exitedAfterDisconnect) {

View File

@@ -153,6 +153,7 @@ cluster.fork = function (env) {
});
worker.process.once("disconnect", () => {
worker.process.channel = null;
/*
* Now is a good time to remove the handles
* associated with this worker because it is

View File

@@ -241,6 +241,7 @@ function execFile(file, args, options, callback) {
windowsVerbatimArguments: options.windowsVerbatimArguments,
shell: options.shell,
signal: options.signal,
maxBuffer: options.maxBuffer,
});
let encoding;
@@ -270,16 +271,15 @@ function execFile(file, args, options, callback) {
if (!callback) return;
const readableEncoding = child?.stdout?.readableEncoding;
// merge chunks
let stdout;
let stderr;
if (encoding || (child.stdout && readableEncoding)) {
if (child.stdout?.readableEncoding) {
stdout = ArrayPrototypeJoin.$call(_stdout, "");
} else {
stdout = BufferConcat(_stdout);
}
if (encoding || (child.stderr && readableEncoding)) {
if (child.stderr?.readableEncoding) {
stderr = ArrayPrototypeJoin.$call(_stderr, "");
} else {
stderr = BufferConcat(_stderr);
@@ -339,84 +339,50 @@ function execFile(file, args, options, callback) {
}, options.timeout).unref();
}
const onData = (array, kind) => {
let total = 0;
let encodedLength;
return encoding
? function onDataEncoded(chunk) {
total += chunk.length;
function addOnDataListener(child_buffer, _buffer, kind) {
if (encoding) child_buffer.setEncoding(encoding);
if (total > maxBuffer) {
const out = child[kind];
const encoding = out.readableEncoding;
const actualLen = Buffer.byteLength(chunk, encoding);
if (encodedLength === undefined) {
encodedLength = 0;
let totalLen = 0;
if (maxBuffer === Infinity) {
child_buffer.on("data", function onDataNoMaxBuf(chunk) {
$arrayPush(_buffer, chunk);
});
return;
}
child_buffer.on("data", function onData(chunk) {
const encoding = child_buffer.readableEncoding;
if (encoding) {
const length = Buffer.byteLength(chunk, encoding);
totalLen += length;
for (let i = 0, length = array.length; i < length; i++) {
encodedLength += Buffer.byteLength(array[i], encoding);
}
}
if (totalLen > maxBuffer) {
const truncatedLen = maxBuffer - (totalLen - length);
$arrayPush(_buffer, String.prototype.slice.$call(chunk, 0, truncatedLen));
encodedLength += actualLen;
if (encodedLength > maxBuffer) {
const joined = ArrayPrototypeJoin.$call(array, "");
let combined = joined + chunk;
combined = StringPrototypeSlice.$call(combined, 0, maxBuffer);
array.length = 1;
array[0] = combined;
ex = ERR_CHILD_PROCESS_STDIO_MAXBUFFER(kind);
kill();
} else {
const val = ArrayPrototypeJoin.$call(array, "") + chunk;
array.length = 1;
array[0] = val;
}
} else {
$arrayPush(array, chunk);
}
ex = $ERR_CHILD_PROCESS_STDIO_MAXBUFFER(kind);
kill();
} else {
$arrayPush(_buffer, chunk);
}
: function onDataRaw(chunk) {
total += chunk.length;
} else {
const length = chunk.length;
totalLen += length;
if (total > maxBuffer) {
const truncatedLen = maxBuffer - (total - chunk.length);
$arrayPush(array, chunk.slice(0, truncatedLen));
if (totalLen > maxBuffer) {
const truncatedLen = maxBuffer - (totalLen - length);
$arrayPush(_buffer, chunk.slice(0, truncatedLen));
ex = ERR_CHILD_PROCESS_STDIO_MAXBUFFER(kind);
kill();
} else {
$arrayPush(array, chunk);
}
};
};
if (child.stdout) {
if (encoding) child.stdout.setEncoding(encoding);
child.stdout.on(
"data",
maxBuffer === Infinity
? function onUnlimitedSizeBufferedData(chunk) {
$arrayPush(_stdout, chunk);
}
: onData(_stdout, "stdout"),
);
ex = $ERR_CHILD_PROCESS_STDIO_MAXBUFFER(kind);
kill();
} else {
$arrayPush(_buffer, chunk);
}
}
});
}
if (child.stderr) {
if (encoding) child.stderr.setEncoding(encoding);
child.stderr.on(
"data",
maxBuffer === Infinity
? function onUnlimitedSizeBufferedData(chunk) {
$arrayPush(_stderr, chunk);
}
: onData(_stderr, "stderr"),
);
}
if (child.stdout) addOnDataListener(child.stdout, _stdout, "stdout");
if (child.stderr) addOnDataListener(child.stderr, _stderr, "stderr");
child.addListener("close", exitHandler);
child.addListener("error", errorHandler);
@@ -563,6 +529,7 @@ function spawnSync(file, args, options) {
exitCode,
signalCode,
exitedDueToTimeout,
exitedDueToMaxBuffer,
pid,
} = Bun.spawnSync({
// normalizeSpawnargs has already prepended argv0 to the spawnargs array
@@ -577,6 +544,7 @@ function spawnSync(file, args, options) {
argv0: options.args[0],
timeout: options.timeout,
killSignal: options.killSignal,
maxBuffer: options.maxBuffer,
});
} catch (err) {
error = err;
@@ -616,6 +584,15 @@ function spawnSync(file, args, options) {
"ETIMEDOUT",
);
}
if (exitedDueToMaxBuffer && error == null) {
result.error = new SystemError(
"spawnSync " + options.file + " ENOBUFS (stdout or stderr buffer reached maxBuffer size limit)",
options.file,
"spawnSync " + options.file,
enobufsErrorCode(),
"ENOBUFS",
);
}
if (result.error) {
result.error.syscall = "spawnSync " + options.file;
@@ -625,6 +602,7 @@ function spawnSync(file, args, options) {
return result;
}
const etimedoutErrorCode = $newZigFunction("node_util_binding.zig", "etimedoutErrorCode", 0);
const enobufsErrorCode = $newZigFunction("node_util_binding.zig", "enobufsErrorCode", 0);
/**
* Spawns a file as a shell synchronously.
@@ -1336,6 +1314,7 @@ class ChildProcess extends EventEmitter {
argv0: spawnargs[0],
windowsHide: !!options.windowsHide,
windowsVerbatimArguments: !!options.windowsVerbatimArguments,
maxBuffer: options.maxBuffer,
});
this.pid = this.#handle.pid;
@@ -1348,6 +1327,15 @@ class ChildProcess extends EventEmitter {
if (has_ipc) {
this.send = this.#send;
this.disconnect = this.#disconnect;
this.channel = new Control();
Object.defineProperty(this, "_channel", {
get() {
return this.channel;
},
set(value) {
this.channel = value;
},
});
if (options[kFromNode]) this.#closesNeeded += 1;
}
@@ -1416,8 +1404,8 @@ class ChildProcess extends EventEmitter {
return;
}
$assert(!this.connected);
this.#maybeClose();
process.nextTick(() => this.emit("disconnect"));
process.nextTick(() => this.#maybeClose());
}
#disconnect() {
if (!this.connected) {
@@ -1425,6 +1413,7 @@ class ChildProcess extends EventEmitter {
return;
}
this.#handle.disconnect();
this.channel = null;
}
kill(sig?) {
@@ -1612,6 +1601,12 @@ function abortChildProcess(child, killSignal, reason) {
}
}
class Control extends EventEmitter {
constructor() {
super();
}
}
class ShimmedStdin extends EventEmitter {
constructor() {
super();
@@ -1882,12 +1877,6 @@ function genericNodeError(message, errorProperties) {
// TypeError
// );
function ERR_CHILD_PROCESS_STDIO_MAXBUFFER(stdio) {
const err = Error(`${stdio} maxBuffer length exceeded`);
err.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER";
return err;
}
function ERR_UNKNOWN_SIGNAL(name) {
const err = new TypeError(`Unknown signal: ${name}`);
err.code = "ERR_UNKNOWN_SIGNAL";

View File

@@ -0,0 +1,154 @@
import { bunExe } from "harness";
const { isWindows } = require("../../node/test/common");
async function toUtf8(out: ReadableStream<Uint8Array>): Promise<string> {
const stream = new TextDecoderStream();
out.pipeTo(stream.writable);
let result = "";
for await (const chunk of stream.readable) {
result += chunk;
}
return result;
}
describe("yes is killed", () => {
// TODO
test("Bun.spawn", async () => {
const timeStart = Date.now();
const proc = Bun.spawn([bunExe(), "exec", "yes"], {
maxBuffer: 256,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
await proc.exited;
expect(proc.exitCode).toBe(null);
expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP");
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeLessThan(100); // make sure it's not waiting a full tick
const result = await toUtf8(proc.stdout);
expect(result).toStartWith("y\n".repeat(128));
const stderr = await toUtf8(proc.stderr);
expect(stderr).toBe("");
});
test("Bun.spawnSync", () => {
const timeStart = Date.now();
const proc = Bun.spawnSync([bunExe(), "exec", "yes"], {
maxBuffer: 256,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
expect(proc.exitedDueToMaxBuffer).toBe(true);
expect(proc.exitCode).toBe(null);
expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP");
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeLessThan(100); // make sure it's not waiting a full tick
const result = proc.stdout.toString("utf-8");
expect(result).toStartWith("y\n".repeat(128));
const stderr = proc.stderr.toString("utf-8");
expect(stderr).toBe("");
});
});
describe("maxBuffer infinity does not limit the number of bytes", () => {
const sample = "this is a long example string\n";
const sample_repeat_count = 10000;
test("Bun.spawn", async () => {
const proc = Bun.spawn([bunExe(), "-e", `console.log(${JSON.stringify(sample)}.repeat(${sample_repeat_count}))`], {
maxBuffer: Infinity,
});
await proc.exited;
expect(proc.exitCode).toBe(0);
const result = await toUtf8(proc.stdout);
expect(result).toBe(sample.repeat(sample_repeat_count) + "\n");
});
test("Bun.spawnSync", () => {
const proc = Bun.spawnSync(
[bunExe(), "-e", `console.log(${JSON.stringify(sample)}.repeat(${sample_repeat_count}))`],
{
maxBuffer: Infinity,
},
);
expect(proc.exitCode).toBe(0);
const result = proc.stdout.toString("utf-8");
expect(result).toBe(sample.repeat(sample_repeat_count) + "\n");
});
});
describe("timeout kills the process", () => {
test("Bun.spawn", async () => {
const timeStart = Date.now();
const proc = Bun.spawn([bunExe(), "exec", "sleep 5"], {
timeout: 100,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
await proc.exited;
expect(proc.exitCode).toBe(null);
expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP");
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeLessThan(200); // make sure it's terminating early
const result = await toUtf8(proc.stdout);
expect(result).toBe("");
const stderr = await toUtf8(proc.stderr);
expect(stderr).toBe("");
});
test("Bun.spawnSync", () => {
const timeStart = Date.now();
const proc = Bun.spawnSync([bunExe(), "exec", "sleep 5"], {
timeout: 100,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
expect(proc.exitedDueToTimeout).toBe(true);
expect(proc.exitCode).toBe(null);
expect(proc.signalCode).toBe(isWindows ? "SIGKILL" : "SIGHUP");
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeGreaterThan(100); // make sure it actually waits
expect(timeEnd - timeStart).toBeLessThan(200); // make sure it's terminating early
const result = proc.stdout.toString("utf-8");
expect(result).toBe("");
const stderr = proc.stderr.toString("utf-8");
expect(stderr).toBe("");
});
});
describe("timeout Infinity does not kill the process", () => {
test("Bun.spawn", async () => {
const timeStart = Date.now();
const proc = Bun.spawn([bunExe(), "exec", "sleep 1"], {
timeout: Infinity,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
await proc.exited;
expect(proc.exitCode).toBe(0);
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeGreaterThan(1000); // make sure it actually waits
expect(timeEnd - timeStart).toBeLessThan(1500); // make sure it's terminating early
const result = await toUtf8(proc.stdout);
expect(result).toBe("");
const stderr = await toUtf8(proc.stderr);
expect(stderr).toBe("");
});
test("Bun.spawnSync", () => {
const timeStart = Date.now();
const proc = Bun.spawnSync([bunExe(), "exec", "sleep 1"], {
timeout: Infinity,
killSignal: isWindows ? "SIGKILL" : "SIGHUP",
stdio: ["pipe", "pipe", "pipe"],
});
expect(proc.exitCode).toBe(0);
const timeEnd = Date.now();
expect(timeEnd - timeStart).toBeGreaterThan(1000); // make sure it actually waits
expect(timeEnd - timeStart).toBeLessThan(1500);
const result = proc.stdout.toString("utf-8");
expect(result).toBe("");
const stderr = proc.stderr.toString("utf-8");
expect(stderr).toBe("");
});
});

View File

@@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const exec = require('child_process').exec;
const { promisify } = require('util');
const execPromisifed = promisify(exec);
const invalidArgTypeError = {
code: 'ERR_INVALID_ARG_TYPE',
name: 'TypeError'
};
const waitCommand = common.isWindows ?
// `"` is forbidden for Windows paths, no need for escaping.
`"${process.execPath}" -e "setInterval(()=>{}, 99)"` :
'sleep 2m';
{
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
ac.abort();
assert.rejects(promise, {
name: 'AbortError',
cause: ac.signal.reason,
}).then(common.mustCall());
}
{
const err = new Error('boom');
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, {
name: 'AbortError',
cause: err
}).then(common.mustCall());
ac.abort(err);
}
{
const ac = new AbortController();
const signal = ac.signal;
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, {
name: 'AbortError',
cause: 'boom'
}).then(common.mustCall());
ac.abort('boom');
}
{
assert.throws(() => {
execPromisifed(waitCommand, { signal: {} });
}, invalidArgTypeError);
}
{
function signal() {}
assert.throws(() => {
execPromisifed(waitCommand, { signal });
}, invalidArgTypeError);
}
{
const signal = AbortSignal.abort(); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError' })
.then(common.mustCall());
}
{
const err = new Error('boom');
const signal = AbortSignal.abort(err); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError', cause: err })
.then(common.mustCall());
}
{
const signal = AbortSignal.abort('boom'); // Abort in advance
const promise = execPromisifed(waitCommand, { signal });
assert.rejects(promise, { name: 'AbortError', cause: 'boom' })
.then(common.mustCall());
}

View File

@@ -0,0 +1,146 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const cp = require('child_process');
function runChecks(err, stdio, streamName, expected) {
assert.strictEqual(err.message, `${streamName} maxBuffer length exceeded`);
assert(err instanceof RangeError);
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
assert.deepStrictEqual(stdio[streamName], expected);
}
// The execPath might contain chars that should be escaped in a shell context.
// On non-Windows, we can pass the path via the env; `"` is not a valid char on
// Windows, so we can simply pass the path.
const execNode = (args, optionsOrCallback, callback) => {
const [cmd, opts] = common.escapePOSIXShell`"${process.execPath}" `;
let options = optionsOrCallback;
if (typeof optionsOrCallback === 'function') {
options = undefined;
callback = optionsOrCallback;
}
return cp.exec(
cmd + args,
{ ...opts, ...options },
callback,
);
};
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024))"`, common.mustCall((err) => {
assert(err instanceof RangeError);
assert.strictEqual(err.message, 'stdout maxBuffer length exceeded');
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
}));
}
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024 - 1))"`, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
}));
}
{
const options = { maxBuffer: Infinity };
execNode(`-e "console.log('hello world');"`, options, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'hello world');
assert.strictEqual(stderr, '');
}));
}
{
const cmd = 'echo hello world';
cp.exec(
cmd,
{ maxBuffer: 5 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', 'hello');
})
);
}
// default value
{
execNode(
`-e "console.log('a'.repeat(1024 * 1024))"`,
common.mustCall((err, stdout, stderr) => {
runChecks(
err,
{ stdout, stderr },
'stdout',
'a'.repeat(1024 * 1024)
);
})
);
}
// default value
{
execNode(`-e "console.log('a'.repeat(1024 * 1024 - 1))"`, common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
}));
}
const unicode = '中文测试'; // length = 4, byte length = 12
{
execNode(
`-e "console.log('${unicode}');"`,
{ maxBuffer: 10 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', '中文测试\n');
})
);
}
{
execNode(
`-e "console.error('${unicode}');"`,
{ maxBuffer: 3 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stderr', '中文测');
})
);
}
{
const child = execNode(
`-e "console.log('${unicode}');"`,
{ encoding: null, maxBuffer: 10 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stdout', '中文测试\n');
})
);
child.stdout.setEncoding('utf-8');
}
{
const child = execNode(
`-e "console.error('${unicode}');"`,
{ encoding: null, maxBuffer: 3 },
common.mustCall((err, stdout, stderr) => {
runChecks(err, { stdout, stderr }, 'stderr', '中文测');
})
);
child.stderr.setEncoding('utf-8');
}
{
execNode(
`-e "console.error('${unicode}');"`,
{ encoding: null, maxBuffer: 5 },
common.mustCall((err, stdout, stderr) => {
const buf = Buffer.from(unicode).slice(0, 5);
runChecks(err, { stdout, stderr }, 'stderr', buf);
})
);
}

View File

@@ -0,0 +1,92 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const { execFile } = require('child_process');
function checkFactory(streamName) {
return common.mustCall((err) => {
assert(err instanceof RangeError);
assert.strictEqual(err.message, `${streamName} maxBuffer length exceeded`);
assert.strictEqual(err.code, 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER');
});
}
// default value
{
execFile(
process.execPath,
['-e', 'console.log("a".repeat(1024 * 1024))'],
checkFactory('stdout')
);
}
// default value
{
execFile(
process.execPath,
['-e', 'console.log("a".repeat(1024 * 1024 - 1))'],
common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'a'.repeat(1024 * 1024 - 1));
assert.strictEqual(stderr, '');
})
);
}
{
const options = { maxBuffer: Infinity };
execFile(
process.execPath,
['-e', 'console.log("hello world");'],
options,
common.mustSucceed((stdout, stderr) => {
assert.strictEqual(stdout.trim(), 'hello world');
assert.strictEqual(stderr, '');
})
);
}
{
execFile('echo', ['hello world'], { maxBuffer: 5 }, checkFactory('stdout'));
}
const unicode = '中文测试'; // length = 4, byte length = 12
{
execFile(
process.execPath,
['-e', `console.log('${unicode}');`],
{ maxBuffer: 10 },
checkFactory('stdout'));
}
{
execFile(
process.execPath,
['-e', `console.error('${unicode}');`],
{ maxBuffer: 10 },
checkFactory('stderr')
);
}
{
const child = execFile(
process.execPath,
['-e', `console.log('${unicode}');`],
{ encoding: null, maxBuffer: 10 },
checkFactory('stdout')
);
child.stdout.setEncoding('utf-8');
}
{
const child = execFile(
process.execPath,
['-e', `console.error('${unicode}');`],
{ encoding: null, maxBuffer: 10 },
checkFactory('stderr')
);
child.stderr.setEncoding('utf-8');
}

View File

@@ -0,0 +1,53 @@
'use strict';
require('../common');
// This test checks that the maxBuffer option for child_process.execFileSync()
// works as expected.
const assert = require('assert');
const { getSystemErrorName } = require('util');
const { execFileSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const args = [
'-e',
`console.log("${msgOut}");`,
];
// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execFileSync(process.execPath, args, { maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = execFileSync(process.execPath, args, { maxBuffer: Infinity });
assert.deepStrictEqual(ret, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
assert.throws(() => {
execFileSync(
process.execPath,
['-e', "console.log('a'.repeat(1024 * 1024))"]
);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
return true;
});
}

View File

@@ -0,0 +1,60 @@
'use strict';
const { escapePOSIXShell } = require('../common');
// This test checks that the maxBuffer option for child_process.spawnSync()
// works as expected.
const assert = require('assert');
const { getSystemErrorName } = require('util');
const { execSync } = require('child_process');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const [cmd, opts] = escapePOSIXShell`"${process.execPath}" -e "${`console.log('${msgOut}')`}"`;
// Verify that an error is returned if maxBuffer is surpassed.
{
assert.throws(() => {
execSync(cmd, { ...opts, maxBuffer: 1 });
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(e.stdout, msgOutBuf);
return true;
});
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = execSync(
cmd,
{ ...opts, maxBuffer: Infinity },
);
assert.deepStrictEqual(ret, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
assert.throws(() => {
execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024))"`);
}, (e) => {
assert.ok(e, 'maxBuffer should error');
assert.strictEqual(e.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(e.errno), 'ENOBUFS');
return true;
});
}
// Default maxBuffer size is 1024 * 1024.
{
const ret = execSync(...escapePOSIXShell`"${process.execPath}" -e "console.log('a'.repeat(1024 * 1024 - 1))"`);
assert.deepStrictEqual(
ret.toString().trim(),
'a'.repeat(1024 * 1024 - 1)
);
}

View File

@@ -0,0 +1,88 @@
'use strict';
const common = require('../common');
// Before https://github.com/nodejs/node/pull/2847 a child process trying
// (asynchronously) to use the closed channel to it's creator caused a segfault.
const assert = require('assert');
const cluster = require('cluster');
const net = require('net');
if (!cluster.isPrimary) {
// Exit on first received handle to leave the queue non-empty in primary
process.on('message', function() {
process.exit(1);
});
return;
}
const server = net
.createServer(function(s) {
if (common.isWindows) {
s.on('error', function(err) {
// Prevent possible ECONNRESET errors from popping up
if (err.code !== 'ECONNRESET') throw err;
});
}
setTimeout(function() {
s.destroy();
}, 100);
})
.listen(0, function() {
const worker = cluster.fork();
worker.on('error', function(err) {
if (
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE'
) {
throw err;
}
});
function send(callback) {
const s = net.connect(server.address().port, function() {
worker.send({}, s, callback);
});
// https://github.com/nodejs/node/issues/3635#issuecomment-157714683
// ECONNREFUSED or ECONNRESET errors can happen if this connection is
// still establishing while the server has already closed.
// EMFILE can happen if the worker __and__ the server had already closed.
s.on('error', function(err) {
if (
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE'
) {
throw err;
}
});
}
worker.process.once(
'close',
common.mustCall(function() {
// Otherwise the crash on `channel.fd` access may happen
assert.strictEqual(worker.process.channel, null);
server.close();
})
);
worker.on('online', function() {
send(function(err) {
assert.ifError(err);
send(function(err) {
// Ignore errors when sending the second handle because the worker
// may already have exited.
if (err && err.code !== 'ERR_IPC_CHANNEL_CLOSED' &&
err.code !== 'ECONNRESET' &&
err.code !== 'ECONNREFUSED' &&
err.code !== 'EMFILE') {
throw err;
}
});
});
});
});

View File

@@ -0,0 +1,58 @@
'use strict';
require('../common');
// This test checks that the maxBuffer option for child_process.spawnSync()
// works as expected.
const assert = require('assert');
const spawnSync = require('child_process').spawnSync;
const { getSystemErrorName } = require('util');
const msgOut = 'this is stdout';
const msgOutBuf = Buffer.from(`${msgOut}\n`);
const args = [
'-e',
`console.log("${msgOut}");`,
];
// Verify that an error is returned if maxBuffer is surpassed.
{
const ret = spawnSync(process.execPath, args, { maxBuffer: 1 });
assert.ok(ret.error, 'maxBuffer should error');
assert.strictEqual(ret.error.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(ret.error.errno), 'ENOBUFS');
// We can have buffers larger than maxBuffer because underneath we alloc 64k
// that matches our read sizes.
assert.deepStrictEqual(ret.stdout, msgOutBuf);
}
// Verify that a maxBuffer size of Infinity works.
{
const ret = spawnSync(process.execPath, args, { maxBuffer: Infinity });
assert.ifError(ret.error);
assert.deepStrictEqual(ret.stdout, msgOutBuf);
}
// Default maxBuffer size is 1024 * 1024.
{
const args = ['-e', "console.log('a'.repeat(1024 * 1024))"];
const ret = spawnSync(process.execPath, args);
assert.ok(ret.error, 'maxBuffer should error');
assert.strictEqual(ret.error.code, 'ENOBUFS');
assert.strictEqual(getSystemErrorName(ret.error.errno), 'ENOBUFS');
}
// Default maxBuffer size is 1024 * 1024.
{
const args = ['-e', "console.log('a'.repeat(1024 * 1024 - 1))"];
const ret = spawnSync(process.execPath, args);
assert.ifError(ret.error);
assert.deepStrictEqual(
ret.stdout.toString().trim(),
'a'.repeat(1024 * 1024 - 1)
);
}