heroku-debug
Version:
debugging plugin for the CLI
402 lines (352 loc) • 11.9 kB
JavaScript
var util = require('util')
var shimmer = require('./shimmer')
var wrap = shimmer.wrap
var massWrap = shimmer.massWrap
module.exports = function (ins) {
// Shim activator for functions that have callback last
function activator (fn) {
return function () {
var index = arguments.length - 1
if (typeof arguments[index] === 'function') {
arguments[index] = ins.bindFunction(arguments[index])
}
return fn.apply(this, arguments)
}
}
// Shim activator for functions that have callback first
function activatorFirst (fn) {
return function () {
if (typeof arguments[0] === 'function') {
arguments[0] = ins.bindFunction(arguments[0])
}
return fn.apply(this, arguments)
}
}
var net = require('net')
// a polyfill in our polyfill etc so forth -- taken from node master on 2013/10/30
if (!net._normalizeConnectArgs) {
net._normalizeConnectArgs = function (args) {
var options = {}
function toNumber (x) { return (x = Number(x)) >= 0 ? x : false }
if (typeof args[0] === 'object' && args[0] !== null) {
// connect(options, [cb])
options = args[0]
} else if (typeof args[0] === 'string' && toNumber(args[0]) === false) {
// connect(path, [cb])
options.path = args[0]
} else {
// connect(port, [host], [cb])
options.port = args[0]
if (typeof args[1] === 'string') {
options.host = args[1]
}
}
var cb = args[args.length - 1]
return typeof cb === 'function' ? [options, cb] : [options]
}
}
wrap(net.Server.prototype, '_listen2', function (original) {
return function () {
this.on('connection', function (socket) {
if (socket._handle) {
socket._handle.onread = ins.bindFunction(socket._handle.onread)
}
})
try {
return original.apply(this, arguments)
} finally {
// the handle will only not be set in cases where there has been an error
if (this._handle && this._handle.onconnection) {
this._handle.onconnection = ins.bindFunction(this._handle.onconnection)
}
}
}
})
wrap(net.Socket.prototype, 'connect', function (original) {
return function () {
var args = net._normalizeConnectArgs(arguments)
if (args[1]) args[1] = ins.bindFunction(args[1])
var result = original.apply(this, args)
if (this._handle) {
this._handle.onread = ins.bindFunction(this._handle.onread)
}
return result
}
})
// need unwrapped nextTick for use within < 0.9 async error handling
if (!process._fatalException) {
process._originalNextTick = process.nextTick
}
var processors = []
if (process._nextDomainTick) processors.push('_nextDomainTick')
if (process._tickDomainCallback) processors.push('_tickDomainCallback')
massWrap(
process,
processors,
activator
)
wrap(process, 'nextTick', activatorFirst)
var asynchronizers = [
'setTimeout',
'setInterval'
]
if (global.setImmediate) asynchronizers.push('setImmediate')
var timers = require('timers')
var patchGlobalTimers = global.setTimeout === timers.setTimeout
massWrap(
timers,
asynchronizers,
activatorFirst
)
if (patchGlobalTimers) {
massWrap(
global,
asynchronizers,
activatorFirst
)
}
var dns = require('dns')
massWrap(
dns,
[
'lookup',
'resolve',
'resolve4',
'resolve6',
'resolveCname',
'resolveMx',
'resolveNs',
'resolveTxt',
'resolveSrv',
'reverse'
],
activator
)
if (dns.resolveNaptr) wrap(dns, 'resolveNaptr', activator)
var fs = require('fs')
massWrap(
fs,
[
'watch',
'rename',
'truncate',
'chown',
'fchown',
'chmod',
'fchmod',
'stat',
'lstat',
'fstat',
'link',
'symlink',
'readlink',
'realpath',
'unlink',
'rmdir',
'mkdir',
'readdir',
'close',
'open',
'utimes',
'futimes',
'fsync',
'write',
'read',
'readFile',
'writeFile',
'appendFile',
'watchFile',
'unwatchFile',
'exists'
],
activator
)
// only wrap lchown and lchmod on systems that have them.
if (fs.lchown) wrap(fs, 'lchown', activator)
if (fs.lchmod) wrap(fs, 'lchmod', activator)
// only wrap ftruncate in versions of node that have it
if (fs.ftruncate) wrap(fs, 'ftruncate', activator)
// Wrap zlib streams
var zlib
try { zlib = require('zlib') } catch (err) { }
if (zlib && zlib.Deflate && zlib.Deflate.prototype) {
var proto = Object.getPrototypeOf(zlib.Deflate.prototype)
if (proto._transform) {
// streams2
wrap(proto, '_transform', activator)
} else if (proto.write && proto.flush && proto.end) {
// plain ol' streams
massWrap(
proto,
[
'write',
'flush',
'end'
],
activator
)
}
}
// Wrap Crypto
var crypto
try { crypto = require('crypto') } catch (err) { }
if (crypto) {
massWrap(
crypto,
[
'pbkdf2',
'randomBytes',
'pseudoRandomBytes'
],
activator
)
}
var instrumentPromise = !!global.Promise
// Check that global Promise is native
// if (instrumentPromise) {
// // shoult not use any methods that have already been wrapped
// var promiseListener = process.addAsyncListener({
// create: function create () {
// instrumentPromise = false
// }
// })
// // should not resolve synchronously
// global.Promise.resolve(true).then(function notSync () {
// instrumentPromise = false
// })
// process.removeAsyncListener(promiseListener)
// }
/*
* Native promises use the microtask queue to make all callbacks run
* asynchronously to avoid Zalgo issues. Since the microtask queue is not
* exposed externally, promises need to be modified in a fairly invasive and
* complex way.
*
* The async boundary in promises that must be patched is between the
* fulfillment of the promise and the execution of any callback that is waiting
* for that fulfillment to happen. This means that we need to trigger a create
* when accept or reject is called and trigger before, after and error handlers
* around the callback execution. There may be multiple callbacks for each
* fulfilled promise, so handlers will behave similar to setInterval where
* there may be multiple before after and error calls for each create call.
*
* async-listener monkeypatching has one basic entry point: `wrapCallback`.
* `wrapCallback` should be called when create should be triggered and be
* passed a function to wrap, which will execute the body of the async work.
* The accept and reject calls can be modified fairly easily to call
* `wrapCallback`, but at the time of accept and reject all the work to be done
* on fulfillment may not be defined, since a call to then, chain or fetch can
* be made even after the promise has been fulfilled. To get around this, we
* create a placeholder function which will call a function passed into it,
* since the call to the main work is being made from within the wrapped
* function, async-listener will work correctly.
*
* There is another complication with monkeypatching Promises. Calls to then,
* chain and catch each create new Promises that are fulfilled internally in
* different ways depending on the return value of the callback. When the
* callback return a Promise, the new Promise is resolved asynchronously after
* the returned Promise has been also been resolved. When something other than
* a promise is resolved the accept call for the new Promise is put in the
* microtask queue and asynchronously resolved.
*
* Then must be wrapped so that its returned promise has a wrapper that can be
* used to invoke further continuations. This wrapper cannot be created until
* after the callback has run, since the callback may return either a promise
* or another value. Fortunately we already have a wrapper function around the
* callback we can use (the wrapper created by accept or reject).
*
* By adding an additional argument to this wrapper, we can pass in the
* returned promise so it can have its own wrapper appended. the wrapper
* function can the call the callback, and take action based on the return
* value. If a promise is returned, the new Promise can proxy the returned
* Promise's wrapper (this wrapper may not exist yet, but will by the time the
* wrapper needs to be invoked). Otherwise, a new wrapper can be create the
* same way as in accept and reject. Since this wrapper is created
* synchronously within another wrapper, it will properly appear as a
* continuation from within the callback.
*/
if (instrumentPromise) {
wrapPromise()
}
function wrapPromise () {
var Promise = global.Promise
function wrappedPromise (executor) {
if (!(this instanceof wrappedPromise)) {
return Promise(executor)
}
if (typeof executor !== 'function') {
return new Promise(executor)
}
var context, args
var promise = new Promise(wrappedExecutor)
promise.__proto__ = wrappedPromise.prototype // eslint-disable-line no-proto
try {
executor.apply(context, args)
} catch (err) {
args[1](err)
}
return promise
function wrappedExecutor (accept, reject) {
context = this
args = [wrappedAccept, wrappedReject]
// These wrappers create a function that can be passed a function and an argument to
// call as a continuation from the accept or reject.
function wrappedAccept (val) {
ensureAslWrapper(promise)
return accept(val)
}
function wrappedReject (val) {
ensureAslWrapper(promise)
return reject(val)
}
}
}
util.inherits(wrappedPromise, Promise)
wrap(Promise.prototype, 'then', wrapThen)
wrap(Promise.prototype, 'chain', wrapThen)
var PromiseMethods = ['accept', 'all', 'defer', 'race', 'reject', 'resolve']
PromiseMethods.forEach(function (key) {
wrappedPromise[key] = Promise[key]
})
global.Promise = wrappedPromise
function ensureAslWrapper (promise) {
if (!promise.__asl_wrapper) {
promise.__asl_wrapper = ins.bindFunction(propagateAslWrapper)
}
}
function propagateAslWrapper (ctx, fn, result, next) {
var nextResult
try {
nextResult = fn.call(ctx, result)
return nextResult
} finally {
// Wrap any resulting futures as continuations.
if (nextResult instanceof Promise) {
next.__asl_wrapper = function proxyWrapper () {
var aslWrapper = nextResult.__asl_wrapper || propagateAslWrapper
return aslWrapper.apply(this, arguments)
}
} else {
ensureAslWrapper(next)
}
}
}
function wrapThen (original) {
return function wrappedThen () {
var promise = this
var next = original.apply(promise, Array.prototype.map.call(arguments, bind))
return next
// wrap callbacks (success, error) so that the callbacks will be called as a
// continuations of the accept or reject call using the __asl_wrapper created above.
function bind (fn) {
if (typeof fn !== 'function') return fn
return function (val) {
if (!promise.__asl_wrapper) return fn.call(this, val)
return promise.__asl_wrapper(this, fn, val, next)
}
}
}
}
}
}