Skip to content

Commit

Permalink
feat(process): expand compatibility of global process object
Browse files Browse the repository at this point in the history
Add most of node API for global process object.
Add 'events' module, make global process object an instance of EventEmitter.

Fixes TIMOB-26571
  • Loading branch information
sgtcoolguy committed Mar 27, 2019
1 parent 329e868 commit a14bdf7
Show file tree
Hide file tree
Showing 5 changed files with 974 additions and 53 deletions.
180 changes: 180 additions & 0 deletions common/Resources/ti.internal/extensions/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* @param {EventEmitter} emitter the EventEmitter instance to use to register for it's events
* @param {string} eventName the name of the event to register for
* @param {function} listener the listener callback/function to invoke when the event is emitted
* @param {boolean} prepend whether to prepend or append the listener
* @returns {EventEmitter}
*/
function _addListener(emitter, eventName, listener, prepend) {
// if there's someone listening to 'newListener' events, emit that **before** we add the listener (to avoid infinite recursion)
if (emitter._eventsToListeners.newListener) {
emitter.emit('newListener', eventName, listener);
}

const eventListeners = emitter._eventsToListeners[eventName] || [];
if (prepend) {
eventListeners.unshift(listener);
} else {
eventListeners.push(listener);
}
emitter._eventsToListeners[eventName] = eventListeners;

// Check max listeners and spit out warning if >
const max = emitter.getMaxListeners();
const length = eventListeners.length;
if (max > 0 && length > max) {
const w = new Error(`Possible EventEmitter memory leak detected. ${length} ${eventName} listeners added. Use emitter.setMaxListeners() to increase limit`);
w.name = 'MaxListenersExceededWarning';
w.emitter = emitter;
w.type = eventName;
w.count = length;
process.emitWarning(w);
}
return emitter;
}

function onceWrap(emitter, eventName, listener) {
function wrapper(...args) {
this.emitter.removeListener(this.eventName, this.wrappedFunc); // remove ourselves
this.listener.apply(this.emitter, args); // then forward the event callback
}
// we have to use bind with a custom 'this', because events fire with 'this' pointing at the emitter
const wrapperThis = {
emitter, eventName, listener
};
const bound = wrapper.bind(wrapperThis); // bind to force "this" to refer to our custom object tracking the wrapper/emitter/listener
bound.listener = listener; // have to add listener property for "unwrapping"
wrapperThis.wrappedFunc = bound;
return bound;
}

export default class EventEmitter {
constructor() {
this._eventsToListeners = {};
this._maxListeners = undefined;
}

addListener(eventName, listener) {
return _addListener(this, eventName, listener, false);
}

on(eventName, listener) {
return this.addListener(eventName, listener);
}

prependListener(eventName, listener) {
return _addListener(this, eventName, listener, true);
}

once(eventName, listener) {
this.on(eventName, onceWrap(this, eventName, listener));
}

prependOnceListener(eventName, listener) {
this.prependListener(eventName, onceWrap(this, eventName, listener));
}

removeListener(eventName, listener) {
const eventListeners = this._eventsToListeners[eventName] || [];
const length = eventListeners.length;
let foundIndex = -1;
let unwrappedListener;
// Need to search LIFO, and need to handle wrapped functions (once wrappers)
for (let i = length - 1; i >= 0; i--) {
if (eventListeners[i] === listener || eventListeners[i].listener === listener) {
foundIndex = i;
unwrappedListener = eventListeners[i].listener;
break;
}
}

if (foundIndex !== -1) {
if (length === 1) { // length was 1 and we want to remove last entry, so delete the event type from our listener mapping now!
delete this._eventsToListeners[eventName];
} else { // we had 2+ listeners, so store array without this given listener
eventListeners.splice(foundIndex, 1); // modifies in place, no need to assign to this.listeners[eventName]
}
// Don't emit if there's no listeners for 'removeListener' type!
if (this._eventsToListeners.removeListener) {
this.emit('removeListener', eventName, unwrappedListener || listener);
}
}
return this;
}

off(eventName, listener) {
return this.removeListener(eventName, listener);
}

emit(eventName, ...args) {
const eventListeners = this._eventsToListeners[eventName] || [];
for (const listener of eventListeners) {
listener.call(this, ...args);
}
return eventListeners.length !== 0;
}

listenerCount(eventName) {
const eventListeners = this._eventsToListeners[eventName] || [];
return eventListeners.length;
}

eventNames() {
return Object.getOwnPropertyNames(this._eventsToListeners);
}

listeners(eventName) {
// Need to "unwrap" once wrappers!
const raw = (this._eventsToListeners[eventName] || []);
return raw.map(l => l.listener || l); // here we unwrap the once wrapper if there is one or fall back to listener function
}

rawListeners(eventName) {
return (this._eventsToListeners[eventName] || []).slice(0); // return a copy
}

getMaxListeners() {
return this._maxListeners || EventEmitter.defaultMaxListeners;
}

setMaxListeners(n) {
this._maxListeners = n; // TODO: Type check n, make sure >= 0 (o equals no limit)
return this;
}

removeAllListeners(eventName) {
if (!this._eventsToListeners.removeListener) {
// no need to emit! we can just wipe!
if (eventName === undefined) {
// remove every type!
this._eventsToListeners = {};
} else {
// remove specific type
delete this._eventsToListeners[eventName];
}
return this;
}

// yuck, we'll have to emit 'removeListener' events as we go
if (eventName === undefined) {
// Remove all types (but do 'removeListener' last!)
const names = Object.keys(this._eventsToListeners).filter(name => name !== 'removeListener');
names.forEach(name => this.removeAllListeners(name));
this.removeAllListeners('removeListener');
this._eventsToListeners = {};
} else {
// remove listeners for one type, back to front (Last-in, first-out, except where prepend f-ed it up)
const listeners = this._eventsToListeners[eventName] || [];
for (let i = listeners.length - 1; i >= 0; i--) {
this.removeListener(eventName, listeners[i]);
}
}

return this;
}
}

EventEmitter.defaultMaxListeners = 10;
EventEmitter.listenerCount = function (emitter, eventName) {
return emitter.listenerCount(eventName);
};
200 changes: 147 additions & 53 deletions common/Resources/ti.internal/extensions/process.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import EventEmitter from './events';

// Start our process uptime timer immediately!
const startTime = Date.now();

/**
* This function 'standardizes' the reported architectures to the equivalents reported by Node.js
* node values: 'arm', 'arm64', 'ia32', 'mips', 'mipsel', 'ppc', 'ppc64', 's390', 's390x', 'x32', and 'x64'.
Expand Down Expand Up @@ -44,67 +49,156 @@ function standardizeArch(original) {
}
}

const listeners = {};
const process = {
arch: standardizeArch(Ti.Platform.architecture),
noDeprecation: false,
throwDeprecation: false,
traceDeprecation: false,
pid: 0,
cwd: function () {
return __dirname;
},
// FIXME: Should we try and adopt 'windowsphone'/'windowsstore' to 'win32'?
// FIXME: Should we try and adopt 'ipad'/'iphone' to 'darwin'? or 'ios'?
platform: Ti.Platform.osname,
on: function (eventName, callback) {
const eventListeners = listeners[eventName] || [];
eventListeners.push(callback);
listeners[eventName] = eventListeners;
return this;
},
// TODO: Add #once which is like #on, but should get wrapped to remove itself before getting fired
emit: function (eventName, ...args) {
const eventListeners = listeners[eventName] || [];
for (const listener of eventListeners) {
listener.call(this, ...args);
const process = new EventEmitter();
process.abort = () => {}; // TODO: Do we have equivalent of forcibly killing the process? We have restart, but I think we just want a no-op stub here
process.arch = standardizeArch(Ti.Platform.architecture);
process.argv = []; // TODO: What makes sense here? path to titanium cli for first arg? path to ti.main/app.js for second?
Object.defineProperty(process, 'argv0', {
value: '', // TODO: Path to .app on iOS?
writable: false,
enumerable: true,
configurable: false
});
process.binding = () => {
throw new Error('process.binding is unsupported and not user-facing API');
};
process.channel = undefined;
process.chdir = () => {
throw new Error('process.chdir is unsupported');
};
process.config = {};
process.connected = false;
process.cpuUsage = () => {
// FIXME: Can we look at OS.cpus to get this data?
return {
user: 0,
system: 0
};
};
process.cwd = () => __dirname;
Object.defineProperty(process, 'debugPort', {
get: function () {
let value = 0; // default to 0
try {
if (Ti.Platform.osname === 'android') {
const assets = kroll.binding('assets');
const json = assets.readAsset('deploy.json');
if (json) {
const deployData = JSON.parse(json);
if (deployData.debuggerPort !== -1) { // -1 means not set (not in debug mode)
value = deployData.debuggerPort;
}
}
} else if (Ti.Platform.osname === 'iphone' || Ti.Platform.osname === 'ipad') {
// iOS is 27753 as of ios < 11.3 for simulators
// for 11.3+ it uses a unix socket
// for devices, it uses usbmuxd
value = 27753; // TODO: Can we only return this for simulator < 11.3?
}
} catch (error) {
// ignore
}
return eventListeners.length !== 0;
// overwrite this getter with static value
Object.defineProperty(this, 'debugPort', {
value: value,
writable: true,
enumerable: true,
configurable: true
});
return value;
},
eventNames: () => Object.getOwnPropertyNames(listeners),
emitWarning: function (warning, options, code, ctor) { // eslint-disable-line no-unused-vars
let type;
let detail;
if (typeof options === 'string') {
type = options;
} else if (typeof options === 'object') {
type = options.type;
code = options.code;
detail = options.detail;
enumerable: true,
configurable: true
});
process.disconnect = () => {}; // no-op
process.dlopen = () => {
throw new Error('process.dlopen is not supported');
};
process.emitWarning = function (warning, options, code, ctor) { // eslint-disable-line no-unused-vars
let type;
let detail;
if (typeof options === 'string') {
type = options;
} else if (typeof options === 'object') {
type = options.type;
code = options.code;
detail = options.detail;
}
if (typeof warning === 'string') {
// TODO: make use of `ctor` arg for limiting stack traces? Can only really be used on V8
// set stack trace limit to 0, then call Error.captureStackTrace(warning, ctor);
warning = new Error(warning);
warning.name = type || 'Warning';
if (code !== undefined) {
warning.code = code;
}
if (typeof warning === 'string') {
// TODO: make use of `ctor` arg for limiting stack traces? Can only really be used on V8
// set stack trace limit to 0, then call Error.captureStackTrace(warning, ctor);
warning = new Error(warning);
warning.name = type || 'Warning';
if (code !== undefined) {
warning.code = code;
}
if (detail !== undefined) {
warning.detail = detail;
}
if (detail !== undefined) {
warning.detail = detail;
}
// TODO: Throw TypeError if not an instanceof Error at this point!
const isDeprecation = (warning.name === 'DeprecationWarning');
if (isDeprecation && process.noDeprecation) {
return; // ignore
}
// TODO: Throw TypeError if not an instanceof Error at this point!
const isDeprecation = (warning.name === 'DeprecationWarning');
if (isDeprecation && process.noDeprecation) {
return; // ignore
}
if (isDeprecation && process.throwDeprecation) {
throw warning;
}
this.emit('warning', warning);
};
process.env = {};
process.execArgv = [];
process.execPath = ''; // FIXME: What makes sense here? Path to titanium CLI here?
process.exit = () => {
throw new Error('process.exit is not supported');
};
process.exitCode = undefined;
process.noDeprecation = false;
process.pid = 0;
// FIXME: Should we try and adopt 'windowsphone'/'windowsstore' to 'win32'?
// FIXME: Should we try and adopt 'ipad'/'iphone' to 'darwin'? or 'ios'?
process.platform = Ti.Platform.osname;
process.ppid = 0;
// TODO: Add release property (Object)
// TODO: Can we expose stdout/stderr/stdin natively?
process.stderr = {
isTTY: false,
writable: true,
write: (chunk, encoding, callback) => {
console.error(chunk);
if (callback) {
callback();
}
if (isDeprecation && process.throwDeprecation) {
throw warning;
return true;
}
};
process.stdout = {
isTTY: false,
writable: true,
write: (chunk, encoding, callback) => {
console.log(chunk);
if (callback) {
callback();
}
this.emit('warning', warning);
return true;
}
};
process.title = Ti.App.name;
process.throwDeprecation = false;
process.traceDeprecation = false;
process.umask = () => 0; // just always return 0
process.uptime = () => {
const diffMs = Date.now() - startTime;
return diffMs / 1000.0; // convert to "seconds" with fractions
};
process.version = Ti.version;
process.versions = {
modules: '', // TODO: Report module api version (for current platform!)
v8: '', // TODO: report android's v8 version (if on Android!)
jsc: '' // TODO: report javascriptcore version for iOS/WIndows?
// TODO: Report ios/Android/Windows platform versions?
};

global.process = process;
// handle spitting out warnings
const WARNING_PREFIX = `(titanium:${process.pid}) `;
Expand Down

0 comments on commit a14bdf7

Please sign in to comment.