UNPKG

ztrace

Version:
504 lines (412 loc) 14 kB
'use strict'; const getLibs = require('./get-libs.js'); const formatting = require('./formatting.js'); const isPrimitive = require('./is-primitive.js'); const stackSupport = require('./stack-support.js'); const safejoin = formatting.safejoin; const EventEmitter = require('events'); /*const fsWriteSync = process.binding('fs').writeString const safelog = (s) => { fsWriteSync(2, s) }*/ const Object = global.Object; class ZTrace { constructor(options) { this.kIsHooked = Symbol('kIsHooked'); this.kIsProxyHooked = Symbol('kIsProxyHooked'); this.kCachedProxy = Symbol('kCachedProxy'); this.kUnwrappedObject = Symbol('kUnwrappedObject'); this.kKnownAs = Symbol('kKnownAs'); this.kHookCreationContext = Symbol('kHookCreationContext'); this.kNameOverride = Symbol('kNameOverride'); this.kSpecialProperties = Symbol('kSpecialProperties'); this.kDescriptorSourceObject = Symbol('kDescriptorSourceObject'); this.kGlobalObjectHooksInPlace = Symbol('kGlobalObjectHooksInPlace'); this.kNeverHook = Symbol('hNeverHook'); this.currentNesting = 0; this.inTraceEvent = 0; this.callID = 0; this.emitter = null; this.inUntracedMode = 0; this.emitter = new EventEmitter(); options = options || {}; this.options = Object.assign({ gatherCallSites: false, filter({name}) { return name !== 'process.EventEmitter'; }, suppressStartupWarnings: true }, options, { trace: Object.assign({ binding: true, module: true, global: true, passed: true, ret: true, moduleLoading: false }, options.trace) }); // XXX Check whether this is actually necessary once there are proper tests in place. const oldFormatArgs = formatting.formatArgs; formatting.formatArgs = (...args) => this.runUntraced(() => oldFormatArgs(...args)); // Hooking Node’s fatal exception handler gets nasty. process._fatalException[this.kNeverHook] = true; // Hooking coverage gathering objects is possible but pointless and slow. if (global.__coverage__) { global.__coverage__[this.kNeverHook] = true; } // Never hook this object itself. this[this.kNeverHook] = true; if (this.options.suppressStartupWarnings) { const realSetupHooks = this.setupHooks; this.setupHooks = (...args) => this.runWithoutProcessWarnings(() => realSetupHooks.call(this, ...args)); } this.baseContext = { from: null, parentContext: null, existingObject: true, type: null }; } getNiceStack() { const depth = typeof this.options.gatherCallSites === 'number' ? this.options.gatherCallSites : 40; const stack_ = stackSupport.trace(depth); const thisFileName = stack_[1].getFileName(); let i; for (i = 1; i < stack_.length; i++) if (stack_[i].getFileName() !== thisFileName) break; const stack = stack_.slice(i). filter(frame => frame.getFileName() !== thisFileName); stack.realStack = stack_; return stack; } filter(info) { if (this.options.gatherCallSites) { info.stack = this.getNiceStack(); } return this.options.filter(info); } runUntraced(fn, ...args) { this.inUntracedMode++; try { return fn.call(this, ...args); } finally { this.inUntracedMode--; } } runWithoutProcessWarnings(fn, ...args) { const oldEmitWarning = process.emitWarning; process.emitWarning = () => {}; try { return fn.call(this, ...args); } finally { process.emitWarning = oldEmitWarning; } } hookPropertyDescriptorAccessors() { if (Object[this.kGlobalObjectHooksInPlace]) { return; } Object[this.kGlobalObjectHooksInPlace] = true; // Hook up the ways for setting property descriptors so that some ways // of cloning objects work reliably. const ztrace = this; const realGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; const realDefineProperty = Object.defineProperty; const realCreate = Object.create; Object.getOwnPropertyDescriptor = (obj, name) => { const realResult = realGetOwnPropertyDescriptor(obj, name); if (realResult && obj) { realResult[ztrace.kDescriptorSourceObject] = obj; } return realResult; }; Object.defineProperty = (obj, name, descriptor) => { const sourceObject = descriptor[ztrace.kDescriptorSourceObject]; if (sourceObject) { const specialProperties = sourceObject[ztrace.kSpecialProperties]; if (specialProperties) { const specialProperty = specialProperties[name]; if (specialProperty) { const { sValue } = specialProperty; obj[sValue] = sourceObject[sValue]; } } } return realDefineProperty(obj, name, descriptor); }; Object.create = (proto, properties) => { const obj = realCreate(proto); for (let k in properties) { Object.defineProperty(obj, k, properties[k]); } return obj; }; } setupHooks() { this.hookPropertyDescriptorAccessors(); return this.emitter; } isHookedObject(object) { return Object.prototype.hasOwnProperty.call(object, this.kIsHooked); } hookObject(objectName, object, context) { if (this.isHookedObject(object)) { return; } const specialProperties = Object.create(null); if (Object.isExtensible(object)) { object[this.kSpecialProperties] = specialProperties; object[this.kIsHooked] = true; object[this.kHookCreationContext] = context; } const ztrace = this; const properties = Object.getOwnPropertyNames(object); for (let property of properties) { const name = safejoin `${objectName}.${property}`; if (!this.filter({name, property, object})) { continue; } const sValue = Symbol(`${name}:[[Value]]`); specialProperties[property] = { sValue }; const descriptor = Object.getOwnPropertyDescriptor(object, property); if (!descriptor.configurable) { let value; try { value = object[property]; } catch(e) {} this._hookValue(name, value, context); continue; } if ('value' in descriptor) { if (isPrimitive(descriptor.value) || descriptor.value[this.kNeverHook]) continue; } object[sValue] = descriptor.value; Object.defineProperty(object, property, { enumerable: descriptor.enumerable, configurable: true, get() { if (descriptor.get) { const oldValue = this[sValue]; this[sValue] = descriptor.get.call(this); } return ztrace._hookValue(name, this[sValue], context); }, set(value) { this[sValue] = value; if (descriptor.set) { descriptor.set.call(this, value); return true; } } }); } } hookValue(name, object, context) { // `context` is optional. const ctx = Object.assign({}, this.baseContext, context); return this._hookValue(name, object, ctx); } _hookValue(name, value, context) { if (isPrimitive(value)) { return value; } if (this.inUntracedMode > 0 || value[this.kNeverHook]) { const unproxied = value[this.kUnwrappedObject]; if (unproxied) return unproxied; return value; } name = value[this.kNameOverride] || name; if (this.isHookedObject(value)) { if (typeof value !== 'function') return value; if (value[this.kIsProxyHooked] && value[this.kKnownAs] === name) return value; } // Call the user-provided filter last, always. if (!this.filter({name, value})) { return value; } if (context.existingObject || context.origin === 'binding') { this.hookObject(name, value, context); if (typeof value === 'object') return value; } if (value[this.kCachedProxy]) { const p = value[this.kCachedProxy]; if (p[this.kKnownAs] === name) { return p; } value = p[this.kUnwrappedObject]; } if (Object.isExtensible(value)) { value[this.kIsHooked] = true; } const wrapCall = (newTarget, target, thisArg, argumentsList, fn) => { let localName = name; if (newTarget !== null) { if (localName.includes('(')) localName = `(${localName})`; localName = `new ${localName}`; } if (this.options.trace.passed) { argumentsList = this._hookValue(safejoin `(${localName} args)`, argumentsList, Object.assign({}, context, { type: 'passed', existingObject: false })); } const callID = this.callID++; this.currentNesting++; let ret, exception; try { const depth = this.currentNesting - 1; const info = { newTarget, name, thisArg, argumentsList, depth, callID, context, localName }; if (this.options.gatherCallSites) { info.stack = this.getNiceStack(); } this.emitEvent('enter', info); try { let thisArg_ = thisArg; let stringRep = ''; try { stringRep = Function.prototype.toString.call(target); } catch (e) {} if (/\[native code\]/.test(stringRep) && thisArg && thisArg[this.kIsProxyHooked]) { thisArg_ = thisArg[this.kUnwrappedObject]; } ret = fn(thisArg_, argumentsList); } catch (err) { exception = err; } Object.assign(info, { ret, exception }); this.emitEvent('leave', info); if (this.options.trace.ret) { ret = this._hookValue(`${localName}()`, ret, Object.assign({}, context, { type: 'ret', origin: context.origin || context.type, from: info, existingObject: false, parentContext: context })); } } finally { this.currentNesting--; } if (exception) throw exception; return ret; } const ztrace = this; const proxy = new Proxy(value, { apply(target, thisArg, argumentsList) { const thisArgCtx = thisArg && thisArg[ztrace.kHookCreationContext]; if (thisArgCtx && thisArgCtx.origin === 'binding') { const unwrapped = thisArg[ztrace.kUnwrappedObject]; if (unwrapped) thisArg = unwrapped; } return wrapCall(null, target, thisArg, argumentsList, (thisArg_, argumentsList_) => target.apply(thisArg_, argumentsList_)); }, construct(target, argumentsList, newTarget) { return wrapCall(newTarget, target, null, argumentsList, (thisArg_, argumentsList_) => Reflect.construct(target, argumentsList, newTarget)); }, get(target, property, receiver) { if (property === ztrace.kUnwrappedObject) { return target; } else if (property === ztrace.kIsProxyHooked) { return true; } else if (property === ztrace.kKnownAs) { return name; } else if (property === ztrace.kHookCreationContext) { return context; } else if (typeof property === 'symbol') { return target[property]; } else { return ztrace._hookValue(safejoin `${name}.${property}`, target[property], Object.assign({}, context, { existingObject: false })); } } }); if (Object.isExtensible(value)) { value[this.kCachedProxy] = proxy; } return proxy; } emitEvent(...args) { const level = this.inTraceEvent++; try { if (level === 0 && this.emitter !== null) { this.emitter.emit(...args); } } finally { this.inTraceEvent--; } } } class ZTraceGlobal extends ZTrace { constructor(options) { super(options); } // Note: If `options.suppressStartupWarnings` is true, this function gets // wrapped using runWithoutProcessWarnings(). setupHooks() { super.setupHooks(); const libs = getLibs(); const Module = libs.modules.module; if (this.options.trace.binding) { for (let lib in libs.bindings) { this.hookValue(`@${lib}`, libs.bindings[lib], { type: 'binding' })[this.kNameOverride] = `@${lib}`; } } if (this.options.trace.global) { for (let key in global) { if (key === 'global') continue; const newValue = this.hookValue(key, global[key], { type: 'global' }, this.baseContext); try { newValue[this.kNameOverride] = key; global[key] = newValue; } catch(e) {} } } if (this.options.trace.module) { for (let lib in libs.modules) { this.hookValue(lib, libs.modules[lib], { type: 'module' }, this.baseContext)[this.kNameOverride] = lib; } } if (!this.options.trace.moduleLoading) { const realLoad = Module._load; const run = fn => fn(); Module._load = (request, parent, isMain) => { return (isMain ? run : this.runUntraced).call(this, () => { return realLoad(request, parent, isMain); }); }; } return this.emitter; } } module.exports = { ZTraceGlobal, ZTrace };