Summary: Using `process.exit()` stops the node process without waiting for the event loop to finish, so when using async i/o, which is what happens when piped, if the output is buffered, the process can terminate before it finishes flushing the buffer. This means you only get some of the output and the JSON is malformed. This fixes it by calling `process.exit()` inside the flushed callback. Reviewed By: passy Differential Revision: D15624806 fbshipit-source-id: ea540ed5a40fb1811e5b705b190da96c8e54730d
261 lines
7.4 KiB
JavaScript
261 lines
7.4 KiB
JavaScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
* @format
|
|
*/
|
|
|
|
import path from 'path';
|
|
import {createStore} from 'redux';
|
|
import {applyMiddleware} from 'redux';
|
|
import yargs from 'yargs';
|
|
import dispatcher from '../src/dispatcher/index.js';
|
|
import {init as initLogger} from '../src/fb-stubs/Logger.js';
|
|
import reducers from '../src/reducers/index.js';
|
|
import {exportStore, pluginsClassMap} from '../src/utils/exportData.js';
|
|
import {
|
|
exportMetricsWithoutTrace,
|
|
exportMetricsFromTrace,
|
|
} from '../src/utils/exportMetrics.js';
|
|
import {listDevices} from '../src/utils/listDevices';
|
|
// $FlowFixMe this file exist, trust me, flow!
|
|
import setup from '../static/setup.js';
|
|
import type {Store} from '../src/reducers';
|
|
|
|
type UserArguments = {|
|
|
securePort: string,
|
|
insecurePort: string,
|
|
dev: boolean,
|
|
exit: 'sigint' | 'disconnect',
|
|
verbose: boolean,
|
|
metrics: string,
|
|
listDevices: boolean,
|
|
device: string,
|
|
|};
|
|
|
|
yargs
|
|
.usage('$0 [args]')
|
|
.command(
|
|
'*',
|
|
'Start a headless Flipper instance',
|
|
yargs => {
|
|
yargs.option('secure-port', {
|
|
default: '8088',
|
|
describe: 'Secure port the Flipper server should run on.',
|
|
type: 'string',
|
|
});
|
|
yargs.option('insecure-port', {
|
|
default: '8089',
|
|
describe: 'Insecure port the Flipper server should run on.',
|
|
type: 'string',
|
|
});
|
|
yargs.option('dev', {
|
|
default: false,
|
|
describe:
|
|
'Enable redux-devtools. Tries to connect to devtools running on port 8181',
|
|
type: 'boolean',
|
|
});
|
|
yargs.option('exit', {
|
|
describe: 'Controls when to exit and dump the store to stdout.',
|
|
choices: ['sigint', 'disconnect'],
|
|
default: 'sigint',
|
|
});
|
|
yargs.option('v', {
|
|
alias: 'verbose',
|
|
default: false,
|
|
describe: 'Enable verbose logging',
|
|
type: 'boolean',
|
|
});
|
|
yargs.option('metrics', {
|
|
default: undefined,
|
|
describe: 'Will export metrics instead of data when flipper terminates',
|
|
type: 'string',
|
|
});
|
|
yargs.option('list-devices', {
|
|
default: false,
|
|
describe: 'Will print the list of devices in the terminal',
|
|
type: 'boolean',
|
|
});
|
|
yargs.option('device', {
|
|
default: undefined,
|
|
describe:
|
|
'The identifier passed will be matched against the udid of the available devices and the matched device would be selected',
|
|
type: 'string',
|
|
});
|
|
},
|
|
startFlipper,
|
|
)
|
|
.version(global.__VERSION__)
|
|
.help().argv; // http://yargs.js.org/docs/#api-argv
|
|
|
|
function shouldExportMetric(metrics): boolean {
|
|
if (!metrics) {
|
|
return process.argv.includes('--metrics');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function outputAndExit(output: string): void {
|
|
process.stdout.write(output, () => {
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
async function earlyExitActions(
|
|
userArguments: UserArguments,
|
|
originalConsole: typeof global.console,
|
|
): Promise<void> {
|
|
if (userArguments.listDevices) {
|
|
const devices = await listDevices();
|
|
outputAndExit(devices.toString());
|
|
}
|
|
}
|
|
|
|
async function exitActions(
|
|
userArguments: UserArguments,
|
|
originalConsole: typeof global.console,
|
|
store: Store,
|
|
): Promise<void> {
|
|
const {metrics, exit} = userArguments;
|
|
if (shouldExportMetric(metrics) && metrics && metrics.length > 0) {
|
|
try {
|
|
const payload = await exportMetricsFromTrace(
|
|
metrics,
|
|
pluginsClassMap(store.getState()),
|
|
);
|
|
outputAndExit(payload.toString());
|
|
} catch (error) {
|
|
console.error(error);
|
|
process.exit();
|
|
}
|
|
}
|
|
if (exit == 'sigint') {
|
|
process.on('SIGINT', async () => {
|
|
try {
|
|
if (shouldExportMetric(metrics) && !metrics) {
|
|
const state = store.getState();
|
|
const payload = await exportMetricsWithoutTrace(
|
|
store,
|
|
state.pluginStates,
|
|
);
|
|
outputAndExit(payload.toString());
|
|
} else {
|
|
const {serializedString, errorArray} = await exportStore(store);
|
|
errorArray.forEach(console.error);
|
|
outputAndExit(serializedString);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function storeModifyingActions(
|
|
userArguments: UserArguments,
|
|
originalConsole: typeof global.console,
|
|
store: Store,
|
|
): Promise<void> {
|
|
const {device: selectedDeviceID} = userArguments;
|
|
if (selectedDeviceID) {
|
|
//$FlowFixMe: Checked the class name before calling reverse.
|
|
const devices = await listDevices();
|
|
const matchedDevice = devices.find(
|
|
device => device.serial === selectedDeviceID,
|
|
);
|
|
if (matchedDevice) {
|
|
if (matchedDevice.constructor.name === 'AndroidDevice') {
|
|
const ports = store.getState().application.serverPorts;
|
|
matchedDevice.reverse([ports.secure, ports.insecure]);
|
|
}
|
|
store.dispatch({
|
|
type: 'REGISTER_DEVICE',
|
|
payload: matchedDevice,
|
|
});
|
|
store.dispatch({
|
|
type: 'SELECT_DEVICE',
|
|
payload: matchedDevice,
|
|
});
|
|
} else {
|
|
console.error(`No device matching the serial ${selectedDeviceID}`);
|
|
process.exit();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function startFlipper(userArguments: UserArguments) {
|
|
const {verbose, metrics, exit, insecurePort, securePort} = userArguments;
|
|
console.error(`
|
|
_____ _ _
|
|
| __| |_|___ ___ ___ ___
|
|
| __| | | . | . | -_| _|
|
|
|__| |_|_| _| _|___|_| v${global.__VERSION__}
|
|
|_| |_|
|
|
`);
|
|
// redirect all logging to stderr
|
|
const originalConsole = global.console;
|
|
global.console = new Proxy(console, {
|
|
get: function(obj, prop) {
|
|
return (...args) => {
|
|
if (prop === 'error' || verbose) {
|
|
originalConsole.error(`[${prop}] `, ...args);
|
|
}
|
|
};
|
|
},
|
|
});
|
|
|
|
// Polyfills
|
|
global.WebSocket = require('ws'); // used for redux devtools
|
|
global.fetch = require('node-fetch/lib/index');
|
|
|
|
process.env.BUNDLED_PLUGIN_PATH =
|
|
process.env.BUNDLED_PLUGIN_PATH ||
|
|
path.join(path.dirname(process.execPath), 'plugins');
|
|
|
|
process.env.FLIPPER_PORTS = `${insecurePort},${securePort}`;
|
|
|
|
// needs to be required after WebSocket polyfill is loaded
|
|
const devToolsEnhancer = require('remote-redux-devtools');
|
|
|
|
const headlessMiddleware = store => next => action => {
|
|
if (exit == 'disconnect' && action.type == 'CLIENT_REMOVED') {
|
|
// TODO(T42325892): Investigate why the export stalls without exiting the
|
|
// current eventloop task here.
|
|
setTimeout(() => {
|
|
if (shouldExportMetric(metrics) && !metrics) {
|
|
const state = store.getState();
|
|
exportMetricsWithoutTrace(state, state.pluginStates)
|
|
.then(payload => {
|
|
outputAndExit(payload || '');
|
|
})
|
|
.catch(console.error);
|
|
} else {
|
|
exportStore(store)
|
|
.then(({serializedString}) => {
|
|
outputAndExit(serializedString);
|
|
})
|
|
.catch(console.error);
|
|
}
|
|
}, 10);
|
|
}
|
|
return next(action);
|
|
};
|
|
|
|
setup({});
|
|
const store = createStore(
|
|
reducers,
|
|
devToolsEnhancer.composeWithDevTools(applyMiddleware(headlessMiddleware)),
|
|
);
|
|
const logger = initLogger(store, {isHeadless: true});
|
|
|
|
await earlyExitActions(userArguments, originalConsole);
|
|
|
|
const cleanupDispatchers = dispatcher(store, logger);
|
|
|
|
await storeModifyingActions(userArguments, originalConsole, store);
|
|
|
|
await exitActions(userArguments, originalConsole, store);
|
|
|
|
await cleanupDispatchers();
|
|
}
|