diff --git a/desktop/flipper-server-core/src/app-connectivity/SecureServerWebSocket.tsx b/desktop/flipper-server-core/src/app-connectivity/SecureServerWebSocket.tsx index 373e341be..7bcb381bf 100644 --- a/desktop/flipper-server-core/src/app-connectivity/SecureServerWebSocket.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/SecureServerWebSocket.tsx @@ -18,6 +18,7 @@ import { import WebSocketClientConnection from './WebSocketClientConnection'; import {serializeError} from 'serialize-error'; import {WSCloseCode} from '../utils/WSCloseCode'; +import {recorder} from '../recorder'; export interface SecureConnectionCtx extends ConnectionCtx { clientQuery?: SecureClientQuery; @@ -39,15 +40,15 @@ class SecureServerWebSocket extends ServerWebSocket { const {clientQuery, ws} = ctx; assertNotNull(clientQuery); - console.info( - `[conn] Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`, + recorder.log( + clientQuery, + `Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device}.`, ); + this.listener.onSecureConnectionAttempt(clientQuery); const clientConnection = new WebSocketClientConnection(ws); - // TODO: Could we just await it here? How much time could it be, potentially? - // DRI: @aigoncharov const clientPromise: Promise = this.listener .onConnectionCreated(clientQuery, clientConnection) .then((client) => { @@ -96,7 +97,6 @@ class SecureServerWebSocket extends ServerWebSocket { } // Received an "execute" message - if (client) { this.listener.onClientMessage(client.id, rawMessage); } else { diff --git a/desktop/flipper-server-core/src/app-connectivity/ServerController.tsx b/desktop/flipper-server-core/src/app-connectivity/ServerController.tsx index a501d0e5f..45d0cfc2f 100644 --- a/desktop/flipper-server-core/src/app-connectivity/ServerController.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/ServerController.tsx @@ -23,11 +23,7 @@ import {ClientConnection, ConnectionStatus} from './ClientConnection'; import {EventEmitter} from 'events'; import invariant from 'invariant'; import DummyDevice from '../devices/DummyDevice'; -import { - appNameWithUpdateHint, - assertNotNull, - cloneClientQuerySafeForLogging, -} from './Utilities'; +import {appNameWithUpdateHint, assertNotNull} from './Utilities'; import ServerWebSocketBase, {ServerEventsListener} from './ServerWebSocketBase'; import { createBrowserServer, @@ -40,12 +36,13 @@ import { getFlipperServerConfig, } from '../FlipperServerConfig'; import { - extractAppNameFromCSR, + extractBundleIdFromCSR, loadSecureServerConfig, } from './certificate-exchange/certificate-utils'; import DesktopCertificateProvider from '../devices/desktop/DesktopCertificateProvider'; import WWWCertificateProvider from '../fb-stubs/WWWCertificateProvider'; -import {tracker} from '../utils/tracker'; +import {tracker} from '../tracker'; +import {recorder} from '../recorder'; type ClientTimestampTracker = { insecureStart?: number; @@ -114,10 +111,10 @@ export class ServerController const options = await loadSecureServerConfig(); - console.info('[conn] secure server listening at port: ', secure); + console.info('[ws] secure server listening at port: ', secure); this.secureServer = await createServer(secure, this, options); const {secure: altSecure} = getServerPortsConfig().altServerPorts; - console.info('[conn] secure server (ws) listening at port: ', altSecure); + console.info('[ws] secure server listening at port: ', altSecure); this.altSecureServer = await createServer( altSecure, this, @@ -125,13 +122,10 @@ export class ServerController TransportType.WebSocket, ); - console.info('[conn] insecure server listening at port: ', insecure); + console.info('[ws] insecure server listening at port: ', insecure); this.insecureServer = await createServer(insecure, this); const {insecure: altInsecure} = getServerPortsConfig().altServerPorts; - console.info( - '[conn] insecure server (ws) listening at port: ', - altInsecure, - ); + console.info('[ws] insecure server listening at port: ', altInsecure); this.altInsecureServer = await createServer( altInsecure, this, @@ -140,7 +134,7 @@ export class ServerController ); const browserPort = getServerPortsConfig().browserPort; - console.info('[conn] Browser server (ws) listening at port: ', browserPort); + console.info('[ws] Browser server listening at port: ', browserPort); this.browserServer = await createBrowserServer(browserPort, this); } @@ -174,11 +168,7 @@ export class ServerController rsocket, } = clientQuery; - console.info( - `[conn] Connection established: ${app} on ${device_id}. Medium ${medium}. CSR: ${csr_path}`, - cloneClientQuerySafeForLogging(clientQuery), - ); - + recorder.log(clientQuery, 'Connection established'); tracker.track('app-connection-created', { app, os, @@ -233,17 +223,25 @@ export class ServerController medium: clientQuery.medium, }); - const {os, app, device_id} = clientQuery; - // without these checks, the user might see a connection timeout error instead, which would be much harder to track down - if (os === 'iOS' && !getFlipperServerConfig().settings.enableIOS) { - console.error( - `Refusing connection from ${app} on ${device_id}, since iOS support is disabled in settings`, + // Without these checks, the user might see a connection timeout error instead, + // which would be much harder to track down + if ( + clientQuery.os === 'iOS' && + !getFlipperServerConfig().settings.enableIOS + ) { + recorder.error( + clientQuery, + `Refusing connection since iOS support is disabled in settings`, ); return; } - if (os === 'Android' && !getFlipperServerConfig().settings.enableAndroid) { - console.error( - `Refusing connection from ${app} on ${device_id}, since Android support is disabled in settings`, + if ( + clientQuery.os === 'Android' && + !getFlipperServerConfig().settings.enableAndroid + ) { + recorder.error( + clientQuery, + `Refusing connection since Android support is disabled in settings`, ); return; } @@ -268,14 +266,24 @@ export class ServerController } } + /** + * A connection has been established between a running app and Flipper Desktop. + * The connection sole purpose is to perform the certificate exchange. + * @param clientQuery Client query defines the arguments passed down from the app + * to Flipper Desktop. + */ onConnectionAttempt(clientQuery: ClientQuery): void { + // Remove the device id from the query, if found. + // Instead, set the device id as 'unknown'. const strippedClientQuery = (({device_id, ...o}) => o)(clientQuery); const id = buildClientId({device_id: 'unknown', ...strippedClientQuery}); + this.timestamps.set(id, { insecureStart: Date.now(), }); tracker.track('app-connection-insecure-attempt', clientQuery); + recorder.log(clientQuery, 'Insecure connection attempt'); this.connectionTracker.logConnectionAttempt(clientQuery); @@ -325,23 +333,30 @@ export class ServerController } default: { throw new Error( - `ServerController.onProcessCSR -> os ${clientQuery.os} does not support certificate exchange.`, + `OS '${clientQuery.os}' does not support certificate exchange.`, ); } } certificateProvider.verifyMedium(clientQuery.medium); + recorder.log(clientQuery, 'Certificate Signing Request being processed'); + return new Promise((resolve, reject) => { reportPlatformFailures( certificateProvider.processCertificateSigningRequest( + clientQuery, unsanitizedCSR, - clientQuery.os, appDirectory, ), 'processCertificateSigningRequest', ) .then((response) => { + recorder.log( + clientQuery, + 'Certificate Signing Request successfully processed', + ); + const client: UninitializedClient = { os: clientQuery.os, deviceName: clientQuery.device, @@ -392,12 +407,7 @@ export class ServerController } onClientSetupError(clientQuery: ClientQuery, e: any) { - console.warn( - `[conn] Failed to exchange certificate with ${clientQuery.app} on ${ - clientQuery.device || clientQuery.device_id - }`, - e, - ); + recorder.error(clientQuery, 'Failed to exchange certificate', e); const client: UninitializedClient = { os: clientQuery.os, deviceName: clientQuery.device, @@ -405,58 +415,56 @@ export class ServerController }; this.emit('client-setup-error', { client, - error: `[conn] Failed to exchange certificate with ${ - clientQuery.app - } on ${clientQuery.device || clientQuery.device_id}: ${e}`, + error: `Failed to exchange certificate with ${clientQuery.app} on ${ + clientQuery.device || clientQuery.device_id + }: ${e}`, }); } /** * Creates a Client and sets the underlying connection. * @param connection A client connection to communicate between server and client. - * @param query The client query created from the initial handshake. + * @param clientQuery The client query created from the initial handshake. * @param csrQuery The CSR query which contains CSR related information. */ async addConnection( connection: ClientConnection, - query: SecureClientQuery, + clientQuery: SecureClientQuery, silentReplace?: boolean, ): Promise { - invariant(query, 'expected query'); + invariant(clientQuery, 'expected query'); // try to get id by comparing giving `csr` to file from `csr_path` - // otherwise, use given device_id - const {csr_path, csr} = query; + // otherwise, use given device_id. + const {csr_path, csr} = clientQuery; // For Android, device id might change - if (csr_path && csr && query.os === 'Android') { - const app_name = await extractAppNameFromCSR(csr); + if (csr_path && csr && clientQuery.os === 'Android') { + const bundleId = await extractBundleIdFromCSR(csr); assertNotNull(this.flipperServer.android); - // TODO: allocate new object, kept now as is to keep changes minimal - (query as any).device_id = + (clientQuery as any).device_id = await this.flipperServer.android.certificateProvider.getTargetDeviceId( - app_name, + bundleId, csr_path, csr, ); - console.info( - `[conn] Detected ${app_name} on ${query.device_id} in certificate`, - query, + recorder.log( + clientQuery, + `Detected ${bundleId} on ${clientQuery.device_id} in certificate`, ); } - // TODO: allocate new object, kept now as is to keep changes minimal - (query as any).app = appNameWithUpdateHint(query); + (clientQuery as any).app = appNameWithUpdateHint(clientQuery); - const id = buildClientId(query); - console.info( - `[conn] Matching device for ${query.app} on ${query.device_id}...`, - query, + const id = buildClientId(clientQuery); + recorder.log( + clientQuery, + `Matching device for ${clientQuery.app} on ${clientQuery.device_id}`, ); const client: ClientDescription = { id, - query, + query: clientQuery, }; const info = { @@ -464,9 +472,9 @@ export class ServerController connection: connection, }; - console.info( - `[conn] Initializing client ${query.app} on ${query.device_id}...`, - query, + recorder.log( + clientQuery, + `Initializing client ${clientQuery.app} on ${clientQuery.device_id}`, ); connection.subscribeToEvents((status: ConnectionStatus) => { @@ -478,7 +486,7 @@ export class ServerController } }); - console.debug(`[conn] Device client initialized: ${id}.`, 'server', query); + recorder.log(clientQuery, `Device client initialized: ${id}`); /* If a device gets disconnected without being cleaned up properly, * Flipper won't be aware until it attempts to reconnect. @@ -508,12 +516,15 @@ export class ServerController const start = tracker.insecureStart ? tracker.insecureStart : tracker.secureStart; - const elapsed = Math.round(end - start!); - this.logger.track('performance', 'client-connection-tracker', { - 'time-to-connection': elapsed, - ...query, - }); - this.timestamps.delete(id); + + if (start) { + const elapsed = Math.round(end - start); + this.logger.track('performance', 'client-connection-tracker', { + 'time-to-connection': elapsed, + ...clientQuery, + }); + this.timestamps.delete(id); + } } return client; @@ -534,9 +545,9 @@ export class ServerController removeConnection(id: string) { const info = this.connections.get(id); if (info) { - console.info( - `[conn] Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`, + recorder.log( info.client.query, + `Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`, ); this.flipperServer.emit('client-disconnected', {id}); this.connections.delete(id); @@ -566,21 +577,21 @@ class ConnectionTracker { this.logger = logger; } - logConnectionAttempt(client: ClientQuery) { - const key = `${client.os}-${client.device}-${client.app}`; + logConnectionAttempt(clientQuery: ClientQuery) { + const key = `${clientQuery.os}-${clientQuery.device}-${clientQuery.app}`; const time = Date.now(); + let entry = this.connectionAttempts.get(key) || []; entry.push(time); entry = entry.filter((t) => t >= time - this.timeWindowMillis); this.connectionAttempts.set(key, entry); if (entry.length >= this.connectionProblemThreshold) { - console.warn( - `[conn] Connection loop detected with ${key}. Connected ${ + recorder.error( + clientQuery, + `Connection loop detected with ${key}. Connected ${ this.connectionProblemThreshold } times within ${this.timeWindowMillis / 1000}s.`, - 'server', - client, ); } } diff --git a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx index ee3ff9ef6..8bfe99c42 100644 --- a/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/ServerWebSocket.tsx @@ -28,6 +28,7 @@ import {SecureServerConfig} from './certificate-exchange/certificate-utils'; import {Server} from 'net'; import {serializeError} from 'serialize-error'; import {WSCloseCode} from '../utils/WSCloseCode'; +import {recorder} from '../recorder'; export interface ConnectionCtx { clientQuery?: ClientQuery; @@ -73,7 +74,9 @@ class ServerWebSocket extends ServerWebSocketBase { wsServer.once('error', onConnectionError); server.listen(port, () => { console.debug( - `${sslConfig ? 'Secure' : 'Insecure'} server started on port ${port}`, + `[ws] ${ + sslConfig ? 'Secure' : 'Insecure' + } server started on port ${port}`, 'server', ); @@ -96,7 +99,7 @@ class ServerWebSocket extends ServerWebSocketBase { 'connection', (ws: WebSocket, request: IncomingMessage) => { ws.on('error', (error) => { - console.error('[conn] WS connection error:', error); + console.error('[ws] Connection error:', error); this.listener.onError(error); }); @@ -123,7 +126,7 @@ class ServerWebSocket extends ServerWebSocketBase { }, ); this.wsServer.on('error', (error) => { - console.error('[conn] WS server error:', error); + console.error('[ws] Server error:', error); this.listener.onError(error); }); @@ -136,7 +139,7 @@ class ServerWebSocket extends ServerWebSocketBase { } await new Promise((resolve, reject) => { - console.info('[conn] Stopping WS server'); + console.info('[ws] Stopping server'); assertNotNull(this.wsServer); this.wsServer.close((err) => { if (err) { @@ -147,7 +150,7 @@ class ServerWebSocket extends ServerWebSocketBase { }); }); await new Promise((resolve, reject) => { - console.info('[conn] Stopping HTTP server'); + console.info('[ws] Stopping HTTP server'); assertNotNull(this.httpServer); this.httpServer.close((err) => { if (err) { @@ -175,7 +178,10 @@ class ServerWebSocket extends ServerWebSocketBase { ws.on('message', async (message: WebSocket.RawData) => { const messageString = message.toString(); try { - const parsedMessage = this.handleMessageDeserialization(messageString); + const parsedMessage = this.handleMessageDeserialization( + ctx, + messageString, + ); // Successful deserialization is a proof that the message is a string this.handleMessage(ctx, parsedMessage, messageString); } catch (error) { @@ -183,7 +189,7 @@ class ServerWebSocket extends ServerWebSocketBase { // all other plugins might still be working correctly. So let's just report it. // This avoids ping-ponging connections if an individual plugin sends garbage (e.g. T129428800) // or throws an error when handling messages - console.error('[conn] Failed to handle message', messageString, error); + console.error('[ws] Failed to handle message', messageString, error); } }); } @@ -205,11 +211,11 @@ class ServerWebSocket extends ServerWebSocketBase { if (!clientQuery) { console.warn( - '[conn] Unable to extract the client query from the request URL.', + '[ws] Unable to extract the client query from the request URL.', request.url, ); throw new UnableToExtractClientQueryError( - '[conn] Unable to extract the client query from the request URL.', + 'Unable to extract the client query from the request URL.', ); } @@ -220,17 +226,24 @@ class ServerWebSocket extends ServerWebSocketBase { const {clientQuery} = ctx; assertNotNull(clientQuery); - console.info( - `[conn] Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`, + recorder.log( + clientQuery, + `Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`, ); this.listener.onConnectionAttempt(clientQuery); } - protected handleMessageDeserialization(message: unknown): object { + protected handleMessageDeserialization( + ctx: ConnectionCtx, + message: unknown, + ): object { + const {clientQuery} = ctx; + assertNotNull(clientQuery); + const parsedMessage = parseMessageToJson(message); if (!parsedMessage) { - console.error('[conn] Failed to parse message', message); - throw new Error(`[conn] Failed to parse message`); + recorder.error(clientQuery, 'Failed to parse message', message); + throw new Error(`Failed to parse message`); } return parsedMessage; } diff --git a/desktop/flipper-server-core/src/app-connectivity/Utilities.tsx b/desktop/flipper-server-core/src/app-connectivity/Utilities.tsx index 12dd0405e..bae385dcf 100644 --- a/desktop/flipper-server-core/src/app-connectivity/Utilities.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/Utilities.tsx @@ -71,7 +71,7 @@ export function isWsResponseMessage( return typeof (message as ResponseMessage).id === 'number'; } -const certExchangeSupportedOSes = new Set([ +const supportedOSForCertificateExchange = new Set([ 'Android', 'iOS', 'MacOS', @@ -85,7 +85,7 @@ const certExchangeSupportedOSes = new Set([ export function verifyClientQueryComesFromCertExchangeSupportedOS( query: ClientQuery | undefined, ): ClientQuery | undefined { - if (!query || !certExchangeSupportedOSes.has(query.os)) { + if (!query || !supportedOSForCertificateExchange.has(query.os)) { return; } return query; @@ -141,22 +141,22 @@ export function parseClientQuery( throw new Error('Unsupported exchange medium: ' + medium); } + let sdk_version: number | undefined; + if (typeof query.sdk_version === 'string') { + sdk_version = parseInt(query.sdk_version, 10); + } else if (typeof query.sdk_version === 'number') { + sdk_version = query.sdk_version; + } + const clientQuery: ClientQuery = { device_id, device, app, os, medium: transformCertificateExchangeMediumToType(medium), + sdk_version, }; - if (typeof query.sdk_version === 'string') { - const sdk_version = parseInt(query.sdk_version, 10); - if (sdk_version) { - // TODO: allocate new object, kept now as is to keep changes minimal - (clientQuery as any).sdk_version = sdk_version; - } - } - return clientQuery; } @@ -213,7 +213,6 @@ export function cloneClientQuerySafeForLogging(clientQuery: SecureClientQuery) { return {...clientQuery, csr: !clientQuery.csr ? clientQuery.csr : ''}; } -// TODO: Merge with the same fn in desktop/app/src/utils export function assertNotNull( value: T, message: string = 'Unexpected null/undefined value found', diff --git a/desktop/flipper-server-core/src/app-connectivity/WebSocketClientConnection.tsx b/desktop/flipper-server-core/src/app-connectivity/WebSocketClientConnection.tsx index b31aeb04d..dd6ed69f1 100644 --- a/desktop/flipper-server-core/src/app-connectivity/WebSocketClientConnection.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/WebSocketClientConnection.tsx @@ -41,9 +41,11 @@ export default class WebSocketClientConnection implements ClientConnection { const callbacks = this.pendingRequests.get(id); if (!callbacks) { - console.debug(`[conn] Pending request ${id} is not found. Ignore.`); + console.debug(`[ws] Pending request ${id} is not found. Ignore.`); // It must be a response for a message from the older connection. Ignore. - // TODO: When we decide to bump sdk_version, make `id` a string equal to `connectionId:messageId`. Ignore messages only from other conections. Raise an error for missing mesages from this connection. + // TODO: When we decide to bump sdk_version, make `id` a string equal to `connectionId:messageId`. + // Ignore messages only from other conections. + // Raise an error for missing mesages from this connection. return; } diff --git a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/CertificateProvider.tsx b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/CertificateProvider.tsx index 04609f234..8edb07a8c 100644 --- a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/CertificateProvider.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/CertificateProvider.tsx @@ -7,12 +7,13 @@ * @format */ -import {CertificateExchangeMedium} from 'flipper-common'; +import {CertificateExchangeMedium, ClientQuery} from 'flipper-common'; +import {recorder} from '../../recorder'; import { deviceCAcertFile, deviceClientCertFile, ensureOpenSSLIsAvailable, - extractAppNameFromCSR, + extractBundleIdFromCSR, generateClientCertificate, getCACertificate, } from './certificate-utils'; @@ -28,48 +29,37 @@ export default abstract class CertificateProvider { } async processCertificateSigningRequest( + clientQuery: ClientQuery, unsanitizedCsr: string, - os: string, appDirectory: string, ): Promise<{deviceId: string}> { - console.debug( - `${this.constructor.name}.processCertificateSigningRequest`, - unsanitizedCsr, - os, - appDirectory, - ); const csr = this.santitizeString(unsanitizedCsr); if (csr === '') { - return Promise.reject(new Error(`Received empty CSR from ${os} device`)); + const msg = `Received empty CSR from ${clientQuery.os} device`; + recorder.error(clientQuery, msg); + return Promise.reject(new Error(msg)); } - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> ensureOpenSSLIsAvailable`, - os, - appDirectory, - ); + + recorder.log(clientQuery, 'Ensure OpenSSL is available'); await ensureOpenSSLIsAvailable(); + + recorder.log(clientQuery, 'Obtain CA certificate'); const caCert = await getCACertificate(); - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> deploy caCert`, - os, - appDirectory, - ); + + recorder.log(clientQuery, 'Deploy CA certificate to application sandbox'); await this.deployOrStageFileForDevice( appDirectory, deviceCAcertFile, caCert, csr, ); - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> generateClientCertificate`, - os, - appDirectory, - ); + + recorder.log(clientQuery, 'Generate client certificate'); const clientCert = await generateClientCertificate(csr); - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> deploy clientCert`, - os, - appDirectory, + + recorder.log( + clientQuery, + 'Deploy client certificate to application sandbox', ); await this.deployOrStageFileForDevice( appDirectory, @@ -77,20 +67,19 @@ export default abstract class CertificateProvider { clientCert, csr, ); - const appName = await extractAppNameFromCSR(csr); - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> getTargetDeviceId`, - os, - appDirectory, - appName, + + recorder.log(clientQuery, 'Extract application name from CSR'); + const bundleId = await extractBundleIdFromCSR(csr); + + recorder.log( + clientQuery, + 'Get target device from CSR and application name', ); - const deviceId = await this.getTargetDeviceId(appName, appDirectory, csr); - console.debug( - `${this.constructor.name}.processCertificateSigningRequest -> done`, - os, - appDirectory, - appName, - deviceId, + const deviceId = await this.getTargetDeviceId(bundleId, appDirectory, csr); + + recorder.log( + clientQuery, + `Finished processing CSR, device identifier is '${deviceId}'`, ); return { deviceId, @@ -98,9 +87,9 @@ export default abstract class CertificateProvider { } abstract getTargetDeviceId( - _appName: string, - _appDirectory: string, - _csr: string, + bundleId: string, + appDirectory: string, + csr: string, ): Promise; protected abstract deployOrStageFileForDevice( diff --git a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx index c6a6432bc..b02bd9b14 100644 --- a/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx +++ b/desktop/flipper-server-core/src/app-connectivity/certificate-exchange/certificate-utils.tsx @@ -92,7 +92,7 @@ export const loadSecureServerConfig = async (): Promise => { return serverConfig; }; -export const extractAppNameFromCSR = async (csr: string): Promise => { +export const extractBundleIdFromCSR = async (csr: string): Promise => { const path = await writeToTempFile(csr); const subject = await openssl('req', { in: path, diff --git a/desktop/flipper-server-core/src/devices/DummyDevice.tsx b/desktop/flipper-server-core/src/devices/DummyDevice.tsx index 69d6807f5..5673acc3f 100644 --- a/desktop/flipper-server-core/src/devices/DummyDevice.tsx +++ b/desktop/flipper-server-core/src/devices/DummyDevice.tsx @@ -12,7 +12,12 @@ import {FlipperServerImpl} from '../FlipperServerImpl'; import {ServerDevice} from './ServerDevice'; /** - * Use this device when you do not have the actual uuid of the device. For example, it is currently used in the case when, we do certificate exchange through WWW mode. In this mode we do not know the device id of the app and we generate a fake one. + * Use this device when you do not have the actual uuid of the device. + * For example, it is currently used in the case when, we do certificate + * exchange through WWW mode. + * + * In this mode we do not know the device id of the app and we + * generate a fake one. */ export default class DummyDevice extends ServerDevice { constructor( diff --git a/desktop/flipper-server-core/src/devices/ServerDevice.tsx b/desktop/flipper-server-core/src/devices/ServerDevice.tsx index 89dd1439e..5d65f8454 100644 --- a/desktop/flipper-server-core/src/devices/ServerDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ServerDevice.tsx @@ -58,30 +58,28 @@ export abstract class ServerDevice { } async startScreenCapture(_destination: string): Promise { - throw new Error('startScreenCapture not implemented on BaseDevice '); + throw new Error('startScreenCapture not implemented'); } async stopScreenCapture(): Promise { - throw new Error('stopScreenCapture not implemented on BaseDevice '); + throw new Error('stopScreenCapture not implemented'); } async executeShell(_command: string): Promise { - throw new Error('executeShell not implemented on BaseDevice'); + throw new Error('executeShell not implemented'); } async forwardPort(_local: string, _remote: string): Promise { - throw new Error('forwardPort not implemented on BaseDevice'); + throw new Error('forwardPort not implemented'); } - async clearLogs(): Promise { - // no-op on most devices - } + async clearLogs(): Promise {} async navigateToLocation(_location: string) { - throw new Error('navigateLocation not implemented on BaseDevice'); + throw new Error('navigateLocation not implemented'); } async installApp(_appBundlePath: string): Promise { - throw new Error('Install not implemented'); + throw new Error('installApp not implemented'); } } diff --git a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx index c69578a8f..b38118664 100644 --- a/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx +++ b/desktop/flipper-server-core/src/devices/android/AndroidCertificateProvider.tsx @@ -12,7 +12,7 @@ import {Client} from 'adbkit'; import * as androidUtil from './androidContainerUtility'; import { csrFileName, - extractAppNameFromCSR, + extractBundleIdFromCSR, } from '../../app-connectivity/certificate-exchange/certificate-utils'; const logTag = 'AndroidCertificateProvider'; @@ -98,7 +98,7 @@ export default class AndroidCertificateProvider extends CertificateProvider { contents: string, csr: string, ) { - const appName = await extractAppNameFromCSR(csr); + const appName = await extractBundleIdFromCSR(csr); const deviceId = await this.getTargetDeviceId(appName, destination, csr); await androidUtil.push( this.adb, diff --git a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx index 55c410bf3..5487b727b 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSBridge.tsx @@ -197,16 +197,14 @@ export class SimctlBridge implements IOSBridge { async getInstalledApps( _serial: string, ): Promise { - // TODO: Implement me throw new Error( - 'SimctlBridge does not support getInstalledApps. Install IDB (https://fbidb.io/).', + 'SimctlBridge does not support getInstalledApps. Install idb (https://fbidb.io/).', ); } async ls(_serial: string, _appBundleId: string): Promise { - // TODO: Implement me throw new Error( - 'SimctlBridge does not support ls. Install IDB (https://fbidb.io/).', + 'SimctlBridge does not support ls. Install idb (https://fbidb.io/).', ); } @@ -354,7 +352,7 @@ function makeTempScreenshotFilePath() { } async function unzip(filePath: string, destination: string): Promise { - //todo this probably shouldn't involve shelling out... + // TODO: probably shouldn't involve shelling out. await exec(`unzip -qq -o ${filePath} -d ${destination}`); if (!(await fs.pathExists(path.join(destination, 'Payload')))) { throw new Error( @@ -381,12 +379,11 @@ export async function makeIOSBridge( enablePhysicalDevices: boolean, isAvailableFn: (idbPath: string) => Promise = isAvailable, ): Promise { - // prefer idb if (await isAvailableFn(idbPath)) { return new IDBBridge(idbPath, enablePhysicalDevices); } - // no idb, if it's a simulator and xcode is available, we can use xcrun + // If Xcode is available then xcrun instead of idb is used. if (isXcodeDetected) { return new SimctlBridge(); } diff --git a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx index d5ab8fad7..3e88ad3b8 100644 --- a/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx +++ b/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx @@ -58,14 +58,16 @@ export default class IOSDevice this.serial, this.info.deviceType, ); - // It is OK not to await the start of the log listener. We just spawn it and handle errors internally. + // It is OK not to await the start of the log listener. + // We just spawn it and handle errors internally. this.logListener .start() .catch((e) => console.error('IOSDevice.logListener.start -> unexpected error', e), ); this.crashWatcher = new iOSCrashWatcher(this); - // It is OK not to await the start of the crash watcher. We just spawn it and handle errors internally. + // It is OK not to await the start of the crash watcher. + // We just spawn it and handle errors internally. this.crashWatcher .start() .catch((e) => diff --git a/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx b/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx index 47a06ce4e..9e9daf6e1 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSCertificateProvider.tsx @@ -14,7 +14,7 @@ import {promisify} from 'util'; import tmp, {DirOptions} from 'tmp'; import { csrFileName, - extractAppNameFromCSR, + extractBundleIdFromCSR, } from '../../app-connectivity/certificate-exchange/certificate-utils'; import path from 'path'; @@ -37,9 +37,11 @@ export default class iOSCertificateProvider extends CertificateProvider { ): Promise { const matches = /\/Devices\/([^/]+)\//.exec(appDirectory); if (matches && matches.length == 2) { - // It's a simulator, the deviceId is in the filepath. + // It's a simulator, the device identifier is in the filepath. return matches[1]; } + + // Get all available targets const targets = await iosUtil.targets( this.idbConfig.idbPath, this.idbConfig.enablePhysicalIOS, @@ -58,7 +60,7 @@ export default class iOSCertificateProvider extends CertificateProvider { return {id: target.udid, isMatch}; } catch (e) { console.info( - `Unable to check for matching CSR in ${target.udid}:${appName}`, + `[conn] Unable to check for matching CSR in ${target.udid}:${appName}`, logTag, e, ); @@ -71,7 +73,7 @@ export default class iOSCertificateProvider extends CertificateProvider { throw new Error(`No matching device found for app: ${appName}`); } if (matchingIds.length > 1) { - console.warn(`Multiple devices found for app: ${appName}`); + console.warn(`[conn] Multiple devices found for app: ${appName}`); } return matchingIds[0]; } @@ -82,37 +84,33 @@ export default class iOSCertificateProvider extends CertificateProvider { contents: string, csr: string, ) { - console.debug('iOSCertificateProvider.deployOrStageFileForDevice', { + console.debug('[conn] Deploying file to device ', { destination, filename, }); - const appName = await extractAppNameFromCSR(csr); + const bundleId = await extractBundleIdFromCSR(csr); try { await fs.writeFile(destination + filename, contents); } catch (err) { - // Writing directly to FS failed. It's probably a physical device. console.debug( - 'iOSCertificateProvider.deployOrStageFileForDevice -> physical device', + '[conn] Deploying file using idb as physical device is inferred', ); const relativePathInsideApp = this.getRelativePathInAppContainer(destination); - console.debug( - 'iOSCertificateProvider.deployOrStageFileForDevice: realtive path', - relativePathInsideApp, - ); + console.debug(`[conn] Relative path '${relativePathInsideApp}'`); - const udid = await this.getTargetDeviceId(appName, destination, csr); + const udid = await this.getTargetDeviceId(bundleId, destination, csr); await this.pushFileToiOSDevice( udid, - appName, + bundleId, relativePathInsideApp, filename, contents, ); } - console.debug('iOSCertificateProvider.deployOrStageFileForDevice -> done'); + console.debug('[conn] Deploying file to device complete'); } private getRelativePathInAppContainer(absolutePath: string) { @@ -131,12 +129,12 @@ export default class iOSCertificateProvider extends CertificateProvider { contents: string, ): Promise { const dir = await tmpDir({unsafeCleanup: true}); - const filePath = path.resolve(dir, filename); - await fs.writeFile(filePath, contents); + const src = path.resolve(dir, filename); + await fs.writeFile(src, contents); await iosUtil.push( udid, - filePath, + src, bundleId, destination, this.idbConfig.idbPath, @@ -149,69 +147,46 @@ export default class iOSCertificateProvider extends CertificateProvider { bundleId: string, csr: string, ): Promise { - const originalFile = this.getRelativePathInAppContainer( + const src = this.getRelativePathInAppContainer( path.resolve(directory, csrFileName), ); - const dir = await tmpDir({unsafeCleanup: true}); + const dst = await tmpDir({unsafeCleanup: true}); - // Workaround for idb weirdness - // Originally started at D27590885 - // Re-appared at https://github.com/facebook/flipper/issues/3009 - // - // People reported various workarounds. None of them worked consistently for everyone. - // Usually, the workarounds included re-building idb from source or re-installing it. - // - // The only more or less reasonable explanation I was able to find is that the final behavior depends on whether the idb_companion is local or not. - // - // This is how idb_companion sets its locality - // https://github.com/facebook/idb/blob/main/idb_companion/Server/FBIDBServiceHandler.mm#L1507 - // idb sends a connection request and provides a file path to a temporary file. idb_companion checks if it can access that file. - // - // So when it is "local", the pulled filed is written directly to the destination path - // https://github.com/facebook/idb/blob/main/idb/grpc/client.py#L698 - // So it is expected that the destination path ends with a file to write to. - // However, if the companion is remote, then we seem to get here https://github.com/facebook/idb/blob/71791652efa2d5e6f692cb8985ff0d26b69bf08f/idb/common/tar.py#L232 - // Where we create a tree of directories and write the file stream there. - // - // So the only explanation I could come up with is that somehow, by re-installing idb and playing with the env, people could affect the locality of the idb_companion. - // - // The ultimate workaround is to try pulling the cert file without the cert name attached first, if it fails, try to append it. try { - await iosUtil.pull( - deviceId, - originalFile, - bundleId, - dir, - this.idbConfig.idbPath, - ); + await iosUtil.pull(deviceId, src, bundleId, dst, this.idbConfig.idbPath); } catch (e) { console.warn( - 'Original idb pull failed. Most likely it is a physical device that requires us to handle the dest path dirrently. Forcing a re-try with the updated dest path. See D32106952 for details. Original error:', + `[conn] Original idb pull failed. Most likely it is a physical device + that requires us to handle the dest path dirrently. + Forcing a re-try with the updated dest path. See D32106952 for details.`, e, ); await iosUtil.pull( deviceId, - originalFile, + src, bundleId, - path.join(dir, csrFileName), + path.join(dst, csrFileName), this.idbConfig.idbPath, ); console.info( - 'Subsequent idb pull succeeded. Nevermind previous wranings.', + '[conn] Subsequent idb pull succeeded. Nevermind previous wranings.', ); } - const items = await fs.readdir(dir); + const items = await fs.readdir(dst); if (items.length > 1) { - throw new Error('Conflict in temp dir'); + throw new Error('Conflict in temporary dir'); } if (items.length === 0) { - throw new Error('Failed to pull CSR from device'); + throw new Error('No CSR found on device'); } - const fileName = items[0]; - const copiedFile = path.resolve(dir, fileName); - console.debug('Trying to read CSR from', copiedFile); - const data = await fs.readFile(copiedFile); + + const filename = items[0]; + const pulledFile = path.resolve(dst, filename); + + console.debug(`[conn] Read CSR from '${pulledFile}'`); + + const data = await fs.readFile(pulledFile); const csrFromDevice = this.santitizeString(data.toString()); return csrFromDevice === this.santitizeString(csr); } diff --git a/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx b/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx index 9247f6b99..7822fc147 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSContainerUtility.tsx @@ -17,6 +17,7 @@ import {promisify} from 'util'; import child_process from 'child_process'; import fs from 'fs-extra'; import path from 'path'; +import {recorder} from '../../recorder'; const exec = promisify(child_process.exec); export type IdbConfig = { @@ -69,12 +70,22 @@ async function safeExec( async function queryTargetsWithXcode(): Promise> { const cmd = 'xcrun xctrace list devices'; + const description = 'Query available devices with Xcode'; + const troubleshoot = `Xcode command line tools are not installed. + Run 'xcode-select --install' from terminal.`; + try { const {stdout} = await safeExec(cmd); if (!stdout) { + recorder.event('cmd', {cmd, description, success: false, troubleshoot}); throw new Error('No output from command'); } - + recorder.event('cmd', { + cmd, + description, + success: true, + stdout: stdout.toString(), + }); return stdout .toString() .split('\n') @@ -87,7 +98,13 @@ async function queryTargetsWithXcode(): Promise> { return {udid, type: 'physical', name}; }); } catch (e) { - console.warn(`Failed to query devices using '${cmd}'`, e); + recorder.event('cmd', { + cmd, + description, + success: false, + troubleshoot, + stderr: e.toString(), + }); return []; } } @@ -96,14 +113,33 @@ async function queryTargetsWithIdb( idbPath: string, ): Promise> { const cmd = `${idbPath} list-targets --json`; + const description = 'Query available devices with idb'; + const troubleshoot = `Either idb is not installed or needs to be reset. + Run 'idb kill' from terminal.`; + try { const {stdout} = await safeExec(cmd); if (!stdout) { + recorder.event('cmd', {cmd, description, success: false, troubleshoot}); throw new Error('No output from command'); } + + recorder.event('cmd', { + cmd, + description, + success: true, + stdout: stdout.toString(), + }); + return parseIdbTargets(stdout.toString()); } catch (e) { - console.warn(`Failed to execute '${cmd}' for targets.`, e); + recorder.event('cmd', { + cmd, + description, + success: false, + troubleshoot, + stderr: e.toString(), + }); return []; } } @@ -114,6 +150,7 @@ async function queryTargetsWithIdbCompanion( ): Promise> { if (await isAvailable(idbCompanionPath)) { const cmd = `${idbCompanionPath} --list 1 --only device`; + recorder.rawLog(`Query devices with idb companion '${cmd}'`); try { const {stdout} = await safeExec(cmd); if (!stdout) { @@ -122,18 +159,18 @@ async function queryTargetsWithIdbCompanion( const devices = parseIdbTargets(stdout.toString()); if (devices.length > 0 && !isPhysicalDeviceEnabled) { - console.warn( + recorder.rawError( `You are trying to connect Physical Device. Please enable the toggle "Enable physical iOS device" from the setting screen.`, ); } return devices; } catch (e) { - console.warn(`Failed to execute '${cmd}' for targets:`, e); + recorder.rawError(`Failed to query devices using '${cmd}'`, e); return []; } } else { - console.warn( + recorder.rawError( `Unable to locate idb_companion in '${idbCompanionPath}'. Try running sudo yum install -y fb-idb`, ); @@ -177,6 +214,7 @@ async function idbDescribeTarget( idbPath: string, ): Promise { const cmd = `${idbPath} describe --json`; + recorder.rawLog(`Describe target '${cmd}'`); try { const {stdout} = await safeExec(cmd); if (!stdout) { @@ -184,7 +222,7 @@ async function idbDescribeTarget( } return parseIdbTarget(stdout.toString()); } catch (e) { - console.warn(`Failed to execute '${cmd}' to describe a target.`, e); + recorder.rawError(`Failed to execute '${cmd}' to describe a target.`, e); return undefined; } } @@ -209,8 +247,7 @@ async function targets( const isXcodeInstalled = await isXcodeDetected(); if (!isXcodeInstalled) { if (!isPhysicalDeviceEnabled) { - // TODO: Show a notification to enable the toggle or integrate Doctor to better suggest this advice. - console.warn( + recorder.rawError( 'You are trying to connect a physical device. Please enable the toggle "Enable physical iOS device" from the setting screen.', ); } @@ -243,11 +280,13 @@ async function push( await memoize(checkIdbIsInstalled)(idbPath); const push_ = async () => { + const cmd = `${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`; try { - await safeExec( - `${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, - ); + recorder.rawLog(`Push file to device '${cmd}'`); + await safeExec(cmd); + recorder.rawLog(`Successfully pushed file to device`); } catch (e) { + recorder.rawError(`Failed to push file to device`, e); handleMissingIdb(e, idbPath); throw e; } @@ -266,11 +305,13 @@ async function pull( await memoize(checkIdbIsInstalled)(idbPath); const pull_ = async () => { + const cmd = `${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`; try { - await safeExec( - `${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, - ); + recorder.rawLog(`Pull file from device '${cmd}'`); + await safeExec(cmd); + recorder.rawLog(`Successfully pulled file from device`); } catch (e) { + recorder.rawError(`Failed to pull file from device`, e); handleMissingIdb(e, idbPath); handleMissingPermissions(e); throw e; diff --git a/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx index 49f37880f..96f16e466 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSCrashUtils.tsx @@ -72,7 +72,8 @@ export function shouldShowiOSCrashNotification( ): boolean { const appPath = legacy ? parsePathLegacy(content) : parsePathModern(content); if (!appPath || !appPath.includes(serial)) { - // Do not show notifications for the app which are not running on this device + // Do not show notifications for the app which + // are not running on this device. return false; } return true; diff --git a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx index ac6ed2c09..7cc27a598 100644 --- a/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx +++ b/desktop/flipper-server-core/src/devices/ios/iOSDeviceManager.tsx @@ -117,7 +117,7 @@ export class IOSDeviceManager { if (currentDeviceIDs.has(udid)) { currentDeviceIDs.delete(udid); } else { - console.info(`[conn] detected new iOS device ${udid}`, activeDevice); + console.info(`[conn] Detected new iOS device ${udid}`, activeDevice); const iOSDevice = new IOSDevice( this.flipperServer, bridge, diff --git a/desktop/flipper-server-core/src/index.tsx b/desktop/flipper-server-core/src/index.tsx index ddcbd44af..a43cd9ca0 100644 --- a/desktop/flipper-server-core/src/index.tsx +++ b/desktop/flipper-server-core/src/index.tsx @@ -9,7 +9,7 @@ export {FlipperServerImpl} from './FlipperServerImpl'; export {loadSettings} from './utils/settings'; -export * from './utils/tracker'; +export * from './tracker'; export {loadLauncherSettings} from './utils/launcherSettings'; export {loadProcessConfig} from './utils/processConfig'; export {getEnvironmentInfo} from './utils/environmentInfo'; diff --git a/desktop/flipper-server-core/src/recorder.tsx b/desktop/flipper-server-core/src/recorder.tsx new file mode 100644 index 000000000..ca4f73eb6 --- /dev/null +++ b/desktop/flipper-server-core/src/recorder.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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 {ClientQuery} from 'flipper-common'; + +type CommandEventPayload = { + cmd: string; + description: string; + success: boolean; + stdout?: string; + stderr?: string; + troubleshoot?: string; +}; + +type ConnectionRecorderEvents = { + cmd: CommandEventPayload; +}; + +class Recorder { + private handler_ = { + cmd: (_payload: CommandEventPayload) => { + // The output from logging the whole command can be quite + // verbose. So, disable it as is. + // this.rawLog(_payload); + }, + }; + + event( + event: Event, + payload: ConnectionRecorderEvents[Event], + ): void { + const handler: (...args: any[]) => void = this.handler_[event]; + if (!handler) { + return; + } + handler(payload); + } + + rawLog(...args: any[]) { + console.log('[conn]', ...args); + } + log(clientQuery: ClientQuery, ...args: any[]) { + console.log('[conn]', ...args); + } + rawError(...args: any[]) { + console.error('[conn]', ...args); + } + error(clientQuery: ClientQuery, ...args: any[]) { + console.error('[conn]', ...args); + } +} + +const recorder = new Recorder(); +export {recorder}; diff --git a/desktop/flipper-server-core/src/server/startServer.tsx b/desktop/flipper-server-core/src/server/startServer.tsx index bfab9dc09..daf7ba8d7 100644 --- a/desktop/flipper-server-core/src/server/startServer.tsx +++ b/desktop/flipper-server-core/src/server/startServer.tsx @@ -24,7 +24,7 @@ import {attachSocketServer} from './attachSocketServer'; import {FlipperServerImpl} from '../FlipperServerImpl'; import {FlipperServerCompanionEnv} from 'flipper-server-companion'; import {validateAuthToken} from '../app-connectivity/certificate-exchange/certificate-utils'; -import {tracker} from '../utils/tracker'; +import {tracker} from '../tracker'; type Config = { port: number; diff --git a/desktop/flipper-server-core/src/utils/tracker.tsx b/desktop/flipper-server-core/src/tracker.tsx similarity index 100% rename from desktop/flipper-server-core/src/utils/tracker.tsx rename to desktop/flipper-server-core/src/tracker.tsx diff --git a/desktop/flipper-ui-core/src/chrome/ConnectionTroubleshootTools.tsx b/desktop/flipper-ui-core/src/chrome/ConnectivityHub.tsx similarity index 87% rename from desktop/flipper-ui-core/src/chrome/ConnectionTroubleshootTools.tsx rename to desktop/flipper-ui-core/src/chrome/ConnectivityHub.tsx index 1179fb91d..1d710f8b8 100644 --- a/desktop/flipper-ui-core/src/chrome/ConnectionTroubleshootTools.tsx +++ b/desktop/flipper-ui-core/src/chrome/ConnectivityHub.tsx @@ -10,6 +10,6 @@ import {Layout} from '../ui'; import React from 'react'; -export function ConnectionTroubleshootTools() { +export function ConnectivityHub() { return Connection Troubleshoot; } diff --git a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx index 4f7f2365b..bd10da789 100644 --- a/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx +++ b/desktop/flipper-ui-core/src/sandy-chrome/SandyApp.tsx @@ -44,7 +44,7 @@ import {isFBEmployee} from '../utils/fbEmployee'; import {notification} from 'antd'; import isProduction from '../utils/isProduction'; import {getRenderHostInstance} from 'flipper-frontend-core'; -import {ConnectionTroubleshootTools} from '../chrome/ConnectionTroubleshootTools'; +import {ConnectivityHub} from '../chrome/ConnectivityHub'; export type ToplevelNavItem = | 'appinspect' @@ -91,7 +91,7 @@ export function SandyApp() { dispatch(setStaticView(FlipperDevTools)); break; case 'connectivity': - dispatch(setStaticView(ConnectionTroubleshootTools)); + dispatch(setStaticView(ConnectivityHub)); break; default: }