Back out "Back out "[flipper][js] JS apps support 1/n""

Summary:
Original commit changeset: ff84080d43fa

This re-adds JS client support. The original version had a small bug that inadvertantly wrapped support for Android emulators in the dropdown in a GK that was only meant to cover JS clients. This is addressed here.

Reviewed By: timur-valiev

Differential Revision: D18707485

fbshipit-source-id: ceea8e279a21111f96073f8b784e852f6313e2a4
This commit is contained in:
Pascal Hartig
2019-11-26 08:07:29 -08:00
committed by Facebook Github Bot
parent e99f2bcd8b
commit 0a7c57f5a0
10 changed files with 499 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ import ShareSheetExportUrl from './chrome/ShareSheetExportUrl';
import SignInSheet from './chrome/SignInSheet'; import SignInSheet from './chrome/SignInSheet';
import ExportDataPluginSheet from './chrome/ExportDataPluginSheet'; import ExportDataPluginSheet from './chrome/ExportDataPluginSheet';
import ShareSheetExportFile from './chrome/ShareSheetExportFile'; import ShareSheetExportFile from './chrome/ShareSheetExportFile';
import JSEmulatorLauncherSheet from './chrome/JSEmulatorLauncherSheet';
import PluginContainer from './PluginContainer'; import PluginContainer from './PluginContainer';
import Sheet from './chrome/Sheet'; import Sheet from './chrome/Sheet';
import {ipcRenderer, remote} from 'electron'; import {ipcRenderer, remote} from 'electron';
@@ -34,6 +35,7 @@ import {
ACTIVE_SHEET_SHARE_DATA_IN_FILE, ACTIVE_SHEET_SHARE_DATA_IN_FILE,
ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT, ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT,
ACTIVE_SHEET_PLUGIN_SHEET, ACTIVE_SHEET_PLUGIN_SHEET,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
} from './reducers/application'; } from './reducers/application';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import BugReporter from './fb-stubs/BugReporter'; import BugReporter from './fb-stubs/BugReporter';
@@ -122,6 +124,8 @@ export class App extends React.Component<Props> {
case ACTIVE_SHEET_PLUGIN_SHEET: case ACTIVE_SHEET_PLUGIN_SHEET:
// Currently unused. // Currently unused.
return null; return null;
case ACTIVE_SHEET_JS_EMULATOR_LAUNCHER:
return <JSEmulatorLauncherSheet onHide={onHide} />;
default: default:
return null; return null;
} }

View File

@@ -13,7 +13,8 @@ import {App} from './App.js';
import {Logger} from './fb-interfaces/Logger'; import {Logger} from './fb-interfaces/Logger';
import {Store} from './reducers/index'; import {Store} from './reducers/index';
import {setPluginState} from './reducers/pluginStates'; import {setPluginState} from './reducers/pluginStates';
import {ReactiveSocket} from 'rsocket-types'; import {Payload, ConnectionStatus} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
import {reportPlatformFailures, reportPluginFailures} from './utils/metrics'; import {reportPlatformFailures, reportPluginFailures} from './utils/metrics';
import {notNull} from './utils/typeUtils'; import {notNull} from './utils/typeUtils';
@@ -97,6 +98,13 @@ const handleError = (
} }
}; };
export interface FlipperClientConnection<D, M> {
connectionStatus(): Flowable<ConnectionStatus>;
close(): void;
fireAndForget(payload: Payload<D, M>): void;
requestResponse(payload: Payload<D, M>): Single<Payload<D, M>>;
}
export default class Client extends EventEmitter { export default class Client extends EventEmitter {
app: App | undefined; app: App | undefined;
connected: boolean; connected: boolean;
@@ -105,7 +113,7 @@ export default class Client extends EventEmitter {
sdkVersion: number; sdkVersion: number;
messageIdCounter: number; messageIdCounter: number;
plugins: Plugins; plugins: Plugins;
connection: ReactiveSocket<any, any> | null | undefined; connection: FlipperClientConnection<any, any> | null | undefined;
store: Store; store: Store;
activePlugins: Set<string>; activePlugins: Set<string>;
device: Promise<BaseDevice>; device: Promise<BaseDevice>;
@@ -129,7 +137,7 @@ export default class Client extends EventEmitter {
constructor( constructor(
id: string, id: string,
query: ClientQuery, query: ClientQuery,
conn: ReactiveSocket<any, any> | null | undefined, conn: FlipperClientConnection<any, any> | null | undefined,
logger: Logger, logger: Logger,
store: Store, store: Store,
plugins?: Plugins | null | undefined, plugins?: Plugins | null | undefined,

View File

@@ -12,11 +12,17 @@ import {connect, ReactReduxContext} from 'react-redux';
import {spawn} from 'child_process'; import {spawn} from 'child_process';
import {dirname} from 'path'; import {dirname} from 'path';
import {selectDevice, preferDevice} from '../reducers/connections'; import {selectDevice, preferDevice} from '../reducers/connections';
import {
setActiveSheet,
ActiveSheet,
ACTIVE_SHEET_JS_EMULATOR_LAUNCHER,
} from '../reducers/application';
import {default as which} from 'which'; import {default as which} from 'which';
import {showOpenDialog} from '../utils/exportData'; import {showOpenDialog} from '../utils/exportData';
import BaseDevice from '../devices/BaseDevice'; import BaseDevice from '../devices/BaseDevice';
import React, {Component} from 'react'; import React, {Component} from 'react';
import {State} from '../reducers'; import {State} from '../reducers';
import GK from '../fb-stubs/GK';
type StateFromProps = { type StateFromProps = {
selectedDevice: BaseDevice | null | undefined; selectedDevice: BaseDevice | null | undefined;
@@ -27,6 +33,7 @@ type StateFromProps = {
type DispatchFromProps = { type DispatchFromProps = {
selectDevice: (device: BaseDevice) => void; selectDevice: (device: BaseDevice) => void;
preferDevice: (device: string) => void; preferDevice: (device: string) => void;
setActiveSheet: (sheet: ActiveSheet) => void;
}; };
type OwnProps = {}; type OwnProps = {};
@@ -154,6 +161,19 @@ class DevicesButton extends Component<Props> {
label: name, label: name,
click: () => this.launchEmulator(name), click: () => this.launchEmulator(name),
})); }));
// Launch JS emulator
if (GK.get('flipper_js_client_emulator')) {
dropdown.push(
{type: 'separator' as 'separator'},
{
label: 'Launch JS Web App',
click: () =>
this.props.setActiveSheet(ACTIVE_SHEET_JS_EMULATOR_LAUNCHER),
},
);
}
if (emulators.length > 0) { if (emulators.length > 0) {
dropdown.push( dropdown.push(
{type: 'separator' as 'separator'}, {type: 'separator' as 'separator'},
@@ -165,6 +185,7 @@ class DevicesButton extends Component<Props> {
); );
} }
} }
if (dropdown.length > 0) { if (dropdown.length > 0) {
dropdown.push({type: 'separator' as 'separator'}); dropdown.push({type: 'separator' as 'separator'});
} }
@@ -196,5 +217,6 @@ export default connect<StateFromProps, DispatchFromProps, OwnProps, State>(
{ {
selectDevice, selectDevice,
preferDevice, preferDevice,
setActiveSheet,
}, },
)(DevicesButton); )(DevicesButton);

View File

@@ -0,0 +1,122 @@
/**
* 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 {
FlexColumn,
Button,
styled,
Text,
FlexRow,
Spacer,
Input,
Label,
} from 'flipper';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {State as Store} from '../reducers';
import {launchJsEmulator} from '../utils/js-client/serverUtils';
const Container = styled(FlexColumn)({
padding: 20,
width: 800,
});
const Title = styled(Text)({
marginBottom: 18,
marginRight: 10,
fontWeight: 100,
fontSize: '40px',
});
const textareaStyle = {
margin: 0,
marginBottom: 10,
};
const TitleInput = styled(Input)({
...textareaStyle,
height: 30,
});
type OwnProps = {
onHide: () => void;
};
type StateFromProps = {};
type DispatchFromProps = {};
type State = {
url: string;
width: number;
height: number;
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class JSEmulatorLauncherSheet extends Component<Props, State> {
state: State = {
url: 'http://localhost:8888',
width: 800,
height: 600,
};
onUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({url: e.target.value});
};
onHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({height: Number(e.target.value)});
};
onWidthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({width: Number(e.target.value)});
};
render() {
const {url, height, width} = this.state;
return (
<Container>
<Title>Launch Web App</Title>
<Label>Url</Label>
<TitleInput value={url} onChange={this.onUrlChange} />
<Label>Height</Label>
<TitleInput value={height} onChange={this.onHeightChange} />
<Label>Width</Label>
<TitleInput value={width} onChange={this.onWidthChange} />
<br />
<FlexRow>
<Spacer />
<Button compact padded onClick={this.props.onHide}>
Cancel
</Button>
<Button
type="primary"
compact
padded
onClick={() => {
launchJsEmulator(
this.state.url,
this.state.height,
this.state.width,
);
this.props.onHide();
}}>
Launch
</Button>
</FlexRow>
</Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
() => ({}),
{},
)(JSEmulatorLauncherSheet);

View File

@@ -52,7 +52,7 @@ export type DeviceExport = {
logs: Array<DeviceLogEntry>; logs: Array<DeviceLogEntry>;
}; };
export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS'; export type OS = 'iOS' | 'Android' | 'Windows' | 'MacOS' | 'JSWebApp';
export default class BaseDevice { export default class BaseDevice {
constructor(serial: string, deviceType: DeviceType, title: string, os: OS) { constructor(serial: string, deviceType: DeviceType, title: string, os: OS) {

20
src/devices/JSDevice.tsx Normal file
View File

@@ -0,0 +1,20 @@
/**
* 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 BaseDevice from './BaseDevice';
export default class JSDevice extends BaseDevice {
webContentsId: number;
constructor(serial: string, title: string, webContentsId: number) {
super(serial, 'emulator', title, 'JSWebApp');
this.devicePlugins = [];
this.webContentsId = webContentsId;
}
}

View File

@@ -26,6 +26,8 @@ export const ACTIVE_SHEET_SHARE_DATA_IN_FILE: 'SHARE_DATA_IN_FILE' =
export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' = export const SET_EXPORT_STATUS_MESSAGE: 'SET_EXPORT_STATUS_MESSAGE' =
'SET_EXPORT_STATUS_MESSAGE'; 'SET_EXPORT_STATUS_MESSAGE';
export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE'; export const UNSET_SHARE: 'UNSET_SHARE' = 'UNSET_SHARE';
export const ACTIVE_SHEET_JS_EMULATOR_LAUNCHER: 'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER' =
'ACTIVE_SHEET_JS_EMULATOR_LAUNCHER';
export type ActiveSheet = export type ActiveSheet =
| typeof ACTIVE_SHEET_PLUGIN_SHEET | typeof ACTIVE_SHEET_PLUGIN_SHEET
@@ -37,6 +39,7 @@ export type ActiveSheet =
| typeof ACTIVE_SHEET_DOCTOR | typeof ACTIVE_SHEET_DOCTOR
| typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE | typeof ACTIVE_SHEET_SHARE_DATA_IN_FILE
| typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT | typeof ACTIVE_SHEET_SELECT_PLUGINS_TO_EXPORT
| typeof ACTIVE_SHEET_JS_EMULATOR_LAUNCHER
| null; | null;
export type LauncherMsg = { export type LauncherMsg = {

View File

@@ -16,6 +16,7 @@ import {RSocketServer} from 'rsocket-core';
import RSocketTCPServer from 'rsocket-tcp-server'; import RSocketTCPServer from 'rsocket-tcp-server';
import {Single} from 'rsocket-flowable'; import {Single} from 'rsocket-flowable';
import Client from './Client'; import Client from './Client';
import {FlipperClientConnection} from './Client';
import {UninitializedClient} from './UninitializedClient'; import {UninitializedClient} from './UninitializedClient';
import {reportPlatformFailures} from './utils/metrics'; import {reportPlatformFailures} from './utils/metrics';
import EventEmitter from 'events'; import EventEmitter from 'events';
@@ -23,9 +24,11 @@ import invariant from 'invariant';
import tls from 'tls'; import tls from 'tls';
import net, {Socket} from 'net'; import net, {Socket} from 'net';
import {Responder, Payload, ReactiveSocket} from 'rsocket-types'; import {Responder, Payload, ReactiveSocket} from 'rsocket-types';
import GK from './fb-stubs/GK';
import {initJsEmulatorIPC} from './utils/js-client/serverUtils';
type ClientInfo = { type ClientInfo = {
connection: ReactiveSocket<any, any> | null | undefined; connection: FlipperClientConnection<any, any> | null | undefined;
client: Client; client: Client;
}; };
@@ -83,6 +86,11 @@ class Server extends EventEmitter {
return; return;
}); });
reportPlatformFailures(this.initialisePromise, 'initializeServer'); reportPlatformFailures(this.initialisePromise, 'initializeServer');
if (GK.get('flipper_js_client_emulator')) {
initJsEmulatorIPC(this.store, this.logger, this, this.connections);
}
return this.initialisePromise; return this.initialisePromise;
} }
@@ -303,7 +311,7 @@ class Server extends EventEmitter {
} }
async addConnection( async addConnection(
conn: ReactiveSocket<any, any>, conn: FlipperClientConnection<any, any>,
query: ClientQuery, query: ClientQuery,
csrQuery: ClientCsrQuery, csrQuery: ClientCsrQuery,
): Promise<Client> { ): Promise<Client> {

View File

@@ -0,0 +1,216 @@
/**
* 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 Client, {ClientQuery} from '../../Client';
import {FlipperClientConnection} from '../../Client';
import {ipcRenderer, remote} from 'electron';
import JSDevice from '../../devices/JSDevice';
import {Store} from 'src/reducers';
import {Logger} from 'src/fb-interfaces/Logger';
import {Payload, ConnectionStatus, ISubscriber} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable';
import Server from 'src/server';
const connections: Map<number, JSClientFlipperConnection<any>> = new Map();
const availablePlugins: Map<number, Array<string>> = new Map();
function jsDeviceId(windowId: number): string {
return 'test_js_device' + windowId;
}
export function initJsEmulatorIPC(
store: Store,
logger: Logger,
flipperServer: Server,
flipperConnections: Map<
string,
{
connection: FlipperClientConnection<any, any> | null | undefined;
client: Client;
}
>,
) {
ipcRenderer.on(
'from-js-emulator-init-client',
(_event: string, message: any) => {
const {windowId} = message;
const {plugins, appName} = message.payload;
store.dispatch({
type: 'REGISTER_DEVICE',
payload: new JSDevice(jsDeviceId(windowId), 'jsEmulator', windowId),
});
const connection = new JSClientFlipperConnection(windowId);
connections.set(windowId, connection);
availablePlugins.set(windowId, plugins);
const query: ClientQuery = {
app: appName,
os: 'JSWebApp',
device: 'jsEmulator',
device_id: jsDeviceId(windowId),
sdk_version: 2, // hack to bybass callbacks in Client, will be fixed when JS Connection will be fully implemented
};
const clientId = `${query.app}#${query.os}#${query.device}#${query.device_id}`;
const client = new Client(
clientId,
query,
connection,
logger,
store,
plugins,
);
flipperConnections.set(clientId, {
connection: connection,
client: client,
});
connection.connectionStatus().subscribe({
onNext(payload) {
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
console.debug(`Device disconnected ${client.id}`, 'server');
flipperServer.removeConnection(client.id);
const toUnregister = new Set<string>();
toUnregister.add(jsDeviceId(windowId));
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: toUnregister,
});
connections.delete(windowId);
availablePlugins.delete(windowId);
}
},
onSubscribe(subscription) {
subscription.request(Number.MAX_SAFE_INTEGER);
},
});
client.init().then(() => {
console.log(client);
flipperServer.emit('new-client', client);
flipperServer.emit('clients-change');
client.emit('plugins-change');
ipcRenderer.on('from-js-emulator', (_event: string, message: any) => {
const {command, payload} = message;
if (command === 'sendFlipperObject') {
client.onMessage(
JSON.stringify({
params: {
api: payload.api,
method: payload.method,
params: JSON.parse(payload.params),
},
method: 'execute',
}),
);
}
});
});
},
);
}
export function launchJsEmulator(url: string, height: number, width: number) {
const BrowserWindow = remote.BrowserWindow;
const win = new BrowserWindow({
height: height,
width: width,
webPreferences: {
preload: require('path').join(
remote.app.getAppPath(),
'SupportJSClientPreload.js',
),
nodeIntegration: false,
contextIsolation: false,
allowRunningInsecureContent: true,
},
});
win.webContents.on('preload-error', (_event, path, error) => {
console.log(path, error);
});
win.loadURL(url);
win.webContents.on('did-finish-load', () => {
win.webContents.send('parent-window-id', remote.getCurrentWebContents().id);
const childWindowId = win.webContents.id;
win.on('closed', () => {
connections.get(childWindowId)?.close();
});
});
}
export class JSClientFlipperConnection<M>
implements FlipperClientConnection<string, M> {
webContentsId: number;
connStatusSubscribers: Set<ISubscriber<ConnectionStatus>> = new Set();
connStatus: ConnectionStatus;
constructor(webContentsId: number) {
this.webContentsId = webContentsId;
this.connStatus = {kind: 'CONNECTED'};
}
connectionStatus(): Flowable<ConnectionStatus> {
return new Flowable<ConnectionStatus>(subscriber => {
subscriber.onSubscribe({
cancel: () => {
this.connStatusSubscribers.delete(subscriber);
},
request: _ => {
this.connStatusSubscribers.add(subscriber);
subscriber.onNext(this.connStatus);
},
});
});
}
close(): void {
this.connStatus = {kind: 'CLOSED'};
this.connStatusSubscribers.forEach(subscriber => {
subscriber.onNext(this.connStatus);
});
}
fireAndForget(payload: Payload<string, M>): void {
ipcRenderer.sendTo(
this.webContentsId,
'message-to-plugin',
JSON.parse(payload.data != null ? payload.data : '{}'),
);
}
// TODO: fully implement and return actual result
requestResponse(payload: Payload<string, M>): Single<Payload<string, M>> {
return new Single(subscriber => {
const method =
payload.data != null ? JSON.parse(payload.data).method : 'not-defined';
if (method != 'getPlugins') {
this.fireAndForget(payload);
}
subscriber.onSubscribe(() => {});
subscriber.onComplete(
method == 'getPlugins'
? {
data: JSON.stringify({
success: {plugins: availablePlugins.get(this.webContentsId)},
}),
}
: {data: JSON.stringify({success: null})},
);
});
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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
*/
// ==============
// Preload script
// ==============
const {remote, ipcRenderer} = require('electron');
let FlipperMainWindowId = 0;
ipcRenderer.on('parent-window-id', (event, message) => {
FlipperMainWindowId = message;
});
let FlipperIsClientInit = false;
let FlipperMemoizedPlugins;
function initClient(plugins) {
if (FlipperIsClientInit) {
return;
}
if (plugins) {
FlipperMemoizedPlugins = plugins;
}
if (FlipperMainWindowId != 0) {
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator-init-client', {
command: 'initClient',
windowId: remote.getCurrentWebContents().id,
payload: {
plugins: plugins ? plugins : FlipperMemoizedPlugins,
appName: 'kite/weblite',
},
});
FlipperIsClientInit = true;
}
}
window.FlipperWebviewBridge = {
registerPlugins: function(plugins) {
console.log(plugins);
if (FlipperMainWindowId != 0) {
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
command: 'registerPlugins',
payload: plugins,
});
}
},
start: function() {
console.log('start');
if (FlipperMainWindowId != 0) {
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
command: 'start',
payload: null,
});
}
},
sendFlipperObject: function(plugin, method, data) {
console.log(plugin, method, data);
initClient();
if (FlipperMainWindowId != 0) {
ipcRenderer.sendTo(FlipperMainWindowId, 'from-js-emulator', {
command: 'sendFlipperObject',
payload: {
api: plugin,
method: method,
params: data,
},
});
}
},
isFlipperSupported: true,
initClient: initClient,
};
ipcRenderer.on('message-to-plugin', (event, message) => {
const flipper = window.flipper;
if (!flipper) {
return;
}
const receiver = flipper.FlipperWebviewMessageReceiver.receive;
const {api, method, params} = message.params;
receiver(api, method, JSON.stringify(params));
});