Files
flipper/headless/index.tsx
Daniel Büchele 853d80f182 headless
Summary: _typescript_

Reviewed By: passy

Differential Revision: D16962733

fbshipit-source-id: 6583c5a4471b12f91895f6449ed9c510265a63e2
2019-08-23 09:33:56 -07:00

389 lines
11 KiB
TypeScript

/**
* 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, {Argv} from 'yargs';
import dispatcher from '../src/dispatcher/index';
import reducers from '../src/reducers/index';
import {init as initLogger} from '../src/fb-stubs/Logger';
import {exportStore, pluginsClassMap} from '../src/utils/exportData';
import {
exportMetricsWithoutTrace,
exportMetricsFromTrace,
} from '../src/utils/exportMetrics';
import {listDevices} from '../src/utils/listDevices';
import setup from '../static/setup.js';
import {Store} from '../src/reducers/index';
import {getPersistentPlugins} from '../src/utils/pluginUtils';
import {serialize} from '../src/utils/serialization';
import {getStringFromErrorLike} from '../src/utils/index';
import AndroidDevice from '../src/devices/AndroidDevice';
type Action = {exit: boolean; result?: string};
type UserArguments = {
securePort: string;
insecurePort: string;
dev: boolean;
exit: 'sigint' | 'disconnect';
verbose: boolean;
metrics: string;
listDevices: boolean;
device: string;
listPlugins: boolean;
selectPlugins: Array<string>;
};
yargs
.usage('$0 [args]')
.command<UserArguments>(
'*',
'Start a headless Flipper instance',
(yargs: Argv<UserArguments>) => {
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('list-plugins', {
default: false,
describe: 'Will print the list of supported plugins in the terminal',
type: 'boolean',
});
yargs.option('select-plugins', {
default: [],
describe:
'The data/metrics would be exported only for the selected plugins',
type: 'array',
});
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',
});
return yargs;
},
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 {
console.log(`Finished. Outputting ${output.length} characters.`);
process.stdout.write(output, () => {
process.exit(0);
});
}
function errorAndExit(error: any): void {
process.stderr.write(getStringFromErrorLike(error), () => {
process.exit(1);
});
}
async function earlyExitActions(
exitClosures: Array<(userArguments: UserArguments) => Promise<Action>>,
userArguments: UserArguments,
_originalConsole?: typeof global.console,
): Promise<void> {
for (const exitAction of exitClosures) {
try {
const action = await exitAction(userArguments);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
}
async function exitActions(
exitClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
>,
userArguments: UserArguments,
store: Store,
): Promise<void> {
const {metrics, exit} = userArguments;
for (const exitAction of exitClosures) {
try {
const action = await exitAction(userArguments, store);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
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) {
errorAndExit(e);
}
});
}
}
async function storeModifyingActions(
storeModifyingClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
>,
userArguments: UserArguments,
store: Store,
): Promise<void> {
for (const closure of storeModifyingClosures) {
try {
const action = await closure(userArguments, store);
if (action.exit) {
outputAndExit(action.result);
}
} catch (e) {
errorAndExit(e);
}
}
}
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(`[${String(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(e => {
errorAndExit(e);
});
} else {
exportStore(store)
.then(({serializedString}) => {
outputAndExit(serializedString);
})
.catch(e => {
errorAndExit(e);
});
}
}, 10);
}
return next(action);
};
setup({});
const store = createStore(
reducers,
devToolsEnhancer.composeWithDevTools(applyMiddleware(headlessMiddleware)),
);
const logger = initLogger(store, {isHeadless: true});
const earlyExitClosures: Array<
(userArguments: UserArguments) => Promise<Action>
> = [
async (userArguments: UserArguments) => {
if (userArguments.listDevices) {
const devices = await listDevices();
const mapped = devices.map(device => {
return {
os: device.os,
title: device.title,
deviceType: device.deviceType,
serial: device.serial,
};
});
return {exit: true, result: await serialize(mapped)};
}
return Promise.resolve({exit: false});
},
];
await earlyExitActions(earlyExitClosures, userArguments);
const cleanupDispatchers = dispatcher(store, logger);
const storeModifyingClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
> = [
async (userArguments: UserArguments, store: Store) => {
const {device: selectedDeviceID} = userArguments;
if (selectedDeviceID) {
const devices = await listDevices();
const matchedDevice = devices.find(
device => device.serial === selectedDeviceID,
);
if (matchedDevice) {
if (matchedDevice instanceof 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,
});
return {exit: false};
}
throw new Error(`No device matching the serial ${selectedDeviceID}`);
}
return {
exit: false,
};
},
async (userArguments: UserArguments, store: Store) => {
const {selectPlugins} = userArguments;
const selectedPlugins = selectPlugins.filter(selectPlugin => {
return selectPlugin != undefined;
});
if (selectedPlugins) {
store.dispatch({
type: 'SELECTED_PLUGINS',
payload: selectedPlugins,
});
}
return {
exit: false,
};
},
];
const exitActionClosures: Array<
(userArguments: UserArguments, store: Store) => Promise<Action>
> = [
async (userArguments: UserArguments, store: Store) => {
const {listPlugins} = userArguments;
if (listPlugins) {
return Promise.resolve({
exit: true,
result: await serialize(
getPersistentPlugins(store.getState().plugins),
),
});
}
return Promise.resolve({
exit: false,
});
},
async (userArguments: UserArguments, store: Store) => {
const {metrics} = userArguments;
if (shouldExportMetric(metrics) && metrics && metrics.length > 0) {
try {
const payload = await exportMetricsFromTrace(
metrics,
pluginsClassMap(store.getState().plugins),
store.getState().plugins.selectedPlugins,
);
return {exit: true, result: payload ? payload.toString() : ''};
} catch (error) {
return {exit: true, result: error};
}
}
return Promise.resolve({exit: false});
},
];
await storeModifyingActions(storeModifyingClosures, userArguments, store);
await exitActions(exitActionClosures, userArguments, store);
await cleanupDispatchers();
}