Copy RenderHost, FlipperLib initialization, device definition to flipper-frontend-core
Reviewed By: passy Differential Revision: D36129746 fbshipit-source-id: 15e32b9482d7fe3a24567d2e6bc087095b98226e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
db49673d8a
commit
f0b5e7cadb
158
desktop/flipper-frontend-core/src/RenderHost.tsx
Normal file
158
desktop/flipper-frontend-core/src/RenderHost.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 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 {FlipperLib, Notification} from 'flipper-plugin';
|
||||
import {FlipperServer, FlipperServerConfig} from 'flipper-common';
|
||||
|
||||
type NotificationEvents = 'show' | 'click' | 'close' | 'reply' | 'action';
|
||||
type PluginNotification = {
|
||||
notification: Notification;
|
||||
pluginId: string;
|
||||
client: null | string; // id
|
||||
};
|
||||
type Icon = {
|
||||
name: string;
|
||||
variant: 'outline' | 'filled';
|
||||
size: number;
|
||||
density: number;
|
||||
};
|
||||
|
||||
interface NotificationAction {
|
||||
// Docs: https://electronjs.org/docs/api/structures/notification-action
|
||||
|
||||
/**
|
||||
* The label for the given action.
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* The type of action, can be `button`.
|
||||
*/
|
||||
type: 'button';
|
||||
}
|
||||
|
||||
// Subset of electron.d.ts
|
||||
interface NotificationConstructorOptions {
|
||||
/**
|
||||
* A title for the notification, which will be shown at the top of the notification
|
||||
* window when it is shown.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The body text of the notification, which will be displayed below the title or
|
||||
* subtitle.
|
||||
*/
|
||||
body: string;
|
||||
/**
|
||||
* Actions to add to the notification. Please read the available actions and
|
||||
* limitations in the `NotificationAction` documentation.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
actions?: NotificationAction[];
|
||||
/**
|
||||
* A custom title for the close button of an alert. An empty string will cause the
|
||||
* default localized text to be used.
|
||||
*
|
||||
* @platform darwin
|
||||
*/
|
||||
closeButtonText?: string;
|
||||
}
|
||||
|
||||
// Events that are emitted from the main.ts ovr the IPC process bridge in Electron
|
||||
type MainProcessEvents = {
|
||||
'flipper-protocol-handler': [query: string];
|
||||
'open-flipper-file': [url: string];
|
||||
notificationEvent: [
|
||||
eventName: NotificationEvents,
|
||||
pluginNotification: PluginNotification,
|
||||
arg: null | string | number,
|
||||
];
|
||||
trackUsage: any[];
|
||||
getLaunchTime: [launchStartTime: number];
|
||||
};
|
||||
|
||||
// Events that are emitted by the child process, to the main process
|
||||
type ChildProcessEvents = {
|
||||
setTheme: [theme: 'dark' | 'light' | 'system'];
|
||||
sendNotification: [
|
||||
{
|
||||
payload: NotificationConstructorOptions;
|
||||
pluginNotification: PluginNotification;
|
||||
closeAfter?: number;
|
||||
},
|
||||
];
|
||||
getLaunchTime: [];
|
||||
componentDidMount: [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Utilities provided by the render host, e.g. Electron, the Browser, etc
|
||||
*/
|
||||
export interface RenderHost {
|
||||
readTextFromClipboard(): string | undefined;
|
||||
writeTextToClipboard(text: string): void;
|
||||
/**
|
||||
* @deprecated
|
||||
* WARNING!
|
||||
* It is a low-level API call that might be removed in the future.
|
||||
* It is not really deprecated yet, but we'll try to make it so.
|
||||
* TODO: Remove in favor of "exportFile"
|
||||
*/
|
||||
showSaveDialog?(options: {
|
||||
defaultPath?: string;
|
||||
message?: string;
|
||||
title?: string;
|
||||
}): Promise<string | undefined>;
|
||||
/**
|
||||
* @deprecated
|
||||
* WARNING!
|
||||
* It is a low-level API call that might be removed in the future.
|
||||
* It is not really deprecated yet, but we'll try to make it so.
|
||||
* TODO: Remove in favor of "importFile"
|
||||
*/
|
||||
showOpenDialog?(options: {
|
||||
defaultPath?: string;
|
||||
filter?: {
|
||||
extensions: string[];
|
||||
name: string;
|
||||
};
|
||||
}): Promise<string | undefined>;
|
||||
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
|
||||
importFile: FlipperLib['importFile'];
|
||||
exportFile: FlipperLib['exportFile'];
|
||||
hasFocus(): boolean;
|
||||
onIpcEvent<Event extends keyof MainProcessEvents>(
|
||||
event: Event,
|
||||
callback: (...arg: MainProcessEvents[Event]) => void,
|
||||
): void;
|
||||
sendIpcEvent<Event extends keyof ChildProcessEvents>(
|
||||
event: Event,
|
||||
...args: ChildProcessEvents[Event]
|
||||
): void;
|
||||
shouldUseDarkColors(): boolean;
|
||||
restartFlipper(update?: boolean): void;
|
||||
openLink(url: string): void;
|
||||
loadDefaultPlugins(): Record<string, any>;
|
||||
GK(gatekeeper: string): boolean;
|
||||
flipperServer: FlipperServer;
|
||||
serverConfig: FlipperServerConfig;
|
||||
requirePlugin(path: string): Promise<any>;
|
||||
getStaticResourceUrl(relativePath: string): string;
|
||||
// given the requested icon and proposed public url of the icon, rewrite it to a local icon if needed
|
||||
getLocalIconUrl?(icon: Icon, publicUrl: string): string;
|
||||
unloadModule?(path: string): void;
|
||||
getPercentCPUUsage?(): number;
|
||||
}
|
||||
|
||||
export function getRenderHostInstance(): RenderHost {
|
||||
if (!FlipperRenderHostInstance) {
|
||||
throw new Error('global FlipperRenderHostInstance was never set');
|
||||
}
|
||||
return FlipperRenderHostInstance;
|
||||
}
|
||||
84
desktop/flipper-frontend-core/src/devices/ArchivedDevice.tsx
Normal file
84
desktop/flipper-frontend-core/src/devices/ArchivedDevice.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 BaseDevice from './BaseDevice';
|
||||
import type {DeviceOS, DeviceType} from 'flipper-plugin';
|
||||
|
||||
export default class ArchivedDevice extends BaseDevice {
|
||||
isArchived = true;
|
||||
|
||||
constructor(options: {
|
||||
serial: string;
|
||||
deviceType: DeviceType;
|
||||
title: string;
|
||||
os: DeviceOS;
|
||||
screenshotHandle?: string | null;
|
||||
source?: string;
|
||||
supportRequestDetails?: object;
|
||||
}) {
|
||||
super(
|
||||
{
|
||||
async connect() {},
|
||||
close() {},
|
||||
exec(command, ..._args: any[]) {
|
||||
throw new Error(
|
||||
`[Archived device] Cannot invoke command ${command} on an archived device`,
|
||||
);
|
||||
},
|
||||
on(event) {
|
||||
console.warn(
|
||||
`Cannot subscribe to server events from an Archived device: ${event}`,
|
||||
);
|
||||
},
|
||||
off() {},
|
||||
},
|
||||
{
|
||||
deviceType: options.deviceType,
|
||||
title: options.title,
|
||||
os: options.os,
|
||||
serial: options.serial,
|
||||
icon: 'box',
|
||||
features: {
|
||||
screenCaptureAvailable: false,
|
||||
screenshotAvailable: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
this.connected.set(false);
|
||||
this.source = options.source || '';
|
||||
this.supportRequestDetails = options.supportRequestDetails;
|
||||
this.archivedScreenshotHandle = options.screenshotHandle ?? null;
|
||||
}
|
||||
|
||||
archivedScreenshotHandle: string | null;
|
||||
|
||||
displayTitle(): string {
|
||||
return `${this.title} ${this.source ? '(Imported)' : '(Offline)'}`;
|
||||
}
|
||||
|
||||
supportRequestDetails?: object;
|
||||
|
||||
getArchivedScreenshotHandle(): string | null {
|
||||
return this.archivedScreenshotHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async startLogging() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async stopLogging() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
378
desktop/flipper-frontend-core/src/devices/BaseDevice.tsx
Normal file
378
desktop/flipper-frontend-core/src/devices/BaseDevice.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
12
desktop/flipper-frontend-core/src/fb-stubs/constants.tsx
Normal file
12
desktop/flipper-frontend-core/src/fb-stubs/constants.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export default Object.freeze({
|
||||
IS_PUBLIC_BUILD: true,
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 {assertNever, DownloadFileUpdate} from 'flipper-common';
|
||||
import {FlipperLib, DownloadFileResponse} from 'flipper-plugin';
|
||||
import {RenderHost} from '../RenderHost';
|
||||
|
||||
export const downloadFileFactory =
|
||||
(renderHost: RenderHost): FlipperLib['remoteServerContext']['downloadFile'] =>
|
||||
async (url, dest, {onProgressUpdate, ...options} = {}) => {
|
||||
const downloadDescriptor = (await renderHost.flipperServer.exec(
|
||||
'download-file-start',
|
||||
url,
|
||||
dest,
|
||||
options,
|
||||
// Casting to DownloadFileResponse to add `completed` field to `downloadDescriptor`.
|
||||
)) as DownloadFileResponse;
|
||||
|
||||
let onProgressUpdateWrapped: (progressUpdate: DownloadFileUpdate) => void;
|
||||
const completed = new Promise<number>((resolve, reject) => {
|
||||
onProgressUpdateWrapped = (progressUpdate: DownloadFileUpdate) => {
|
||||
if (progressUpdate.id === downloadDescriptor.id) {
|
||||
const {status} = progressUpdate;
|
||||
switch (status) {
|
||||
case 'downloading': {
|
||||
onProgressUpdate?.(progressUpdate);
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
resolve(progressUpdate.downloaded);
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
reject(
|
||||
new Error(
|
||||
`File download failed. Last message: ${JSON.stringify(
|
||||
progressUpdate,
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertNever(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
renderHost.flipperServer.on(
|
||||
'download-file-update',
|
||||
onProgressUpdateWrapped,
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
completed.finally(() => {
|
||||
renderHost.flipperServer.off(
|
||||
'download-file-update',
|
||||
onProgressUpdateWrapped,
|
||||
);
|
||||
});
|
||||
|
||||
downloadDescriptor.completed = completed;
|
||||
|
||||
return downloadDescriptor;
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 {RemoteServerContext, FlipperLib} from 'flipper-plugin';
|
||||
import {
|
||||
BufferEncoding,
|
||||
ExecOptions,
|
||||
fsConstants,
|
||||
Logger,
|
||||
MkdirOptions,
|
||||
RmOptions,
|
||||
} from 'flipper-common';
|
||||
import constants from '../fb-stubs/constants';
|
||||
import {RenderHost} from '../RenderHost';
|
||||
import {downloadFileFactory} from './downloadFile';
|
||||
import {Base64} from 'js-base64';
|
||||
|
||||
export function baseFlipperLibImplementation(
|
||||
renderHost: RenderHost,
|
||||
logger: Logger,
|
||||
): Omit<
|
||||
FlipperLib,
|
||||
'enableMenuEntries' | 'selectPlugin' | 'showNotification' | 'createPaste'
|
||||
> {
|
||||
return {
|
||||
isFB: !constants.IS_PUBLIC_BUILD,
|
||||
logger,
|
||||
GK: renderHost.GK,
|
||||
writeTextToClipboard: renderHost.writeTextToClipboard,
|
||||
openLink: renderHost.openLink,
|
||||
importFile: renderHost.importFile,
|
||||
exportFile: renderHost.exportFile,
|
||||
paths: {
|
||||
appPath: renderHost.serverConfig.paths.appPath,
|
||||
homePath: renderHost.serverConfig.paths.homePath,
|
||||
staticPath: renderHost.serverConfig.paths.staticPath,
|
||||
tempPath: renderHost.serverConfig.paths.tempPath,
|
||||
},
|
||||
environmentInfo: {
|
||||
os: renderHost.serverConfig.environmentInfo.os,
|
||||
},
|
||||
remoteServerContext: {
|
||||
childProcess: {
|
||||
exec: async (
|
||||
command: string,
|
||||
options?: ExecOptions & {encoding?: BufferEncoding},
|
||||
) => renderHost.flipperServer.exec('node-api-exec', command, options),
|
||||
},
|
||||
fs: {
|
||||
access: async (path: string, mode?: number) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-access', path, mode),
|
||||
pathExists: async (path: string, mode?: number) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-pathExists', path, mode),
|
||||
unlink: async (path: string) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-unlink', path),
|
||||
mkdir: (async (
|
||||
path: string,
|
||||
options?: {recursive?: boolean} & MkdirOptions,
|
||||
) =>
|
||||
renderHost.flipperServer.exec(
|
||||
'node-api-fs-mkdir',
|
||||
path,
|
||||
options,
|
||||
)) as RemoteServerContext['fs']['mkdir'],
|
||||
rm: async (path: string, options?: RmOptions) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-rm', path, options),
|
||||
copyFile: async (src: string, dest: string, flags?: number) =>
|
||||
renderHost.flipperServer.exec(
|
||||
'node-api-fs-copyFile',
|
||||
src,
|
||||
dest,
|
||||
flags,
|
||||
),
|
||||
constants: fsConstants,
|
||||
stat: async (path: string) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-stat', path),
|
||||
readlink: async (path: string) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-readlink', path),
|
||||
readFile: (path, options) =>
|
||||
renderHost.flipperServer.exec('node-api-fs-readfile', path, options),
|
||||
readFileBinary: async (path) =>
|
||||
Base64.toUint8Array(
|
||||
await renderHost.flipperServer.exec(
|
||||
'node-api-fs-readfile-binary',
|
||||
path,
|
||||
),
|
||||
),
|
||||
writeFile: (path, contents, options) =>
|
||||
renderHost.flipperServer.exec(
|
||||
'node-api-fs-writefile',
|
||||
path,
|
||||
contents,
|
||||
options,
|
||||
),
|
||||
writeFileBinary: async (path, contents) => {
|
||||
const base64contents = Base64.fromUint8Array(contents);
|
||||
return await renderHost.flipperServer.exec(
|
||||
'node-api-fs-writefile-binary',
|
||||
path,
|
||||
base64contents,
|
||||
);
|
||||
},
|
||||
},
|
||||
downloadFile: downloadFileFactory(renderHost),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 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 {
|
||||
ExecuteMessage,
|
||||
FlipperServer,
|
||||
ServerAddOnControls,
|
||||
deserializeRemoteError,
|
||||
} from 'flipper-common';
|
||||
|
||||
type PluginName = string;
|
||||
type Method = string;
|
||||
|
||||
export const createServerAddOnControls = (
|
||||
flipperServer: FlipperServer,
|
||||
): ServerAddOnControls => {
|
||||
const methodHandlers = new Map<
|
||||
PluginName,
|
||||
Map<Method, (data: unknown) => void>
|
||||
>();
|
||||
const catchAllHandlers = new Map<
|
||||
PluginName,
|
||||
(method: string, data: unknown) => void
|
||||
>();
|
||||
|
||||
let subscribed = false;
|
||||
const subscriptionCb = ({params}: ExecuteMessage) => {
|
||||
const pluginName = params.api;
|
||||
|
||||
const methodHandler = methodHandlers.get(pluginName)?.get(params.method);
|
||||
|
||||
if (methodHandler) {
|
||||
methodHandler(params.params);
|
||||
return;
|
||||
}
|
||||
|
||||
const catchAllHandler = catchAllHandlers.get(pluginName);
|
||||
catchAllHandler?.(params.method, params.params);
|
||||
};
|
||||
|
||||
return {
|
||||
start: (pluginName, details, owner) =>
|
||||
flipperServer.exec(
|
||||
'plugins-server-add-on-start',
|
||||
pluginName,
|
||||
details,
|
||||
owner,
|
||||
),
|
||||
stop: (pluginName, owner) =>
|
||||
flipperServer.exec('plugins-server-add-on-stop', pluginName, owner),
|
||||
sendMessage: async (pluginName, method, params) => {
|
||||
const res = await flipperServer.exec(
|
||||
'plugins-server-add-on-request-response',
|
||||
{
|
||||
method: 'execute',
|
||||
params: {
|
||||
method,
|
||||
api: pluginName,
|
||||
params,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.error) {
|
||||
throw deserializeRemoteError(res.error);
|
||||
}
|
||||
|
||||
return res.success;
|
||||
},
|
||||
receiveMessage: (pluginName, method, receiver) => {
|
||||
if (!methodHandlers.has(pluginName)) {
|
||||
methodHandlers.set(pluginName, new Map());
|
||||
}
|
||||
methodHandlers.get(pluginName)!.set(method, receiver);
|
||||
|
||||
// Subscribe client/device to messages from flipper server only when the first plugin subscribes to them
|
||||
if (!subscribed) {
|
||||
subscribed = true;
|
||||
flipperServer.on('plugins-server-add-on-message', subscriptionCb);
|
||||
}
|
||||
},
|
||||
receiveAnyMessage: (pluginName, receiver) => {
|
||||
catchAllHandlers.set(pluginName, receiver);
|
||||
},
|
||||
unsubscribePlugin: (pluginName) => {
|
||||
methodHandlers.delete(pluginName);
|
||||
catchAllHandlers.delete(pluginName);
|
||||
},
|
||||
unsubscribe: () => {
|
||||
flipperServer.off('plugins-server-add-on-message', subscriptionCb);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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 {PluginDetails} from 'flipper-common';
|
||||
import semver from 'semver';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
export function isPluginCompatible(
|
||||
plugin: PluginDetails,
|
||||
flipperVersion: string,
|
||||
) {
|
||||
return (
|
||||
getRenderHostInstance().GK('flipper_disable_plugin_compatibility_checks') ||
|
||||
flipperVersion === '0.0.0' ||
|
||||
!plugin.engines?.flipper ||
|
||||
semver.lte(plugin.engines?.flipper, flipperVersion)
|
||||
);
|
||||
}
|
||||
|
||||
export default isPluginCompatible;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 {ConcretePluginDetails} from 'flipper-common';
|
||||
import semver from 'semver';
|
||||
import isPluginCompatible from './isPluginCompatible';
|
||||
|
||||
export function isPluginVersionMoreRecent(
|
||||
versionDetails: ConcretePluginDetails,
|
||||
otherVersionDetails: ConcretePluginDetails,
|
||||
flipperVersion: string,
|
||||
) {
|
||||
const isPlugin1Compatible = isPluginCompatible(
|
||||
versionDetails,
|
||||
flipperVersion,
|
||||
);
|
||||
const isPlugin2Compatible = isPluginCompatible(
|
||||
otherVersionDetails,
|
||||
flipperVersion,
|
||||
);
|
||||
|
||||
// prefer compatible plugins
|
||||
if (isPlugin1Compatible && !isPlugin2Compatible) return true;
|
||||
if (!isPlugin1Compatible && isPlugin2Compatible) return false;
|
||||
|
||||
// prefer plugins with greater version
|
||||
if (semver.gt(versionDetails.version, otherVersionDetails.version)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
semver.eq(versionDetails.version, otherVersionDetails.version) &&
|
||||
versionDetails.isBundled
|
||||
) {
|
||||
// prefer bundled versions
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
semver.eq(versionDetails.version, otherVersionDetails.version) &&
|
||||
versionDetails.isActivatable &&
|
||||
!otherVersionDetails.isActivatable
|
||||
) {
|
||||
// prefer locally available versions to the versions available remotely on marketplace
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default isPluginVersionMoreRecent;
|
||||
27
desktop/flipper-frontend-core/src/utils/pluginKey.tsx
Normal file
27
desktop/flipper-frontend-core/src/utils/pluginKey.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
export function getPluginKey(
|
||||
selectedAppId: string | null | undefined,
|
||||
baseDevice: {serial: string} | null | undefined,
|
||||
pluginID: string,
|
||||
): string {
|
||||
if (selectedAppId) {
|
||||
return `${selectedAppId}#${pluginID}`;
|
||||
}
|
||||
if (baseDevice) {
|
||||
// If selected App is not defined, then the plugin is a device plugin
|
||||
return `${baseDevice.serial}#${pluginID}`;
|
||||
}
|
||||
return `unknown#${pluginID}`;
|
||||
}
|
||||
|
||||
export const pluginKey = (serial: string, pluginName: string): string => {
|
||||
return `${serial}#${pluginName}`;
|
||||
};
|
||||
Reference in New Issue
Block a user