Files
flipper/desktop/headless/index.tsx
Pritesh Nandgaonkar 4e3c06cd54 Update the text to be clear that the export was successfull
Summary: This diff adds the capability to show the plugin names for which flipper failed to fetch metadata. These changes are done for both export as URL and export as File. This diff also fixes the logging for the export as a file and logs the result object in scuba.

Reviewed By: jknoxville

Differential Revision: D20724860

fbshipit-source-id: 4c9591267ca05045e0ed084804d96851c9d7636d
2020-03-31 11:02:16 -07:00

402 lines
12 KiB
TypeScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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, Dispatch, Middleware, MiddlewareAPI} from 'redux';
import {applyMiddleware} from 'redux';
import yargs, {Argv} from 'yargs';
import dispatcher from '../app/src/dispatcher/index';
import reducers, {Actions, State} from '../app/src/reducers/index';
import {init as initLogger} from '../app/src/fb-stubs/Logger';
import {exportStore} from '../app/src/utils/exportData';
import {
exportMetricsWithoutTrace,
exportMetricsFromTrace,
} from '../app/src/utils/exportMetrics';
import {listDevices} from '../app/src/utils/listDevices';
import setup from '../static/setup';
import {
getPersistentPlugins,
pluginsClassMap,
} from '../app/src/utils/pluginUtils';
import {serialize} from '../app/src/utils/serialization';
import {getStringFromErrorLike} from '../app/src/utils/index';
import AndroidDevice from '../app/src/devices/AndroidDevice';
import {Store} from 'flipper';
process.env.FLIPPER_HEADLESS = 'true';
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 as Argv<UserArguments>)
.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: string): boolean {
if (!metrics) {
return process.argv.includes('--metrics');
}
return true;
}
function outputAndExit(output: string | null | undefined): void {
output = output || '';
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);
} else {
const {serializedString, fetchMetaDataErrors} = await exportStore(
store,
);
console.error('Error while fetching metadata', fetchMetaDataErrors);
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 overriddenMethods = ['debug', 'info', 'log', 'warn', 'error'];
for (const method of overriddenMethods) {
(global.console as {[key: string]: any})[method] =
method === 'error' || verbose ? global.console.error : () => {};
}
// 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: Middleware<{}, State, any> = (
store: MiddlewareAPI<Dispatch<Actions>, State>,
) => (next: Dispatch<Actions>) => (action: Actions) => {
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(store as Store, state.pluginStates)
.then((payload: string | null) => {
outputAndExit(payload || '');
})
.catch((e: Error) => {
errorAndExit(e);
});
} else {
exportStore(store)
.then(({serializedString}) => {
outputAndExit(serializedString);
})
.catch((e: Error) => {
errorAndExit(e);
});
}
}, 10);
}
return next(action);
};
setup({});
const store = createStore<State, Actions, {}, {}>(
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(store);
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(store);
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]);
}
matchedDevice.loadDevicePlugins(
store.getState().plugins.devicePlugins,
);
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();
}