1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23 24// WARNING: THIS MODULE IS PENDING DEPRECATION. 25// 26// No new pull requests targeting this module will be accepted 27// unless they address existing, critical bugs. 28 29const { 30 ArrayPrototypeEvery, 31 ArrayPrototypeIndexOf, 32 ArrayPrototypeLastIndexOf, 33 ArrayPrototypePush, 34 ArrayPrototypeSlice, 35 ArrayPrototypeSplice, 36 Error, 37 FunctionPrototypeCall, 38 ObjectDefineProperty, 39 Promise, 40 ReflectApply, 41 SafeMap, 42 Symbol, 43} = primordials; 44 45const EventEmitter = require('events'); 46const { 47 ERR_DOMAIN_CALLBACK_NOT_AVAILABLE, 48 ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE, 49 ERR_UNHANDLED_ERROR 50} = require('internal/errors').codes; 51const { createHook } = require('async_hooks'); 52const { useDomainTrampoline } = require('internal/async_hooks'); 53 54// TODO(addaleax): Use a non-internal solution for this. 55const kWeak = Symbol('kWeak'); 56const { WeakReference } = internalBinding('util'); 57 58// Overwrite process.domain with a getter/setter that will allow for more 59// effective optimizations 60const _domain = [null]; 61ObjectDefineProperty(process, 'domain', { 62 enumerable: true, 63 get: function() { 64 return _domain[0]; 65 }, 66 set: function(arg) { 67 return _domain[0] = arg; 68 } 69}); 70 71const pairing = new SafeMap(); 72const asyncHook = createHook({ 73 init(asyncId, type, triggerAsyncId, resource) { 74 if (process.domain !== null && process.domain !== undefined) { 75 // If this operation is created while in a domain, let's mark it 76 pairing.set(asyncId, process.domain[kWeak]); 77 // Promises from other contexts, such as with the VM module, should not 78 // have a domain property as it can be used to escape the sandbox. 79 if (type !== 'PROMISE' || resource instanceof Promise) { 80 ObjectDefineProperty(resource, 'domain', { 81 configurable: true, 82 enumerable: false, 83 value: process.domain, 84 writable: true 85 }); 86 } 87 } 88 }, 89 before(asyncId) { 90 const current = pairing.get(asyncId); 91 if (current !== undefined) { // Enter domain for this cb 92 // We will get the domain through current.get(), because the resource 93 // object's .domain property makes sure it is not garbage collected. 94 // However, we do need to make the reference to the domain non-weak, 95 // so that it cannot be garbage collected before the after() hook. 96 current.incRef(); 97 current.get().enter(); 98 } 99 }, 100 after(asyncId) { 101 const current = pairing.get(asyncId); 102 if (current !== undefined) { // Exit domain for this cb 103 const domain = current.get(); 104 current.decRef(); 105 domain.exit(); 106 } 107 }, 108 destroy(asyncId) { 109 pairing.delete(asyncId); // cleaning up 110 } 111}); 112 113// When domains are in use, they claim full ownership of the 114// uncaught exception capture callback. 115if (process.hasUncaughtExceptionCaptureCallback()) { 116 throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE(); 117} 118 119// Get the stack trace at the point where `domain` was required. 120// eslint-disable-next-line no-restricted-syntax 121const domainRequireStack = new Error('require(`domain`) at this point').stack; 122 123const { setUncaughtExceptionCaptureCallback } = process; 124process.setUncaughtExceptionCaptureCallback = function(fn) { 125 const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE(); 126 err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack; 127 throw err; 128}; 129 130 131let sendMakeCallbackDeprecation = false; 132function emitMakeCallbackDeprecation({ target, method }) { 133 if (!sendMakeCallbackDeprecation) { 134 process.emitWarning( 135 'Using a domain property in MakeCallback is deprecated. Use the ' + 136 'async_context variant of MakeCallback or the AsyncResource class ' + 137 'instead. ' + 138 `(Triggered by calling ${method?.name || '<anonymous>'} ` + 139 `on ${target?.constructor?.name}.)`, 140 'DeprecationWarning', 'DEP0097'); 141 sendMakeCallbackDeprecation = true; 142 } 143} 144 145function topLevelDomainCallback(cb, ...args) { 146 const domain = this.domain; 147 if (exports.active && domain) 148 emitMakeCallbackDeprecation({ target: this, method: cb }); 149 150 if (domain) 151 domain.enter(); 152 const ret = ReflectApply(cb, this, args); 153 if (domain) 154 domain.exit(); 155 156 return ret; 157} 158 159// It's possible to enter one domain while already inside 160// another one. The stack is each entered domain. 161let stack = []; 162exports._stack = stack; 163useDomainTrampoline(topLevelDomainCallback); 164 165function updateExceptionCapture() { 166 if (ArrayPrototypeEvery(stack, 167 (domain) => domain.listenerCount('error') === 0)) { 168 setUncaughtExceptionCaptureCallback(null); 169 } else { 170 setUncaughtExceptionCaptureCallback(null); 171 setUncaughtExceptionCaptureCallback((er) => { 172 return process.domain._errorHandler(er); 173 }); 174 } 175} 176 177 178process.on('newListener', (name, listener) => { 179 if (name === 'uncaughtException' && 180 listener !== domainUncaughtExceptionClear) { 181 // Make sure the first listener for `uncaughtException` always clears 182 // the domain stack. 183 process.removeListener(name, domainUncaughtExceptionClear); 184 process.prependListener(name, domainUncaughtExceptionClear); 185 } 186}); 187 188process.on('removeListener', (name, listener) => { 189 if (name === 'uncaughtException' && 190 listener !== domainUncaughtExceptionClear) { 191 // If the domain listener would be the only remaining one, remove it. 192 const listeners = process.listeners('uncaughtException'); 193 if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear) 194 process.removeListener(name, domainUncaughtExceptionClear); 195 } 196}); 197 198function domainUncaughtExceptionClear() { 199 stack.length = 0; 200 exports.active = process.domain = null; 201 updateExceptionCapture(); 202} 203 204 205class Domain extends EventEmitter { 206 constructor() { 207 super(); 208 209 this.members = []; 210 this[kWeak] = new WeakReference(this); 211 asyncHook.enable(); 212 213 this.on('removeListener', updateExceptionCapture); 214 this.on('newListener', updateExceptionCapture); 215 } 216} 217 218exports.Domain = Domain; 219 220exports.create = exports.createDomain = function createDomain() { 221 return new Domain(); 222}; 223 224// The active domain is always the one that we're currently in. 225exports.active = null; 226Domain.prototype.members = undefined; 227 228// Called by process._fatalException in case an error was thrown. 229Domain.prototype._errorHandler = function(er) { 230 let caught = false; 231 232 if ((typeof er === 'object' && er !== null) || typeof er === 'function') { 233 ObjectDefineProperty(er, 'domain', { 234 configurable: true, 235 enumerable: false, 236 value: this, 237 writable: true 238 }); 239 er.domainThrown = true; 240 } 241 // Pop all adjacent duplicates of the currently active domain from the stack. 242 // This is done to prevent a domain's error handler to run within the context 243 // of itself, and re-entering itself recursively handler as a result of an 244 // exception thrown in its context. 245 while (exports.active === this) { 246 this.exit(); 247 } 248 249 // The top-level domain-handler is handled separately. 250 // 251 // The reason is that if V8 was passed a command line option 252 // asking it to abort on an uncaught exception (currently 253 // "--abort-on-uncaught-exception"), we want an uncaught exception 254 // in the top-level domain error handler to make the 255 // process abort. Using try/catch here would always make V8 think 256 // that these exceptions are caught, and thus would prevent it from 257 // aborting in these cases. 258 if (stack.length === 0) { 259 // If there's no error handler, do not emit an 'error' event 260 // as this would throw an error, make the process exit, and thus 261 // prevent the process 'uncaughtException' event from being emitted 262 // if a listener is set. 263 if (EventEmitter.listenerCount(this, 'error') > 0) { 264 // Clear the uncaughtExceptionCaptureCallback so that we know that, since 265 // the top-level domain is not active anymore, it would be ok to abort on 266 // an uncaught exception at this point 267 setUncaughtExceptionCaptureCallback(null); 268 try { 269 caught = this.emit('error', er); 270 } finally { 271 updateExceptionCapture(); 272 } 273 } 274 } else { 275 // Wrap this in a try/catch so we don't get infinite throwing 276 try { 277 // One of three things will happen here. 278 // 279 // 1. There is a handler, caught = true 280 // 2. There is no handler, caught = false 281 // 3. It throws, caught = false 282 // 283 // If caught is false after this, then there's no need to exit() 284 // the domain, because we're going to crash the process anyway. 285 caught = this.emit('error', er); 286 } catch (er2) { 287 // The domain error handler threw! oh no! 288 // See if another domain can catch THIS error, 289 // or else crash on the original one. 290 updateExceptionCapture(); 291 if (stack.length) { 292 exports.active = process.domain = stack[stack.length - 1]; 293 caught = process.domain._errorHandler(er2); 294 } else { 295 // Pass on to the next exception handler. 296 throw er2; 297 } 298 } 299 } 300 301 // Exit all domains on the stack. Uncaught exceptions end the 302 // current tick and no domains should be left on the stack 303 // between ticks. 304 domainUncaughtExceptionClear(); 305 306 return caught; 307}; 308 309 310Domain.prototype.enter = function() { 311 // Note that this might be a no-op, but we still need 312 // to push it onto the stack so that we can pop it later. 313 exports.active = process.domain = this; 314 ArrayPrototypePush(stack, this); 315 updateExceptionCapture(); 316}; 317 318 319Domain.prototype.exit = function() { 320 // Don't do anything if this domain is not on the stack. 321 const index = ArrayPrototypeLastIndexOf(stack, this); 322 if (index === -1) return; 323 324 // Exit all domains until this one. 325 ArrayPrototypeSplice(stack, index); 326 327 exports.active = stack.length === 0 ? undefined : stack[stack.length - 1]; 328 process.domain = exports.active; 329 updateExceptionCapture(); 330}; 331 332 333// note: this works for timers as well. 334Domain.prototype.add = function(ee) { 335 // If the domain is already added, then nothing left to do. 336 if (ee.domain === this) 337 return; 338 339 // Has a domain already - remove it first. 340 if (ee.domain) 341 ee.domain.remove(ee); 342 343 // Check for circular Domain->Domain links. 344 // They cause big issues. 345 // 346 // For example: 347 // var d = domain.create(); 348 // var e = domain.create(); 349 // d.add(e); 350 // e.add(d); 351 // e.emit('error', er); // RangeError, stack overflow! 352 if (this.domain && (ee instanceof Domain)) { 353 for (let d = this.domain; d; d = d.domain) { 354 if (ee === d) return; 355 } 356 } 357 358 ObjectDefineProperty(ee, 'domain', { 359 configurable: true, 360 enumerable: false, 361 value: this, 362 writable: true 363 }); 364 ArrayPrototypePush(this.members, ee); 365}; 366 367 368Domain.prototype.remove = function(ee) { 369 ee.domain = null; 370 const index = ArrayPrototypeIndexOf(this.members, ee); 371 if (index !== -1) 372 ArrayPrototypeSplice(this.members, index, 1); 373}; 374 375 376Domain.prototype.run = function(fn) { 377 this.enter(); 378 const ret = ReflectApply(fn, this, ArrayPrototypeSlice(arguments, 1)); 379 this.exit(); 380 381 return ret; 382}; 383 384 385function intercepted(_this, self, cb, fnargs) { 386 if (fnargs[0] && fnargs[0] instanceof Error) { 387 const er = fnargs[0]; 388 er.domainBound = cb; 389 er.domainThrown = false; 390 ObjectDefineProperty(er, 'domain', { 391 configurable: true, 392 enumerable: false, 393 value: self, 394 writable: true 395 }); 396 self.emit('error', er); 397 return; 398 } 399 400 self.enter(); 401 const ret = ReflectApply(cb, _this, ArrayPrototypeSlice(fnargs, 1)); 402 self.exit(); 403 404 return ret; 405} 406 407 408Domain.prototype.intercept = function(cb) { 409 const self = this; 410 411 function runIntercepted() { 412 return intercepted(this, self, cb, arguments); 413 } 414 415 return runIntercepted; 416}; 417 418 419function bound(_this, self, cb, fnargs) { 420 self.enter(); 421 const ret = ReflectApply(cb, _this, fnargs); 422 self.exit(); 423 424 return ret; 425} 426 427 428Domain.prototype.bind = function(cb) { 429 const self = this; 430 431 function runBound() { 432 return bound(this, self, cb, arguments); 433 } 434 435 ObjectDefineProperty(runBound, 'domain', { 436 configurable: true, 437 enumerable: false, 438 value: this, 439 writable: true 440 }); 441 442 return runBound; 443}; 444 445// Override EventEmitter methods to make it domain-aware. 446EventEmitter.usingDomains = true; 447 448const eventInit = EventEmitter.init; 449EventEmitter.init = function() { 450 ObjectDefineProperty(this, 'domain', { 451 configurable: true, 452 enumerable: false, 453 value: null, 454 writable: true 455 }); 456 if (exports.active && !(this instanceof exports.Domain)) { 457 this.domain = exports.active; 458 } 459 460 return FunctionPrototypeCall(eventInit, this); 461}; 462 463const eventEmit = EventEmitter.prototype.emit; 464EventEmitter.prototype.emit = function emit(...args) { 465 const domain = this.domain; 466 467 const type = args[0]; 468 const shouldEmitError = type === 'error' && 469 this.listenerCount(type) > 0; 470 471 // Just call original `emit` if current EE instance has `error` 472 // handler, there's no active domain or this is process 473 if (shouldEmitError || domain === null || domain === undefined || 474 this === process) { 475 return ReflectApply(eventEmit, this, args); 476 } 477 478 if (type === 'error') { 479 const er = args.length > 1 && args[1] ? 480 args[1] : new ERR_UNHANDLED_ERROR(); 481 482 if (typeof er === 'object') { 483 er.domainEmitter = this; 484 ObjectDefineProperty(er, 'domain', { 485 configurable: true, 486 enumerable: false, 487 value: domain, 488 writable: true 489 }); 490 er.domainThrown = false; 491 } 492 493 // Remove the current domain (and its duplicates) from the domains stack and 494 // set the active domain to its parent (if any) so that the domain's error 495 // handler doesn't run in its own context. This prevents any event emitter 496 // created or any exception thrown in that error handler from recursively 497 // executing that error handler. 498 const origDomainsStack = ArrayPrototypeSlice(stack); 499 const origActiveDomain = process.domain; 500 501 // Travel the domains stack from top to bottom to find the first domain 502 // instance that is not a duplicate of the current active domain. 503 let idx = stack.length - 1; 504 while (idx > -1 && process.domain === stack[idx]) { 505 --idx; 506 } 507 508 // Change the stack to not contain the current active domain, and only the 509 // domains above it on the stack. 510 if (idx < 0) { 511 stack.length = 0; 512 } else { 513 ArrayPrototypeSplice(stack, idx + 1); 514 } 515 516 // Change the current active domain 517 if (stack.length > 0) { 518 exports.active = process.domain = stack[stack.length - 1]; 519 } else { 520 exports.active = process.domain = null; 521 } 522 523 updateExceptionCapture(); 524 525 domain.emit('error', er); 526 527 // Now that the domain's error handler has completed, restore the domains 528 // stack and the active domain to their original values. 529 exports._stack = stack = origDomainsStack; 530 exports.active = process.domain = origActiveDomain; 531 updateExceptionCapture(); 532 533 return false; 534 } 535 536 domain.enter(); 537 const ret = ReflectApply(eventEmit, this, args); 538 domain.exit(); 539 540 return ret; 541}; 542