/** * 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; pluginStates: Record; }; 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 = new Map(); logEntries: Array = []; isArchived: boolean = false; // if imported, stores the original source location source = ''; // sorted list of supported device plugins devicePlugins: string[] = []; sandyPluginStates: Map = new Map< string, _SandyDevicePluginInstance >(); supportsOS(os: OS) { return os.toLowerCase() === this.os.toLowerCase(); } displayTitle(): string { return this.title; } toJSON(): DeviceExport { const pluginStates: Record = {}; 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 { // 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 { return Promise.reject( new Error('No screenshot support for current device'), ); } async screenCaptureAvailable(): Promise { return false; } async startScreenCapture(_destination: string): Promise { throw new Error('startScreenCapture not implemented on BaseDevice '); } async stopScreenCapture(): Promise { return null; } loadDevicePlugins( devicePlugins?: DevicePluginMap, pluginStates?: Record, ) { 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); } }