• 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  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