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.

397 lines (346 loc) 16.4 kB
Object.defineProperty(exports, '__esModule', { value: true }); /* * 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 _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 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 _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } 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 }; } var _os2; function _os() { return _os2 = _interopRequireDefault(require('os')); } var _rxjsBundlesRxUmdMinJs2; function _rxjsBundlesRxUmdMinJs() { return _rxjsBundlesRxUmdMinJs2 = require('rxjs/bundles/Rx.umd.min.js'); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _commonsNodeProcess2; function _commonsNodeProcess() { return _commonsNodeProcess2 = require('../../commons-node/process'); } var _commonsNodeNice2; function _commonsNodeNice() { return _commonsNodeNice2 = _interopRequireDefault(require('../../commons-node/nice')); } var _FlowHelpers2; function _FlowHelpers() { return _FlowHelpers2 = require('./FlowHelpers'); } var _FlowConstants2; function _FlowConstants() { return _FlowConstants2 = require('./FlowConstants'); } // Names modeled after https://github.com/facebook/flow/blob/master/src/common/flowExitStatus.ml var FLOW_RETURN_CODES = { ok: 0, serverInitializing: 1, typeError: 2, noServerRunning: 6, // This means that the server exists, but it is not responding, typically because it is busy doing // other work. outOfRetries: 7, buildIdMismatch: 9, unexpectedArgument: 64 }; exports.FLOW_RETURN_CODES = FLOW_RETURN_CODES; var SERVER_READY_TIMEOUT_MS = 10 * 1000; var EXEC_FLOW_RETRIES = 5; var FlowProcess = (function () { function FlowProcess(root, execInfoContainer) { var _this = this; _classCallCheck(this, FlowProcess); this._execInfoContainer = execInfoContainer; this._serverStatus = new (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).BehaviorSubject((_FlowConstants2 || _FlowConstants()).ServerStatus.UNKNOWN); this._root = root; this._serverStatus.subscribe(function (status) { logger.info('[' + status + ']: Flow server in ' + _this._root); }); this._serverStatus.filter(function (x) { return x === (_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_RUNNING; }).subscribe(function () { _this._startFlowServer(); _this._pingServer(); }); function isBusyOrInit(status) { return status === (_FlowConstants2 || _FlowConstants()).ServerStatus.BUSY || status === (_FlowConstants2 || _FlowConstants()).ServerStatus.INIT; } this._serverStatus.filter(isBusyOrInit).subscribe(function () { _this._pingServer(); }); this._serverStatus.filter(function (status) { return status === (_FlowConstants2 || _FlowConstants()).ServerStatus.FAILED; }).subscribe(function () { (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('flow-server-failed'); }); } _createClass(FlowProcess, [{ key: 'dispose', value: function dispose() { this._serverStatus.complete(); if (this._startedServer && (0, (_FlowHelpers2 || _FlowHelpers()).getStopFlowOnExit)()) { // The default, SIGTERM, does not reliably kill the flow servers. this._startedServer.kill('SIGKILL'); } } /** * If the Flow server fails we will not try to restart it again automatically. Calling this * method lets us exit that state and retry. */ }, { key: 'allowServerRestart', value: function allowServerRestart() { if (this._serverStatus.getValue() === (_FlowConstants2 || _FlowConstants()).ServerStatus.FAILED) { // We intentionally do not use _setServerStatus because leaving the FAILED state is a // special-case that _setServerStatus does not allow. this._serverStatus.next((_FlowConstants2 || _FlowConstants()).ServerStatus.UNKNOWN); } } }, { key: 'getServerStatusUpdates', value: function getServerStatusUpdates() { return this._serverStatus.asObservable(); } /** * Returns null if Flow cannot be found. */ }, { key: 'execFlow', value: _asyncToGenerator(function* (args, options) { var waitForServer = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2]; var suppressErrors = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3]; var maxRetries = waitForServer ? EXEC_FLOW_RETRIES : 0; if (this._serverStatus.getValue() === (_FlowConstants2 || _FlowConstants()).ServerStatus.FAILED) { return null; } for (var i = 0;; i++) { try { // eslint-disable-next-line babel/no-await-in-loop var result = yield this._rawExecFlow(args, options); return result; } catch (e) { var couldRetry = [(_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_RUNNING, (_FlowConstants2 || _FlowConstants()).ServerStatus.INIT, (_FlowConstants2 || _FlowConstants()).ServerStatus.BUSY].indexOf(this._serverStatus.getValue()) !== -1; if (i < maxRetries && couldRetry) { // eslint-disable-next-line babel/no-await-in-loop yield this._serverIsReady(); // Then try again. } else { // If it couldn't retry, it means there was a legitimate error. If it could retry, we // don't want to log because it just means the server is busy and we don't want to wait. if (!couldRetry && !suppressErrors) { // not sure what happened, but we'll let the caller deal with it logger.error('Flow failed: flow ' + args.join(' ') + '. Error: ' + JSON.stringify(e)); } throw e; } // try again } } }) /** Starts a Flow server in the current root */ }, { key: '_startFlowServer', value: _asyncToGenerator(function* () { var _this2 = this; var flowExecInfo = yield this._execInfoContainer.getFlowExecInfo(this._root); if (flowExecInfo == null) { // This should not happen in normal use. If Flow is not installed we should have caught it by // now. logger.error('Could not find Flow to start server in ' + this._root); this._setServerStatus((_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_INSTALLED); return; } // `flow server` will start a server in the foreground. asyncExecute // will not resolve the promise until the process exits, which in this // case is never. We need to use spawn directly to get access to the // ChildProcess object. // eslint-disable-next-line babel/no-await-in-loop var serverProcess = yield (0, (_commonsNodeNice2 || _commonsNodeNice()).default)(flowExecInfo.pathToFlow, ['server', '--from', 'nuclide', '--max-workers', this._getMaxWorkers().toString(), this._root], flowExecInfo.execOptions); var logIt = function logIt(data) { var pid = serverProcess.pid; logger.debug('flow server (' + pid + '): ' + data); }; serverProcess.stdout.on('data', logIt); serverProcess.stderr.on('data', logIt); serverProcess.on('exit', function (code, signal) { // We only want to blacklist this root if the Flow processes // actually failed, rather than being killed manually. It seems that // if they are killed, the code is null and the signal is 'SIGTERM'. // In the Flow crashes I have observed, the code is 2 and the signal // is null. So, let's blacklist conservatively for now and we can // add cases later if we observe Flow crashes that do not fit this // pattern. if (code === 2 && signal === null) { logger.error('Flow server unexpectedly exited', _this2._root); _this2._setServerStatus((_FlowConstants2 || _FlowConstants()).ServerStatus.FAILED); } }); this._startedServer = serverProcess; }) /** Execute Flow with the given arguments */ }, { key: '_rawExecFlow', value: _asyncToGenerator(function* (args_) { var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; var args = args_; args = [].concat(_toConsumableArray(args), ['--retry-if-init', 'false', '--retries', '0', '--no-auto-start']); try { var result = yield FlowProcess.execFlowClient(args, this._root, this._execInfoContainer, options); this._updateServerStatus(result); return result; } catch (e) { this._updateServerStatus(e); if (e.exitCode === FLOW_RETURN_CODES.typeError) { return e; } else { throw e; } } }) }, { key: '_updateServerStatus', value: function _updateServerStatus(result) { var status = undefined; if (result == null) { status = (_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_INSTALLED; } else { switch (result.exitCode) { case FLOW_RETURN_CODES.ok: // falls through case FLOW_RETURN_CODES.typeError: status = (_FlowConstants2 || _FlowConstants()).ServerStatus.READY; break; case FLOW_RETURN_CODES.serverInitializing: status = (_FlowConstants2 || _FlowConstants()).ServerStatus.INIT; break; case FLOW_RETURN_CODES.noServerRunning: status = (_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_RUNNING; break; case FLOW_RETURN_CODES.outOfRetries: status = (_FlowConstants2 || _FlowConstants()).ServerStatus.BUSY; break; case FLOW_RETURN_CODES.buildIdMismatch: // If the version doesn't match, the server is automatically killed and the client // returns 9. logger.info('Killed flow server with incorrect version in', this._root); status = (_FlowConstants2 || _FlowConstants()).ServerStatus.NOT_RUNNING; break; case FLOW_RETURN_CODES.unexpectedArgument: // If we issued an unexpected argument we have learned nothing about the state of the Flow // server. So, don't update. return; default: logger.error('Unknown return code from Flow: ' + String(result.exitCode)); status = (_FlowConstants2 || _FlowConstants()).ServerStatus.UNKNOWN; } } this._setServerStatus(status); } }, { key: '_setServerStatus', value: function _setServerStatus(status) { var currentStatus = this._serverStatus.getValue(); if ( // Avoid duplicate updates status !== currentStatus && // Avoid moving the status away from FAILED, to let any existing work die out when the // server fails. currentStatus !== (_FlowConstants2 || _FlowConstants()).ServerStatus.FAILED) { this._serverStatus.next(status); } } /** Ping the server until it leaves the current state */ }, { key: '_pingServer', value: _asyncToGenerator(function* () { var tries = arguments.length <= 0 || arguments[0] === undefined ? 5 : arguments[0]; var fromState = this._serverStatus.getValue(); var stateChanged = false; this._serverStatus.filter(function (newState) { return newState !== fromState; }).first().subscribe(function () { stateChanged = true; }); for (var i = 0; !stateChanged && i < tries; i++) { // eslint-disable-next-line babel/no-await-in-loop yield this._rawExecFlow(['status']).catch(function () { return null; }); // Wait 1 second // eslint-disable-next-line babel/no-await-in-loop yield (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).Observable.of(null).delay(1000).toPromise(); } }) /** * Resolves when the server is ready or the request times out, as indicated by the result of the * returned Promise. */ }, { key: '_serverIsReady', value: function _serverIsReady() { return this._serverStatus.filter(function (x) { return x === (_FlowConstants2 || _FlowConstants()).ServerStatus.READY; }).map(function () { return true; }).race((_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).Observable.of(false).delay(SERVER_READY_TIMEOUT_MS)) // If the stream is completed timeout will not return its default value and we will see an // EmptyError. So, provide a defaultValue here so the promise resolves. .first(null, null, false).toPromise(); } }, { key: '_getMaxWorkers', value: function _getMaxWorkers() { return Math.max((_os2 || _os()).default.cpus().length - 2, 1); } /** * This should be used to execute Flow commands that do not rely on a Flow server. So, they do not * need to be associated with a FlowProcess instance and they may be executed from any working * directory. * * Note that using this method means that you get no guarantee that the Flow version specified in * any given .flowconfig is the one that will be executed here, because it has no association with * any given root. If you need this property, create an instance with the appropriate root and use * execFlow. */ }], [{ key: 'execFlowClient', value: _asyncToGenerator(function* (args_, root, execInfoContainer) { var options_ = arguments.length <= 3 || arguments[3] === undefined ? {} : arguments[3]; var args = args_; var options = options_; args = [].concat(_toConsumableArray(args), ['--from', 'nuclide']); var execInfo = yield execInfoContainer.getFlowExecInfo(root); if (execInfo == null) { return null; } options = _extends({}, execInfo.execOptions, options); var ret = yield (0, (_commonsNodeProcess2 || _commonsNodeProcess()).asyncExecute)(execInfo.pathToFlow, args, options); if (ret.exitCode !== 0) { // TODO: bubble up the exit code via return value instead throw ret; } return ret; }) }]); return FlowProcess; })(); exports.FlowProcess = FlowProcess; // If we had to start a Flow server, store the process here so we can kill it when we shut down. // The current state of the Flow server in this directory // The path to the directory where the .flowconfig is -- i.e. the root of the Flow project.