Files
flipper/desktop/app/src/devices/BaseDevice.tsx
Michel Weststrate 32bde8cace add support for device plugin import / export
Summary: Sandy device plugins weren't exported till now (the only stateful plugin so far was Logs, but logs were stored hardcoded on the device rather than using the plugin export mechanisms). This diff makes sure that SandyDevicePlugins will be exported as well if they are persistable.

Reviewed By: nikoant

Differential Revision: D22724822

fbshipit-source-id: a10354a9c7e02f3e696d0cdda0f2c6be6f5ac61e
2021-02-01 11:43:29 -08:00

231 lines
5.4 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 stream from 'stream';
import {
DeviceLogEntry,
_SandyDevicePluginInstance,
_SandyPluginDefinition,
DeviceType,
DeviceLogListener,
} from 'flipper-plugin';
import type {DevicePluginDefinition, DevicePluginMap} from '../plugin';
import {getFlipperLibImplementation} from '../utils/flipperLibImplementation';
import {DeviceSpec, OS as PluginOS} from 'flipper-plugin-lib';
export type DeviceShell = {
stdout: stream.Readable;
stderr: stream.Readable;
stdin: stream.Writable;
};
export type OS = PluginOS | 'Windows' | 'MacOS' | 'JSWebApp';
export type DeviceExport = {
os: OS;
title: string;
deviceType: DeviceType;
serial: string;
logs: Array<DeviceLogEntry>;
pluginStates: Record<string, any>;
};
export default class BaseDevice {
constructor(
serial: string,
deviceType: DeviceType,
title: string,
os: OS,
specs: DeviceSpec[] = [],
) {
this.serial = serial;
this.title = title;
this.deviceType = deviceType;
this.os = os;
this.specs = specs;
}
// operating system of this device
os: OS;
// human readable name for this device
title: string;
// type of this device
deviceType: DeviceType;
// serial number for this device
serial: string;
// additional device specs used for plugin compatibility checks
specs: DeviceSpec[];
// possible src of icon to display next to the device title
icon: string | null | undefined;
logListeners: Map<Symbol, DeviceLogListener> = new Map();
logEntries: Array<DeviceLogEntry> = [];
isArchived: boolean = false;
// if imported, stores the original source location
source = '';
// sorted list of supported device plugins
devicePlugins: string[] = [];
sandyPluginStates: Map<string, _SandyDevicePluginInstance> = new Map<
string,
_SandyDevicePluginInstance
>();
supportsOS(os: OS) {
return os.toLowerCase() === this.os.toLowerCase();
}
displayTitle(): string {
return this.title;
}
toJSON(): DeviceExport {
const pluginStates: Record<string, any> = {};
for (const instance of this.sandyPluginStates.values()) {
if (instance.isPersistable()) {
pluginStates[instance.definition.id] = instance.exportState();
}
}
return {
os: this.os,
title: this.title,
deviceType: this.deviceType,
serial: this.serial,
logs: this.getLogs(),
pluginStates,
};
}
teardown() {
for (const instance of this.sandyPluginStates.values()) {
instance.destroy();
}
}
addLogListener(callback: DeviceLogListener): Symbol {
const id = Symbol();
this.logListeners.set(id, callback);
return id;
}
_notifyLogListeners(entry: DeviceLogEntry) {
if (this.logListeners.size > 0) {
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);
}
});
}
}
addLogEntry(entry: DeviceLogEntry) {
this.logEntries.push(entry);
this._notifyLogListeners(entry);
}
// TODO: remove getLogs T70688226
getLogs(startDate: Date | null = null) {
return startDate != null
? this.logEntries.filter((log) => {
return log.date > startDate;
})
: this.logEntries;
}
clearLogs(): Promise<void> {
// Only for device types that allow clearing.
this.logEntries = [];
return Promise.resolve();
}
removeLogListener(id: Symbol) {
this.logListeners.delete(id);
}
navigateToLocation(_location: string) {
throw new Error('unimplemented');
}
archive(): any | null | undefined {
return null;
}
screenshot(): Promise<Buffer> {
return Promise.reject(
new Error('No screenshot support for current device'),
);
}
async screenCaptureAvailable(): Promise<boolean> {
return false;
}
async startScreenCapture(_destination: string): Promise<void> {
throw new Error('startScreenCapture not implemented on BaseDevice ');
}
async stopScreenCapture(): Promise<string | null> {
return null;
}
loadDevicePlugins(
devicePlugins?: DevicePluginMap,
pluginStates?: Record<string, any>,
) {
if (!devicePlugins) {
return;
}
const plugins = Array.from(devicePlugins.values());
for (const plugin of plugins) {
this.loadDevicePlugin(plugin, pluginStates?.[plugin.id]);
}
}
loadDevicePlugin(plugin: DevicePluginDefinition, initialState?: any) {
if (plugin instanceof _SandyPluginDefinition) {
if (plugin.asDevicePluginModule().supportsDevice(this as any)) {
this.devicePlugins.push(plugin.id);
this.sandyPluginStates.set(
plugin.id,
new _SandyDevicePluginInstance(
getFlipperLibImplementation(),
plugin,
this,
initialState,
),
);
}
} else {
if (plugin.supportsDevice(this)) {
this.devicePlugins.push(plugin.id);
}
}
}
unloadDevicePlugin(pluginId: string) {
const instance = this.sandyPluginStates.get(pluginId);
if (instance) {
instance.destroy();
this.sandyPluginStates.delete(pluginId);
}
this.devicePlugins.splice(this.devicePlugins.indexOf(pluginId), 1);
}
}