UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

665 lines (608 loc) 25.2 kB
Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { var callNext = step.bind(null, 'next'); var callThrow = step.bind(null, 'throw'); function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(callNext, callThrow); } } callNext(); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _utils2; function _utils() { return _utils2 = _interopRequireDefault(require('./utils')); } var _helpers2; function _helpers() { return _helpers2 = require('./helpers'); } var _Connection2; function _Connection() { return _Connection2 = require('./Connection'); } var _config2; function _config() { return _config2 = require('./config'); } var _settings2; function _settings() { return _settings2 = require('./settings'); } var _ConnectionUtils2; function _ConnectionUtils() { return _ConnectionUtils2 = require('./ConnectionUtils'); } var _BreakpointStore2; function _BreakpointStore() { return _BreakpointStore2 = require('./BreakpointStore'); } var _DbgpConnector2; function _DbgpConnector() { return _DbgpConnector2 = require('./DbgpConnector'); } var _DbgpSocket2; function _DbgpSocket() { return _DbgpSocket2 = require('./DbgpSocket'); } var _events2; function _events() { return _events2 = require('events'); } var _assert2; function _assert() { return _assert2 = _interopRequireDefault(require('assert')); } var _ClientCallback2; function _ClientCallback() { return _ClientCallback2 = require('./ClientCallback'); } var _commonsNodeEvent2; function _commonsNodeEvent() { return _commonsNodeEvent2 = require('../../commons-node/event'); } var CONNECTION_MUX_STATUS_EVENT = 'connection-mux-status'; var CONNECTION_MUX_NOTIFICATION_EVENT = 'connection-mux-notification'; // The ConnectionMultiplexer makes multiple debugger connections appear to be // a single connection to the debugger UI. // // The initialization sequence occurs as follows: // - the constructor is called // - onStatus is called to hook up event handlers // - initial breakpoints may be added here. // - listen() is called indicating that all initial Breakpoints have been set // and debugging may commence. // // Once initialized, the ConnectionMultiplexer can be in one of 3 main states: // running, break-disabled, and break-enabled. // // Running state means that all connections are in the running state. // Note that running includes the state where there are no connections. // // Break-disabled state has at least one connection in break state. // And none of the connections is enabled. Once in break-disabled state, // the connection mux will immediately enable one of the broken connections // and move to break-enabled state. // // Break-enabled state has a single connection which is in break-enabled // state. There may be connections in break-disabled state and running state // as well. The enabled connection will be shown in the debugger UI and all // commands will go to the enabled connection. // // The ConnectionMultiplexer will close only if there are no connections // and if either the attach or launch DbgpConnectors are closed. The DbgpConnectors will likely only // close if HHVM crashes or is stopped. var ConnectionMultiplexer = (function () { function ConnectionMultiplexer(clientCallback) { _classCallCheck(this, ConnectionMultiplexer); this._clientCallback = clientCallback; this._status = (_DbgpSocket2 || _DbgpSocket()).STATUS_STARTING; this._connectionStatusEmitter = new (_events2 || _events()).EventEmitter(); this._previousConnection = null; this._enabledConnection = null; this._dummyConnection = null; this._connections = new Set(); this._attachConnector = null; this._launchConnector = null; this._dummyRequestProcess = null; this._breakpointStore = new (_BreakpointStore2 || _BreakpointStore()).BreakpointStore(); this._launchedScriptProcess = null; this._launchedScriptProcessPromise = null; this._requestSwitchMessage = null; this._lastEnabledConnection = null; } _createClass(ConnectionMultiplexer, [{ key: 'onStatus', value: function onStatus(callback) { return (0, (_commonsNodeEvent2 || _commonsNodeEvent()).attachEvent)(this._connectionStatusEmitter, CONNECTION_MUX_STATUS_EVENT, callback); } }, { key: 'onNotification', value: function onNotification(callback) { return (0, (_commonsNodeEvent2 || _commonsNodeEvent()).attachEvent)(this._connectionStatusEmitter, CONNECTION_MUX_NOTIFICATION_EVENT, callback); } }, { key: 'listen', value: function listen() { var _this = this; var _ref = (0, (_config2 || _config()).getConfig)(); var xdebugAttachPort = _ref.xdebugAttachPort; var xdebugLaunchingPort = _ref.xdebugLaunchingPort; var launchScriptPath = _ref.launchScriptPath; if (launchScriptPath == null) { // When in attach mode we are guaranteed that the two ports are not equal. (0, (_assert2 || _assert()).default)(xdebugAttachPort !== xdebugLaunchingPort, 'xdebug ports are equal in attach mode'); // In this case we need to listen for incoming connections to attach to, as well as on the // port that the dummy connection will use. this._attachConnector = this._setupConnector(xdebugAttachPort, this._disposeAttachConnector.bind(this)); } // If we are only doing script debugging, then the dummy connection listener's port can also be // used to listen for the script's xdebug requests. this._launchConnector = this._setupConnector(xdebugLaunchingPort, this._disposeLaunchConnector.bind(this)); this._status = (_DbgpSocket2 || _DbgpSocket()).STATUS_RUNNING; var pleaseWaitMessage = { level: 'warning', text: 'Pre-loading, please wait...' }; this._clientCallback.sendUserMessage('console', pleaseWaitMessage); this._clientCallback.sendUserMessage('outputWindow', pleaseWaitMessage); this._dummyRequestProcess = (0, (_ConnectionUtils2 || _ConnectionUtils()).sendDummyRequest)(); if (launchScriptPath != null) { this._launchedScriptProcessPromise = new Promise(function (resolve) { _this._launchedScriptProcess = (0, (_helpers2 || _helpers()).launchPhpScriptWithXDebugEnabled)(launchScriptPath, function (text) { _this._clientCallback.sendUserMessage('outputWindow', { level: 'info', text: text }); resolve(); }); }); } } }, { key: '_setupConnector', value: function _setupConnector(port, disposeConnector) { var connector = new (_DbgpConnector2 || _DbgpConnector()).DbgpConnector(port); connector.onAttach(this._onAttach.bind(this)); connector.onClose(disposeConnector); connector.onError(this._handleAttachError.bind(this)); connector.listen(); return connector; } }, { key: '_handleDummyConnection', value: _asyncToGenerator(function* (socket) { var _this2 = this; (_utils2 || _utils()).default.log('ConnectionMultiplexer successfully got dummy connection.'); var dummyConnection = new (_Connection2 || _Connection()).Connection(socket, function (status, message) { switch (status) { case (_DbgpSocket2 || _DbgpSocket()).STATUS_STDOUT: _this2._sendOutput(message, 'log'); break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_STDERR: _this2._sendOutput(message, 'info'); break; } }); yield this._handleSetupForConnection(dummyConnection); // Continue from loader breakpoint to hit xdebug_break() // which will load whole www repo for evaluation if possible. yield dummyConnection.sendContinuationCommand((_DbgpSocket2 || _DbgpSocket()).COMMAND_RUN); this._dummyConnection = dummyConnection; var text = 'Pre-loading is done! You can use console window now.'; this._clientCallback.sendUserMessage('console', { text: text, level: 'warning' }); this._clientCallback.sendUserMessage('outputWindow', { text: text, level: 'success' }); }) // For testing purpose. }, { key: 'getDummyConnection', value: function getDummyConnection() { return this._dummyConnection; } }, { key: '_onAttach', value: _asyncToGenerator(function* (params) { var socket = params.socket; var message = params.message; if (!(0, (_ConnectionUtils2 || _ConnectionUtils()).isCorrectConnection)(message)) { (0, (_ConnectionUtils2 || _ConnectionUtils()).failConnection)(socket, 'Discarding connection ' + JSON.stringify(message)); return; } if ((0, (_ConnectionUtils2 || _ConnectionUtils()).isDummyConnection)(message)) { yield this._handleDummyConnection(socket); } else { yield this._handleNewConnection(socket, message); } }) }, { key: '_handleNewConnection', value: _asyncToGenerator(function* (socket, message) { var connection = new (_Connection2 || _Connection()).Connection(socket, this._connectionOnStatus.bind(this), this._handleNotification.bind(this)); this._connections.add(connection); yield this._handleSetupForConnection(connection); yield this._breakpointStore.addConnection(connection); this._connectionOnStatus(connection, connection.getStatus()); }) }, { key: '_handleNotification', value: function _handleNotification(connection, notifyName, notify) { switch (notifyName) { case (_DbgpSocket2 || _DbgpSocket()).BREAKPOINT_RESOLVED_NOTIFICATION: var xdebugBreakpoint = notify; var breakpointId = this._breakpointStore.getBreakpointIdFromConnection(connection, xdebugBreakpoint); if (breakpointId == null) { throw Error('Cannot find xdebug breakpoint ' + JSON.stringify(xdebugBreakpoint) + ' in connection.'); } this._breakpointStore.updateBreakpoint(breakpointId, xdebugBreakpoint); var breakpoint = this._breakpointStore.getBreakpoint(breakpointId); this._emitNotification((_DbgpSocket2 || _DbgpSocket()).BREAKPOINT_RESOLVED_NOTIFICATION, breakpoint); break; default: (_utils2 || _utils()).default.logError('Unknown notify: ' + notifyName); break; } } }, { key: '_connectionOnStatus', value: function _connectionOnStatus(connection, status) { (_utils2 || _utils()).default.log('Mux got status: ' + status + ' on connection ' + connection.getId()); switch (status) { case (_DbgpSocket2 || _DbgpSocket()).STATUS_STARTING: // Starting status has no stack. // step before reporting initial status to get to the first instruction. // TODO: Use loader breakpoint configuration to choose between step/run. connection.status = status; connection.sendContinuationCommand((_DbgpSocket2 || _DbgpSocket()).COMMAND_RUN); return; case (_DbgpSocket2 || _DbgpSocket()).STATUS_STOPPING: // TODO: May want to enable post-mortem features? connection.status = status; connection.sendContinuationCommand((_DbgpSocket2 || _DbgpSocket()).COMMAND_RUN); return; case (_DbgpSocket2 || _DbgpSocket()).STATUS_RUNNING: connection.status = status; if (connection === this._enabledConnection) { this._disableConnection(); } break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK: connection.status = status; if (connection === this._enabledConnection) { // This can happen when we step. (_utils2 || _utils()).default.log('Mux break on enabled connection'); this._emitStatus((_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK); return; } break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_ERROR: var message = 'The debugger encountered a problem and the connection had to be shut down.'; if (arguments[2] != null) { message = message + ' Error message: ' + arguments[2]; } this._clientCallback.sendUserMessage('notification', { type: 'error', message: message }); this._removeConnection(connection); break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_STOPPED: case (_DbgpSocket2 || _DbgpSocket()).STATUS_END: connection.status = status; this._removeConnection(connection); break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_STDOUT: this._sendOutput(arguments[2], 'log'); break; case (_DbgpSocket2 || _DbgpSocket()).STATUS_STDERR: this._sendOutput(arguments[2], 'info'); break; } this._updateStatus(); } }, { key: '_sendOutput', value: function _sendOutput(message, level) { this._clientCallback.sendUserMessage('outputWindow', { level: level, text: message }); } }, { key: '_updateStatus', value: function _updateStatus() { if (this._status === (_DbgpSocket2 || _DbgpSocket()).STATUS_END) { return; } if (this._status === (_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK) { (_utils2 || _utils()).default.log('Mux already in break status'); return; } // now check if we can move from running to break... for (var connection of this._connections) { if (connection.getStatus() === (_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK) { if (!(0, (_settings2 || _settings()).getSettings)().singleThreadStepping || this._lastEnabledConnection === null || connection === this._lastEnabledConnection) { this._enableConnection(connection); break; } } } } }, { key: '_enableConnection', value: function _enableConnection(connection) { (_utils2 || _utils()).default.log('Mux enabling connection'); this._enabledConnection = connection; this._handlePotentialRequestSwitch(connection); this._lastEnabledConnection = connection; this._setStatus((_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK); } }, { key: '_setStatus', value: function _setStatus(status) { if (status !== this._status) { this._status = status; this._emitStatus(status); } } }, { key: '_handlePotentialRequestSwitch', value: function _handlePotentialRequestSwitch(connection) { if (this._previousConnection != null && connection !== this._previousConnection) { // The enabled connection is different than it was last time the debugger paused // so we know that the active request has switched so we should alert the user. this._requestSwitchMessage = 'Active request switched'; } this._previousConnection = connection; } }, { key: '_handleAttachError', value: function _handleAttachError(error) { this._clientCallback.sendUserMessage('notification', { type: 'error', message: error }); } }, { key: '_emitStatus', value: function _emitStatus(status) { this._connectionStatusEmitter.emit(CONNECTION_MUX_STATUS_EVENT, status); } }, { key: '_emitNotification', value: function _emitNotification(status, params) { this._connectionStatusEmitter.emit(CONNECTION_MUX_NOTIFICATION_EVENT, status, params); } }, { key: 'runtimeEvaluate', value: _asyncToGenerator(function* (expression) { (_utils2 || _utils()).default.log('runtimeEvaluate() on dummy connection for: ' + expression); if (this._dummyConnection != null) { // Global runtime evaluation on dummy connection does not care about // which frame it is being evaluated on so choose top frame here. var result = yield this._dummyConnection.runtimeEvaluate(0, expression); this._reportEvaluationFailureIfNeeded(expression, result); return result; } else { throw this._noConnectionError(); } }) }, { key: 'evaluateOnCallFrame', value: _asyncToGenerator(function* (frameIndex, expression) { if (this._enabledConnection) { var result = yield this._enabledConnection.evaluateOnCallFrame(frameIndex, expression); this._reportEvaluationFailureIfNeeded(expression, result); return result; } else { throw this._noConnectionError(); } }) }, { key: '_reportEvaluationFailureIfNeeded', value: function _reportEvaluationFailureIfNeeded(expression, result) { if (result.wasThrown) { var _message = { text: 'Failed to evaluate ' + ('"' + expression + '": (' + result.error.$.code + ') ' + result.error.message[0]), level: 'error' }; this._clientCallback.sendUserMessage('console', _message); this._clientCallback.sendUserMessage('outputWindow', _message); } } }, { key: 'getBreakpointStore', value: function getBreakpointStore() { return this._breakpointStore; } }, { key: 'removeBreakpoint', value: function removeBreakpoint(breakpointId) { return this._breakpointStore.removeBreakpoint(breakpointId); } }, { key: 'getStackFrames', value: function getStackFrames() { if (this._enabledConnection) { return this._enabledConnection.getStackFrames(); } else { // This occurs on startup with the loader breakpoint. return Promise.resolve({ stack: {} }); } } }, { key: 'getScopesForFrame', value: function getScopesForFrame(frameIndex) { if (this._enabledConnection) { return this._enabledConnection.getScopesForFrame(frameIndex); } else { throw this._noConnectionError(); } } }, { key: 'getStatus', value: function getStatus() { return this._status; } }, { key: 'sendContinuationCommand', value: function sendContinuationCommand(command) { if (command === (_DbgpSocket2 || _DbgpSocket()).COMMAND_RUN) { // For now we will have only single thread stepping, not single thread running. this._lastEnabledConnection = null; } if (this._enabledConnection) { this._enabledConnection.sendContinuationCommand(command); } else { throw this._noConnectionError(); } } }, { key: 'sendBreakCommand', value: function sendBreakCommand() { if (this._enabledConnection) { return this._enabledConnection.sendBreakCommand(); } else { return Promise.resolve(false); } } }, { key: 'getProperties', value: function getProperties(remoteId) { if (this._enabledConnection && this._status === (_DbgpSocket2 || _DbgpSocket()).STATUS_BREAK) { return this._enabledConnection.getProperties(remoteId); } else if (this._dummyConnection) { return this._dummyConnection.getProperties(remoteId); } else { throw this._noConnectionError(); } } }, { key: '_removeConnection', value: function _removeConnection(connection) { connection.dispose(); this._connections.delete(connection); if (connection === this._enabledConnection) { this._disableConnection(); this._lastEnabledConnection = null; } this._checkForEnd(); } }, { key: '_disableConnection', value: function _disableConnection() { (_utils2 || _utils()).default.log('Mux disabling connection'); this._enabledConnection = null; this._setStatus((_DbgpSocket2 || _DbgpSocket()).STATUS_RUNNING); } }, { key: '_disposeAttachConnector', value: function _disposeAttachConnector() { // Avoid recursion with connector's onClose event. var connector = this._attachConnector; if (connector != null) { this._attachConnector = null; connector.dispose(); } this._checkForEnd(); } }, { key: '_disposeLaunchConnector', value: function _disposeLaunchConnector() { // Avoid recursion with connector's onClose event. var connector = this._launchConnector; if (connector != null) { this._launchConnector = null; connector.dispose(); } this._checkForEnd(); } }, { key: '_checkForEnd', value: _asyncToGenerator(function* () { if (this._connections.size === 0 && (this._attachConnector == null || this._launchConnector == null || (0, (_config2 || _config()).getConfig)().endDebugWhenNoRequests)) { if (this._launchedScriptProcessPromise != null) { yield this._launchedScriptProcessPromise; this._launchedScriptProcessPromise = null; } this._setStatus((_DbgpSocket2 || _DbgpSocket()).STATUS_END); } }) }, { key: '_noConnectionError', value: function _noConnectionError() { // This is an indication of a bug in the state machine. // .. we are seeing a request in a state that should not generate // that request. return new Error('No connection'); } }, { key: '_handleSetupForConnection', value: _asyncToGenerator(function* (connection) { yield this._setupStdStreams(connection); yield this._setupFeatures(connection); }) }, { key: '_setupStdStreams', value: _asyncToGenerator(function* (connection) { var stdoutRequestSucceeded = yield connection.sendStdoutRequest(); if (!stdoutRequestSucceeded) { (_utils2 || _utils()).default.logError('HHVM returned failure for a stdout request'); this._clientCallback.sendUserMessage('outputWindow', { level: 'error', text: 'HHVM failed to redirect stdout, so no output will be sent to the output window.' }); } // TODO: Stderr redirection is not implemented in HHVM so we won't check this return value. yield connection.sendStderrRequest(); }) }, { key: '_setupFeatures', value: _asyncToGenerator(function* (connection) { // max_depth sets the depth that the debugger engine respects when // returning hierarchical data. var setFeatureSucceeded = yield connection.setFeature('max_depth', '5'); if (!setFeatureSucceeded) { (_utils2 || _utils()).default.logError('HHVM returned failure for setting feature max_depth'); } // show_hidden allows the client to request data from private class members. setFeatureSucceeded = yield connection.setFeature('show_hidden', '1'); if (!setFeatureSucceeded) { (_utils2 || _utils()).default.logError('HHVM returned failure for setting feature show_hidden'); } // Turn on notifications. setFeatureSucceeded = yield connection.setFeature('notify_ok', '1'); if (!setFeatureSucceeded) { (_utils2 || _utils()).default.logError('HHVM returned failure for setting feature notify_ok'); } }) }, { key: 'getRequestSwitchMessage', value: function getRequestSwitchMessage() { return this._requestSwitchMessage; } }, { key: 'resetRequestSwitchMessage', value: function resetRequestSwitchMessage() { this._requestSwitchMessage = null; } }, { key: 'dispose', value: function dispose() { if (this._launchedScriptProcess != null) { this._launchedScriptProcessPromise = null; this._launchedScriptProcess.kill('SIGKILL'); this._launchedScriptProcess = null; } for (var connection of this._connections.keys()) { this._removeConnection(connection); } if (this._dummyRequestProcess) { this._dummyRequestProcess.kill('SIGKILL'); } this._disposeLaunchConnector(); this._disposeAttachConnector(); } }]); return ConnectionMultiplexer; })(); exports.ConnectionMultiplexer = ConnectionMultiplexer;