diff --git a/src/App.js b/src/App.js index 3dabeef24..c0ba3f410 100644 --- a/src/App.js +++ b/src/App.js @@ -14,7 +14,8 @@ import SonarTitleBar from './chrome/SonarTitleBar.js'; import BaseDevice from './devices/BaseDevice.js'; import MainSidebar from './chrome/MainSidebar.js'; import {SonarBasePlugin} from './plugin.js'; -import {Server, Client} from './server.js'; +import Server from './server.js'; +import Client from './Client.js'; import * as reducers from './reducers.js'; import React from 'react'; import BugReporter from './fb-stubs/BugReporter.js'; diff --git a/src/Client.js b/src/Client.js new file mode 100644 index 000000000..09646a701 --- /dev/null +++ b/src/Client.js @@ -0,0 +1,291 @@ +/** + * 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 {SonarPlugin} from './plugin.js'; +import type {App} from './App.js'; +import type BaseDevice from './devices/BaseDevice.js'; +import plugins from './plugins/index.js'; +import {ReactiveSocket, PartialResponder} from 'rsocket-core'; + +const EventEmitter = (require('events'): any); +const invariant = require('invariant'); + +type Plugins = Array; + +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(app: App, id: string, query: ClientQuery, conn: ReactiveSocket) { + super(); + + this.connected = true; + this.plugins = []; + this.connection = conn; + this.id = id; + this.query = query; + this.messageIdCounter = 0; + this.app = app; + + 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); + }, + }); + } + + 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; + + broadcastCallbacks: Map>>; + + requestCallbacks: Map< + number, + {| + resolve: (data: any) => void, + reject: (err: Error) => void, + metadata: RequestMetadata, + |}, + >; + + getDevice(): ?BaseDevice { + const {device_id} = this.query; + + if (device_id == null) { + return null; + } else { + return this.app.getDevice(device_id); + } + } + + supportsPlugin(Plugin: Class>): boolean { + return this.plugins.includes(Plugin.id); + } + + getFirstSupportedPlugin(): ?string { + for (const Plugin of plugins) { + if (this.supportsPlugin(Plugin)) { + return Plugin.id; + } + } + } + + async init() { + await this.getPlugins(); + } + + // get the supported plugins + async getPlugins(): Promise { + 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.log(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 apiCallbacks = this.broadcastCallbacks.get(params.api); + if (!apiCallbacks) { + return; + } + + const methodCallbacks: ?Set = 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 null; + } + + 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 { + 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.log(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.app.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.log(data, 'message:send'); + this.connection.fireAndForget({data: JSON.stringify(data)}); + } + + call(api: string, method: string, params?: Object): Promise { + return this.rawCall('execute', {api, method, params}); + } + + send(api: string, method: string, params?: Object): void { + return this.rawSend('execute', {api, method, params}); + } +} diff --git a/src/chrome/MainSidebar.js b/src/chrome/MainSidebar.js index 699934f8b..e0d05a8ce 100644 --- a/src/chrome/MainSidebar.js +++ b/src/chrome/MainSidebar.js @@ -6,7 +6,7 @@ */ import type {SonarBasePlugin} from '../plugin.js'; -import type {Client} from '../server.js'; +import type Client from '../Client.js'; import { Component, diff --git a/src/plugin.js b/src/plugin.js index f9d5be19e..02a687a61 100644 --- a/src/plugin.js +++ b/src/plugin.js @@ -7,7 +7,7 @@ import type {KeyboardActions} from './MenuBar.js'; import type {App} from './App.js'; -import type {Client} from './server.js'; +import type Client from './Client.js'; import BaseDevice from './devices/BaseDevice.js'; import {AndroidDevice, IOSDevice} from 'sonar'; diff --git a/src/reducers.js b/src/reducers.js index 9fa1df651..808c17543 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -18,7 +18,7 @@ import {SonarPlugin, SonarDevicePlugin} from 'sonar'; import {PluginStateContainer} from './plugin.js'; import BaseDevice from './devices/BaseDevice.js'; import plugins from './plugins/index.js'; -import {Client} from './server.js'; +import Client from './Client.js'; const invariant = require('invariant'); diff --git a/src/server.js b/src/server.js index 886ca1313..343b28557 100644 --- a/src/server.js +++ b/src/server.js @@ -5,22 +5,20 @@ * @format */ -import type BaseDevice from './devices/BaseDevice.js'; import type {App} from './App.js'; -import type {SonarPlugin} from './plugin.js'; -import plugins from './plugins/index.js'; -import CertificateProvider from './utils/CertificateProvider'; import type {SecureServerConfig} from './utils/CertificateProvider'; import type Logger from './fb-stubs/Logger'; +import type {ClientQuery} from './Client.js'; -import {RSocketServer, ReactiveSocket, PartialResponder} from 'rsocket-core'; +import CertificateProvider from './utils/CertificateProvider'; +import {RSocketServer, ReactiveSocket} from 'rsocket-core'; import RSocketTCPServer from 'rsocket-tcp-server'; -const tls = require('tls'); -const net = require('net'); +import Client from './Client.js'; const EventEmitter = (require('events'): any); - const invariant = require('invariant'); +const tls = require('tls'); +const net = require('net'); const SECURE_PORT = 8088; const INSECURE_PORT = 8089; @@ -36,283 +34,7 @@ type ClientInfo = {| client: Client, |}; -type Plugins = Array; - -type ClientQuery = {| - app: string, - os: string, - device: string, - device_id: ?string, -|}; - -type RequestMetadata = {method: string, id: number, params: ?Object}; - -export class Client extends EventEmitter { - constructor(app: App, id: string, query: ClientQuery, conn: ReactiveSocket) { - super(); - - this.connected = true; - this.plugins = []; - this.connection = conn; - this.id = id; - this.query = query; - this.messageIdCounter = 0; - this.app = app; - - 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); - }, - }); - } - - 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; - - broadcastCallbacks: Map>>; - - requestCallbacks: Map< - number, - {| - resolve: (data: any) => void, - reject: (err: Error) => void, - metadata: RequestMetadata, - |}, - >; - - getDevice(): ?BaseDevice { - const {device_id} = this.query; - - if (device_id == null) { - return null; - } else { - return this.app.getDevice(device_id); - } - } - - supportsPlugin(Plugin: Class>): boolean { - return this.plugins.includes(Plugin.id); - } - - getFirstSupportedPlugin(): ?string { - for (const Plugin of plugins) { - if (this.supportsPlugin(Plugin)) { - return Plugin.id; - } - } - } - - async init() { - await this.getPlugins(); - } - - // get the supported plugins - async getPlugins(): Promise { - 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.log(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 apiCallbacks = this.broadcastCallbacks.get(params.api); - if (!apiCallbacks) { - return; - } - - const methodCallbacks: ?Set = 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 null; - } - - 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 { - 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.log(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.app.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.log(data, 'message:send'); - this.connection.fireAndForget({data: JSON.stringify(data)}); - } - - call(api: string, method: string, params?: Object): Promise { - return this.rawCall('execute', {api, method, params}); - } - - send(api: string, method: string, params?: Object): void { - return this.rawSend('execute', {api, method, params}); - } -} - -export class Server extends EventEmitter { +export default class Server extends EventEmitter { connections: Map; secureServer: RSocketServer; insecureServer: RSocketServer; @@ -361,10 +83,7 @@ export class Server extends EventEmitter { transportServer .on('error', err => { server.emit('error', err); - console.error( - `Error opening server on port ${port}`, - 'server', - ); + console.error(`Error opening server on port ${port}`, 'server'); }) .on('listening', () => { console.warn( @@ -401,10 +120,7 @@ export class Server extends EventEmitter { conn.connectionStatus().subscribe({ onNext(payload) { if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') { - console.warn( - `Device disconnected ${client.id}`, - 'connection', - ); + console.warn(`Device disconnected ${client.id}`, 'connection'); server.removeConnection(client.id); } }, @@ -433,10 +149,7 @@ export class Server extends EventEmitter { try { rawData = JSON.parse(payload.data); } catch (err) { - console.error( - `Invalid JSON: ${payload.data}`, - 'clientMessage', - ); + console.error(`Invalid JSON: ${payload.data}`, 'clientMessage'); return; }