diff --git a/.travis.yml b/.travis.yml index 192adcb3f..7b6681af0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,4 +58,4 @@ matrix: script: - cd iOS/Sample - - xcodebuild clean build -workspace Sample.xcworkspace -scheme Pods-Sample + - xcodebuild clean build -workspace Sample.xcworkspace -scheme Pods-Sample -sdk iphonesimulator11.2 diff --git a/docs/getting-started.md b/docs/getting-started.md index 3a9831f7c..6437a3a9d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -63,23 +63,17 @@ public class MyApplication extends Application { To integrate with an iOS app, you can use [CocoaPods](https://cocoapods.org). Add the mobile Sonar SDK and its dependencies to your `Podfile`: ```ruby -platform :ios, '8.0' -swift_version = '4.1' +project 'MyApp.xcodeproj' +source 'https://github.com/facebook/Sonar.git' +source 'https://github.com/CocoaPods/Specs' +# Uncomment the next line to define a global platform for your project +swift_version = "4.1" target 'MyApp' do - pod 'RSocket', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/RSocket.podspec' - pod 'DoubleConversion', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/DoubleConversion.podspec' - pod 'glog', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/glog.podspec' - pod 'Folly', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/Folly.podspec' - pod 'PeerTalk', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/PeerTalk.podspec' - pod 'Yoga','~>1.8.1', :modular_headers => true - pod 'Sonar', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/xplat/Sonar/Sonar.podspec' - pod 'SonarKit', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/SonarKit.podspec' - pod 'SonarKit/SonarKitLayoutComponentKitSupport', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/SonarKit.podspec' - pod 'SonarKit/SKIOSNetworkPlugin', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/SonarKit.podspec' - pod 'ComponentKit', :podspec => 'https://raw.githubusercontent.com/facebook/Sonar/master/iOS/third-party-podspecs/ComponentKit.podspec' + pod 'SonarKit', '~>0.0.1' post_install do |installer| + installer.pods_project.targets.each do |target| if ['YogaKit'].include? target.name target.build_configurations.each do |config| @@ -112,7 +106,7 @@ and install the dependencies by running `pod install`. When you open the Xcode w ```
-* We haven't released the dependency to CocoaPods, because we weren't able to successfully validate the podspec of SonarKit. You could help us out by fixing this [issue](https://github.com/facebook/Sonar/issues/11) by submitting a PR to the repo. +* We haven't released the dependency to CocoaPods yet, here is the [issue](https://github.com/facebook/Sonar/issues/132) by which you can track. * If you do not use CocoaPods as a dependency management tool then currently there is no way to integrate SonarKit other than manually including all the dependencies and building it. * For Android, Sonar works with both emulators and physical devices connected through USB. However on iOS, we don't yet support physical devices. * Also Sonar doesn't work with swift projects as its written in C++ and had C++ dependencies. But we are working on supporting sonar for swift projects. You can find this issue [here](https://github.com/facebook/Sonar/issues/13) diff --git a/src/App.js b/src/App.js index 8e2f6dfae..4c77e5e92 100644 --- a/src/App.js +++ b/src/App.js @@ -4,185 +4,41 @@ * LICENSE file in the root directory of this source tree. * @format */ + +import React from 'react'; import {FlexColumn, FlexRow} from 'sonar'; import {connect} from 'react-redux'; import {toggleBugDialogVisible} from './reducers/application.js'; import WelcomeScreen from './chrome/WelcomeScreen.js'; 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 from './server.js'; -import Client from './Client.js'; -import React from 'react'; -import BugReporter from './fb-stubs/BugReporter.js'; import BugReporterDialog from './chrome/BugReporterDialog.js'; import ErrorBar from './chrome/ErrorBar.js'; -import Logger from './fb-stubs/Logger.js'; import PluginContainer from './PluginContainer.js'; import PluginManager from './chrome/PluginManager.js'; -const electron = require('electron'); -const yargs = require('yargs'); -export type {Client}; - -export type StatePluginInfo = { - plugin: ?SonarBasePlugin<>, - state: Object, -}; - -export type StateClientPlugins = { - [pluginKey: string]: StatePluginInfo, -}; - -export type StatePlugins = { - [appKey: string]: StateClientPlugins, -}; - -export type State = { - activePluginKey: ?string, - activeAppKey: ?string, - plugins: StatePlugins, - error: ?string, -}; +import type Logger from './fb-stubs/Logger.js'; +import type BugReporter from './fb-stubs/BugReporter.js'; type Props = { - devices: Array, + logger: Logger, + bugReporter: BugReporter, leftSidebarVisible: boolean, bugDialogVisible: boolean, pluginManagerVisible: boolean, selectedDeviceIndex: number, - selectedApp: ?string, + error: ?string, toggleBugDialogVisible: (visible?: boolean) => void, }; -export class App extends React.Component { - constructor() { +export class App extends React.Component { + constructor(props: Props) { performance.mark('init'); - super(); - this.initTracking(); - - setupEnvironment(); - this.logger = new Logger(); - replaceGlobalConsole(this.logger); - this.server = this.initServer(); - - this.state = { - activeAppKey: null, - activePluginKey: null, - error: null, - devices: {}, - plugins: {}, - }; - - this.bugReporter = new BugReporter(this.logger); - this.commandLineArgs = yargs.parse(electron.remote.process.argv); + super(props); } - server: Server; - bugReporter: BugReporter; - logger: Logger; - commandLineArgs: Object; - _hasActivatedPreferredPlugin: boolean = false; - componentDidMount() { - this.logger.trackTimeSince('init'); - - // close socket before reloading - window.addEventListener('beforeunload', () => { - this.server.close(); - }); - } - - toJSON() { - return null; - } - - initServer(): Server { - const server = new Server(this); - server.addListener('new-client', client => { - client.addListener('close', () => { - this.setState(state => { - this.forceUpdate(); - // TODO: - //reducers.TeardownClient(this, state, {appKey: client.id}), - }); - if (this.state.activeAppKey === client.id) { - this.forceUpdate(); - } - }); - - client.addListener('plugins-change', () => { - this.forceUpdate(); - }); - }); - - server.addListener('clients-change', () => { - this.forceUpdate(); - }); - - server.addListener('error', err => { - if (err.code === 'EADDRINUSE') { - this.setState({ - error: - "Couldn't start websocket server. " + - 'Looks like you have multiple copies of Sonar running.', - }); - } else { - // unknown error - this.setState({ - error: err.message, - }); - } - }); - - return server; - } - - initTracking = () => { - electron.ipcRenderer.on('trackUsage', () => { - // check if there's a plugin currently active - const {activeAppKey, activePluginKey} = this.state; - if (activeAppKey == null || activePluginKey == null) { - return; - } - - // app plugins - const client = this.getClient(activeAppKey); - if (client) { - this.logger.track('usage', 'ping', { - app: client.query.app, - device: client.query.device, - os: client.query.os, - plugin: activePluginKey, - }); - return; - } - - // device plugins - const device: ?BaseDevice = this.getDevice(activeAppKey); - if (device) { - this.logger.track('usage', 'ping', { - os: device.os, - plugin: activePluginKey, - device: device.title, - }); - } - }); - }; - - getDevice = (id: string): ?BaseDevice => - this.props.devices.find((device: BaseDevice) => device.serial === id); - - getClient(appKey: ?string): ?Client { - if (appKey == null) { - return null; - } - - const info = this.server.connections.get(appKey); - if (info != null) { - return info.client; - } + this.props.logger.trackTimeSince('init'); } render() { @@ -191,30 +47,21 @@ export class App extends React.Component { {this.props.bugDialogVisible && ( this.props.toggleBugDialogVisible(false)} /> )} {this.props.selectedDeviceIndex > -1 ? ( - {this.props.leftSidebarVisible && ( - client, - )} - /> - )} - + {this.props.leftSidebarVisible && } + ) : this.props.pluginManagerVisible ? ( ) : ( )} - + ); } @@ -223,39 +70,14 @@ export class App extends React.Component { export default connect( ({ application: {pluginManagerVisible, bugDialogVisible, leftSidebarVisible}, - connections: {devices, selectedDeviceIndex, selectedApp}, + connections: {selectedDeviceIndex}, + server: {error}, }) => ({ pluginManagerVisible, bugDialogVisible, leftSidebarVisible, - devices, selectedDeviceIndex, - selectedApp, + error, }), {toggleBugDialogVisible}, )(App); - -function replaceGlobalConsole(logger: Logger) { - const loggerMethods = { - log: logger.info, - warn: logger.warn, - error: logger.error, - }; - const consoleHandler = { - get: function(obj, prop) { - return prop in loggerMethods - ? args => { - obj[prop] && obj[prop](args); - return loggerMethods[prop].bind(logger)(args); - } - : obj[prop]; - }, - }; - window.console = new Proxy(console, consoleHandler); -} - -function setupEnvironment() { - if (!process.env.ANDROID_HOME) { - process.env.ANDROID_HOME = '/opt/android_sdk'; - } -} diff --git a/src/Client.js b/src/Client.js index 09646a701..a6916bd1c 100644 --- a/src/Client.js +++ b/src/Client.js @@ -7,7 +7,8 @@ import type {SonarPlugin} from './plugin.js'; import type {App} from './App.js'; -import type BaseDevice from './devices/BaseDevice.js'; +import type Logger from './fb-stubs/Logger.js'; + import plugins from './plugins/index.js'; import {ReactiveSocket, PartialResponder} from 'rsocket-core'; @@ -26,7 +27,12 @@ export type ClientQuery = {| type RequestMetadata = {method: string, id: number, params: ?Object}; export default class Client extends EventEmitter { - constructor(app: App, id: string, query: ClientQuery, conn: ReactiveSocket) { + constructor( + id: string, + query: ClientQuery, + conn: ReactiveSocket, + logger: Logger, + ) { super(); this.connected = true; @@ -35,7 +41,7 @@ export default class Client extends EventEmitter { this.id = id; this.query = query; this.messageIdCounter = 0; - this.app = app; + this.logger = logger; this.broadcastCallbacks = new Map(); this.requestCallbacks = new Map(); @@ -82,16 +88,6 @@ export default class Client extends EventEmitter { |}, >; - 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); } @@ -193,7 +189,7 @@ export default class Client extends EventEmitter { } toJSON() { - return null; + return ``; } subscribe( @@ -257,7 +253,7 @@ export default class Client extends EventEmitter { finishTimingRequestResponse(data: RequestMetadata) { const mark = this.getPerformanceMark(data); const logEventName = this.getLogEventName(data); - this.app.logger.trackTimeSince(mark, logEventName); + this.logger.trackTimeSince(mark, logEventName); } getPerformanceMark(data: RequestMetadata): string { diff --git a/src/PluginContainer.js b/src/PluginContainer.js index d338e26db..7858eca78 100644 --- a/src/PluginContainer.js +++ b/src/PluginContainer.js @@ -36,8 +36,9 @@ type Props = { logger: LogManager, selectedDeviceIndex: number, selectedPlugin: ?string, + selectedApp: ?string, pluginStates: Object, - client: ?Client, + clients: Array, devices: Array, setPluginState: (payload: { pluginKey: string, @@ -101,13 +102,15 @@ class PluginContainer extends Component { if (activePlugin) { pluginKey = `${device.serial}#${activePlugin.id}`; } else { + target = props.clients.find( + (client: Client) => client.id === props.selectedApp, + ); activePlugin = plugins.find( (p: Class>) => p.id === props.selectedPlugin, ); - if (!activePlugin || !props.client) { + if (!activePlugin || !target) { return null; } - target = props.client; pluginKey = `${target.id}#${activePlugin.id}`; } @@ -161,13 +164,16 @@ class PluginContainer extends Component { export default connect( ({ application: {rightSidebarVisible, rightSidebarAvailable}, - connections: {selectedPlugin, devices, selectedDeviceIndex}, + connections: {selectedPlugin, devices, selectedDeviceIndex, selectedApp}, pluginStates, + server: {clients}, }) => ({ selectedPlugin, devices, selectedDeviceIndex, pluginStates, + selectedApp, + clients, }), { setPluginState, diff --git a/src/chrome/MainSidebar.js b/src/chrome/MainSidebar.js index 4a7b7f97c..599090ba3 100644 --- a/src/chrome/MainSidebar.js +++ b/src/chrome/MainSidebar.js @@ -221,11 +221,13 @@ class MainSidebar extends Component { export default connect( ({ connections: {devices, selectedDeviceIndex, selectedPlugin, selectedApp}, + server: {clients}, }) => ({ devices, selectedDeviceIndex, selectedPlugin, selectedApp, + clients, }), { selectPlugin, diff --git a/src/device-plugins/logs/index.js b/src/device-plugins/logs/index.js index 52def9bfd..169815300 100644 --- a/src/device-plugins/logs/index.js +++ b/src/device-plugins/logs/index.js @@ -554,6 +554,7 @@ export default class LogTable extends SonarDevicePlugin { defaultFilters={DEFAULT_FILTERS} zebra={false} actions={} + stickyBottom={true} /> ); diff --git a/src/dispatcher/androidDevice.js b/src/dispatcher/androidDevice.js index b11bfae01..950d847df 100644 --- a/src/dispatcher/androidDevice.js +++ b/src/dispatcher/androidDevice.js @@ -9,6 +9,7 @@ import AndroidDevice from '../devices/AndroidDevice'; import child_process from 'child_process'; import type {Store} from '../reducers/index.js'; import type BaseDevice from '../devices/BaseDevice'; +import type Logger from '../fb-stubs/Logger.js'; const adb = require('adbkit-fb'); function createDecive(client, device): Promise { @@ -47,7 +48,7 @@ function getRunningEmulatorName(id: string): Promise { }); } -export default (store: Store) => { +export default (store: Store, logger: Logger) => { const client = adb.createClient(); // get emulators diff --git a/src/dispatcher/application.js b/src/dispatcher/application.js index 0a4190347..21ba4a56c 100644 --- a/src/dispatcher/application.js +++ b/src/dispatcher/application.js @@ -7,8 +7,9 @@ import {remote} from 'electron'; import type {Store} from '../reducers/index.js'; +import type Logger from '../fb-stubs/Logger.js'; -export default (store: Store) => { +export default (store: Store, logger: Logger) => { const currentWindow = remote.getCurrentWindow(); currentWindow.on('focus', () => store.dispatch({ diff --git a/src/dispatcher/iOSDevice.js b/src/dispatcher/iOSDevice.js index 559ca7fca..a3dc5c0e2 100644 --- a/src/dispatcher/iOSDevice.js +++ b/src/dispatcher/iOSDevice.js @@ -7,6 +7,8 @@ import type {ChildProcess} from 'child_process'; import type {Store} from '../reducers/index.js'; +import type Logger from '../fb-stubs/Logger.js'; + import child_process from 'child_process'; import IOSDevice from '../devices/IOSDevice'; @@ -49,7 +51,7 @@ function querySimulatorDevices(): Promise { }); } -export default (store: Store) => { +export default (store: Store, logger: Logger) => { // monitoring iOS devices only available on MacOS. if (process.platform !== 'darwin') { return; diff --git a/src/dispatcher/index.js b/src/dispatcher/index.js index dc22d6de5..66f4152b2 100644 --- a/src/dispatcher/index.js +++ b/src/dispatcher/index.js @@ -8,7 +8,13 @@ import androidDevice from './androidDevice'; import iOSDevice from './iOSDevice'; import application from './application'; +import tracking from './tracking'; +import server from './server'; + +import type Logger from '../fb-stubs/Logger.js'; import type {Store} from '../reducers/index.js'; -export default (store: Store) => - [application, androidDevice, iOSDevice].forEach(fn => fn(store)); +export default (store: Store, logger: Logger) => + [application, androidDevice, iOSDevice, tracking, server].forEach(fn => + fn(store, logger), + ); diff --git a/src/dispatcher/server.js b/src/dispatcher/server.js new file mode 100644 index 000000000..72568d4ca --- /dev/null +++ b/src/dispatcher/server.js @@ -0,0 +1,44 @@ +/** + * 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 Server from '../server.js'; + +import type {Store} from '../reducers/index.js'; +import type Logger from '../fb-stubs/Logger.js'; + +export default (store: Store, logger: Logger) => { + const server = new Server(logger); + server.addListener('new-client', (client: Client) => { + store.dispatch({ + type: 'NEW_CLIENT', + payload: client, + }); + }); + + server.addListener('removed-client', (id: string) => { + store.dispatch({ + type: 'CLIENT_REMOVED', + payload: id, + }); + }); + + server.addListener('error', err => { + const payload: string = + err.code === 'EADDRINUSE' + ? "Couldn't start websocket server. Looks like you have multiple copies of Sonar running." + : err.message || 'Unknown error'; + + store.dispatch({ + type: 'SERVER_ERROR', + payload, + }); + }); + + window.addEventListener('beforeunload', () => { + server.close(); + }); +}; diff --git a/src/dispatcher/tracking.js b/src/dispatcher/tracking.js new file mode 100644 index 000000000..9f4c8758f --- /dev/null +++ b/src/dispatcher/tracking.js @@ -0,0 +1,44 @@ +/** + * 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 {ipcRenderer} from 'electron'; + +import type BaseDevice from '../devices/BaseDevice.js'; +import type {Store} from '../reducers/index.js'; +import type Logger from '../fb-stubs/Logger.js'; + +export default (store: Store, logger: Logger) => { + ipcRenderer.on('trackUsage', () => { + const { + devices, + selectedDeviceIndex, + selectedPlugin, + selectedApp, + } = store.getState().connections; + + const device: ?BaseDevice = + selectedDeviceIndex > -1 ? devices[selectedDeviceIndex] : null; + console.log(1, 2, 3); + if (!device || !selectedPlugin) { + return; + } + if (selectedApp) { + logger.track('usage', 'ping', { + app: selectedApp, + device, + os: device.os, + plugin: selectedPlugin, + }); + } else { + logger.track('usage', 'ping', { + os: device.os, + plugin: selectedPlugin, + device: device.title, + }); + } + }); +}; diff --git a/src/init.js b/src/init.js index 90a61d2fe..1c2915ae7 100644 --- a/src/init.js +++ b/src/init.js @@ -10,7 +10,9 @@ import ReactDOM from 'react-dom'; import {ContextMenuProvider} from 'sonar'; import {precachedIcons} from './utils/icons.js'; import GK from './fb-stubs/GK.js'; +import Logger from './fb-stubs/Logger.js'; import App from './App.js'; +import BugReporter from './fb-stubs/BugReporter.js'; import {createStore} from 'redux'; import reducers from './reducers/index.js'; import dispatcher from './dispatcher/index.js'; @@ -22,15 +24,16 @@ const store = createStore( window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ); -dispatcher(store); - +const logger = new Logger(); +const bugReporter = new BugReporter(logger); +dispatcher(store, logger); GK.init(); setupMenuBar(); const AppFrame = () => ( - + ); diff --git a/src/reducers/index.js b/src/reducers/index.js index 2af1bbd96..f16a3e02a 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -8,6 +8,7 @@ import {combineReducers} from 'redux'; import application from './application.js'; import connections from './connections.js'; +import server from './server.js'; import pluginStates from './pluginStates.js'; import type { State as ApplicationState, @@ -21,6 +22,7 @@ import type { State as PluginsState, Action as PluginsAction, } from './pluginStates.js'; +import type {State as ServerState, Action as ServerAction} from './server.js'; import type {Store as ReduxStore} from 'redux'; export type Store = ReduxStore< @@ -28,8 +30,14 @@ export type Store = ReduxStore< application: ApplicationState, connections: DevicesState, pluginStates: PluginsState, + server: ServerState, }, - ApplicationAction | DevicesAction | PluginsAction, + ApplicationAction | DevicesAction | PluginsAction | ServerAction, >; -export default combineReducers({application, connections, pluginStates}); +export default combineReducers({ + application, + connections, + pluginStates, + server, +}); diff --git a/src/reducers/server.js b/src/reducers/server.js new file mode 100644 index 000000000..84e342855 --- /dev/null +++ b/src/reducers/server.js @@ -0,0 +1,54 @@ +/** + * 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 + */ + +export type State = { + error: ?string, + clients: Array, +}; + +export type Action = + | { + type: 'SERVER_ERROR', + payload: ?string, + } + | { + type: 'NEW_CLIENT', + payload: Client, + } + | { + type: 'CLIENT_REMOVED', + payload: string, + }; + +const INITIAL_STATE: State = { + error: null, + clients: [], +}; + +export default function reducer( + state: State = INITIAL_STATE, + action: Action, +): State { + if (action.type === 'NEW_CLIENT') { + const {payload} = action; + return { + ...state, + clients: state.clients.concat(payload), + }; + } else if (action.type === 'CLIENT_REMOVED') { + const {payload} = action; + return { + ...state, + clients: state.clients.filter((client: Client) => client.id !== payload), + }; + } else if (action.type === 'SERVER_ERROR') { + const {payload} = action; + return {...state, error: payload}; + } else { + return state; + } +} diff --git a/src/server.js b/src/server.js index 343b28557..8873fc74b 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,6 @@ * @format */ -import type {App} from './App.js'; import type {SecureServerConfig} from './utils/CertificateProvider'; import type Logger from './fb-stubs/Logger'; import type {ClientQuery} from './Client.js'; @@ -40,14 +39,14 @@ export default class Server extends EventEmitter { insecureServer: RSocketServer; certificateProvider: CertificateProvider; connectionTracker: ConnectionTracker; - app: App; + logger: Logger; - constructor(app: App) { + constructor(logger: Logger) { super(); - this.app = app; + this.logger = logger; this.connections = new Map(); - this.certificateProvider = new CertificateProvider(this, app.logger); - this.connectionTracker = new ConnectionTracker(app.logger); + this.certificateProvider = new CertificateProvider(this, logger); + this.connectionTracker = new ConnectionTracker(logger); this.init(); } @@ -186,7 +185,7 @@ export default class Server extends EventEmitter { const id = `${query.app}-${query.os}-${query.device}`; console.warn(`Device connected: ${id}`, 'connection'); - const client = new Client(this.app, id, query, conn); + const client = new Client(id, query, conn, this.logger); const info = { client, @@ -235,6 +234,7 @@ export default class Server extends EventEmitter { info.client.emit('close'); this.connections.delete(id); this.emit('clients-change'); + this.emit('removed-client', id); } } } diff --git a/static/index.js b/static/index.js index ad1286379..faff72c7a 100644 --- a/static/index.js +++ b/static/index.js @@ -11,6 +11,10 @@ const fs = require('fs'); const compilePlugins = require('./compilePlugins.js'); const os = require('os'); +if (!process.env.ANDROID_HOME) { + process.env.ANDROID_HOME = '/opt/android_sdk'; +} + // ensure .sonar folder and config exist const sonarDir = path.join(os.homedir(), '.sonar'); if (!fs.existsSync(sonarDir)) {