/** * 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 fs from 'fs'; import os from 'os'; import yargs from 'yargs'; import { FlipperServerImpl, loadLauncherSettings, loadProcessConfig, loadSettings, } from 'flipper-server-core'; import { ClientDescription, Logger, DeviceDescription, setLoggerInstance, } from 'flipper-common'; import path from 'path'; import {stdout} from 'process'; // eslint-disable-next-line const packageJson = JSON.parse(fs.readFileSync('../package.json', 'utf-8')); const argv = yargs .usage('$0 [args]') .options({ device: { describe: 'The device name to listen to', type: 'string', }, client: { describe: 'The application name to listen to', type: 'string', }, plugin: { describe: 'Plugin id to listen to', type: 'string', }, // TODO: support filtering events // TODO: support verbose mode // TODO: support post processing messages }) .version(packageJson.version) .help() // .strict() .parse(process.argv.slice(1)); async function start(deviceTitle: string, appName: string, pluginId: string) { return new Promise(async (_resolve, reject) => { let device: DeviceDescription | undefined; let deviceResolver: () => void; const devicePromise: Promise = new Promise((resolve) => { deviceResolver = resolve; }); let client: ClientDescription | undefined; const logger = createLogger(); setLoggerInstance(logger); // avoid logging to STDOUT! console.log = console.error; console.debug = () => {}; console.info = console.error; // TODO: initialise FB user manager to be able to do certificate exchange const server = new FlipperServerImpl( { env: process.env, gatekeepers: {}, isProduction: false, paths: { staticPath: path.resolve(__dirname, '..', '..', 'static'), tempPath: os.tmpdir(), appPath: `/dev/null`, homePath: `/dev/null`, execPath: process.execPath, desktopPath: `/dev/null`, }, launcherSettings: await loadLauncherSettings(), processConfig: loadProcessConfig(process.env), settings: await loadSettings(), validWebSocketOrigins: [], }, logger, ); logger.info( `Waiting for device '${deviceTitle}' client '${appName}' plugin '${pluginId}' ...`, ); server.on('notification', ({type, title, description}) => { if (type === 'error') { reject(new Error(`${title}: ${description}`)); } }); server.on('server-error', reject); server.on('device-connected', (deviceInfo) => { logger.info( `Detected device [${deviceInfo.os}] ${deviceInfo.title} ${deviceInfo.serial}`, ); if (deviceInfo.title === deviceTitle) { logger.info('Device matched'); device = deviceInfo; deviceResolver(); } }); server.on('device-disconnected', (deviceInfo) => { if (device && deviceInfo.serial === device.serial) { reject(new Error('Device disconnected: ' + deviceInfo.serial)); } }); server.on('client-setup', (client) => { logger.info( `Connection attempt: ${client.appName} on ${client.deviceName}`, ); }); server.on( 'client-connected', async (clientDescription: ClientDescription) => { // device matching is promisified, as we clients can arrive before device is detected await devicePromise; if (clientDescription.query.app === appName) { if (clientDescription.query.device_id === device!.serial) { logger.info(`Client matched: ${clientDescription.id}`); client = clientDescription; try { // fetch plugins const response = await server.exec( 'client-request-response', client.id, { method: 'getPlugins', }, ); logger.info(JSON.stringify(response)); if (response.error) { reject(response.error); return; } const plugins: string[] = (response.success as any).plugins; logger.info('Detected plugins ' + plugins.join(',')); if (!plugins.includes(pluginId)) { // TODO: what if it only registers later? throw new Error( `Plugin ${pluginId} was not registered on client ${client.id}`, ); } logger.info(`Starting plugin ` + pluginId); const response2 = await server.exec( 'client-request-response', client.id, { method: 'init', params: {plugin: pluginId}, }, ); if (response2.error) { reject(response2.error); } logger.info('Plugin initialised'); } catch (e) { reject(e); } } } }, ); server.on('client-disconnected', ({id}) => { if (id === client?.id) { // TODO: maybe we need a flag to signal that this might be undesired? logger.info('Target application disconnected, exiting...'); process.exit(0); } }); server.on('client-message', ({id, message}) => { if (id === client?.id) { const parsed = JSON.parse(message); if (parsed.method === 'execute') { if (parsed.params.api === pluginId) { // TODO: customizable format stdout.write( `\n\n\n[${parsed.params.method}]\n${JSON.stringify( parsed.params.params, null, 2, )}\n`, ); } } else { logger.warn('Dropping message ', message); } } }); server .connect() .then(() => { logger.info( 'Flipper server started and accepting device / client connections', ); }) .catch(reject); }); } function createLogger(): Logger { return { track() { // no-op }, trackTimeSince() { // no-op }, debug() { // TODO: support this with a --verbose flag }, error(...args: any[]) { console.error(...args); }, warn(...args: any[]) { console.warn(...args); }, info(...args: any[]) { // we want to redirect info level logging to STDERR! So that STDOUT is used merely for plugin output console.error(...args); }, }; } if (!argv.device) { console.error('--device not specified'); process.exit(1); } if (!argv.client) { console.error('--client not specified'); process.exit(1); } if (!argv.plugin) { console.error('--plugin not specified'); process.exit(1); } start(argv.device!, argv.client!, argv.plugin!).catch((e) => { // eslint-disable-next-line console.error(e); process.exit(1); });