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