Files
flipper/desktop/flipper-frontend-core/src/devices/BaseDevice.tsx
Andrey Goncharov f0b5e7cadb Copy RenderHost, FlipperLib initialization, device definition to flipper-frontend-core
Reviewed By: passy

Differential Revision: D36129746

fbshipit-source-id: 15e32b9482d7fe3a24567d2e6bc087095b98226e
2022-05-10 05:13:24 -07:00

379 lines
9.9 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 {
Device,
_SandyDevicePluginInstance,
_SandyPluginDefinition,
DeviceLogListener,
Idler,
createState,
getFlipperLib,
CrashLogListener,
} from 'flipper-plugin';
import {
DeviceLogEntry,
DeviceOS,
DeviceType,
DeviceDescription,
FlipperServer,
CrashLog,
ServerAddOnControls,
} from 'flipper-common';
import {DeviceSpec, PluginDetails} from 'flipper-common';
import {getPluginKey} from '../utils/pluginKey';
import {Base64} from 'js-base64';
import {createServerAddOnControls} from '../utils/createServerAddOnControls';
type PluginDefinition = _SandyPluginDefinition;
type PluginMap = Map<string, PluginDefinition>;
export type DeviceExport = {
os: DeviceOS;
title: string;
deviceType: DeviceType;
serial: string;
pluginStates: Record<string, any>;
};
export default class BaseDevice implements Device {
description: DeviceDescription;
flipperServer: FlipperServer;
isArchived = false;
hasDevicePlugins = false; // true if there are device plugins for this device (not necessarily enabled)
private readonly serverAddOnControls: ServerAddOnControls;
constructor(flipperServer: FlipperServer, description: DeviceDescription) {
this.flipperServer = flipperServer;
this.description = description;
this.serverAddOnControls = createServerAddOnControls(this.flipperServer);
}
get isConnected(): boolean {
return this.connected.get();
}
// operating system of this device
get os() {
return this.description.os;
}
// human readable name for this device
get title(): string {
return this.description.title;
}
// type of this device
get deviceType() {
return this.description.deviceType;
}
// serial number for this device
get serial() {
return this.description.serial;
}
// additional device specs used for plugin compatibility checks
get specs(): DeviceSpec[] {
return this.description.specs ?? [];
}
// possible src of icon to display next to the device title
get icon() {
return this.description.icon;
}
logListeners: Map<Symbol, DeviceLogListener> = new Map();
crashListeners: Map<Symbol, CrashLogListener> = new Map();
readonly connected = createState(true);
// if imported, stores the original source location
source = '';
// TODO: ideally we don't want BasePlugin to know about the concept of plugins
sandyPluginStates: Map<string, _SandyDevicePluginInstance> = new Map<
string,
_SandyDevicePluginInstance
>();
supportsOS(os: DeviceOS) {
return os.toLowerCase() === this.os.toLowerCase();
}
displayTitle(): string {
return this.connected.get() ? this.title : `${this.title} (Offline)`;
}
async exportState(
idler: Idler,
onStatusMessage: (msg: string) => void,
selectedPlugins: string[],
): Promise<Record<string, any>> {
const pluginStates: Record<string, any> = {};
for (const instance of this.sandyPluginStates.values()) {
if (
selectedPlugins.includes(instance.definition.id) &&
instance.isPersistable()
) {
pluginStates[instance.definition.id] = await instance.exportState(
idler,
onStatusMessage,
);
}
}
return pluginStates;
}
toJSON() {
return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
};
}
private deviceLogEventHandler = (payload: {
serial: string;
entry: DeviceLogEntry;
}) => {
if (payload.serial === this.serial && this.logListeners.size > 0) {
this.addLogEntry(payload.entry);
}
};
addLogEntry(entry: DeviceLogEntry) {
this.logListeners.forEach((listener) => {
// prevent breaking other listeners, if one listener doesn't work.
try {
listener(entry);
} catch (e) {
console.error(`Log listener exception:`, e);
}
});
}
async startLogging() {
this.flipperServer.on('device-log', this.deviceLogEventHandler);
}
stopLogging() {
this.flipperServer.off('device-log', this.deviceLogEventHandler);
}
addLogListener(callback: DeviceLogListener): Symbol {
if (this.logListeners.size === 0) {
this.startLogging();
}
const id = Symbol();
this.logListeners.set(id, callback);
return id;
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
if (this.logListeners.size === 0) {
this.stopLogging();
}
}
private crashLogEventHandler = (payload: {
serial: string;
crash: CrashLog;
}) => {
if (payload.serial === this.serial && this.crashListeners.size > 0) {
this.addCrashEntry(payload.crash);
}
};
addCrashEntry(entry: CrashLog) {
this.crashListeners.forEach((listener) => {
// prevent breaking other listeners, if one listener doesn't work.
try {
listener(entry);
} catch (e) {
console.error(`Crash listener exception:`, e);
}
});
}
async startCrashWatcher() {
this.flipperServer.on('device-crash', this.crashLogEventHandler);
}
stopCrashWatcher() {
this.flipperServer.off('device-crash', this.crashLogEventHandler);
}
addCrashListener(callback: CrashLogListener): Symbol {
if (this.crashListeners.size === 0) {
this.startCrashWatcher();
}
const id = Symbol();
this.crashListeners.set(id, callback);
return id;
}
removeCrashListener(id: Symbol) {
this.crashListeners.delete(id);
if (this.crashListeners.size === 0) {
this.stopCrashWatcher();
}
}
async navigateToLocation(location: string) {
return this.flipperServer.exec('device-navigate', this.serial, location);
}
async screenshot(): Promise<Uint8Array | undefined> {
if (!this.description.features.screenshotAvailable || this.isArchived) {
return;
}
return Base64.toUint8Array(
await this.flipperServer.exec('device-take-screenshot', this.serial),
);
}
async startScreenCapture(destination: string): Promise<void> {
return this.flipperServer.exec(
'device-start-screencapture',
this.serial,
destination,
);
}
async stopScreenCapture(): Promise<string | null> {
return this.flipperServer.exec('device-stop-screencapture', this.serial);
}
async executeShell(command: string): Promise<string> {
return this.flipperServer.exec('device-shell-exec', this.serial, command);
}
async sendMetroCommand(command: string): Promise<void> {
return this.flipperServer.exec('metro-command', this.serial, command);
}
async forwardPort(local: string, remote: string): Promise<boolean> {
return this.flipperServer.exec(
'device-forward-port',
this.serial,
local,
remote,
);
}
async clearLogs() {
return this.flipperServer.exec('device-clear-logs', this.serial);
}
supportsPlugin(plugin: PluginDefinition | PluginDetails) {
let pluginDetails: PluginDetails;
if (plugin instanceof _SandyPluginDefinition) {
pluginDetails = plugin.details;
if (!pluginDetails.pluginType && !pluginDetails.supportedDevices) {
// TODO T84453692: this branch is to support plugins defined with the legacy approach. Need to remove this branch after some transition period when
// all the plugins will be migrated to the new approach with static compatibility metadata in package.json.
if (plugin instanceof _SandyPluginDefinition) {
return (
plugin.isDevicePlugin &&
(plugin.asDevicePluginModule().supportsDevice?.(this as any) ??
false)
);
} else {
return (plugin as any).supportsDevice(this);
}
}
} else {
pluginDetails = plugin;
}
return (
pluginDetails.pluginType === 'device' &&
(!pluginDetails.supportedDevices ||
pluginDetails.supportedDevices?.some(
(d) =>
(!d.os || d.os === this.os) &&
(!d.type || d.type === this.deviceType) &&
(d.archived === undefined || d.archived === this.isArchived) &&
(!d.specs || d.specs.every((spec) => this.specs.includes(spec))),
))
);
}
loadDevicePlugins(
devicePlugins: PluginMap,
enabledDevicePlugins: Set<string>,
pluginStates?: Record<string, any>,
) {
if (!devicePlugins) {
return;
}
const plugins = Array.from(devicePlugins.values()).filter((p) =>
enabledDevicePlugins?.has(p.id),
);
for (const plugin of plugins) {
this.loadDevicePlugin(plugin, pluginStates?.[plugin.id]);
}
}
loadDevicePlugin(plugin: PluginDefinition, initialState?: any) {
if (!this.supportsPlugin(plugin)) {
return;
}
this.hasDevicePlugins = true;
if (plugin instanceof _SandyPluginDefinition) {
try {
this.sandyPluginStates.set(
plugin.id,
new _SandyDevicePluginInstance(
this.serverAddOnControls,
getFlipperLib(),
plugin,
this,
// break circular dep, one of those days again...
getPluginKey(undefined, {serial: this.serial}, plugin.id),
initialState,
),
);
} catch (e) {
console.error(`Failed to start device plugin '${plugin.id}': `, e);
}
}
}
unloadDevicePlugin(pluginId: string) {
const instance = this.sandyPluginStates.get(pluginId);
if (instance) {
instance.destroy();
this.sandyPluginStates.delete(pluginId);
}
}
disconnect() {
this.logListeners.clear();
this.stopLogging();
this.crashListeners.clear();
this.stopCrashWatcher();
this.connected.set(false);
}
destroy() {
this.disconnect();
this.sandyPluginStates.forEach((instance) => {
instance.destroy();
});
this.sandyPluginStates.clear();
this.serverAddOnControls.unsubscribe();
}
}