Summary: This change gives priority to a user option settingsString to set up the flipper-server configuration and load them from a json string. Also giving the user the chance to avoid flipper-server looking at the launcher config files in the computer Reviewed By: mweststrate Differential Revision: D34210110 fbshipit-source-id: 9e852b79da106b5140c59116fd7d0c0f3155e620
276 lines
7.8 KiB
TypeScript
276 lines
7.8 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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,
|
|
getEnvironmentInfo,
|
|
loadLauncherSettings,
|
|
loadProcessConfig,
|
|
loadSettings,
|
|
} from 'flipper-server-core';
|
|
import {
|
|
ClientDescription,
|
|
Logger,
|
|
DeviceDescription,
|
|
setLoggerInstance,
|
|
parseEnvironmentVariables,
|
|
} 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 or serial/udid to listen to',
|
|
type: 'string',
|
|
demandOption: true,
|
|
},
|
|
client: {
|
|
describe: 'The application name to listen to',
|
|
type: 'string',
|
|
demandOption: true,
|
|
},
|
|
plugin: {
|
|
describe: 'Plugin id to listen to',
|
|
type: 'string',
|
|
demandOption: true,
|
|
},
|
|
settingsString: {
|
|
describe: `override the existing defaults settings of flipper (settings.json file) e.g "{"androidHome":"/usr/local/bin","enableAndroid":true}"`,
|
|
type: 'string',
|
|
default: '',
|
|
},
|
|
launcherSettings: {
|
|
describe:
|
|
'Open Flipper with the configuration stored in .config folder for the launcher',
|
|
type: 'boolean',
|
|
default: true,
|
|
},
|
|
// 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(deviceQuery: string, appName: string, pluginId: string) {
|
|
return new Promise(async (_resolve, reject) => {
|
|
const staticPath = path.resolve(__dirname, '..', '..', 'static');
|
|
let device: DeviceDescription | undefined;
|
|
let deviceResolver: () => void;
|
|
const devicePromise: Promise<void> = 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;
|
|
|
|
const environmentInfo = await getEnvironmentInfo(staticPath, false);
|
|
// TODO: initialise FB user manager to be able to do certificate exchange
|
|
|
|
const server = new FlipperServerImpl(
|
|
{
|
|
environmentInfo,
|
|
env: parseEnvironmentVariables(process.env),
|
|
gatekeepers: {},
|
|
paths: {
|
|
staticPath,
|
|
tempPath: os.tmpdir(),
|
|
appPath: `/dev/null`,
|
|
homePath: `/dev/null`,
|
|
execPath: process.execPath,
|
|
desktopPath: `/dev/null`,
|
|
},
|
|
launcherSettings: await loadLauncherSettings(argv.launcherSettings),
|
|
processConfig: loadProcessConfig(process.env),
|
|
settings: await loadSettings(argv.settingsString),
|
|
validWebSocketOrigins: [],
|
|
},
|
|
logger,
|
|
);
|
|
|
|
logger.info(
|
|
`Waiting for device '${deviceQuery}' 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('server-state', ({state, error}) => {
|
|
if (state === 'error') {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
server.on('device-connected', (deviceInfo) => {
|
|
logger.info(
|
|
`Detected device [${deviceInfo.os}] ${deviceInfo.title} ${deviceInfo.serial}`,
|
|
);
|
|
if (deviceInfo.serial == deviceQuery) {
|
|
logger.info(`Device matched on device serial ${deviceQuery}`);
|
|
device = deviceInfo;
|
|
deviceResolver();
|
|
} else if (deviceInfo.title === deviceQuery) {
|
|
logger.info(`Device matched on device title ${deviceQuery}`);
|
|
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);
|
|
},
|
|
};
|
|
}
|
|
|
|
start(argv.device!, argv.client!, argv.plugin!).catch((e) => {
|
|
// eslint-disable-next-line
|
|
console.error(e);
|
|
process.exit(1);
|
|
});
|