UNPKG

devil-windows

Version:

Debugger, profiler and runtime with embedded WebKit DevTools client (for Windows).

421 lines (360 loc) 13.3 kB
var util = require('util'), spawn = require('child_process').spawn, EventEmitter = require('events').EventEmitter, async = require('async'), WebSockets = require('../../lib/ws').Server, Session = require('./Session'); Object.defineProperty(Error.prototype, 'toJSON', { value: function () { var alt = {}; Object.getOwnPropertyNames(this).forEach(function (key) { alt[key] = this[key]; }, this); return alt; }, configurable: true, enumerable: false }); /** * Server class * * @param {string} host * @param {number} port * @constructor */ function Server (host, port) { var _port = port || 9999; var _host = host || '127.0.0.1'; var _file = null; var _args = null; var _hidden = []; var _v8port = 5858; var _break = false; var _preload = true; var _mute = false; var _saveFiles = false; var _ws = null; var _token = null; var _session = null; var _sessionErrorSent = false; var _sessionReady = false; var _queue = []; var _client = null; var _devtools = null; var _reason = null; var $this = this; var _isPortAvailable = function (port, fn) { var net = require('net'); var tester = net.createServer().once('error', function (err) { if (err.code != 'EADDRINUSE') return fn(err); fn(null, false); }).once('listening', function () { tester.once('close', function () { fn(null, true); }).close(); }).listen(port); }; var argsRegex = /(?:[^\s"]+|"[^"]*")+/g; /** * Public methods, accessible from the client * * @type {{run: Function, stop: Function, pause: Function, resume: Function}} * @private */ var _methods = { run: function (params, callback) { _file = params.file || null; _args = params.args || ''; _hidden = params.hidden || []; _v8port = params.v8port || 5858; _break = params.break || false; _preload = params.preload || true; _mute = params.mute || false; _saveFiles = params.saveFiles || false; if (!_file) { return callback("Please select a file path to execute."); } var options = { mute: _mute, breakFirst: _break, preload: _preload, saveLiveEdit: _saveFiles, stackTraceLimit: 10, // TODO: make a setting in future hidden: _hidden, debuggerPort: _v8port }; // Parse the arguments _args = _args.match(argsRegex); if (!_args) _args = []; // Stop a running instance if (_session) { if (_session.isRunning()) { _session.stop(function (err) { if (err) return callback(err); _reason = null; _sessionReady = false; _sessionErrorSent = false; _session = new Session(_file, _args, options); _session.on('error', _sessionErrorHandler); _session.on('pause', _sessionPauseHandler); _session.on('resume', _sessionResumeHandler); _session.once('stop', _sessionStopHandler); _session.once('ready', function () { _sessionReady = true; }); _session.start(callback); }); return; } _session = null; } _reason = null; _sessionReady = false; _sessionErrorSent = false; _session = new Session(_file, _args, options); _session.on('error', _sessionErrorHandler); _session.on('pause', _sessionPauseHandler); _session.on('resume', _sessionResumeHandler); _session.once('stop', _sessionStopHandler); _session.once('ready', function () { _sessionReady = true; }); _session.start(callback); }, stop: function (params, callback) { if (!_session || !_session.isRunning()) return callback(new Error("There is no session running.")); callback(); _session.stop(function (err) { _session = null; }); }, pause: function (params, callback) { _session.pause(callback); }, resume: function (params, callback) { _session.resume(callback); } }; /** * Send a message to the Devil client * * @param {Object} message * @private */ var _send = function _send (message) { if (message.id) { // Response if (!_client) return; if (message.error) { if (typeof message.error === 'string') message.error = { message: message.error, name: 'ServerError' }; } } else { // Notification if (!_client) { console.log("NO CLIENT"); _queue.push(message); return; } } var payload = typeof message == 'string' ? message : JSON.stringify(message); _client.send(payload); }; /** * Request handler for the Devil client messages * * @param {string} message * @private */ var _clientRequestHandler = function _clientRequestHandler (message) { try { var request = JSON.parse(message); } catch (e) { console.error("[ERROR] Cannot parse JSON request: " + message); return; } if (request && request.method && _methods[request.method]) { // Handle the request if (!request.params) request.params = {}; console.log("CALL", request.method, request.params); _methods[request.method].call($this, request.params, function (err, result) { console.log("CALLBACK"); _send({id: request.id, error: err, result: result}) }); } else if (request && request.id) { _send({id: request.id, error: 'Wrong request or not implemented method.'}); } }; /** * Request handler for the devtools client * * @param {string} message * @private */ var _devtoolsRequestHandler = function _devtoolsRequestHandler (message) { try { var request = JSON.parse(message); } catch (e) { console.error("[ERROR] Cannot parse JSON request: " + message); return; } if (!_sessionReady) { _session.once('ready', function () { _session.request(request); }); } else { _session.request(request); } }; /** * Connection handler for both kinds of clients. * * @param {Object} client * @returns {boolean} * @private */ var _connectionHandler = function _connectionHandler (client) { // Try to connect this client and reject him if the session is full console.demonicLog("[INFO] New client connected."); if (_devtools) { console.demonicLog("[WARNING] No room for a new client. Disconnect."); client.close(); return false; } if (_client) { // Devtools connection console.demonicLog("[INFO] It's the DevTools."); if (!_session || (_session.isStarted() && !_session.isRunning())) { console.demonicLog('[WARNING] There is no debugger or debuggee started yet. DevTools is not allowed to connect.'); if (_reason) { for (var ii = 0; ii < 10; ii++) client.send(JSON.stringify({id: ii})); client.send(JSON.stringify({method: 'Inspector.detached', params: {reason: _reason}}), function () { client.close(); }); _reason = null; } else client.close(); return false; } var url = client.upgradeReq.url; if ('/' + _token !== url) { console.demonicLog('[WARNING] DevTools does not have the same auth token. Hack attempt or wtf?'); client.close(); return false; } _devtools = client; _devtools.on('message', _devtoolsRequestHandler); _devtools.once('close', function () { console.demonicLog('[INFO] Client (devtools) disconnected.'); _devtools.removeAllListeners(); _devtools = null; if (_session) _session.detachClient(); }); _session.attachClient(_devtools); } else { // Normal connection console.demonicLog("[INFO] It's a Devil client."); // Generate a fresh token since we've got the first client connection. _token = ""; var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < 32; i++) _token += chars.charAt(Math.floor(Math.random() * chars.length)); // Register the client _client = client; _client.once('message', function (msg, flags) { if (msg != 'Greetings, Master!') return _client.close(); _client.send('Don\'t speak to me, scum, I am the Devil!'); _send({ method: 'connected', params: { token: _token } }); _client.on('message', _clientRequestHandler); }); _client.on('close', function () { console.demonicLog('[INFO] Client disconnected.'); _client = null; if (_devtools) { _devtools.close(); _devtools = null; } }); } }; /** * Error handler * * @param {Error|string} error * @private */ var _sessionErrorHandler = function _sessionErrorHandler (error) { if (_sessionErrorSent) return; _send({method: 'error', params: error}); _sessionErrorSent = true; }; /** * Stop handler * * @param {string} reason * @private */ var _sessionStopHandler = function _sessionStopHandler (reason) { _reason = reason; _send({method: 'stop', params: {reason: reason}}); }; /** * Pause handler * * @private */ var _sessionPauseHandler = function _sessionPauseHandler () { _send({method: 'pause'}); }; /** * Resume handler * * @private */ var _sessionResumeHandler = function _sessionResumeHandler () { _send({method: 'resume'}); }; /** * Start * * @param {Function} callback */ this.start = function (callback) { var _calledBack = false; var _callback = function (err) { if (!_calledBack) { callback(err); _calledBack = true; } else { if (err) { console.demonicLog("[ERROR] ", err.message); //$this.emit('error', err); } } }; _isPortAvailable(_port, function (err, isFree) { if (err) return _callback(err); if (!isFree) return _callback(new Error("Port " + _port + " is not available. Please choose another port (see help for more info).")); _ws = new WebSockets({port: _port, host: _host}); _ws.once('listening', function () { console.demonicLog("[INFO] Server listening on " + _host + ":" + _port + "."); _callback(); }); _ws.on('close', function () { console.demonicLog("[ERROR] Server closed unexpectedly."); _callback(new Error("Server closed unexpectedly.")); }); _ws.on('error', function (error) { console.demonicLog("[ERROR] Failed to start server on " + _host + ":" + _port + ". " + error.message); _callback(error); }); _ws.on('connection', _connectionHandler); }); }; } util.inherits(Server, EventEmitter); module.exports = Server;