Files
flipper/desktop/app/src/server/FlipperServerImpl.tsx
Michel Weststrate 11a27f9e1a Support forward command [6/n]
Summary: Support the adb forward command

Reviewed By: jameslawson

Differential Revision: D31055957

fbshipit-source-id: bc0593320d0e187ddfc8120c1684746f9e9c4cf5
2021-09-22 09:03:33 -07:00

320 lines
10 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 EventEmitter from 'events';
import Client from '../Client';
import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import ServerController from './comms/ServerController';
import {UninitializedClient} from './UninitializedClient';
import {addErrorNotification} from '../reducers/notifications';
import {CertificateExchangeMedium} from './utils/CertificateProvider';
import {isLoggedIn} from '../fb-stubs/user';
import React from 'react';
import {Typography} from 'antd';
import {
ACTIVE_SHEET_SIGN_IN,
ServerPorts,
setActiveSheet,
} from '../reducers/application';
import {AndroidDeviceManager} from './devices/android/androidDeviceManager';
import {IOSDeviceManager} from './devices/ios/iOSDeviceManager';
import metroDevice from './devices/metro/metroDeviceManager';
import desktopDevice from './devices/desktop/desktopDeviceManager';
import {
FlipperServerEvents,
FlipperServerState,
FlipperServerCommands,
FlipperServer,
} from 'flipper-plugin';
import {ServerDevice} from './devices/ServerDevice';
import {Base64} from 'js-base64';
import MetroDevice from './devices/metro/MetroDevice';
export interface FlipperServerConfig {
enableAndroid: boolean;
androidHome: string;
enableIOS: boolean;
idbPath: string;
enablePhysicalIOS: boolean;
serverPorts: ServerPorts;
}
// defaultConfig should be used for testing only, and disables by default all features
const defaultConfig: FlipperServerConfig = {
androidHome: '',
enableAndroid: false,
enableIOS: false,
enablePhysicalIOS: false,
idbPath: '',
serverPorts: {
insecure: -1,
secure: -1,
},
};
/**
* FlipperServer takes care of all incoming device & client connections.
* It will set up managers per device type, and create the incoming
* RSocket/WebSocket server to handle incoming client connections.
*
* The server should be largely treated as event emitter, by listening to the relevant events
* using '.on'. All events are strongly typed.
*/
export class FlipperServerImpl implements FlipperServer {
public config: FlipperServerConfig;
private readonly events = new EventEmitter();
// server handles the incoming RSocket / WebSocket connections from Flipper clients
readonly server: ServerController;
readonly disposers: ((() => void) | void)[] = [];
private readonly devices = new Map<string, ServerDevice>();
state: FlipperServerState = 'pending';
android: AndroidDeviceManager;
ios: IOSDeviceManager;
// TODO: remove store argument
constructor(
config: Partial<FlipperServerConfig>,
/** @deprecated remove! */
public store: Store,
public logger: Logger,
) {
this.config = {...defaultConfig, ...config};
const server = (this.server = new ServerController(this));
this.android = new AndroidDeviceManager(this);
this.ios = new IOSDeviceManager(this);
server.addListener('new-client', (client: Client) => {
this.emit('client-connected', client);
});
server.addListener('error', (err) => {
this.emit('server-error', err);
});
server.addListener('start-client-setup', (client: UninitializedClient) => {
this.store.dispatch({
type: 'START_CLIENT_SETUP',
payload: client,
});
});
server.addListener(
'client-setup-error',
({client, error}: {client: UninitializedClient; error: Error}) => {
this.store.dispatch(
addErrorNotification(
`Connection to '${client.appName}' on '${client.deviceName}' failed`,
'Failed to start client connection',
error,
),
);
},
);
server.addListener(
'client-unresponsive-error',
({
client,
medium,
}: {
client: UninitializedClient;
medium: CertificateExchangeMedium;
deviceID: string;
}) => {
this.store.dispatch(
addErrorNotification(
`Timed out establishing connection with "${client.appName}" on "${client.deviceName}".`,
medium === 'WWW' ? (
<>
Verify that both your computer and mobile device are on
Lighthouse/VPN{' '}
{!isLoggedIn().get() && (
<>
and{' '}
<Typography.Link
onClick={() =>
this.store.dispatch(
setActiveSheet(ACTIVE_SHEET_SIGN_IN),
)
}>
log in to Facebook Intern
</Typography.Link>
</>
)}{' '}
so they can exchange certificates.{' '}
<Typography.Link href="https://www.internalfb.com/intern/wiki/Ops/Network/Enterprise_Network_Engineering/ene_wlra/VPN_Help/Vpn/mobile/">
Check this link
</Typography.Link>{' '}
on how to enable VPN on mobile device.
</>
) : (
'Verify that your client is connected to Flipper and that there is no error related to idb.'
),
),
);
},
);
}
setServerState(state: FlipperServerState, error?: Error) {
this.state = state;
this.emit('server-state', {state, error});
}
/**
* Starts listening to parts and watching for devices
*/
async start() {
if (this.state !== 'pending') {
throw new Error('Server already started');
}
this.setServerState('starting');
try {
await this.server.init();
await this.startDeviceListeners();
this.setServerState('started');
} catch (e) {
console.error('Failed to start FlipperServer', e);
this.setServerState('error', e);
}
}
async startDeviceListeners() {
this.disposers.push(
await this.android.watchAndroidDevices(),
await this.ios.watchIOSDevices(),
metroDevice(this),
desktopDevice(this),
);
}
on<Event extends keyof FlipperServerEvents>(
event: Event,
callback: (payload: FlipperServerEvents[Event]) => void,
): void {
this.events.on(event, callback);
}
off<Event extends keyof FlipperServerEvents>(
event: Event,
callback: (payload: FlipperServerEvents[Event]) => void,
): void {
this.events.off(event, callback);
}
/**
* @internal
*/
emit<Event extends keyof FlipperServerEvents>(
event: Event,
payload: FlipperServerEvents[Event],
): void {
this.events.emit(event, payload);
}
exec<Event extends keyof FlipperServerCommands>(
event: Event,
...args: Parameters<FlipperServerCommands[Event]>
): ReturnType<FlipperServerCommands[Event]> {
console.debug(`[FlipperServer] command ${event}: `, args);
const handler: (...args: any[]) => Promise<any> =
this.commandHandler[event];
if (!handler) {
throw new Error(`Unimplemented server command: ${event}`);
}
return handler(...args) as any;
}
private commandHandler: FlipperServerCommands = {
'device-start-logging': async (serial: string) =>
this.getDevice(serial).startLogging(),
'device-stop-logging': async (serial: string) =>
this.getDevice(serial).stopLogging(),
'device-supports-screenshot': async (serial: string) =>
this.getDevice(serial).screenshotAvailable(),
'device-supports-screencapture': async (serial: string) =>
this.getDevice(serial).screenCaptureAvailable(),
'device-take-screenshot': async (serial: string) =>
Base64.fromUint8Array(await this.getDevice(serial).screenshot()),
'device-start-screencapture': async (serial, destination) =>
this.getDevice(serial).startScreenCapture(destination),
'device-stop-screencapture': async (serial: string) =>
this.getDevice(serial).stopScreenCapture(),
'device-shell-exec': async (serial: string, command: string) =>
this.getDevice(serial).executeShell(command),
'metro-command': async (serial: string, command: string) => {
const device = this.getDevice(serial);
if (!(device instanceof MetroDevice)) {
throw new Error('Not a Metro device: ' + serial);
}
device.sendCommand(command);
},
'device-forward-port': async (serial, local, remote) =>
this.getDevice(serial).forwardPort(local, remote),
};
registerDevice(device: ServerDevice) {
// destroy existing device
const {serial} = device.info;
const existing = this.devices.get(serial);
if (existing) {
// assert different kind of devices aren't accidentally reusing the same serial
if (Object.getPrototypeOf(existing) !== Object.getPrototypeOf(device)) {
throw new Error(
`Tried to register a new device type for existing serial '${serial}': Trying to replace existing '${
Object.getPrototypeOf(existing).constructor.name
}' with a new '${Object.getPrototypeOf(device).constructor.name}`,
);
}
// clean up connection
existing.disconnect();
}
// register new device
this.devices.set(device.info.serial, device);
this.emit('device-connected', device.info);
}
unregisterDevice(serial: string) {
const device = this.devices.get(serial);
if (!device) {
return;
}
device.disconnect(); // we'll only destroy upon replacement
this.emit('device-disconnected', device.info);
}
getDevice(serial: string): ServerDevice {
const device = this.devices.get(serial);
if (!device) {
throw new Error('No device with serial: ' + serial);
}
return device;
}
getDeviceSerials(): string[] {
return Array.from(this.devices.keys());
}
getDevices(): ServerDevice[] {
return Array.from(this.devices.values());
}
public async close() {
this.server.close();
for (const device of this.devices.values()) {
device.disconnect();
}
this.disposers.forEach((f) => f?.());
this.setServerState('closed');
}
}