Files
flipper/src/Client.js
Pritesh Nandgaonkar fd022e3c73 Improvise UI of crash reporter plugin
Summary:
- New improved UI
- Instead of sending the callstack as a string from android, now sending it as an array
- Deeplink to Logs support just for android. In iOS crash is not automatically logged in Logs plugin, atleast thats what happens in sample app

Reviewed By: jknoxville

Differential Revision: D13216477

fbshipit-source-id: d8b77549c83572d0442e431ce88a8f01f42c9565
2018-11-30 05:28:46 -08:00

325 lines
8.5 KiB
JavaScript

/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {FlipperPlugin, FlipperBasePlugin} from './plugin.js';
import {FlipperDevicePlugin} from './plugin.js';
import type BaseDevice from './devices/BaseDevice.js';
import type {App} from './App.js';
import type Logger from './fb-stubs/Logger.js';
import type {Store} from './reducers/index.js';
import {setPluginState} from './reducers/pluginStates.js';
import {ReactiveSocket, PartialResponder} from 'rsocket-core';
const EventEmitter = (require('events'): any);
const invariant = require('invariant');
type Plugins = Array<string>;
export type ClientQuery = {|
app: string,
os: string,
device: string,
device_id: string,
|};
type RequestMetadata = {method: string, id: number, params: ?Object};
export default class Client extends EventEmitter {
constructor(
id: string,
query: ClientQuery,
conn: ReactiveSocket,
logger: Logger,
store: Store,
) {
super();
this.connected = true;
this.plugins = [];
this.connection = conn;
this.id = id;
this.query = query;
this.messageIdCounter = 0;
this.logger = logger;
this.store = store;
this.broadcastCallbacks = new Map();
this.requestCallbacks = new Map();
const client = this;
this.responder = {
fireAndForget: (payload: {data: string}) => {
client.onMessage(payload.data);
},
};
conn.connectionStatus().subscribe({
onNext(payload) {
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
client.connected = false;
}
},
onSubscribe(subscription) {
subscription.request(Number.MAX_SAFE_INTEGER);
},
});
}
getDevice = (): ?BaseDevice =>
this.store
.getState()
.connections.devices.find(
(device: BaseDevice) => device.serial === this.query.device_id,
);
on: ((event: 'plugins-change', callback: () => void) => void) &
((event: 'close', callback: () => void) => void);
app: App;
connected: boolean;
id: string;
query: ClientQuery;
messageIdCounter: number;
plugins: Plugins;
connection: ReactiveSocket;
responder: PartialResponder;
store: Store;
broadcastCallbacks: Map<?string, Map<string, Set<Function>>>;
requestCallbacks: Map<
number,
{|
resolve: (data: any) => void,
reject: (err: Error) => void,
metadata: RequestMetadata,
|},
>;
supportsPlugin(Plugin: Class<FlipperPlugin<>>): boolean {
return this.plugins.includes(Plugin.id);
}
async init() {
await this.getPlugins();
}
// get the supported plugins
async getPlugins(): Promise<Plugins> {
const plugins = await this.rawCall('getPlugins').then(data => data.plugins);
this.plugins = plugins;
return plugins;
}
// get the plugins, and update the UI
async refreshPlugins() {
await this.getPlugins();
this.emit('plugins-change');
}
onMessage(msg: string) {
if (typeof msg !== 'string') {
return;
}
let rawData;
try {
rawData = JSON.parse(msg);
} catch (err) {
console.error(`Invalid JSON: ${msg}`, 'clientMessage');
return;
}
const data: {|
id?: number,
method?: string,
params?: Object,
success?: Object,
error?: Object,
|} = rawData;
console.debug(data, 'message:receive');
const {id, method} = data;
if (id == null) {
const {error} = data;
if (error != null) {
console.error(
`Error received from device ${
method ? `when calling ${method}` : ''
}: ${error.message} + \nDevice Stack Trace: ${error.stacktrace}`,
'deviceError',
);
} else if (method === 'refreshPlugins') {
this.refreshPlugins();
} else if (method === 'execute') {
const params = data.params;
invariant(params, 'expected params');
const persistingPlugin: ?Class<FlipperBasePlugin<>> =
this.store.getState().plugins.clientPlugins.get(params.api) ||
this.store.getState().plugins.devicePlugins.get(params.api);
if (persistingPlugin && persistingPlugin.persistedStateReducer) {
let pluginKey = `${this.id}#${params.api}`;
//$FlowFixMe
if (persistingPlugin.prototype instanceof FlipperDevicePlugin) {
// For device plugins, we are just using the device id instead of client id as the prefix.
pluginKey = `${this.getDevice()?.serial || ''}#${params.api}`;
}
const persistedState = {
...persistingPlugin.defaultPersistedState,
...this.store.getState().pluginStates[pluginKey],
};
// $FlowFixMe: We checked persistedStateReducer exists
const newPluginState = persistingPlugin.persistedStateReducer(
persistedState,
params.method,
params.params,
);
if (persistedState !== newPluginState) {
this.store.dispatch(
setPluginState({
pluginKey,
state: newPluginState,
}),
);
}
} else {
const apiCallbacks = this.broadcastCallbacks.get(params.api);
if (!apiCallbacks) {
return;
}
const methodCallbacks: ?Set<Function> = apiCallbacks.get(
params.method,
);
if (methodCallbacks) {
for (const callback of methodCallbacks) {
callback(params.params);
}
}
}
}
return;
}
const callbacks = this.requestCallbacks.get(id);
if (!callbacks) {
return;
}
this.requestCallbacks.delete(id);
this.finishTimingRequestResponse(callbacks.metadata);
if (data.success) {
callbacks.resolve(data.success);
} else if (data.error) {
callbacks.reject(data.error);
} else {
// ???
}
}
toJSON() {
return `<Client#${this.id}>`;
}
subscribe(
api: ?string = null,
method: string,
callback: (params: Object) => void,
) {
let apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
apiCallbacks = new Map();
this.broadcastCallbacks.set(api, apiCallbacks);
}
let methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
methodCallbacks = new Set();
apiCallbacks.set(method, methodCallbacks);
}
methodCallbacks.add(callback);
}
unsubscribe(api: ?string = null, method: string, callback: Function) {
const apiCallbacks = this.broadcastCallbacks.get(api);
if (!apiCallbacks) {
return;
}
const methodCallbacks = apiCallbacks.get(method);
if (!methodCallbacks) {
return;
}
methodCallbacks.delete(callback);
}
rawCall(method: string, params?: Object): Promise<Object> {
return new Promise((resolve, reject) => {
const id = this.messageIdCounter++;
const metadata: RequestMetadata = {
method,
id,
params,
};
this.requestCallbacks.set(id, {reject, resolve, metadata});
const data = {
id,
method,
params,
};
console.debug(data, 'message:call');
this.startTimingRequestResponse({method, id, params});
this.connection.fireAndForget({data: JSON.stringify(data)});
});
}
startTimingRequestResponse(data: RequestMetadata) {
performance.mark(this.getPerformanceMark(data));
}
finishTimingRequestResponse(data: RequestMetadata) {
const mark = this.getPerformanceMark(data);
const logEventName = this.getLogEventName(data);
this.logger.trackTimeSince(mark, logEventName);
}
getPerformanceMark(data: RequestMetadata): string {
const {method, id} = data;
return `request_response_${method}_${id}`;
}
getLogEventName(data: RequestMetadata): string {
const {method, params} = data;
return params && params.api && params.method
? `request_response_${method}_${params.api}_${params.method}`
: `request_response_${method}`;
}
rawSend(method: string, params?: Object): void {
const data = {
method,
params,
};
console.debug(data, 'message:send');
this.connection.fireAndForget({data: JSON.stringify(data)});
}
call(api: string, method: string, params?: Object): Promise<Object> {
return this.rawCall('execute', {api, method, params});
}
send(api: string, method: string, params?: Object): void {
return this.rawSend('execute', {api, method, params});
}
}