diff --git a/lib/internal/debugger/inspect_helpers.js b/lib/internal/debugger/inspect_helpers.js index f83876e96bc0..68d7901b38c5 100644 --- a/lib/internal/debugger/inspect_helpers.js +++ b/lib/internal/debugger/inspect_helpers.js @@ -1,11 +1,20 @@ 'use strict'; const { + ArrayPrototypePop, + ArrayPrototypePush, ArrayPrototypePushApply, + MapPrototypeGet, Number, Promise, + PromiseWithResolvers, RegExpPrototypeExec, + RegExpPrototypeSymbolReplace, + SafePromiseRace, StringPrototypeEndsWith, + StringPrototypeIndexOf, + StringPrototypeSlice, + StringPrototypeStartsWith, } = primordials; const { spawn } = require('child_process'); @@ -18,12 +27,24 @@ const { AbortController, } = require('internal/abort_controller'); -const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; +const { + ERR_DEBUGGER_ERROR, + ERR_DEBUGGER_STARTUP_ERROR, +} = require('internal/errors').codes; const { exitCodes: { kInvalidCommandLineArgument, }, } = internalBinding('errors'); +const { + types: { + kBoolean, + kNoOp, + kV8Option, + }, +} = internalBinding('options'); + +const { getCLIOptionsInfo } = require('internal/options'); const debugRegex = /Debugger listening on ws:\/\/\[?(.+?)\]?:(\d+)\//; @@ -61,6 +82,176 @@ function ensureTrailingNewline(text) { return StringPrototypeEndsWith(text, '\n') ? text : `${text}\n`; } +// Mirror OptionsParser::Parse() far enough to find the child script. Options +// before it must not undo the inspector setup added by launchChildProcess(). +function validateChildArgs(childArgs) { + const { options, aliases } = getCLIOptionsInfo(); + const syntheticArgs = []; + let breakFirstLine = true; + let childArgIndex = 0; + let inspectorEnabled = true; + + function peekArg() { + return syntheticArgs.length > 0 ? + syntheticArgs[syntheticArgs.length - 1] : + childArgs[childArgIndex]; + } + + function shiftArg() { + return syntheticArgs.length > 0 ? + ArrayPrototypePop(syntheticArgs) : + childArgs[childArgIndex++]; + } + + while (true) { + const nextArg = peekArg(); + if (nextArg === undefined || nextArg.length <= 1 || nextArg[0] !== '-') { + break; + } + + const isSynthetic = syntheticArgs.length > 0; + const arg = shiftArg(); + if (arg === '--') { break; } + if (!isSynthetic && + (arg === '--experimental-config-file' || + arg === '--experimental-default-config-file')) { + // ConfigReader rewrites these to an inline default path before parsing. + continue; + } + if (!isSynthetic && + StringPrototypeStartsWith( + arg, '--experimental-default-config-file=')) { + // ConfigReader rejects this form before parsing the remaining options. + return; + } + + const equalsIndex = arg[1] === '-' ? StringPrototypeIndexOf(arg, '=') : -1; + let name = equalsIndex === -1 ? arg : StringPrototypeSlice(arg, 0, equalsIndex); + if (name.length > 2) { + name = `${StringPrototypeSlice(name, 0, 2)}${ + RegExpPrototypeSymbolReplace(/_/g, StringPrototypeSlice(name, 2), '-')}`; + } + + let isNegation = false; + if (StringPrototypeStartsWith(name, '--no-')) { + name = `--${StringPrototypeSlice(name, 5)}`; + isNegation = true; + } + + while (true) { + let expansion = MapPrototypeGet(aliases, name); + if (expansion === undefined && equalsIndex !== -1) { + expansion = MapPrototypeGet(aliases, `${name}=`); + } + const aliasArg = peekArg(); + if (expansion === undefined && + aliasArg !== undefined && + aliasArg.length > 0 && + aliasArg[0] !== '-') { + expansion = MapPrototypeGet(aliases, `${name} `); + } + if (expansion === undefined) { break; } + + const previousName = name; + // process.allowedNodeEnvironmentFlags may remove a self-recursive + // first entry from the cached alias metadata. Preserve the native + // parser's synthetic option terminator in that case. + if (expansion[0] === '--') { + for (let i = expansion.length - 1; i >= 0; i--) { + ArrayPrototypePush(syntheticArgs, expansion[i]); + } + break; + } + name = expansion[0]; + for (let i = expansion.length - 1; i > 0; i--) { + ArrayPrototypePush(syntheticArgs, expansion[i]); + } + if (name === previousName) { break; } + } + + const info = MapPrototypeGet(options, name); + if (info === undefined) { continue; } + if (isNegation && info.type !== kBoolean && info.type !== kV8Option) { + return; + } + if (info.type === kBoolean || info.type === kNoOp || info.type === kV8Option) { + if (name === '--inspect') { + inspectorEnabled = !isNegation; + } else if (name === '--inspect-brk') { + breakFirstLine = !isNegation; + if (!isNegation) { inspectorEnabled = true; } + } else if (!isNegation && + (name === '--inspect-wait' || + name === '--inspect-brk-node')) { + inspectorEnabled = true; + } + continue; + } + + if (equalsIndex !== -1) { + if (equalsIndex === arg.length - 1) { return; } + continue; + } + + const value = peekArg(); + if (value === undefined || (value.length > 0 && value[0] === '-')) { + return; + } + shiftArg(); + } + + if (!inspectorEnabled) { + throw new ERR_DEBUGGER_STARTUP_ERROR( + '--no-inspect is incompatible with node inspect before the child script'); + } + if (!breakFirstLine) { + throw new ERR_DEBUGGER_STARTUP_ERROR( + '--no-inspect-brk is incompatible with node inspect before the child script'); + } +} + +async function waitForDebugger( + client, + callMethod = (method) => client.callMethod(method), +) { + const { + promise: waitingPromise, + resolve: resolveWaiting, + } = PromiseWithResolvers(); + const { + promise: closedPromise, + reject: rejectClosed, + } = PromiseWithResolvers(); + const onWaiting = () => resolveWaiting(); + const onClose = () => { + rejectClosed(new ERR_DEBUGGER_ERROR( + 'Debugger session ended while waiting for target startup')); + }; + + // The inspector can accept a connection before the target reaches its + // startup wait. Enabling NodeRuntime makes that state observable whether + // the target was already waiting or starts waiting later. + client.once('NodeRuntime.waitingForDebugger', onWaiting); + client.once('close', onClose); + try { + await SafePromiseRace([ + callMethod('NodeRuntime.enable'), + closedPromise, + ]); + await SafePromiseRace([ + waitingPromise, + closedPromise, + ]); + await SafePromiseRace([ + callMethod('NodeRuntime.disable'), + closedPromise, + ]); + } finally { + client.removeListener('NodeRuntime.waitingForDebugger', onWaiting); + client.removeListener('close', onClose); + } +} + function writeInspectUsageAndExit(invokedAs, message, exitCode) { const code = exitCode ?? (message ? kInvalidCommandLineArgument : 0); const out = code === 0 ? process.stdout : process.stderr; @@ -141,6 +332,8 @@ probe output schema. async function launchChildProcess(childArgs, inspectHost, inspectPort, childOutput, options = { __proto__: null }) { + validateChildArgs(childArgs); + if (!options.skipPortPreflight) { await portIsFree(inspectHost, inspectPort); } @@ -189,5 +382,6 @@ async function launchChildProcess(childArgs, inspectHost, inspectPort, module.exports = { ensureTrailingNewline, launchChildProcess, + waitForDebugger, writeInspectUsageAndExit, }; diff --git a/lib/internal/debugger/inspect_probe.js b/lib/internal/debugger/inspect_probe.js index b6cac6dc5779..3b77a46f1ba1 100644 --- a/lib/internal/debugger/inspect_probe.js +++ b/lib/internal/debugger/inspect_probe.js @@ -33,6 +33,7 @@ const InspectClient = require('internal/debugger/inspect_client'); const { ensureTrailingNewline, launchChildProcess, + waitForDebugger, } = require('internal/debugger/inspect_helpers'); const { ERR_DEBUGGER_STARTUP_ERROR } = require('internal/errors').codes; @@ -1044,6 +1045,17 @@ class ProbeInspectorSession { this.connected = true; try { + try { + await waitForDebugger( + this.client, + (method) => this.callCdp(method), + ); + } catch (err) { + // A close event may have completed the structured report while the + // readiness helper was rejecting its disconnect race. + if (this.finished) { throw kInspectorFailedSentinel; } + throw err; + } await this.callCdp('Runtime.enable'); await this.callCdp('Debugger.enable'); await this.bindBreakpoints(); diff --git a/lib/internal/debugger/inspect_repl.js b/lib/internal/debugger/inspect_repl.js index 548df089fb14..69ca174dd241 100644 --- a/lib/internal/debugger/inspect_repl.js +++ b/lib/internal/debugger/inspect_repl.js @@ -60,6 +60,7 @@ const { fileURLToPath } = require('internal/url'); const { customInspectSymbol, SideEffectFreeRegExpPrototypeSymbolReplace } = require('internal/util'); const { inspect: utilInspect } = require('internal/util/inspect'); const { isObjectLiteral } = require('internal/repl/utils'); +const { waitForDebugger } = require('internal/debugger/inspect_helpers'); const debuglog = require('internal/util/debuglog').debuglog('inspect'); const SHORTCUTS = { @@ -1204,9 +1205,13 @@ function createRepl(inspector) { } async function initAfterStart() { + const waitForDebuggerOnStart = !!inspector.options?.script; waitForInitialBreakRender = - !!inspector.options?.script && + waitForDebuggerOnStart && process.env.NODE_INSPECT_RESUME_ON_START !== '1'; + if (waitForDebuggerOnStart) { + await waitForDebugger(inspector.client); + } await Runtime.enable(); await Profiler.enable(); await Profiler.setSamplingInterval({ interval: 100 }); diff --git a/lib/internal/streams/iter/broadcast.js b/lib/internal/streams/iter/broadcast.js index a1384c9e4d51..4c63774bbb32 100644 --- a/lib/internal/streams/iter/broadcast.js +++ b/lib/internal/streams/iter/broadcast.js @@ -116,17 +116,32 @@ class BroadcastImpl { push(...args) { const { transforms, options } = parsePullArgs(args); + const signal = options?.signal; + validateAbortSignal(signal, 'options.signal'); + + // Avoid registering a consumer that the pre-aborted pipeline will never + // read or detach. + if (signal?.aborted) { + return { + __proto__: null, + // eslint-disable-next-line require-yield + async *[SymbolAsyncIterator]() { + throw signal.reason; + }, + }; + } + const rawConsumer = this.#createRawConsumer(); // When transforms are present, delegate to pull() which creates its // own internal AbortController that follows the external signal. // When no transforms, return rawConsumer directly (controller elided // per PULL-02 optimization -- no transforms means no signal recipient). - if (transforms.length > 0 || options?.signal) { + if (transforms.length > 0 || signal) { const pullArgs = [...transforms]; - if (options?.signal) { + if (signal) { ArrayPrototypePush(pullArgs, - { __proto__: null, signal: options.signal }); + { __proto__: null, signal }); } return pullWithTransforms(rawConsumer, ...pullArgs); } diff --git a/lib/internal/streams/iter/share.js b/lib/internal/streams/iter/share.js index 97efe7f3065b..662e57a7df55 100644 --- a/lib/internal/streams/iter/share.js +++ b/lib/internal/streams/iter/share.js @@ -93,11 +93,29 @@ class ShareImpl { pull(...args) { const { transforms, options } = parsePullArgs(args); + const signal = options?.signal; + validateAbortSignal(signal, 'options.signal'); + + // Avoid registering a consumer that the pre-aborted pipeline will never + // read or detach. + if (signal?.aborted) { + return { + __proto__: null, + // eslint-disable-next-line require-yield + async *[SymbolAsyncIterator]() { + throw signal.reason; + }, + }; + } + const rawConsumer = this.#createRawConsumer(); - if (transforms.length > 0 || options?.signal) { - if (options) { - return pullWithTransforms(rawConsumer, ...transforms, options); + if (transforms.length > 0 || signal) { + if (signal) { + return pullWithTransforms( + rawConsumer, + ...transforms, + { __proto__: null, signal }); } return pullWithTransforms(rawConsumer, ...transforms); } diff --git a/lib/internal/streams/iter/transform.js b/lib/internal/streams/iter/transform.js index 583c6e9b192d..cb35906ded02 100644 --- a/lib/internal/streams/iter/transform.js +++ b/lib/internal/streams/iter/transform.js @@ -40,6 +40,7 @@ const { isArrayBufferView, isAnyArrayBuffer } = require('internal/util/types'); const { kValidatedTransform } = require('internal/streams/iter/types'); const { checkRangesOrGetDefault, + kValidateObjectAllowArray, validateFiniteNumber, validateObject, } = require('internal/validators'); @@ -106,9 +107,7 @@ function validateDictionary(dictionary) { function validateParams(params, maxParam, errClass) { if (params === undefined) return; - if (typeof params !== 'object' || params === null) { - throw new ERR_INVALID_ARG_TYPE('options.params', 'Object', params); - } + validateObject(params, 'options.params', kValidateObjectAllowArray); const keys = ObjectKeys(params); for (let i = 0; i < keys.length; i++) { const origKey = keys[i]; diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 2582217febc6..6b608a66733a 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -761,7 +761,7 @@ function setEquiv(a, b, mode, memo) { // If the specified value doesn't exist in the second set it's a object // (or in loose mode: a non-matching primitive). Find the // deep-(mode-)equal element in a set copy to reduce duplicate checks. - array.push(val); + ArrayPrototypePush(array, val); } } @@ -891,7 +891,7 @@ function mapEquiv(a, b, mode, memo) { } array = []; } - array.push(key2); + ArrayPrototypePush(array, key2); } else { // By directly retrieving the value we prevent another b.has(key2) check in // almost all possible cases. @@ -907,7 +907,7 @@ function mapEquiv(a, b, mode, memo) { if (array === undefined) { array = []; } - array.push(key2); + ArrayPrototypePush(array, key2); } } } diff --git a/src/node_sqlite.cc b/src/node_sqlite.cc index 8e6a230f625a..d0da887062bd 100644 --- a/src/node_sqlite.cc +++ b/src/node_sqlite.cc @@ -105,6 +105,24 @@ void BindingData::CreatePerContextProperties(Local target, principal->AddBindingData(target); } +inline MaybeLocal IntegerToValue(Isolate* isolate, + sqlite3_int64 value, + bool use_big_ints) { + if (use_big_ints) { + return BigInt::New(isolate, value); + } + + if (value < -kMaxSafeJsInteger || value > kMaxSafeJsInteger) { + THROW_ERR_OUT_OF_RANGE( + isolate, + "Value is too large to be represented as a JavaScript number: %" PRId64, + value); + return MaybeLocal(); + } + + return Number::New(isolate, value); +} + #define CHECK_ERROR_OR_THROW(isolate, db, expr, expected, ret) \ do { \ int r_ = (expr); \ @@ -159,16 +177,7 @@ void BindingData::CreatePerContextProperties(Local target, switch (sqlite3_##from##_type(__VA_ARGS__)) { \ case SQLITE_INTEGER: { \ sqlite3_int64 val = sqlite3_##from##_int64(__VA_ARGS__); \ - if ((use_big_int_args)) { \ - (result) = BigInt::New((isolate), val); \ - } else if (std::abs(val) <= kMaxSafeJsInteger) { \ - (result) = Number::New((isolate), val); \ - } else { \ - THROW_ERR_OUT_OF_RANGE((isolate), \ - "Value is too large to be represented as a " \ - "JavaScript number: %" PRId64, \ - val); \ - } \ + (result) = IntegerToValue((isolate), val, (use_big_int_args)); \ break; \ } \ case SQLITE_FLOAT: { \ @@ -3251,12 +3260,10 @@ MaybeLocal StatementExecutionHelper::Run(Environment* env, Local last_insert_rowid_val; Local changes_val; - if (use_big_ints) { - last_insert_rowid_val = BigInt::New(isolate, last_insert_rowid); - changes_val = BigInt::New(isolate, changes); - } else { - last_insert_rowid_val = Number::New(isolate, last_insert_rowid); - changes_val = Number::New(isolate, changes); + if (!IntegerToValue(isolate, last_insert_rowid, use_big_ints) + .ToLocal(&last_insert_rowid_val) || + !IntegerToValue(isolate, changes, use_big_ints).ToLocal(&changes_val)) { + return MaybeLocal(); } auto run_result_template = env->sqlite_run_result_template(); diff --git a/test/parallel/test-debugger-no-inspect-brk.js b/test/parallel/test-debugger-no-inspect-brk.js new file mode 100644 index 000000000000..611d28fd9f3e --- /dev/null +++ b/test/parallel/test-debugger-no-inspect-brk.js @@ -0,0 +1,158 @@ +// Flags: --expose-internals + +// This tests that child --no-inspect and --no-inspect-brk options cannot leave +// the inspector setup disabled, while remaining valid as application args. +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const { + spawnSyncAndAssert, + spawnSyncAndExit, +} = require('../common/child_process'); +const { assertProbeJson } = require('../common/debugger-probe'); +const { launchChildProcess } = require('internal/debugger/inspect_helpers'); + +const cwd = fixtures.path('debugger'); +const probeUrl = fixtures.fileURL('debugger', 'probe.js').href; +const probeArgs = [ + '--probe', 'probe.js:12', + '--expr', 'finalValue', +]; +const incompatibleInspectBrk = + /--no-inspect-brk is incompatible with node inspect before the child script/; +const incompatibleInspect = + /--no-inspect is incompatible with node inspect before the child script/; + +function assertSuccessfulProbe(childArgs) { + spawnSyncAndAssert(process.execPath, [ + 'inspect', + '--json', + ...probeArgs, + '--', + ...childArgs, + ], { cwd }, { + stdout(output) { + assertProbeJson(output, { + v: 2, + probes: [{ + expr: 'finalValue', + target: { suffix: 'probe.js', line: 12 }, + }], + results: [{ + probe: 0, + event: 'hit', + hit: 1, + location: { url: probeUrl, line: 12, column: 1 }, + result: { type: 'number', value: 81, description: '81' }, + }, { + event: 'completed', + }], + }); + }, + trim: true, + }); +} + +for (const childOptions of [ + ['--require', 'assert', '--no-inspect-brk'], + ['--require=assert', '--no-inspect-brk'], + ['-r', 'assert', '--no_inspect_brk'], +]) { + spawnSyncAndExit(process.execPath, [ + 'inspect', + ...probeArgs, + '--', + ...childOptions, + 'probe.js', + ], { cwd }, { + signal: null, + status: 1, + stderr: incompatibleInspectBrk, + trim: true, + }); +} + +spawnSyncAndExit(process.execPath, [ + 'inspect', + ...probeArgs, + '--', + '--require', 'assert', + '--no-inspect', + 'probe.js', +], { cwd }, { + signal: null, + status: 1, + stderr: incompatibleInspect, + trim: true, +}); + +for (const { option, error } of [ + { option: '--no-inspect-brk', error: incompatibleInspectBrk }, + { option: '--no-inspect', error: incompatibleInspect }, +]) { + spawnSyncAndExit(process.execPath, [ + 'inspect', + option, + 'probe.js', + ], { cwd }, { + signal: null, + status: 1, + stderr: error, + trim: true, + }); + + assertSuccessfulProbe(['probe.js', option]); +} + +// Node options are last-write-wins. A later --inspect-brk restores both +// startup requirements. +assertSuccessfulProbe([ + '--no-inspect', + '--no-inspect-brk', + '--inspect-brk', + 'probe.js', +]); + +// ConfigReader rewrites these bare options to use the default path without +// consuming the following argument. +Promise.all([ + assert.rejects( + launchChildProcess([ + '--experimental-config-file', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + assert.rejects( + launchChildProcess([ + '--experimental-default-config-file', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + // These options imply --inspect, but do not restore --inspect-brk. + assert.rejects( + launchChildProcess([ + '--no-inspect', + '--inspect-wait', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), + assert.rejects( + launchChildProcess([ + '--no-inspect', + '--inspect-brk-node', + '--no-inspect-brk', + 'probe.js', + ], '127.0.0.1', 0, () => {}), + incompatibleInspectBrk, + ), +]).then(common.mustCall()); diff --git a/test/parallel/test-debugger-probe-startup-disconnect.js b/test/parallel/test-debugger-probe-startup-disconnect.js new file mode 100644 index 000000000000..68c8432dc552 --- /dev/null +++ b/test/parallel/test-debugger-probe-startup-disconnect.js @@ -0,0 +1,56 @@ +// Flags: --expose-internals +// This tests that a disconnect while probe mode is waiting for target startup +// is reported as a structured probe failure instead of an internal error. +'use strict'; + +const common = require('../common'); +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { assertProbeJson } = require('../common/debugger-probe'); +const { ProbeInspectorSession } = require('internal/debugger/inspect_probe'); + +const probe = { + expr: 'value', + target: { suffix: 'probe-target.js', line: 1 }, +}; +const client = new EventEmitter(); +client.connect = common.mustCall(); +client.callMethod = common.mustCall((method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + setImmediate(() => client.emit('close')); + return new Promise(() => {}); +}); +client.reset = common.mustCall(); + +const session = new ProbeInspectorSession({ + childArgv: ['-e', ''], + host: '127.0.0.1', + port: 0, + probes: [probe], + skipPortPreflight: true, +}); +session.client = client; + +session.run().then(common.mustCall(({ code, report }) => { + assert.strictEqual(code, 1); + assertProbeJson(report, { + v: 2, + probes: [probe], + results: [{ + event: 'error', + pending: [0], + error: { + code: 'probe_failure', + message: + 'Inspector connection lost before probes started before probes: ' + + 'probe-target.js:1. The target startup may have torn down the ' + + 'inspector. If startup does not touch the inspector, this is likely ' + + 'a Node.js bug. Please file an issue.', + stderr: '', + details: { lastCdpMethod: 'NodeRuntime.enable' }, + }, + }], + }); +})); diff --git a/test/parallel/test-debugger-run-restart-init.js b/test/parallel/test-debugger-run-restart-init.js index 78f237353baf..b57939135f80 100644 --- a/test/parallel/test-debugger-run-restart-init.js +++ b/test/parallel/test-debugger-run-restart-init.js @@ -79,9 +79,29 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { const runGate = createGate(); const restartGate = createGate(); const gates = [null, runGate, restartGate]; + const client = new EventEmitter(); + let nodeRuntimeEnableCount = 0; + client.callMethod = common.mustCall(async (method) => { + calls.push(method); + if (method === 'NodeRuntime.enable') { + const emitWaiting = () => { + calls.push('NodeRuntime.waitingForDebugger'); + client.emit('NodeRuntime.waitingForDebugger'); + }; + // Cover notifications arriving both before and after the enable reply. + if (nodeRuntimeEnableCount++ % 2 === 0) { + emitWaiting(); + } else { + setImmediate(emitWaiting); + } + } else { + assert.strictEqual(method, 'NodeRuntime.disable'); + } + }, 6); const inspector = { - client: new EventEmitter(), + client, domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: { script: 'debugger-target.js' }, stdin: new PassThrough(), stdout: new PassThrough(), run: common.mustCall(async () => { @@ -101,6 +121,29 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { await assertCommandWaitsForInit(repl, 'run', runGate, calls); await assertCommandWaitsForInit(repl, 'restart', restartGate, calls); + assert.deepStrictEqual( + calls.filter((call) => ( + call === 'NodeRuntime.enable' || + call === 'NodeRuntime.waitingForDebugger' || + call === 'NodeRuntime.disable' || + call === 'Runtime.runIfWaitingForDebugger' + )), + [ + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + 'NodeRuntime.enable', + 'NodeRuntime.waitingForDebugger', + 'NodeRuntime.disable', + 'Runtime.runIfWaitingForDebugger', + ], + ); + assert.deepStrictEqual( calls.filter((call) => ( call === 'inspector.run' || @@ -116,4 +159,25 @@ async function assertCommandWaitsForInit(repl, command, gate, calls) { ); repl.close(); + + const attachCalls = []; + const attachClient = new EventEmitter(); + attachClient.callMethod = common.mustNotCall(); + const attachInspector = { + client: attachClient, + domainNames: ['Debugger', 'HeapProfiler', 'Profiler', 'Runtime'], + options: {}, + stdin: new PassThrough(), + stdout: new PassThrough(), + suspendReplWhile(fn) { + return fn(); + }, + }; + + for (const domain of attachInspector.domainNames) { + attachInspector[domain] = createAgent(domain, attachCalls, []); + } + + const attachRepl = await createRepl(attachInspector)(); + attachRepl.close(); })().then(common.mustCall()); diff --git a/test/parallel/test-debugger-wait-for-debugger.js b/test/parallel/test-debugger-wait-for-debugger.js new file mode 100644 index 000000000000..b438147ea832 --- /dev/null +++ b/test/parallel/test-debugger-wait-for-debugger.js @@ -0,0 +1,137 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +common.skipIfInspectorDisabled(); + +const assert = require('assert'); +const { EventEmitter } = require('events'); +const { + waitForDebugger, +} = require('internal/debugger/inspect_helpers'); + +function assertListenersRemoved(client) { + assert.strictEqual( + client.listenerCount('NodeRuntime.waitingForDebugger'), + 0, + ); + assert.strictEqual(client.listenerCount('close'), 0); +} + +async function testWaitingNotification(beforeEnableReply) { + const client = new EventEmitter(); + const calls = []; + client.callMethod = common.mustCall(async (method) => { + calls.push(method); + const emitWaiting = () => { + client.emit('NodeRuntime.waitingForDebugger'); + }; + if (method === 'NodeRuntime.enable') { + if (beforeEnableReply) { + emitWaiting(); + } else { + setImmediate(emitWaiting); + } + } else { + assert.strictEqual(method, 'NodeRuntime.disable'); + } + }, 2); + + await waitForDebugger(client); + assert.deepStrictEqual(calls, [ + 'NodeRuntime.enable', + 'NodeRuntime.disable', + ]); + assertListenersRemoved(client); +} + +async function testCloseWhileWaiting(beforeEnableReply) { + const client = new EventEmitter(); + client.callMethod = common.mustCall((method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + setImmediate(() => client.emit('close')); + return beforeEnableReply ? new Promise(() => {}) : Promise.resolve(); + }); + + await assert.rejects( + waitForDebugger(client), + { + code: 'ERR_DEBUGGER_ERROR', + message: 'Debugger session ended while waiting for target startup', + }, + ); + assertListenersRemoved(client); +} + +async function testCloseWhileDisabling() { + const client = new EventEmitter(); + client.callMethod = common.mustCall((method) => { + if (method === 'NodeRuntime.enable') { + client.emit('NodeRuntime.waitingForDebugger'); + return Promise.resolve(); + } + assert.strictEqual(method, 'NodeRuntime.disable'); + setImmediate(() => client.emit('close')); + return new Promise(() => {}); + }, 2); + + await assert.rejects( + waitForDebugger(client), + { + code: 'ERR_DEBUGGER_ERROR', + message: 'Debugger session ended while waiting for target startup', + }, + ); + assertListenersRemoved(client); +} + +async function testEnableFailure() { + const client = new EventEmitter(); + const expected = new Error('NodeRuntime.enable failed'); + client.callMethod = common.mustCall(async (method) => { + assert.strictEqual(method, 'NodeRuntime.enable'); + throw expected; + }); + + await assert.rejects( + waitForDebugger(client), + (error) => { + assert.strictEqual(error, expected); + return true; + }, + ); + assertListenersRemoved(client); +} + +async function testDisableFailure() { + const client = new EventEmitter(); + const expected = new Error('NodeRuntime.disable failed'); + client.callMethod = common.mustCall(async (method) => { + if (method === 'NodeRuntime.enable') { + client.emit('NodeRuntime.waitingForDebugger'); + return; + } + assert.strictEqual(method, 'NodeRuntime.disable'); + throw expected; + }, 2); + + await assert.rejects( + waitForDebugger(client), + (error) => { + assert.strictEqual(error, expected); + return true; + }, + ); + assertListenersRemoved(client); +} + +(async () => { + await testWaitingNotification(true); + await testWaitingNotification(false); + await testCloseWhileWaiting(true); + await testCloseWhileWaiting(false); + await testCloseWhileDisabling(); + await testEnableFailure(); + await testDisableFailure(); +})().then(common.mustCall()); diff --git a/test/parallel/test-http-server-stale-close.js b/test/parallel/test-http-server-stale-close.js index d33c756a4a30..909d02c920eb 100644 --- a/test/parallel/test-http-server-stale-close.js +++ b/test/parallel/test-http-server-stale-close.js @@ -35,7 +35,7 @@ if (process.env.NODE_TEST_FORK_PORT) { req.write('BAM'); req.end(); } else { - const server = http.createServer(common.mustCallAtLeast((req, res) => { + const server = http.createServer(common.mustCall((req, res) => { res.writeHead(200, { 'Content-Length': '42' }); req.pipe(res); assert.strictEqual(req.destroyed, false); @@ -45,9 +45,13 @@ if (process.env.NODE_TEST_FORK_PORT) { res.end(); })); })); - server.listen(0, function() { - fork(__filename, { + server.listen(0, common.mustCall(function() { + const cp = fork(__filename, { + stdio: 'inherit', env: { ...process.env, NODE_TEST_FORK_PORT: this.address().port } }); - }); + cp.once('exit', common.mustCall((code) => { + assert.strictEqual(code, 0); + })); + })); } diff --git a/test/parallel/test-sqlite-statement-sync.js b/test/parallel/test-sqlite-statement-sync.js index b13da41e9d71..44eb482f9bdb 100644 --- a/test/parallel/test-sqlite-statement-sync.js +++ b/test/parallel/test-sqlite-statement-sync.js @@ -821,6 +821,25 @@ suite('StatementSync.prototype.setReadBigInts()', () => { }); }); + test('BigInt is required for reading large last insert row IDs', (t) => { + using db = new DatabaseSync(':memory:'); + db.exec('CREATE TABLE data(key INTEGER PRIMARY KEY) STRICT'); + const insert = db.prepare('INSERT INTO data VALUES (?)'); + + t.assert.throws(() => { + insert.run(9007199254740993n); + }, { + code: 'ERR_OUT_OF_RANGE', + message: /^Value is too large to be represented as a JavaScript number: 9007199254740993$/, + }); + + insert.setReadBigInts(true); + t.assert.deepStrictEqual(insert.run(9007199254740995n), { + changes: 1n, + lastInsertRowid: 9007199254740995n, + }); + }); + test('throws if the statement is already finalized', (t) => { using db = new DatabaseSync(':memory:'); const stmt = db.prepare('CREATE TABLE storage(key TEXT, val TEXT)'); diff --git a/test/parallel/test-stream-iter-broadcast-basic.js b/test/parallel/test-stream-iter-broadcast-basic.js index 125f386210c9..4438e82a7817 100644 --- a/test/parallel/test-stream-iter-broadcast-basic.js +++ b/test/parallel/test-stream-iter-broadcast-basic.js @@ -203,6 +203,17 @@ async function testPushAbortSignalRejectsPendingNext() { await rejected; } +async function testPushPreAbortedSignalDoesNotAddConsumer() { + const reason = new Error('already aborted'); + const signal = AbortSignal.abort(reason); + const { broadcast: bc } = broadcast(); + const iter = bc.push({ signal })[Symbol.asyncIterator](); + + assert.strictEqual(bc.consumerCount, 0); + await assert.rejects(iter.next(), (error) => error === reason); + assert.strictEqual(bc.consumerCount, 0); +} + // ============================================================================= // Writer fail detaches consumers // ============================================================================= @@ -331,6 +342,7 @@ Promise.all([ testCancelWithFalsyReason(), testPendingNextSettlesAfterReturn(), testPushAbortSignalRejectsPendingNext(), + testPushPreAbortedSignalDoesNotAddConsumer(), testFailDetachesConsumers(), testWriterFailIdempotent(), testLateJoinerSeesBufferedData(), diff --git a/test/parallel/test-stream-iter-share-async.js b/test/parallel/test-stream-iter-share-async.js index 314e7bfcf01c..c96a0cb0f3c3 100644 --- a/test/parallel/test-stream-iter-share-async.js +++ b/test/parallel/test-stream-iter-share-async.js @@ -226,6 +226,17 @@ async function testSharePullAbortSignalRejectsPendingNext() { shared.cancel(); } +async function testSharePullPreAbortedSignalDoesNotAddConsumer() { + const reason = new Error('already aborted'); + const signal = AbortSignal.abort(reason); + const shared = share(from('data')); + const iter = shared.pull({ signal })[Symbol.asyncIterator](); + + assert.strictEqual(shared.consumerCount, 0); + await assert.rejects(iter.next(), (error) => error === reason); + assert.strictEqual(shared.consumerCount, 0); +} + async function testShareAlreadyAborted() { const shared = share(from('data'), { signal: AbortSignal.abort() }); const consumer = shared.pull(); @@ -372,6 +383,7 @@ Promise.all([ testShareAbortSignal(), testShareAbortSignalWhileSourcePullPending(), testSharePullAbortSignalRejectsPendingNext(), + testSharePullPreAbortedSignalDoesNotAddConsumer(), testShareAlreadyAborted(), testShareSourceError(), testShareLateJoiningConsumer(), diff --git a/test/parallel/test-stream-iter-transform-params.js b/test/parallel/test-stream-iter-transform-params.js new file mode 100644 index 000000000000..aa029f9713c9 --- /dev/null +++ b/test/parallel/test-stream-iter-transform-params.js @@ -0,0 +1,30 @@ +// Flags: --experimental-stream-iter +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { from, pull, bytes } = require('stream/iter'); +const { compressBrotli, compressZstd } = require('zlib/iter'); + +// Type validation of options.params in zlib/iter transforms: plain +// objects and arrays pass the check, any other value rejects with +// ERR_INVALID_ARG_TYPE. Arrays have always passed the typeof-based +// check, so this behavior must be preserved by any refactor. + +const consume = (transform) => bytes(pull(from('test'), transform)); + +(async () => { + for (const compress of [compressBrotli, compressZstd]) { + for (const params of [42, 'bad', true, Symbol(), () => {}, null]) { + await assert.rejects( + consume(compress({ params })), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + + // An empty array has no own keys, so it passes both the type check + // and the per-key validation and compression succeeds. + const out = await consume(compress({ params: [] })); + assert.ok(out.byteLength > 0); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-stream-iter-validation.js b/test/parallel/test-stream-iter-validation.js index 9f77fe330478..8dcfb46f173f 100644 --- a/test/parallel/test-stream-iter-validation.js +++ b/test/parallel/test-stream-iter-validation.js @@ -156,6 +156,15 @@ assert.throws(() => broadcast({ budget: 16383 }), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => broadcast({ signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => broadcast({ backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); +// Broadcast consumer options.signal must be AbortSignal and validation must +// not leave a consumer registered. +{ + const { broadcast: bc } = broadcast(); + assert.throws(() => bc.push({ signal: {} }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.strictEqual(bc.consumerCount, 0); +} + // BroadcastWriter options.signal must be AbortSignal { const { writer } = broadcast(); @@ -212,6 +221,15 @@ assert.throws(() => share(from('a'), { budget: Number.MAX_SAFE_INTEGER + 1 }), assert.throws(() => share(from('a'), { signal: {} }), { code: 'ERR_INVALID_ARG_TYPE' }); assert.throws(() => share(from('a'), { backpressure: 'bad' }), { code: 'ERR_INVALID_ARG_VALUE' }); +// Share consumer options.signal must be AbortSignal and validation must not +// leave a consumer registered. +{ + const shared = share(from('a')); + assert.throws(() => shared.pull({ signal: {} }), + { code: 'ERR_INVALID_ARG_TYPE' }); + assert.strictEqual(shared.consumerCount, 0); +} + // share() values < 16384 are rejected assert.throws(() => share(from('a'), { budget: 0 }), { code: 'ERR_OUT_OF_RANGE' }); assert.throws(() => share(from('a'), { budget: -1 }), { code: 'ERR_OUT_OF_RANGE' }); diff --git a/typings/globals.d.ts b/typings/globals.d.ts index b294042d790b..e64b8e6d89fc 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -8,6 +8,7 @@ import { ConstantsBinding } from './internalBinding/constants'; import { CredentialsBinding } from './internalBinding/credentials'; import { CryptoBinding } from './internalBinding/crypto'; import { DebugBinding } from './internalBinding/debug'; +import { DiagnosticsChannelBinding } from './internalBinding/diagnostics_channel'; import { EncodingBinding } from './internalBinding/encoding_binding'; import { FsBinding } from './internalBinding/fs'; import { FsDirBinding } from './internalBinding/fs_dir'; @@ -26,6 +27,7 @@ import { ProcessBinding } from './internalBinding/process'; import { ProcessWrapBinding } from './internalBinding/process_wrap'; import { SeaBinding } from './internalBinding/sea'; import { SerdesBinding } from './internalBinding/serdes'; +import { SignalWrapBinding } from './internalBinding/signal_wrap'; import { StringDecoderBinding } from './internalBinding/string_decoder'; import { SymbolsBinding } from './internalBinding/symbols'; import { TimersBinding } from './internalBinding/timers'; @@ -35,6 +37,7 @@ import { URLPatternBinding } from "./internalBinding/url_pattern"; import { UtilBinding } from './internalBinding/util'; import { UVBinding } from './internalBinding/uv'; import { WASIBinding } from './internalBinding/wasi'; +import { WatchdogBinding } from './internalBinding/watchdog'; import { WorkerBinding } from './internalBinding/worker'; import { ZlibBinding } from './internalBinding/zlib'; @@ -49,6 +52,7 @@ interface InternalBindingMap { credentials: CredentialsBinding; crypto: CryptoBinding; debug: DebugBinding; + diagnostics_channel: DiagnosticsChannelBinding; encoding_binding: EncodingBinding; fs: FsBinding; fs_dir: FsDirBinding; @@ -67,6 +71,7 @@ interface InternalBindingMap { process_wrap: ProcessWrapBinding; sea: SeaBinding; serdes: SerdesBinding; + signal_wrap: SignalWrapBinding; string_decoder: StringDecoderBinding; symbols: SymbolsBinding; timers: TimersBinding; @@ -76,6 +81,7 @@ interface InternalBindingMap { util: UtilBinding; uv: UVBinding; wasi: WASIBinding; + watchdog: WatchdogBinding; worker: WorkerBinding; zlib: ZlibBinding; } diff --git a/typings/internalBinding/diagnostics_channel.d.ts b/typings/internalBinding/diagnostics_channel.d.ts new file mode 100644 index 000000000000..e6297d45ace0 --- /dev/null +++ b/typings/internalBinding/diagnostics_channel.d.ts @@ -0,0 +1,6 @@ +export interface DiagnosticsChannelBinding { + subscribers: Uint32Array; + linkNativeChannel( + callback: (name: string, index: number) => object | undefined, + ): void; +} diff --git a/typings/internalBinding/signal_wrap.d.ts b/typings/internalBinding/signal_wrap.d.ts new file mode 100644 index 000000000000..4c6473cbee22 --- /dev/null +++ b/typings/internalBinding/signal_wrap.d.ts @@ -0,0 +1,16 @@ +declare namespace InternalSignalWrapBinding { + class Signal { + constructor(); + onsignal?: (signum: number) => void; + start(signum: number): number | undefined; + stop(): number; + close(callback?: () => void): void; + hasRef(): boolean; + ref(): void; + unref(): void; + } +} + +export interface SignalWrapBinding { + Signal: typeof InternalSignalWrapBinding.Signal; +} diff --git a/typings/internalBinding/watchdog.d.ts b/typings/internalBinding/watchdog.d.ts new file mode 100644 index 000000000000..917ee3b96d48 --- /dev/null +++ b/typings/internalBinding/watchdog.d.ts @@ -0,0 +1,15 @@ +declare namespace InternalWatchdogBinding { + class TraceSigintWatchdog { + constructor(); + start(): void; + stop(): void; + close(callback?: () => void): void; + hasRef(): boolean; + ref(): void; + unref(): void; + } +} + +export interface WatchdogBinding { + TraceSigintWatchdog: typeof InternalWatchdogBinding.TraceSigintWatchdog; +}