Summary: Adds an argument to the headless Flipper to print the list of available plugins. Added `--list-plugins`. Currently the startFlipper function is not scalable enough to add new arguments and its implementation. I am planning to tackle this with the list of Actions which will be closure. So adding a new argument with its implementation will be just appending a closure at the correct location in an array. Will work on that in the later diff stacked on the current one. Reviewed By: jknoxville Differential Revision: D15789778 fbshipit-source-id: 91ba472617d593c3490bb932590a06d83597cba7
283 lines
8.1 KiB
JavaScript
283 lines
8.1 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';
|
|
import {getActivePluginNames} from '../src/utils/pluginUtils.js';
|
|
import {serialize} from '../src/utils/serialization';
|
|
|
|
type UserArguments = {|
|
|
securePort: string,
|
|
insecurePort: string,
|
|
dev: boolean,
|
|
exit: 'sigint' | 'disconnect',
|
|
verbose: boolean,
|
|
metrics: string,
|
|
listDevices: boolean,
|
|
device: string,
|
|
listPlugins: boolean,
|
|
|};
|
|
|
|
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('list-plugins', {
|
|
default: false,
|
|
describe: 'Will print the list of supported plugins 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);
|
|
});
|
|
}
|
|
|
|
function errorAndExit(error: string): void {
|
|
process.stderr.write(error, () => {
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
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> {
|
|
if (userArguments.listPlugins) {
|
|
outputAndExit(serialize(getActivePluginNames(store.getState().plugins)));
|
|
}
|
|
|
|
const {metrics, exit} = userArguments;
|
|
if (shouldExportMetric(metrics) && metrics && metrics.length > 0) {
|
|
try {
|
|
const payload = await exportMetricsFromTrace(
|
|
metrics,
|
|
pluginsClassMap(store.getState().plugins),
|
|
);
|
|
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(e => {
|
|
errorAndExit(e.message);
|
|
});
|
|
} else {
|
|
exportStore(store)
|
|
.then(({serializedString}) => {
|
|
outputAndExit(serializedString);
|
|
})
|
|
.catch(e => {
|
|
errorAndExit(e.message);
|
|
});
|
|
}
|
|
}, 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();
|
|
}
|