Move desktop-related code to "desktop" subfolder (#872)
Summary: Pull Request resolved: https://github.com/facebook/flipper/pull/872 Move all the JS code related to desktop app to "desktop" subfolder. The structure of "desktop" folder: - `src` - JS code of Flipper desktop app executing in Electron Renderer (Chrome) process. This folder also contains all the Flipper plugins in subfolder "src/plugins". - `static` - JS code of Flipper desktop app bootstrapping executing in Electron Main (Node.js) process - `pkg` - Flipper packaging lib and CLI tool - `doctor` - Flipper diagnostics lib and CLI tool - `scripts` - Build scripts for Flipper desktop app - `headless` - Headless version of Flipper app - `headless-tests` - Integration tests running agains Flipper headless version Reviewed By: passy Differential Revision: D20249304 fbshipit-source-id: 9a51c63b51b92b758a02fc8ebf7d3d116770efe9
This commit is contained in:
committed by
Facebook GitHub Bot
parent
a60e6fee87
commit
85c13bb1f3
394
desktop/headless/index.tsx
Normal file
394
desktop/headless/index.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* 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 '../src/dispatcher/index';
|
||||
import reducers, {Actions, State} from '../src/reducers/index';
|
||||
import {init as initLogger} from '../src/fb-stubs/Logger';
|
||||
import {exportStore} from '../src/utils/exportData';
|
||||
import {
|
||||
exportMetricsWithoutTrace,
|
||||
exportMetricsFromTrace,
|
||||
} from '../src/utils/exportMetrics';
|
||||
import {listDevices} from '../src/utils/listDevices';
|
||||
import setup from '../static/setup';
|
||||
import {getPersistentPlugins, pluginsClassMap} from '../src/utils/pluginUtils';
|
||||
import {serialize} from '../src/utils/serialization';
|
||||
import {getStringFromErrorLike} from '../src/utils/index';
|
||||
import AndroidDevice from '../src/devices/AndroidDevice';
|
||||
import {Store} from 'flipper';
|
||||
|
||||
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, 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 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();
|
||||
}
|
||||
Reference in New Issue
Block a user