fbshipit-source-id: c71048dfea2a03cf83650b55aa9d1e463251920c

This commit is contained in:
Daniel Buchele
2018-07-04 07:19:44 -07:00
parent e6fa377d75
commit 5163f8b9a3
18 changed files with 235 additions and 247 deletions

View File

@@ -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

View File

@@ -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
```
<div class='warning'>
* 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)

View File

@@ -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<BaseDevice>,
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<Props, State> {
constructor() {
export class App extends React.Component<Props> {
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<Props, State> {
<SonarTitleBar />
{this.props.bugDialogVisible && (
<BugReporterDialog
bugReporter={this.bugReporter}
bugReporter={this.props.bugReporter}
close={() => this.props.toggleBugDialogVisible(false)}
/>
)}
{this.props.selectedDeviceIndex > -1 ? (
<FlexRow fill={true}>
{this.props.leftSidebarVisible && (
<MainSidebar
clients={Array.from(this.server.connections.values()).map(
({client}) => client,
)}
/>
)}
<PluginContainer
logger={this.logger}
client={this.getClient(this.props.selectedApp)}
/>
{this.props.leftSidebarVisible && <MainSidebar />}
<PluginContainer logger={this.props.logger} />
</FlexRow>
) : this.props.pluginManagerVisible ? (
<PluginManager />
) : (
<WelcomeScreen />
)}
<ErrorBar text={this.state.error} />
<ErrorBar text={this.props.error} />
</FlexColumn>
);
}
@@ -223,39 +70,14 @@ export class App extends React.Component<Props, State> {
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';
}
}

View File

@@ -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<SonarPlugin<>>): boolean {
return this.plugins.includes(Plugin.id);
}
@@ -193,7 +189,7 @@ export default class Client extends EventEmitter {
}
toJSON() {
return null;
return `<Client#${this.id}>`;
}
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 {

View File

@@ -36,8 +36,9 @@ type Props = {
logger: LogManager,
selectedDeviceIndex: number,
selectedPlugin: ?string,
selectedApp: ?string,
pluginStates: Object,
client: ?Client,
clients: Array<Client>,
devices: Array<BaseDevice>,
setPluginState: (payload: {
pluginKey: string,
@@ -101,13 +102,15 @@ class PluginContainer extends Component<Props, State> {
if (activePlugin) {
pluginKey = `${device.serial}#${activePlugin.id}`;
} else {
target = props.clients.find(
(client: Client) => client.id === props.selectedApp,
);
activePlugin = plugins.find(
(p: Class<SonarPlugin<>>) => 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<Props, State> {
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,

View File

@@ -221,11 +221,13 @@ class MainSidebar extends Component<MainSidebarProps> {
export default connect(
({
connections: {devices, selectedDeviceIndex, selectedPlugin, selectedApp},
server: {clients},
}) => ({
devices,
selectedDeviceIndex,
selectedPlugin,
selectedApp,
clients,
}),
{
selectPlugin,

View File

@@ -554,6 +554,7 @@ export default class LogTable extends SonarDevicePlugin<LogsState> {
defaultFilters={DEFAULT_FILTERS}
zebra={false}
actions={<Button onClick={this.clearLogs}>Clear Logs</Button>}
stickyBottom={true}
/>
</LogTable.ContextMenu>
);

View File

@@ -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<AndroidDevice> {
@@ -47,7 +48,7 @@ function getRunningEmulatorName(id: string): Promise<?string> {
});
}
export default (store: Store) => {
export default (store: Store, logger: Logger) => {
const client = adb.createClient();
// get emulators

View File

@@ -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({

View File

@@ -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<IOSDeviceMap> {
});
}
export default (store: Store) => {
export default (store: Store, logger: Logger) => {
// monitoring iOS devices only available on MacOS.
if (process.platform !== 'darwin') {
return;

View File

@@ -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),
);

44
src/dispatcher/server.js Normal file
View File

@@ -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();
});
};

View File

@@ -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,
});
}
});
};

View File

@@ -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 = () => (
<ContextMenuProvider>
<Provider store={store}>
<App />
<App logger={logger} bugReporter={bugReporter} />
</Provider>
</ContextMenuProvider>
);

View File

@@ -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,
});

54
src/reducers/server.js Normal file
View File

@@ -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<Client>,
};
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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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)) {