UNPKG

@kurtharriger/nel

Version:

Node.js Evaluation Loop (NEL): npm package to implement a Node.js REPL session

664 lines (550 loc) 16.7 kB
/* * BSD 3-Clause License * * Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ import stream from "stream"; import util from "util"; import vm from "vm"; import console from "console"; import io from 'socket.io'; import EventEmitter from 'events'; let channel; const DEBUG = !!process.env.DEBUG; let log = DEBUG ? function doLog() { var msg = { log: "SERVER: " + util.format.apply(this, arguments), } if (channel) { channel.send(msg); } console.log(msg); } : function noop() {}; function Stdout(id, opt) { stream.Transform.call(this, opt); this._id = id; } Stdout.prototype = Object.create(stream.Transform.prototype); Stdout.prototype._transform = function(data, encoding, callback) { var response = { id: this._id, stdout: data.toString(), }; log("STDOUT:", response); channel.send(response); this.push(data); callback(); }; function Stderr(id, opt) { stream.Transform.call(this, opt); this._id = id; } Stderr.prototype = Object.create(stream.Transform.prototype); Stderr.prototype._transform = function(data, encoding, callback) { var response = { id: this._id, stderr: data.toString(), }; log("STDERR:", response); channel.send(response); this.push(data); callback(); }; function Context(id) { this.id = id; this.stdout = new Stdout(this.id); this.stderr = new Stderr(this.id); this.console = new console.Console(this.stdout, this.stderr); this._capturedStdout = null; this._capturedStderr = null; this._capturedConsole = null; this._async = false; this._done = false; // `$$` provides an interface for users to access the execution context this.$$ = Object.create(null); this.$$.async = Context.prototype.async.bind(this); this.$$.sendResult = Context.prototype.sendResult.bind(this); this.$$.sendError = Context.prototype.sendError.bind(this); this.$$.done = Context.prototype.done.bind(this); this.$$.mime = (function sendMime(mimeBundle) { this.done({ mime: mimeBundle }); }).bind(this); this.$$.text = (function sendText(text) { this.done({ mime: { "text/plain": text } }); }).bind(this); this.$$.html = (function sendHtml(html) { this.done({ mime: { "text/html": html } }); }).bind(this); this.$$.svg = (function sendSvg(svg) { this.done({ mime: { "image/svg+xml": svg } }); }).bind(this); this.$$.png = (function sendPng(png) { this.done({ mime: { "image/png": png } }); }).bind(this); this.$$.jpeg = (function sendJpeg(jpeg) { this.done({ mime: { "image/jpeg": jpeg } }); }).bind(this); } Context.prototype.send = function send(message) { message.id = this.id; if (this._done) { log("RESULT: DROPPED:", message); return; } log("RESULT:", message); channel.send(message); }; Context.prototype.async = function async() { this._async = true; }; Context.prototype.sendResult = function sendResult(result) { this.done({ mime: toMime(result) }); }; Context.prototype.sendError = function sendError(error) { this.done({ error: formatError(error) }); }; Context.prototype.done = function done(response) { response = response || {}; response.end = true; this.send(response); this._async = false; this._done = true; }; Context.prototype.captureGlobalContext = function captureGlobalContext() { this._capturedStdout = process.stdout; this._capturedStderr = process.stderr; this._capturedConsole = console; this.stdout.pipe(this._capturedStdout); this.stderr.pipe(this._capturedStderr); this.console.Console = this._capturedConsole.Console; delete process.stdout; process.stdout = this.stdout; delete process.stderr; process.stderr = this.stderr; delete global.console; global.console = this.console; delete global.$$; global.$$ = this.$$; if (typeof global.$$mimer$$ !== "function") { global.$$mimer$$ = defaultMimer; } delete global.$$mime$$; Object.defineProperty(global, "$$mime$$", { set: this.$$.mime, configurable: true, enumerable: false, }); delete global.$$html$$; Object.defineProperty(global, "$$html$$", { set: this.$$.html, configurable: true, enumerable: false, }); delete global.$$svg$$; Object.defineProperty(global, "$$svg$$", { set: this.$$.svg, configurable: true, enumerable: false, }); delete global.$$png$$; Object.defineProperty(global, "$$png$$", { set: this.$$.png, configurable: true, enumerable: false, }); delete global.$$jpeg$$; Object.defineProperty(global, "$$jpeg$$", { set: this.$$.jpeg, configurable: true, enumerable: false, }); delete global.$$async$$; Object.defineProperty(global, "$$async$$", { get: (function() { return this._async; }).bind(this), set: (function(value) { if (value) { this.async(); } this._async = value; }).bind(this), configurable: true, enumerable: false, }); global.$$done$$ = this.$$.sendResult; }; Context.prototype.releaseGlobalContext = function releaseGlobalContext() { if (process.stdout === this.stdout) { this.stdout.unpipe(); delete process.stdout; process.stdout = this._capturedStdout; this._capturedStdout = null; } if (process.stderr === this.stderr) { this.stderr.unpipe(); delete process.stderr; process.stderr = this._capturedStderr; this._capturedStderr = null; } if (global.console === this.console) { delete global.console; global.console = this._capturedConsole; this._capturedConsole = null; } }; class IpcChannel { constructor() { if (!process.send) { throw new Error('Process must be spawned with ipc channel'); } } onMessage(callback) { process.on("message", callback); } send(msg) { process.send(msg); } } class SocketChannel { constructor(port = 6001) { this.emitter = new EventEmitter(); this.channel = new io(port); this.channel.on('connection', (socket) => { log('client connected'); socket.on("message", (msg) => this.emitter.emit('message', msg)); }); log(`listening on ${port}`); } onMessage(callback) { this.emitter.on('message', callback); } send(msg) { this.channel.send(msg) } } export class Server { constructor(config = {}) { this.config = config; } start() { channel = this.channel = (this.config.port || !process.send) ? new SocketChannel(this.config.port) : new IpcChannel() // Capture the initial context // (id left undefined to indicate this is the initial context) this.initialContext = new Context(); this.initialContext.captureGlobalContext(); Object.defineProperty(global, "$$defaultMimer$$", { value: defaultMimer, configurable: false, writable: false, enumerable: false, }); channel.onMessage(this.onMessage.bind(this)); process.on("uncaughtException", this.onUncaughtException.bind(this)); } onUncaughtException(error) { log("UNCAUGHTEXCEPTION:", error.stack); channel.send({ stderr: error.stack.toString(), }); } onMessage(request) { log("REQUEST:", request); var action = request[0]; var code = request[1]; var id = request[2]; this.initialContext.releaseGlobalContext(); var context = new Context(id); context.captureGlobalContext(); var cleanup = () => { context.releaseGlobalContext(); this.initialContext.captureGlobalContext(); this.initialContext._done = false; }; return new Promise((resolve, reject) => { if (action === "getAllPropertyNames") { this.onNameRequest(code, context).then(resolve, reject); } else if (action === "inspect") { this.onInspectRequest(code, context).then(resolve, reject); } else if (action === "run") { this.onRunRequest(code, context).then(resolve, reject); } else { this.context.sendError(new Error("NEL: Unhandled action request: " + action)); resolve(); } }).then(cleanup, cleanup); } onNameRequest(code, context) { var message = { id: context.id, names: getAllPropertyNames(this.run(code)), }; log("RESULT:", message); context.done(message); return Promise.resolve(); } onInspectRequest(code, context) { var message = { id: context.id, inspection: inspect(this.run(code)), }; log("RESULT:", message); context.done(message); return Promise.resolve(); } onRunRequest(code, context) { const result = new Promise((resolve) => resolve(this.run(code))); // Drop result if the run request initiated the async mode if (context._async) { return Promise.resolve(); } // Drop result if the run request has already invoked context.done() if (context._done) { return Promise.resolve(); } return result.then((value) => { context.sendResult(value); }, (error) => { context.sendError(error); }); } run(code) { return vm.runInThisContext(code); } } function formatError(error) { return { ename: (error && error.name) ? error.name : typeof error, evalue: (error && error.message) ? error.message : util.inspect(error), traceback: (error && error.stack) ? error.stack.split("\n") : "", }; } function toMime(result) { var mimer = (typeof global.$$mimer$$ === "function") ? global.$$mimer$$ : defaultMimer; return mimer(result); } function defaultMimer(result) { if (typeof result === "undefined") { return { "text/plain": "undefined" }; } if (result === null) { return { "text/plain": "null" }; } var mime; if (result._toMime) { try { mime = result._toMime(); } catch (error) {} } if (typeof mime !== "object") { mime = {}; } if (!("text/plain" in mime)) { try { mime["text/plain"] = util.inspect(result); } catch (error) {} } if (result._toHtml && !("text/html" in mime)) { try { mime["text/html"] = result._toHtml(); } catch (error) {} } if (result._toSvg && !("image/svg+xml" in mime)) { try { mime["image/svg+xml"] = result._toSvg(); } catch (error) {} } if (result._toPng && !("image/png" in mime)) { try { mime["image/png"] = result._toPng(); } catch (error) {} } if (result._toJpeg && !("image/jpeg" in mime)) { try { mime["image/jpeg"] = result._toJpeg(); } catch (error) {} } return mime; } function getAllPropertyNames(object) { var propertyList = []; if (object === undefined) { return []; } if (object === null) { return []; } var prototype; if (typeof object === "boolean") { prototype = Boolean.prototype; } else if (typeof object === "number") { prototype = Number.prototype; } else if (typeof object === "string") { prototype = String.prototype; } else { prototype = object; } var prototypeList = [prototype]; function pushToPropertyList(e) { if (propertyList.indexOf(e) === -1) { propertyList.push(e); } } while (true) { var names; try { names = Object.getOwnPropertyNames(prototype).sort(); } catch (e) { break; } names.forEach(pushToPropertyList); prototype = Object.getPrototypeOf(prototype); if (prototype === null) { break; } if (prototypeList.indexOf(prototype) === -1) { prototypeList.push(prototype); } } return propertyList; } function inspect(object) { if (object === undefined) { return { string: "undefined", type: "Undefined", }; } if (object === null) { return { string: "null", type: "Null", }; } if (typeof object === "boolean") { return { string: object ? "true" : "false", type: "Boolean", constructorList: ["Boolean", "Object"], }; } if (typeof object === "number") { return { string: util.inspect(object), type: "Number", constructorList: ["Number", "Object"], }; } if (typeof object === "string") { return { string: object, type: "String", constructorList: ["String", "Object"], length: object.length, }; } if (typeof object === "function") { return { string: object.toString(), type: "Function", constructorList: ["Function", "Object"], length: object.length, }; } var constructorList = getConstructorList(object); var result = { string: toString(object), type: constructorList[0] || "", constructorList: constructorList, }; if ("length" in object) { result.length = object.length; } return result; function toString(object) { try { return util.inspect(object.valueOf()); } catch (e) { return util.inspect(object); } } function getConstructorList(object) { var constructorList = []; var prototype = Object.getPrototypeOf(object); while (true) { try { constructorList.push(prototype.constructor.name); } catch (e) { break; } prototype = Object.getPrototypeOf(prototype); } return constructorList; } }