UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

539 lines (468 loc) 16.7 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/domain.js import EventEmitter from "nstdlib/lib/events"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { createHook } from "nstdlib/lib/async_hooks"; import { useDomainTrampoline } from "nstdlib/lib/internal/async_hooks"; import { WeakReference } from "nstdlib/lib/internal/util"; // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to permit // persons to whom the Software is furnished to do so, subject to the // following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. // WARNING: THIS MODULE IS PENDING DEPRECATION. // // No new pull requests targeting this module will be accepted // unless they address existing, critical bugs. const { ERR_DOMAIN_CALLBACK_NOT_AVAILABLE, ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE, ERR_UNHANDLED_ERROR, } = __codes__; const kWeak = Symbol("kWeak"); // Overwrite process.domain with a getter/setter that will allow for more // effective optimizations const _domain = [null]; Object.defineProperty(process, "domain", { __proto__: null, enumerable: true, get: function () { return _domain[0]; }, set: function (arg) { return (_domain[0] = arg); }, }); const vmPromises = new WeakMap(); const pairing = new Map(); const asyncHook = createHook({ init(asyncId, type, triggerAsyncId, resource) { if (process.domain !== null && process.domain !== undefined) { // If this operation is created while in a domain, let's mark it pairing.set(asyncId, process.domain[kWeak]); // Promises from other contexts, such as with the VM module, should not // have a domain property as it can be used to escape the sandbox. if (type !== "PROMISE" || resource instanceof Promise) { Object.defineProperty(resource, "domain", { __proto__: null, configurable: true, enumerable: false, value: process.domain, writable: true, }); // Because promises from other contexts don't get a domain field, // the domain needs to be held alive another way. Stuffing it in a // weakmap connected to the promise lifetime can fix that. } else { vmPromises.set(resource, process.domain); } } }, before(asyncId) { const current = pairing.get(asyncId); if (current !== undefined) { // Enter domain for this cb // We will get the domain through current.get(), because the resource // object's .domain property makes sure it is not garbage collected. // However, we do need to make the reference to the domain non-weak, // so that it cannot be garbage collected before the after() hook. current.incRef(); current.get().enter(); } }, after(asyncId) { const current = pairing.get(asyncId); if (current !== undefined) { // Exit domain for this cb const domain = current.get(); current.decRef(); domain.exit(); } }, destroy(asyncId) { pairing.delete(asyncId); // cleaning up }, }); // When domains are in use, they claim full ownership of the // uncaught exception capture callback. if (process.hasUncaughtExceptionCaptureCallback()) { throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE(); } // Get the stack trace at the point where `domain` was required. // eslint-disable-next-line no-restricted-syntax const domainRequireStack = new Error("require(`domain`) at this point").stack; const { setUncaughtExceptionCaptureCallback } = process; process.setUncaughtExceptionCaptureCallback = function (fn) { const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE(); err.stack = err.stack + "\n" + "-".repeat(40) + "\n" + domainRequireStack; throw err; }; let sendMakeCallbackDeprecation = false; function emitMakeCallbackDeprecation({ target, method }) { if (!sendMakeCallbackDeprecation) { process.emitWarning( "Using a domain property in MakeCallback is deprecated. Use the " + "async_context variant of MakeCallback or the AsyncResource class " + "instead. " + `(Triggered by calling ${method?.name || "<anonymous>"} ` + `on ${target?.constructor?.name}.)`, "DeprecationWarning", "DEP0097", ); sendMakeCallbackDeprecation = true; } } function topLevelDomainCallback(cb, ...args) { const domain = this.domain; if (exports.active && domain) emitMakeCallbackDeprecation({ target: this, method: cb }); if (domain) domain.enter(); const ret = ReflectApply(cb, this, args); if (domain) domain.exit(); return ret; } // It's possible to enter one domain while already inside // another one. The stack is each entered domain. let stack = []; export { stack as _stack }; useDomainTrampoline(topLevelDomainCallback); function updateExceptionCapture() { if ( Array.prototype.every.call( stack, (domain) => domain.listenerCount("error") === 0, ) ) { setUncaughtExceptionCaptureCallback(null); } else { setUncaughtExceptionCaptureCallback(null); setUncaughtExceptionCaptureCallback((er) => { return process.domain._errorHandler(er); }); } } process.on("newListener", (name, listener) => { if ( name === "uncaughtException" && listener !== domainUncaughtExceptionClear ) { // Make sure the first listener for `uncaughtException` always clears // the domain stack. process.removeListener(name, domainUncaughtExceptionClear); process.prependListener(name, domainUncaughtExceptionClear); } }); process.on("removeListener", (name, listener) => { if ( name === "uncaughtException" && listener !== domainUncaughtExceptionClear ) { // If the domain listener would be the only remaining one, remove it. const listeners = process.listeners("uncaughtException"); if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear) process.removeListener(name, domainUncaughtExceptionClear); } }); function domainUncaughtExceptionClear() { stack.length = 0; exports.active = process.domain = null; updateExceptionCapture(); } class Domain extends EventEmitter { constructor() { super(); this.members = []; this[kWeak] = new WeakReference(this); asyncHook.enable(); this.on("removeListener", updateExceptionCapture); this.on("newListener", updateExceptionCapture); } } export { Domain }; const _export_create_ = (exports.createDomain = function createDomain() { return new Domain(); }); export { _export_create_ as create }; // The active domain is always the one that we're currently in. const _export_active_ = null; export { _export_active_ as active }; Domain.prototype.members = undefined; // Called by process._fatalException in case an error was thrown. Domain.prototype._errorHandler = function (er) { let caught = false; if ((typeof er === "object" && er !== null) || typeof er === "function") { Object.defineProperty(er, "domain", { __proto__: null, configurable: true, enumerable: false, value: this, writable: true, }); er.domainThrown = true; } // Pop all adjacent duplicates of the currently active domain from the stack. // This is done to prevent a domain's error handler to run within the context // of itself, and re-entering itself recursively handler as a result of an // exception thrown in its context. while (exports.active === this) { this.exit(); } // The top-level domain-handler is handled separately. // // The reason is that if V8 was passed a command line option // asking it to abort on an uncaught exception (currently // "--abort-on-uncaught-exception"), we want an uncaught exception // in the top-level domain error handler to make the // process abort. Using try/catch here would always make V8 think // that these exceptions are caught, and thus would prevent it from // aborting in these cases. if (stack.length === 0) { // If there's no error handler, do not emit an 'error' event // as this would throw an error, make the process exit, and thus // prevent the process 'uncaughtException' event from being emitted // if a listener is set. if (EventEmitter.listenerCount(this, "error") > 0) { // Clear the uncaughtExceptionCaptureCallback so that we know that, since // the top-level domain is not active anymore, it would be ok to abort on // an uncaught exception at this point setUncaughtExceptionCaptureCallback(null); try { caught = this.emit("error", er); } finally { updateExceptionCapture(); } } } else { // Wrap this in a try/catch so we don't get infinite throwing try { // One of three things will happen here. // // 1. There is a handler, caught = true // 2. There is no handler, caught = false // 3. It throws, caught = false // // If caught is false after this, then there's no need to exit() // the domain, because we're going to crash the process anyway. caught = this.emit("error", er); } catch (er2) { // The domain error handler threw! oh no! // See if another domain can catch THIS error, // or else crash on the original one. updateExceptionCapture(); if (stack.length) { exports.active = process.domain = stack[stack.length - 1]; caught = process.domain._errorHandler(er2); } else { // Pass on to the next exception handler. throw er2; } } } // Exit all domains on the stack. Uncaught exceptions end the // current tick and no domains should be left on the stack // between ticks. domainUncaughtExceptionClear(); return caught; }; Domain.prototype.enter = function () { // Note that this might be a no-op, but we still need // to push it onto the stack so that we can pop it later. exports.active = process.domain = this; Array.prototype.push.call(stack, this); updateExceptionCapture(); }; Domain.prototype.exit = function () { // Don't do anything if this domain is not on the stack. const index = Array.prototype.lastIndexOf.call(stack, this); if (index === -1) return; // Exit all domains until this one. Array.prototype.splice.call(stack, index); exports.active = stack.length === 0 ? undefined : stack[stack.length - 1]; process.domain = exports.active; updateExceptionCapture(); }; // note: this works for timers as well. Domain.prototype.add = function (ee) { // If the domain is already added, then nothing left to do. if (ee.domain === this) return; // Has a domain already - remove it first. if (ee.domain) ee.domain.remove(ee); // Check for circular Domain->Domain links. // They cause big issues. // // For example: // var d = domain.create(); // var e = domain.create(); // d.add(e); // e.add(d); // e.emit('error', er); // RangeError, stack overflow! if (this.domain && ee instanceof Domain) { for (let d = this.domain; d; d = d.domain) { if (ee === d) return; } } Object.defineProperty(ee, "domain", { __proto__: null, configurable: true, enumerable: false, value: this, writable: true, }); Array.prototype.push.call(this.members, ee); }; Domain.prototype.remove = function (ee) { ee.domain = null; const index = Array.prototype.indexOf.call(this.members, ee); if (index !== -1) Array.prototype.splice.call(this.members, index, 1); }; Domain.prototype.run = function (fn) { this.enter(); const ret = ReflectApply(fn, this, Array.prototype.slice.call(arguments, 1)); this.exit(); return ret; }; function intercepted(_this, self, cb, fnargs) { if (fnargs[0] && fnargs[0] instanceof Error) { const er = fnargs[0]; er.domainBound = cb; er.domainThrown = false; Object.defineProperty(er, "domain", { __proto__: null, configurable: true, enumerable: false, value: self, writable: true, }); self.emit("error", er); return; } self.enter(); const ret = ReflectApply(cb, _this, Array.prototype.slice.call(fnargs, 1)); self.exit(); return ret; } Domain.prototype.intercept = function (cb) { const self = this; function runIntercepted() { return intercepted(this, self, cb, arguments); } return runIntercepted; }; function bound(_this, self, cb, fnargs) { self.enter(); const ret = ReflectApply(cb, _this, fnargs); self.exit(); return ret; } Domain.prototype.bind = function (cb) { const self = this; function runBound() { return bound(this, self, cb, arguments); } Object.defineProperty(runBound, "domain", { __proto__: null, configurable: true, enumerable: false, value: this, writable: true, }); return runBound; }; // Override EventEmitter methods to make it domain-aware. EventEmitter.usingDomains = true; const eventInit = EventEmitter.init; EventEmitter.init = function (opts) { Object.defineProperty(this, "domain", { __proto__: null, configurable: true, enumerable: false, value: null, writable: true, }); if (exports.active && !(this instanceof exports.Domain)) { this.domain = exports.active; } return Function.prototype.call.call(eventInit, this, opts); }; const eventEmit = EventEmitter.prototype.emit; EventEmitter.prototype.emit = function emit(...args) { const domain = this.domain; const type = args[0]; const shouldEmitError = type === "error" && this.listenerCount(type) > 0; // Just call original `emit` if current EE instance has `error` // handler, there's no active domain or this is process if ( shouldEmitError || domain === null || domain === undefined || this === process ) { return ReflectApply(eventEmit, this, args); } if (type === "error") { const er = args.length > 1 && args[1] ? args[1] : new ERR_UNHANDLED_ERROR(); if (typeof er === "object") { er.domainEmitter = this; Object.defineProperty(er, "domain", { __proto__: null, configurable: true, enumerable: false, value: domain, writable: true, }); er.domainThrown = false; } // Remove the current domain (and its duplicates) from the domains stack and // set the active domain to its parent (if any) so that the domain's error // handler doesn't run in its own context. This prevents any event emitter // created or any exception thrown in that error handler from recursively // executing that error handler. const origDomainsStack = Array.prototype.slice.call(stack); const origActiveDomain = process.domain; // Travel the domains stack from top to bottom to find the first domain // instance that is not a duplicate of the current active domain. let idx = stack.length - 1; while (idx > -1 && process.domain === stack[idx]) { --idx; } // Change the stack to not contain the current active domain, and only the // domains above it on the stack. if (idx < 0) { stack.length = 0; } else { Array.prototype.splice.call(stack, idx + 1); } // Change the current active domain if (stack.length > 0) { exports.active = process.domain = stack[stack.length - 1]; } else { exports.active = process.domain = null; } updateExceptionCapture(); domain.emit("error", er); // Now that the domain's error handler has completed, restore the domains // stack and the active domain to their original values. exports._stack = stack = origDomainsStack; exports.active = process.domain = origActiveDomain; updateExceptionCapture(); return false; } domain.enter(); const ret = ReflectApply(eventEmit, this, args); domain.exit(); return ret; };