• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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