From e458ae76f980ea22eb21d6e64760e95e8ee8f6d3 Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Fri, 10 Dec 2021 06:34:37 -0800 Subject: [PATCH] Add exec Node API to FlipperLib Summary: Changelog: Allow flipper plugins to run "exec" Node API on Flipper server. Reviewed By: mweststrate Differential Revision: D32881149 fbshipit-source-id: 46486a47ee9824ca68897c19fd86b4afc7f8bf1d --- desktop/flipper-common/src/server-types.tsx | 39 +++++++++++++++++++ .../flipper-plugin/src/__tests__/api.node.tsx | 1 + desktop/flipper-plugin/src/index.ts | 1 + .../flipper-plugin/src/plugin/FlipperLib.tsx | 14 +++++++ .../src/test-utils/test-utils.tsx | 6 +++ .../src/FlipperServerImpl.tsx | 4 +- .../src/commands/NodeApiExec.tsx | 39 +++++++++++++++++++ .../flipper-server/src/startSocketServer.tsx | 1 + .../src/flipperServerConnection.tsx | 1 + .../src/utils/flipperLibImplementation.tsx | 18 ++++++++- 10 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 desktop/flipper-server-core/src/commands/NodeApiExec.tsx diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index dfe37ba61..d29d07479 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -132,6 +132,13 @@ export type IOSDeviceParams = { }; export type FlipperServerCommands = { + /** + * @throws ExecError + */ + 'node-api-exec': ( + command: string, + options?: ExecOptions & {encoding?: BufferEncoding}, + ) => Promise>; 'get-config': () => Promise; 'get-changelog': () => Promise; 'device-list': () => Promise; @@ -270,6 +277,38 @@ type ENVIRONMENT_PATHS = | 'tempPath' | 'desktopPath'; +export interface ExecOptions { + maxBuffer?: number; + timeout?: number; +} + +export interface ExecError { + message: string; + stdout: string; + stderr: string; + stack?: string; + cmd?: string; + killed?: boolean; + code?: number; +} + +export interface ExecOut { + stdout: StdOutErrType; + stderr: StdOutErrType; +} +export type BufferEncoding = + | 'ascii' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'ucs2' + | 'ucs-2' + | 'base64' + | 'base64url' + | 'latin1' + | 'binary' + | 'hex'; + export type FlipperServerConfig = { gatekeepers: Record; env: Partial>; diff --git a/desktop/flipper-plugin/src/__tests__/api.node.tsx b/desktop/flipper-plugin/src/__tests__/api.node.tsx index 64d7c5b68..bf03a3c5b 100644 --- a/desktop/flipper-plugin/src/__tests__/api.node.tsx +++ b/desktop/flipper-plugin/src/__tests__/api.node.tsx @@ -112,6 +112,7 @@ test('Correct top level API exposed', () => { "NormalizedMenuEntry", "Notification", "PluginClient", + "RemoteNodeAPI", ] `); }); diff --git a/desktop/flipper-plugin/src/index.ts b/desktop/flipper-plugin/src/index.ts index 5752dd7f1..d8a083d36 100644 --- a/desktop/flipper-plugin/src/index.ts +++ b/desktop/flipper-plugin/src/index.ts @@ -39,6 +39,7 @@ export { setFlipperLibImplementation as _setFlipperLibImplementation, FileDescriptor, FileEncoding, + RemoteNodeAPI, } from './plugin/FlipperLib'; export { MenuEntry, diff --git a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx index e9116d75e..0be2da53b 100644 --- a/desktop/flipper-plugin/src/plugin/FlipperLib.tsx +++ b/desktop/flipper-plugin/src/plugin/FlipperLib.tsx @@ -13,6 +13,7 @@ import {NormalizedMenuEntry} from './MenuEntry'; import {RealFlipperClient} from './Plugin'; import {Notification} from './Notification'; import {DetailSidebarProps} from '../ui/DetailSidebar'; +import {ExecOptions, ExecOut, BufferEncoding} from 'flipper-common'; export type FileEncoding = 'utf-8' | 'base64'; @@ -22,6 +23,18 @@ export interface FileDescriptor { path?: string; } +export type RemoteNodeAPI = { + childProcess: { + exec( + command: string, + options?: {encoding?: BufferEncoding} & ExecOptions, + ): Promise>; + }; + fs: { + // TODO: Fill me + }; +}; + /** * This interface exposes all global methods for which an implementation will be provided by Flipper itself */ @@ -105,6 +118,7 @@ export interface FlipperLib { homePath: string; appPath: string; }; + removeNodeAPI: RemoteNodeAPI; } export let flipperLibInstance: FlipperLib | undefined; diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index 83fbfa6a0..b81ee33b7 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -389,6 +389,12 @@ export function createMockFlipperLib(options?: StartPluginOptions): FlipperLib { appPath: process.cwd(), homePath: `/dev/null`, }, + removeNodeAPI: { + childProcess: { + exec: jest.fn(), + }, + fs: {}, + }, }; } diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index a21119631..7f049dbb6 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -8,7 +8,6 @@ */ import EventEmitter from 'events'; -import {Logger} from 'flipper-common'; import ServerController from './comms/ServerController'; import {CertificateExchangeMedium} from './utils/CertificateProvider'; import {AndroidDeviceManager} from './devices/android/androidDeviceManager'; @@ -25,6 +24,7 @@ import { FlipperServer, UninitializedClient, FlipperServerConfig, + Logger, } from 'flipper-common'; import {ServerDevice} from './devices/ServerDevice'; import {Base64} from 'js-base64'; @@ -43,6 +43,7 @@ import { internGraphGETAPIRequest, internGraphPOSTAPIRequest, } from './fb-stubs/internRequests'; +import {commandNodeApiExec} from './commands/NodeApiExec'; export const SERVICE_FLIPPER = 'flipper.oAuthToken'; @@ -213,6 +214,7 @@ export class FlipperServerImpl implements FlipperServer { } private commandHandler: FlipperServerCommands = { + 'node-api-exec': commandNodeApiExec, 'get-config': async () => this.config, 'get-changelog': getChangelog, 'device-list': async () => { diff --git a/desktop/flipper-server-core/src/commands/NodeApiExec.tsx b/desktop/flipper-server-core/src/commands/NodeApiExec.tsx new file mode 100644 index 000000000..14fb4aca0 --- /dev/null +++ b/desktop/flipper-server-core/src/commands/NodeApiExec.tsx @@ -0,0 +1,39 @@ +/** + * 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 {ExecError, FlipperServerCommands} from 'flipper-common'; +import {exec} from 'child_process'; +import assert from 'assert'; + +export const commandNodeApiExec: FlipperServerCommands['node-api-exec'] = + async (command, options) => + new Promise((resolve, reject) => + exec(command, options, (error, stdout, stderr) => { + assert(typeof stdout === 'string'); + assert(typeof stderr === 'string'); + if (error) { + const wrappedError: ExecError = { + message: error.message, + stdout, + stderr, + cmd: error.cmd, + killed: error.killed, + code: error.code, + stack: error.stack, + }; + reject(wrappedError); + return; + } + + resolve({ + stdout, + stderr, + }); + }), + ); diff --git a/desktop/flipper-server/src/startSocketServer.tsx b/desktop/flipper-server/src/startSocketServer.tsx index 1aa60a299..504a6f4ae 100644 --- a/desktop/flipper-server/src/startSocketServer.tsx +++ b/desktop/flipper-server/src/startSocketServer.tsx @@ -36,6 +36,7 @@ export function startSocketServer( }) .catch((error: any) => { if (connected) { + // TODO: Serialize error client.emit('exec-response-error', id, error.toString()); } }); diff --git a/desktop/flipper-ui-browser/src/flipperServerConnection.tsx b/desktop/flipper-ui-browser/src/flipperServerConnection.tsx index 91106a3da..e3af1d1e2 100644 --- a/desktop/flipper-ui-browser/src/flipperServerConnection.tsx +++ b/desktop/flipper-ui-browser/src/flipperServerConnection.tsx @@ -69,6 +69,7 @@ export function createFlipperServer(): Promise { }); socket.on('exec-response-error', (id: number, error: any) => { + // TODO: Deserialize error console.debug('exec <<< [SERVER ERROR]', id, error); const entry = pendingRequests.get(id); if (!entry) { diff --git a/desktop/flipper-ui-core/src/utils/flipperLibImplementation.tsx b/desktop/flipper-ui-core/src/utils/flipperLibImplementation.tsx index 702142732..ea9dea2ca 100644 --- a/desktop/flipper-ui-core/src/utils/flipperLibImplementation.tsx +++ b/desktop/flipper-ui-core/src/utils/flipperLibImplementation.tsx @@ -7,8 +7,8 @@ * @format */ -import {_setFlipperLibImplementation} from 'flipper-plugin'; -import type {Logger} from 'flipper-common'; +import {_setFlipperLibImplementation, RemoteNodeAPI} from 'flipper-plugin'; +import type {BufferEncoding, ExecOptions, Logger} from 'flipper-common'; import type {Store} from '../reducers'; import createPaste from '../fb-stubs/createPaste'; import type BaseDevice from '../devices/BaseDevice'; @@ -63,5 +63,19 @@ export function initializeFlipperLibImplementation( appPath: renderHost.serverConfig.paths.appPath, homePath: renderHost.serverConfig.paths.homePath, }, + removeNodeAPI: { + childProcess: { + exec: (async ( + command: string, + options?: ExecOptions & {encoding?: BufferEncoding}, + ) => + renderHost.flipperServer.exec( + 'node-api-exec', + command, + options, + )) as RemoteNodeAPI['childProcess']['exec'], + }, + fs: {}, + }, }); }