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 Array, 31 Error, 32 Map, 33 ObjectDefineProperty, 34 ReflectApply, 35 Symbol, 36} = primordials; 37 38const EventEmitter = require('events'); 39const { 40 ERR_DOMAIN_CALLBACK_NOT_AVAILABLE, 41 ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE, 42 ERR_UNHANDLED_ERROR 43} = require('internal/errors').codes; 44const { createHook } = require('async_hooks'); 45const { useDomainTrampoline } = require('internal/async_hooks'); 46 47// TODO(addaleax): Use a non-internal solution for this. 48const kWeak = Symbol('kWeak'); 49const { WeakReference } = internalBinding('util'); 50 51// Overwrite process.domain with a getter/setter that will allow for more 52// effective optimizations 53const _domain = [null]; 54ObjectDefineProperty(process, 'domain', { 55 enumerable: true, 56 get: function() { 57 return _domain[0]; 58 }, 59 set: function(arg) { 60 return _domain[0] = arg; 61 } 62}); 63 64const pairing = new Map(); 65const asyncHook = createHook({ 66 init(asyncId, type, triggerAsyncId, resource) { 67 if (process.domain !== null && process.domain !== undefined) { 68 // If this operation is created while in a domain, let's mark it 69 pairing.set(asyncId, process.domain[kWeak]); 70 ObjectDefineProperty(resource, 'domain', { 71 configurable: true, 72 enumerable: false, 73 value: process.domain, 74 writable: true 75 }); 76 } 77 }, 78 before(asyncId) { 79 const current = pairing.get(asyncId); 80 if (current !== undefined) { // Enter domain for this cb 81 // We will get the domain through current.get(), because the resource 82 // object's .domain property makes sure it is not garbage collected. 83 // However, we do need to make the reference to the domain non-weak, 84 // so that it cannot be garbage collected before the after() hook. 85 current.incRef(); 86 current.get().enter(); 87 } 88 }, 89 after(asyncId) { 90 const current = pairing.get(asyncId); 91 if (current !== undefined) { // Exit domain for this cb 92 const domain = current.get(); 93 current.decRef(); 94 domain.exit(); 95 } 96 }, 97 destroy(asyncId) { 98 pairing.delete(asyncId); // cleaning up 99 } 100}); 101 102// When domains are in use, they claim full ownership of the 103// uncaught exception capture callback. 104if (process.hasUncaughtExceptionCaptureCallback()) { 105 throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE(); 106} 107 108// Get the stack trace at the point where `domain` was required. 109// eslint-disable-next-line no-restricted-syntax 110const domainRequireStack = new Error('require(`domain`) at this point').stack; 111 112const { setUncaughtExceptionCaptureCallback } = process; 113process.setUncaughtExceptionCaptureCallback = function(fn) { 114 const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE(); 115 err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack; 116 throw err; 117}; 118 119 120let sendMakeCallbackDeprecation = false; 121function emitMakeCallbackDeprecation() { 122 if (!sendMakeCallbackDeprecation) { 123 process.emitWarning( 124 'Using a domain property in MakeCallback is deprecated. Use the ' + 125 'async_context variant of MakeCallback or the AsyncResource class ' + 126 'instead.', 'DeprecationWarning', 'DEP0097'); 127 sendMakeCallbackDeprecation = true; 128 } 129} 130 131function topLevelDomainCallback(cb, ...args) { 132 const domain = this.domain; 133 if (exports.active && domain) 134 emitMakeCallbackDeprecation(); 135 136 if (domain) 137 domain.enter(); 138 const ret = ReflectApply(cb, this, args); 139 if (domain) 140 domain.exit(); 141 142 return ret; 143} 144 145// It's possible to enter one domain while already inside 146// another one. The stack is each entered domain. 147const stack = []; 148exports._stack = stack; 149useDomainTrampoline(topLevelDomainCallback); 150 151function updateExceptionCapture() { 152 if (stack.every((domain) => domain.listenerCount('error') === 0)) { 153 setUncaughtExceptionCaptureCallback(null); 154 } else { 155 setUncaughtExceptionCaptureCallback(null); 156 setUncaughtExceptionCaptureCallback((er) => { 157 return process.domain._errorHandler(er); 158 }); 159 } 160} 161 162 163process.on('newListener', (name, listener) => { 164 if (name === 'uncaughtException' && 165 listener !== domainUncaughtExceptionClear) { 166 // Make sure the first listener for `uncaughtException` always clears 167 // the domain stack. 168 process.removeListener(name, domainUncaughtExceptionClear); 169 process.prependListener(name, domainUncaughtExceptionClear); 170 } 171}); 172 173process.on('removeListener', (name, listener) => { 174 if (name === 'uncaughtException' && 175 listener !== domainUncaughtExceptionClear) { 176 // If the domain listener would be the only remaining one, remove it. 177 const listeners = process.listeners('uncaughtException'); 178 if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear) 179 process.removeListener(name, domainUncaughtExceptionClear); 180 } 181}); 182 183function domainUncaughtExceptionClear() { 184 stack.length = 0; 185 exports.active = process.domain = null; 186 updateExceptionCapture(); 187} 188 189 190class Domain extends EventEmitter { 191 constructor() { 192 super(); 193 194 this.members = []; 195 this[kWeak] = new WeakReference(this); 196 asyncHook.enable(); 197 198 this.on('removeListener', updateExceptionCapture); 199 this.on('newListener', updateExceptionCapture); 200 } 201} 202 203exports.Domain = Domain; 204 205exports.create = exports.createDomain = function createDomain() { 206 return new Domain(); 207}; 208 209// The active domain is always the one that we're currently in. 210exports.active = null; 211Domain.prototype.members = undefined; 212 213// Called by process._fatalException in case an error was thrown. 214Domain.prototype._errorHandler = function(er) { 215 let caught = false; 216 217 if ((typeof er === 'object' && er !== null) || typeof er === 'function') { 218 ObjectDefineProperty(er, 'domain', { 219 configurable: true, 220 enumerable: false, 221 value: this, 222 writable: true 223 }); 224 er.domainThrown = true; 225 } 226 227 // The top-level domain-handler is handled separately. 228 // 229 // The reason is that if V8 was passed a command line option 230 // asking it to abort on an uncaught exception (currently 231 // "--abort-on-uncaught-exception"), we want an uncaught exception 232 // in the top-level domain error handler to make the 233 // process abort. Using try/catch here would always make V8 think 234 // that these exceptions are caught, and thus would prevent it from 235 // aborting in these cases. 236 if (stack.length === 1) { 237 // If there's no error handler, do not emit an 'error' event 238 // as this would throw an error, make the process exit, and thus 239 // prevent the process 'uncaughtException' event from being emitted 240 // if a listener is set. 241 if (EventEmitter.listenerCount(this, 'error') > 0) { 242 // Clear the uncaughtExceptionCaptureCallback so that we know that, even 243 // if technically the top-level domain is still active, it would 244 // be ok to abort on an uncaught exception at this point 245 setUncaughtExceptionCaptureCallback(null); 246 try { 247 caught = this.emit('error', er); 248 } finally { 249 updateExceptionCapture(); 250 } 251 } 252 } else { 253 // Wrap this in a try/catch so we don't get infinite throwing 254 try { 255 // One of three things will happen here. 256 // 257 // 1. There is a handler, caught = true 258 // 2. There is no handler, caught = false 259 // 3. It throws, caught = false 260 // 261 // If caught is false after this, then there's no need to exit() 262 // the domain, because we're going to crash the process anyway. 263 caught = this.emit('error', er); 264 } catch (er2) { 265 // The domain error handler threw! oh no! 266 // See if another domain can catch THIS error, 267 // or else crash on the original one. 268 // If the user already exited it, then don't double-exit. 269 if (this === exports.active) { 270 stack.pop(); 271 } 272 updateExceptionCapture(); 273 if (stack.length) { 274 exports.active = process.domain = stack[stack.length - 1]; 275 caught = process.domain._errorHandler(er2); 276 } else { 277 // Pass on to the next exception handler. 278 throw er2; 279 } 280 } 281 } 282 283 // Exit all domains on the stack. Uncaught exceptions end the 284 // current tick and no domains should be left on the stack 285 // between ticks. 286 domainUncaughtExceptionClear(); 287 288 return caught; 289}; 290 291 292Domain.prototype.enter = function() { 293 // Note that this might be a no-op, but we still need 294 // to push it onto the stack so that we can pop it later. 295 exports.active = process.domain = this; 296 stack.push(this); 297 updateExceptionCapture(); 298}; 299 300 301Domain.prototype.exit = function() { 302 // Don't do anything if this domain is not on the stack. 303 const index = stack.lastIndexOf(this); 304 if (index === -1) return; 305 306 // Exit all domains until this one. 307 stack.splice(index); 308 309 exports.active = stack[stack.length - 1]; 310 process.domain = exports.active; 311 updateExceptionCapture(); 312}; 313 314 315// note: this works for timers as well. 316Domain.prototype.add = function(ee) { 317 // If the domain is already added, then nothing left to do. 318 if (ee.domain === this) 319 return; 320 321 // Has a domain already - remove it first. 322 if (ee.domain) 323 ee.domain.remove(ee); 324 325 // Check for circular Domain->Domain links. 326 // This causes bad insanity! 327 // 328 // For example: 329 // var d = domain.create(); 330 // var e = domain.create(); 331 // d.add(e); 332 // e.add(d); 333 // e.emit('error', er); // RangeError, stack overflow! 334 if (this.domain && (ee instanceof Domain)) { 335 for (let d = this.domain; d; d = d.domain) { 336 if (ee === d) return; 337 } 338 } 339 340 ObjectDefineProperty(ee, 'domain', { 341 configurable: true, 342 enumerable: false, 343 value: this, 344 writable: true 345 }); 346 this.members.push(ee); 347}; 348 349 350Domain.prototype.remove = function(ee) { 351 ee.domain = null; 352 const index = this.members.indexOf(ee); 353 if (index !== -1) 354 this.members.splice(index, 1); 355}; 356 357 358Domain.prototype.run = function(fn) { 359 let ret; 360 361 this.enter(); 362 if (arguments.length >= 2) { 363 const len = arguments.length; 364 const args = new Array(len - 1); 365 366 for (let i = 1; i < len; i++) 367 args[i - 1] = arguments[i]; 368 369 ret = fn.apply(this, args); 370 } else { 371 ret = fn.call(this); 372 } 373 this.exit(); 374 375 return ret; 376}; 377 378 379function intercepted(_this, self, cb, fnargs) { 380 if (fnargs[0] && fnargs[0] instanceof Error) { 381 const er = fnargs[0]; 382 er.domainBound = cb; 383 er.domainThrown = false; 384 ObjectDefineProperty(er, 'domain', { 385 configurable: true, 386 enumerable: false, 387 value: self, 388 writable: true 389 }); 390 self.emit('error', er); 391 return; 392 } 393 394 const args = []; 395 let ret; 396 397 self.enter(); 398 if (fnargs.length > 1) { 399 for (let i = 1; i < fnargs.length; i++) 400 args.push(fnargs[i]); 401 ret = cb.apply(_this, args); 402 } else { 403 ret = cb.call(_this); 404 } 405 self.exit(); 406 407 return ret; 408} 409 410 411Domain.prototype.intercept = function(cb) { 412 const self = this; 413 414 function runIntercepted() { 415 return intercepted(this, self, cb, arguments); 416 } 417 418 return runIntercepted; 419}; 420 421 422function bound(_this, self, cb, fnargs) { 423 let ret; 424 425 self.enter(); 426 if (fnargs.length > 0) 427 ret = cb.apply(_this, fnargs); 428 else 429 ret = cb.call(_this); 430 self.exit(); 431 432 return ret; 433} 434 435 436Domain.prototype.bind = function(cb) { 437 const self = this; 438 439 function runBound() { 440 return bound(this, self, cb, arguments); 441 } 442 443 ObjectDefineProperty(runBound, 'domain', { 444 configurable: true, 445 enumerable: false, 446 value: this, 447 writable: true 448 }); 449 450 return runBound; 451}; 452 453// Override EventEmitter methods to make it domain-aware. 454EventEmitter.usingDomains = true; 455 456const eventInit = EventEmitter.init; 457EventEmitter.init = function() { 458 ObjectDefineProperty(this, 'domain', { 459 configurable: true, 460 enumerable: false, 461 value: null, 462 writable: true 463 }); 464 if (exports.active && !(this instanceof exports.Domain)) { 465 this.domain = exports.active; 466 } 467 468 return eventInit.call(this); 469}; 470 471const eventEmit = EventEmitter.prototype.emit; 472EventEmitter.prototype.emit = function(...args) { 473 const domain = this.domain; 474 475 const type = args[0]; 476 const shouldEmitError = type === 'error' && 477 this.listenerCount(type) > 0; 478 479 // Just call original `emit` if current EE instance has `error` 480 // handler, there's no active domain or this is process 481 if (shouldEmitError || domain === null || domain === undefined || 482 this === process) { 483 return ReflectApply(eventEmit, this, args); 484 } 485 486 if (type === 'error') { 487 const er = args.length > 1 && args[1] ? 488 args[1] : new ERR_UNHANDLED_ERROR(); 489 490 if (typeof er === 'object') { 491 er.domainEmitter = this; 492 ObjectDefineProperty(er, 'domain', { 493 configurable: true, 494 enumerable: false, 495 value: domain, 496 writable: true 497 }); 498 er.domainThrown = false; 499 } 500 501 domain.emit('error', er); 502 return false; 503 } 504 505 domain.enter(); 506 const ret = ReflectApply(eventEmit, this, args); 507 domain.exit(); 508 509 return ret; 510}; 511