Centralise logging

Summary:
Centralise connectivity logging into a single place. By having all logs go through a single interface, then it becomes trivial to manipulate them as needed.

In this change, this is not done.

In subsequent diffs, logs will be dispatched via an event and will be visualised in the Connectivity Hub.

Reviewed By: passy

Differential Revision: D47185054

fbshipit-source-id: fb5eab98895be0c8f61fb9a77d3e66d6a8dbcb27
This commit is contained in:
Lorenzo Blasa
2023-07-10 04:14:14 -07:00
committed by Facebook GitHub Bot
parent 49d1a8b0fa
commit fc38355eee
22 changed files with 351 additions and 258 deletions

View File

@@ -18,6 +18,7 @@ import {
import WebSocketClientConnection from './WebSocketClientConnection'; import WebSocketClientConnection from './WebSocketClientConnection';
import {serializeError} from 'serialize-error'; import {serializeError} from 'serialize-error';
import {WSCloseCode} from '../utils/WSCloseCode'; import {WSCloseCode} from '../utils/WSCloseCode';
import {recorder} from '../recorder';
export interface SecureConnectionCtx extends ConnectionCtx { export interface SecureConnectionCtx extends ConnectionCtx {
clientQuery?: SecureClientQuery; clientQuery?: SecureClientQuery;
@@ -39,15 +40,15 @@ class SecureServerWebSocket extends ServerWebSocket {
const {clientQuery, ws} = ctx; const {clientQuery, ws} = ctx;
assertNotNull(clientQuery); assertNotNull(clientQuery);
console.info( recorder.log(
`[conn] Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}. Medium ${clientQuery.medium}. CSR: ${clientQuery.csr_path}`, clientQuery,
`Secure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device}.`,
); );
this.listener.onSecureConnectionAttempt(clientQuery); this.listener.onSecureConnectionAttempt(clientQuery);
const clientConnection = new WebSocketClientConnection(ws); const clientConnection = new WebSocketClientConnection(ws);
// TODO: Could we just await it here? How much time could it be, potentially?
// DRI: @aigoncharov
const clientPromise: Promise<ClientDescription> = this.listener const clientPromise: Promise<ClientDescription> = this.listener
.onConnectionCreated(clientQuery, clientConnection) .onConnectionCreated(clientQuery, clientConnection)
.then((client) => { .then((client) => {
@@ -96,7 +97,6 @@ class SecureServerWebSocket extends ServerWebSocket {
} }
// Received an "execute" message // Received an "execute" message
if (client) { if (client) {
this.listener.onClientMessage(client.id, rawMessage); this.listener.onClientMessage(client.id, rawMessage);
} else { } else {

View File

@@ -23,11 +23,7 @@ import {ClientConnection, ConnectionStatus} from './ClientConnection';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import invariant from 'invariant'; import invariant from 'invariant';
import DummyDevice from '../devices/DummyDevice'; import DummyDevice from '../devices/DummyDevice';
import { import {appNameWithUpdateHint, assertNotNull} from './Utilities';
appNameWithUpdateHint,
assertNotNull,
cloneClientQuerySafeForLogging,
} from './Utilities';
import ServerWebSocketBase, {ServerEventsListener} from './ServerWebSocketBase'; import ServerWebSocketBase, {ServerEventsListener} from './ServerWebSocketBase';
import { import {
createBrowserServer, createBrowserServer,
@@ -40,12 +36,13 @@ import {
getFlipperServerConfig, getFlipperServerConfig,
} from '../FlipperServerConfig'; } from '../FlipperServerConfig';
import { import {
extractAppNameFromCSR, extractBundleIdFromCSR,
loadSecureServerConfig, loadSecureServerConfig,
} from './certificate-exchange/certificate-utils'; } from './certificate-exchange/certificate-utils';
import DesktopCertificateProvider from '../devices/desktop/DesktopCertificateProvider'; import DesktopCertificateProvider from '../devices/desktop/DesktopCertificateProvider';
import WWWCertificateProvider from '../fb-stubs/WWWCertificateProvider'; import WWWCertificateProvider from '../fb-stubs/WWWCertificateProvider';
import {tracker} from '../utils/tracker'; import {tracker} from '../tracker';
import {recorder} from '../recorder';
type ClientTimestampTracker = { type ClientTimestampTracker = {
insecureStart?: number; insecureStart?: number;
@@ -114,10 +111,10 @@ export class ServerController
const options = await loadSecureServerConfig(); 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); this.secureServer = await createServer(secure, this, options);
const {secure: altSecure} = getServerPortsConfig().altServerPorts; 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( this.altSecureServer = await createServer(
altSecure, altSecure,
this, this,
@@ -125,13 +122,10 @@ export class ServerController
TransportType.WebSocket, 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); this.insecureServer = await createServer(insecure, this);
const {insecure: altInsecure} = getServerPortsConfig().altServerPorts; const {insecure: altInsecure} = getServerPortsConfig().altServerPorts;
console.info( console.info('[ws] insecure server listening at port: ', altInsecure);
'[conn] insecure server (ws) listening at port: ',
altInsecure,
);
this.altInsecureServer = await createServer( this.altInsecureServer = await createServer(
altInsecure, altInsecure,
this, this,
@@ -140,7 +134,7 @@ export class ServerController
); );
const browserPort = getServerPortsConfig().browserPort; 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); this.browserServer = await createBrowserServer(browserPort, this);
} }
@@ -174,11 +168,7 @@ export class ServerController
rsocket, rsocket,
} = clientQuery; } = clientQuery;
console.info( recorder.log(clientQuery, 'Connection established');
`[conn] Connection established: ${app} on ${device_id}. Medium ${medium}. CSR: ${csr_path}`,
cloneClientQuerySafeForLogging(clientQuery),
);
tracker.track('app-connection-created', { tracker.track('app-connection-created', {
app, app,
os, os,
@@ -233,17 +223,25 @@ export class ServerController
medium: clientQuery.medium, medium: clientQuery.medium,
}); });
const {os, app, device_id} = clientQuery; // Without these checks, the user might see a connection timeout error instead,
// without these checks, the user might see a connection timeout error instead, which would be much harder to track down // which would be much harder to track down
if (os === 'iOS' && !getFlipperServerConfig().settings.enableIOS) { if (
console.error( clientQuery.os === 'iOS' &&
`Refusing connection from ${app} on ${device_id}, since iOS support is disabled in settings`, !getFlipperServerConfig().settings.enableIOS
) {
recorder.error(
clientQuery,
`Refusing connection since iOS support is disabled in settings`,
); );
return; return;
} }
if (os === 'Android' && !getFlipperServerConfig().settings.enableAndroid) { if (
console.error( clientQuery.os === 'Android' &&
`Refusing connection from ${app} on ${device_id}, since Android support is disabled in settings`, !getFlipperServerConfig().settings.enableAndroid
) {
recorder.error(
clientQuery,
`Refusing connection since Android support is disabled in settings`,
); );
return; 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 { 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 strippedClientQuery = (({device_id, ...o}) => o)(clientQuery);
const id = buildClientId({device_id: 'unknown', ...strippedClientQuery}); const id = buildClientId({device_id: 'unknown', ...strippedClientQuery});
this.timestamps.set(id, { this.timestamps.set(id, {
insecureStart: Date.now(), insecureStart: Date.now(),
}); });
tracker.track('app-connection-insecure-attempt', clientQuery); tracker.track('app-connection-insecure-attempt', clientQuery);
recorder.log(clientQuery, 'Insecure connection attempt');
this.connectionTracker.logConnectionAttempt(clientQuery); this.connectionTracker.logConnectionAttempt(clientQuery);
@@ -325,23 +333,30 @@ export class ServerController
} }
default: { default: {
throw new Error( 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); certificateProvider.verifyMedium(clientQuery.medium);
recorder.log(clientQuery, 'Certificate Signing Request being processed');
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
reportPlatformFailures( reportPlatformFailures(
certificateProvider.processCertificateSigningRequest( certificateProvider.processCertificateSigningRequest(
clientQuery,
unsanitizedCSR, unsanitizedCSR,
clientQuery.os,
appDirectory, appDirectory,
), ),
'processCertificateSigningRequest', 'processCertificateSigningRequest',
) )
.then((response) => { .then((response) => {
recorder.log(
clientQuery,
'Certificate Signing Request successfully processed',
);
const client: UninitializedClient = { const client: UninitializedClient = {
os: clientQuery.os, os: clientQuery.os,
deviceName: clientQuery.device, deviceName: clientQuery.device,
@@ -392,12 +407,7 @@ export class ServerController
} }
onClientSetupError(clientQuery: ClientQuery, e: any) { onClientSetupError(clientQuery: ClientQuery, e: any) {
console.warn( recorder.error(clientQuery, 'Failed to exchange certificate', e);
`[conn] Failed to exchange certificate with ${clientQuery.app} on ${
clientQuery.device || clientQuery.device_id
}`,
e,
);
const client: UninitializedClient = { const client: UninitializedClient = {
os: clientQuery.os, os: clientQuery.os,
deviceName: clientQuery.device, deviceName: clientQuery.device,
@@ -405,58 +415,56 @@ export class ServerController
}; };
this.emit('client-setup-error', { this.emit('client-setup-error', {
client, client,
error: `[conn] Failed to exchange certificate with ${ error: `Failed to exchange certificate with ${clientQuery.app} on ${
clientQuery.app clientQuery.device || clientQuery.device_id
} on ${clientQuery.device || clientQuery.device_id}: ${e}`, }: ${e}`,
}); });
} }
/** /**
* Creates a Client and sets the underlying connection. * Creates a Client and sets the underlying connection.
* @param connection A client connection to communicate between server and client. * @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. * @param csrQuery The CSR query which contains CSR related information.
*/ */
async addConnection( async addConnection(
connection: ClientConnection, connection: ClientConnection,
query: SecureClientQuery, clientQuery: SecureClientQuery,
silentReplace?: boolean, silentReplace?: boolean,
): Promise<ClientDescription> { ): Promise<ClientDescription> {
invariant(query, 'expected query'); invariant(clientQuery, 'expected query');
// try to get id by comparing giving `csr` to file from `csr_path` // try to get id by comparing giving `csr` to file from `csr_path`
// otherwise, use given device_id // otherwise, use given device_id.
const {csr_path, csr} = query; const {csr_path, csr} = clientQuery;
// For Android, device id might change // For Android, device id might change
if (csr_path && csr && query.os === 'Android') { if (csr_path && csr && clientQuery.os === 'Android') {
const app_name = await extractAppNameFromCSR(csr); const bundleId = await extractBundleIdFromCSR(csr);
assertNotNull(this.flipperServer.android); assertNotNull(this.flipperServer.android);
// TODO: allocate new object, kept now as is to keep changes minimal (clientQuery as any).device_id =
(query as any).device_id =
await this.flipperServer.android.certificateProvider.getTargetDeviceId( await this.flipperServer.android.certificateProvider.getTargetDeviceId(
app_name, bundleId,
csr_path, csr_path,
csr, csr,
); );
console.info( recorder.log(
`[conn] Detected ${app_name} on ${query.device_id} in certificate`, clientQuery,
query, `Detected ${bundleId} on ${clientQuery.device_id} in certificate`,
); );
} }
// TODO: allocate new object, kept now as is to keep changes minimal (clientQuery as any).app = appNameWithUpdateHint(clientQuery);
(query as any).app = appNameWithUpdateHint(query);
const id = buildClientId(query); const id = buildClientId(clientQuery);
console.info( recorder.log(
`[conn] Matching device for ${query.app} on ${query.device_id}...`, clientQuery,
query, `Matching device for ${clientQuery.app} on ${clientQuery.device_id}`,
); );
const client: ClientDescription = { const client: ClientDescription = {
id, id,
query, query: clientQuery,
}; };
const info = { const info = {
@@ -464,9 +472,9 @@ export class ServerController
connection: connection, connection: connection,
}; };
console.info( recorder.log(
`[conn] Initializing client ${query.app} on ${query.device_id}...`, clientQuery,
query, `Initializing client ${clientQuery.app} on ${clientQuery.device_id}`,
); );
connection.subscribeToEvents((status: ConnectionStatus) => { 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, /* If a device gets disconnected without being cleaned up properly,
* Flipper won't be aware until it attempts to reconnect. * Flipper won't be aware until it attempts to reconnect.
@@ -508,12 +516,15 @@ export class ServerController
const start = tracker.insecureStart const start = tracker.insecureStart
? tracker.insecureStart ? tracker.insecureStart
: tracker.secureStart; : tracker.secureStart;
const elapsed = Math.round(end - start!);
this.logger.track('performance', 'client-connection-tracker', { if (start) {
'time-to-connection': elapsed, const elapsed = Math.round(end - start);
...query, this.logger.track('performance', 'client-connection-tracker', {
}); 'time-to-connection': elapsed,
this.timestamps.delete(id); ...clientQuery,
});
this.timestamps.delete(id);
}
} }
return client; return client;
@@ -534,9 +545,9 @@ export class ServerController
removeConnection(id: string) { removeConnection(id: string) {
const info = this.connections.get(id); const info = this.connections.get(id);
if (info) { if (info) {
console.info( recorder.log(
`[conn] Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`,
info.client.query, info.client.query,
`Disconnected: ${info.client.query.app} on ${info.client.query.device_id}.`,
); );
this.flipperServer.emit('client-disconnected', {id}); this.flipperServer.emit('client-disconnected', {id});
this.connections.delete(id); this.connections.delete(id);
@@ -566,21 +577,21 @@ class ConnectionTracker {
this.logger = logger; this.logger = logger;
} }
logConnectionAttempt(client: ClientQuery) { logConnectionAttempt(clientQuery: ClientQuery) {
const key = `${client.os}-${client.device}-${client.app}`; const key = `${clientQuery.os}-${clientQuery.device}-${clientQuery.app}`;
const time = Date.now(); const time = Date.now();
let entry = this.connectionAttempts.get(key) || []; let entry = this.connectionAttempts.get(key) || [];
entry.push(time); entry.push(time);
entry = entry.filter((t) => t >= time - this.timeWindowMillis); entry = entry.filter((t) => t >= time - this.timeWindowMillis);
this.connectionAttempts.set(key, entry); this.connectionAttempts.set(key, entry);
if (entry.length >= this.connectionProblemThreshold) { if (entry.length >= this.connectionProblemThreshold) {
console.warn( recorder.error(
`[conn] Connection loop detected with ${key}. Connected ${ clientQuery,
`Connection loop detected with ${key}. Connected ${
this.connectionProblemThreshold this.connectionProblemThreshold
} times within ${this.timeWindowMillis / 1000}s.`, } times within ${this.timeWindowMillis / 1000}s.`,
'server',
client,
); );
} }
} }

View File

@@ -28,6 +28,7 @@ import {SecureServerConfig} from './certificate-exchange/certificate-utils';
import {Server} from 'net'; import {Server} from 'net';
import {serializeError} from 'serialize-error'; import {serializeError} from 'serialize-error';
import {WSCloseCode} from '../utils/WSCloseCode'; import {WSCloseCode} from '../utils/WSCloseCode';
import {recorder} from '../recorder';
export interface ConnectionCtx { export interface ConnectionCtx {
clientQuery?: ClientQuery; clientQuery?: ClientQuery;
@@ -73,7 +74,9 @@ class ServerWebSocket extends ServerWebSocketBase {
wsServer.once('error', onConnectionError); wsServer.once('error', onConnectionError);
server.listen(port, () => { server.listen(port, () => {
console.debug( console.debug(
`${sslConfig ? 'Secure' : 'Insecure'} server started on port ${port}`, `[ws] ${
sslConfig ? 'Secure' : 'Insecure'
} server started on port ${port}`,
'server', 'server',
); );
@@ -96,7 +99,7 @@ class ServerWebSocket extends ServerWebSocketBase {
'connection', 'connection',
(ws: WebSocket, request: IncomingMessage) => { (ws: WebSocket, request: IncomingMessage) => {
ws.on('error', (error) => { ws.on('error', (error) => {
console.error('[conn] WS connection error:', error); console.error('[ws] Connection error:', error);
this.listener.onError(error); this.listener.onError(error);
}); });
@@ -123,7 +126,7 @@ class ServerWebSocket extends ServerWebSocketBase {
}, },
); );
this.wsServer.on('error', (error) => { this.wsServer.on('error', (error) => {
console.error('[conn] WS server error:', error); console.error('[ws] Server error:', error);
this.listener.onError(error); this.listener.onError(error);
}); });
@@ -136,7 +139,7 @@ class ServerWebSocket extends ServerWebSocketBase {
} }
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
console.info('[conn] Stopping WS server'); console.info('[ws] Stopping server');
assertNotNull(this.wsServer); assertNotNull(this.wsServer);
this.wsServer.close((err) => { this.wsServer.close((err) => {
if (err) { if (err) {
@@ -147,7 +150,7 @@ class ServerWebSocket extends ServerWebSocketBase {
}); });
}); });
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
console.info('[conn] Stopping HTTP server'); console.info('[ws] Stopping HTTP server');
assertNotNull(this.httpServer); assertNotNull(this.httpServer);
this.httpServer.close((err) => { this.httpServer.close((err) => {
if (err) { if (err) {
@@ -175,7 +178,10 @@ class ServerWebSocket extends ServerWebSocketBase {
ws.on('message', async (message: WebSocket.RawData) => { ws.on('message', async (message: WebSocket.RawData) => {
const messageString = message.toString(); const messageString = message.toString();
try { try {
const parsedMessage = this.handleMessageDeserialization(messageString); const parsedMessage = this.handleMessageDeserialization(
ctx,
messageString,
);
// Successful deserialization is a proof that the message is a string // Successful deserialization is a proof that the message is a string
this.handleMessage(ctx, parsedMessage, messageString); this.handleMessage(ctx, parsedMessage, messageString);
} catch (error) { } catch (error) {
@@ -183,7 +189,7 @@ class ServerWebSocket extends ServerWebSocketBase {
// all other plugins might still be working correctly. So let's just report it. // 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) // This avoids ping-ponging connections if an individual plugin sends garbage (e.g. T129428800)
// or throws an error when handling messages // 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) { if (!clientQuery) {
console.warn( 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, request.url,
); );
throw new UnableToExtractClientQueryError( 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; const {clientQuery} = ctx;
assertNotNull(clientQuery); assertNotNull(clientQuery);
console.info( recorder.log(
`[conn] Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`, clientQuery,
`Insecure websocket connection attempt: ${clientQuery.app} on ${clientQuery.device_id}.`,
); );
this.listener.onConnectionAttempt(clientQuery); 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); const parsedMessage = parseMessageToJson(message);
if (!parsedMessage) { if (!parsedMessage) {
console.error('[conn] Failed to parse message', message); recorder.error(clientQuery, 'Failed to parse message', message);
throw new Error(`[conn] Failed to parse message`); throw new Error(`Failed to parse message`);
} }
return parsedMessage; return parsedMessage;
} }

View File

@@ -71,7 +71,7 @@ export function isWsResponseMessage(
return typeof (message as ResponseMessage).id === 'number'; return typeof (message as ResponseMessage).id === 'number';
} }
const certExchangeSupportedOSes = new Set<DeviceOS>([ const supportedOSForCertificateExchange = new Set<DeviceOS>([
'Android', 'Android',
'iOS', 'iOS',
'MacOS', 'MacOS',
@@ -85,7 +85,7 @@ const certExchangeSupportedOSes = new Set<DeviceOS>([
export function verifyClientQueryComesFromCertExchangeSupportedOS( export function verifyClientQueryComesFromCertExchangeSupportedOS(
query: ClientQuery | undefined, query: ClientQuery | undefined,
): ClientQuery | undefined { ): ClientQuery | undefined {
if (!query || !certExchangeSupportedOSes.has(query.os)) { if (!query || !supportedOSForCertificateExchange.has(query.os)) {
return; return;
} }
return query; return query;
@@ -141,22 +141,22 @@ export function parseClientQuery(
throw new Error('Unsupported exchange medium: ' + medium); 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 = { const clientQuery: ClientQuery = {
device_id, device_id,
device, device,
app, app,
os, os,
medium: transformCertificateExchangeMediumToType(medium), 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; return clientQuery;
} }
@@ -213,7 +213,6 @@ export function cloneClientQuerySafeForLogging(clientQuery: SecureClientQuery) {
return {...clientQuery, csr: !clientQuery.csr ? clientQuery.csr : '<hidden>'}; return {...clientQuery, csr: !clientQuery.csr ? clientQuery.csr : '<hidden>'};
} }
// TODO: Merge with the same fn in desktop/app/src/utils
export function assertNotNull<T extends any>( export function assertNotNull<T extends any>(
value: T, value: T,
message: string = 'Unexpected null/undefined value found', message: string = 'Unexpected null/undefined value found',

View File

@@ -41,9 +41,11 @@ export default class WebSocketClientConnection implements ClientConnection {
const callbacks = this.pendingRequests.get(id); const callbacks = this.pendingRequests.get(id);
if (!callbacks) { 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. // 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; return;
} }

View File

@@ -7,12 +7,13 @@
* @format * @format
*/ */
import {CertificateExchangeMedium} from 'flipper-common'; import {CertificateExchangeMedium, ClientQuery} from 'flipper-common';
import {recorder} from '../../recorder';
import { import {
deviceCAcertFile, deviceCAcertFile,
deviceClientCertFile, deviceClientCertFile,
ensureOpenSSLIsAvailable, ensureOpenSSLIsAvailable,
extractAppNameFromCSR, extractBundleIdFromCSR,
generateClientCertificate, generateClientCertificate,
getCACertificate, getCACertificate,
} from './certificate-utils'; } from './certificate-utils';
@@ -28,48 +29,37 @@ export default abstract class CertificateProvider {
} }
async processCertificateSigningRequest( async processCertificateSigningRequest(
clientQuery: ClientQuery,
unsanitizedCsr: string, unsanitizedCsr: string,
os: string,
appDirectory: string, appDirectory: string,
): Promise<{deviceId: string}> { ): Promise<{deviceId: string}> {
console.debug(
`${this.constructor.name}.processCertificateSigningRequest`,
unsanitizedCsr,
os,
appDirectory,
);
const csr = this.santitizeString(unsanitizedCsr); const csr = this.santitizeString(unsanitizedCsr);
if (csr === '') { 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`, recorder.log(clientQuery, 'Ensure OpenSSL is available');
os,
appDirectory,
);
await ensureOpenSSLIsAvailable(); await ensureOpenSSLIsAvailable();
recorder.log(clientQuery, 'Obtain CA certificate');
const caCert = await getCACertificate(); const caCert = await getCACertificate();
console.debug(
`${this.constructor.name}.processCertificateSigningRequest -> deploy caCert`, recorder.log(clientQuery, 'Deploy CA certificate to application sandbox');
os,
appDirectory,
);
await this.deployOrStageFileForDevice( await this.deployOrStageFileForDevice(
appDirectory, appDirectory,
deviceCAcertFile, deviceCAcertFile,
caCert, caCert,
csr, csr,
); );
console.debug(
`${this.constructor.name}.processCertificateSigningRequest -> generateClientCertificate`, recorder.log(clientQuery, 'Generate client certificate');
os,
appDirectory,
);
const clientCert = await generateClientCertificate(csr); const clientCert = await generateClientCertificate(csr);
console.debug(
`${this.constructor.name}.processCertificateSigningRequest -> deploy clientCert`, recorder.log(
os, clientQuery,
appDirectory, 'Deploy client certificate to application sandbox',
); );
await this.deployOrStageFileForDevice( await this.deployOrStageFileForDevice(
appDirectory, appDirectory,
@@ -77,20 +67,19 @@ export default abstract class CertificateProvider {
clientCert, clientCert,
csr, csr,
); );
const appName = await extractAppNameFromCSR(csr);
console.debug( recorder.log(clientQuery, 'Extract application name from CSR');
`${this.constructor.name}.processCertificateSigningRequest -> getTargetDeviceId`, const bundleId = await extractBundleIdFromCSR(csr);
os,
appDirectory, recorder.log(
appName, clientQuery,
'Get target device from CSR and application name',
); );
const deviceId = await this.getTargetDeviceId(appName, appDirectory, csr); const deviceId = await this.getTargetDeviceId(bundleId, appDirectory, csr);
console.debug(
`${this.constructor.name}.processCertificateSigningRequest -> done`, recorder.log(
os, clientQuery,
appDirectory, `Finished processing CSR, device identifier is '${deviceId}'`,
appName,
deviceId,
); );
return { return {
deviceId, deviceId,
@@ -98,9 +87,9 @@ export default abstract class CertificateProvider {
} }
abstract getTargetDeviceId( abstract getTargetDeviceId(
_appName: string, bundleId: string,
_appDirectory: string, appDirectory: string,
_csr: string, csr: string,
): Promise<string>; ): Promise<string>;
protected abstract deployOrStageFileForDevice( protected abstract deployOrStageFileForDevice(

View File

@@ -92,7 +92,7 @@ export const loadSecureServerConfig = async (): Promise<SecureServerConfig> => {
return serverConfig; return serverConfig;
}; };
export const extractAppNameFromCSR = async (csr: string): Promise<string> => { export const extractBundleIdFromCSR = async (csr: string): Promise<string> => {
const path = await writeToTempFile(csr); const path = await writeToTempFile(csr);
const subject = await openssl('req', { const subject = await openssl('req', {
in: path, in: path,

View File

@@ -12,7 +12,12 @@ import {FlipperServerImpl} from '../FlipperServerImpl';
import {ServerDevice} from './ServerDevice'; 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 { export default class DummyDevice extends ServerDevice {
constructor( constructor(

View File

@@ -58,30 +58,28 @@ export abstract class ServerDevice {
} }
async startScreenCapture(_destination: string): Promise<void> { async startScreenCapture(_destination: string): Promise<void> {
throw new Error('startScreenCapture not implemented on BaseDevice '); throw new Error('startScreenCapture not implemented');
} }
async stopScreenCapture(): Promise<string> { async stopScreenCapture(): Promise<string> {
throw new Error('stopScreenCapture not implemented on BaseDevice '); throw new Error('stopScreenCapture not implemented');
} }
async executeShell(_command: string): Promise<string> { async executeShell(_command: string): Promise<string> {
throw new Error('executeShell not implemented on BaseDevice'); throw new Error('executeShell not implemented');
} }
async forwardPort(_local: string, _remote: string): Promise<boolean> { async forwardPort(_local: string, _remote: string): Promise<boolean> {
throw new Error('forwardPort not implemented on BaseDevice'); throw new Error('forwardPort not implemented');
} }
async clearLogs(): Promise<void> { async clearLogs(): Promise<void> {}
// no-op on most devices
}
async navigateToLocation(_location: string) { async navigateToLocation(_location: string) {
throw new Error('navigateLocation not implemented on BaseDevice'); throw new Error('navigateLocation not implemented');
} }
async installApp(_appBundlePath: string): Promise<void> { async installApp(_appBundlePath: string): Promise<void> {
throw new Error('Install not implemented'); throw new Error('installApp not implemented');
} }
} }

View File

@@ -12,7 +12,7 @@ import {Client} from 'adbkit';
import * as androidUtil from './androidContainerUtility'; import * as androidUtil from './androidContainerUtility';
import { import {
csrFileName, csrFileName,
extractAppNameFromCSR, extractBundleIdFromCSR,
} from '../../app-connectivity/certificate-exchange/certificate-utils'; } from '../../app-connectivity/certificate-exchange/certificate-utils';
const logTag = 'AndroidCertificateProvider'; const logTag = 'AndroidCertificateProvider';
@@ -98,7 +98,7 @@ export default class AndroidCertificateProvider extends CertificateProvider {
contents: string, contents: string,
csr: string, csr: string,
) { ) {
const appName = await extractAppNameFromCSR(csr); const appName = await extractBundleIdFromCSR(csr);
const deviceId = await this.getTargetDeviceId(appName, destination, csr); const deviceId = await this.getTargetDeviceId(appName, destination, csr);
await androidUtil.push( await androidUtil.push(
this.adb, this.adb,

View File

@@ -197,16 +197,14 @@ export class SimctlBridge implements IOSBridge {
async getInstalledApps( async getInstalledApps(
_serial: string, _serial: string,
): Promise<IOSInstalledAppDescriptor[]> { ): Promise<IOSInstalledAppDescriptor[]> {
// TODO: Implement me
throw new Error( 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<string[]> { async ls(_serial: string, _appBundleId: string): Promise<string[]> {
// TODO: Implement me
throw new Error( 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<void> { async function unzip(filePath: string, destination: string): Promise<void> {
//todo this probably shouldn't involve shelling out... // TODO: probably shouldn't involve shelling out.
await exec(`unzip -qq -o ${filePath} -d ${destination}`); await exec(`unzip -qq -o ${filePath} -d ${destination}`);
if (!(await fs.pathExists(path.join(destination, 'Payload')))) { if (!(await fs.pathExists(path.join(destination, 'Payload')))) {
throw new Error( throw new Error(
@@ -381,12 +379,11 @@ export async function makeIOSBridge(
enablePhysicalDevices: boolean, enablePhysicalDevices: boolean,
isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable, isAvailableFn: (idbPath: string) => Promise<boolean> = isAvailable,
): Promise<IOSBridge> { ): Promise<IOSBridge> {
// prefer idb
if (await isAvailableFn(idbPath)) { if (await isAvailableFn(idbPath)) {
return new IDBBridge(idbPath, enablePhysicalDevices); 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) { if (isXcodeDetected) {
return new SimctlBridge(); return new SimctlBridge();
} }

View File

@@ -58,14 +58,16 @@ export default class IOSDevice
this.serial, this.serial,
this.info.deviceType, 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 this.logListener
.start() .start()
.catch((e) => .catch((e) =>
console.error('IOSDevice.logListener.start -> unexpected error', e), console.error('IOSDevice.logListener.start -> unexpected error', e),
); );
this.crashWatcher = new iOSCrashWatcher(this); 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 this.crashWatcher
.start() .start()
.catch((e) => .catch((e) =>

View File

@@ -14,7 +14,7 @@ import {promisify} from 'util';
import tmp, {DirOptions} from 'tmp'; import tmp, {DirOptions} from 'tmp';
import { import {
csrFileName, csrFileName,
extractAppNameFromCSR, extractBundleIdFromCSR,
} from '../../app-connectivity/certificate-exchange/certificate-utils'; } from '../../app-connectivity/certificate-exchange/certificate-utils';
import path from 'path'; import path from 'path';
@@ -37,9 +37,11 @@ export default class iOSCertificateProvider extends CertificateProvider {
): Promise<string> { ): Promise<string> {
const matches = /\/Devices\/([^/]+)\//.exec(appDirectory); const matches = /\/Devices\/([^/]+)\//.exec(appDirectory);
if (matches && matches.length == 2) { 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]; return matches[1];
} }
// Get all available targets
const targets = await iosUtil.targets( const targets = await iosUtil.targets(
this.idbConfig.idbPath, this.idbConfig.idbPath,
this.idbConfig.enablePhysicalIOS, this.idbConfig.enablePhysicalIOS,
@@ -58,7 +60,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
return {id: target.udid, isMatch}; return {id: target.udid, isMatch};
} catch (e) { } catch (e) {
console.info( console.info(
`Unable to check for matching CSR in ${target.udid}:${appName}`, `[conn] Unable to check for matching CSR in ${target.udid}:${appName}`,
logTag, logTag,
e, e,
); );
@@ -71,7 +73,7 @@ export default class iOSCertificateProvider extends CertificateProvider {
throw new Error(`No matching device found for app: ${appName}`); throw new Error(`No matching device found for app: ${appName}`);
} }
if (matchingIds.length > 1) { if (matchingIds.length > 1) {
console.warn(`Multiple devices found for app: ${appName}`); console.warn(`[conn] Multiple devices found for app: ${appName}`);
} }
return matchingIds[0]; return matchingIds[0];
} }
@@ -82,37 +84,33 @@ export default class iOSCertificateProvider extends CertificateProvider {
contents: string, contents: string,
csr: string, csr: string,
) { ) {
console.debug('iOSCertificateProvider.deployOrStageFileForDevice', { console.debug('[conn] Deploying file to device ', {
destination, destination,
filename, filename,
}); });
const appName = await extractAppNameFromCSR(csr); const bundleId = await extractBundleIdFromCSR(csr);
try { try {
await fs.writeFile(destination + filename, contents); await fs.writeFile(destination + filename, contents);
} catch (err) { } catch (err) {
// Writing directly to FS failed. It's probably a physical device.
console.debug( console.debug(
'iOSCertificateProvider.deployOrStageFileForDevice -> physical device', '[conn] Deploying file using idb as physical device is inferred',
); );
const relativePathInsideApp = const relativePathInsideApp =
this.getRelativePathInAppContainer(destination); this.getRelativePathInAppContainer(destination);
console.debug( console.debug(`[conn] Relative path '${relativePathInsideApp}'`);
'iOSCertificateProvider.deployOrStageFileForDevice: realtive path',
relativePathInsideApp,
);
const udid = await this.getTargetDeviceId(appName, destination, csr); const udid = await this.getTargetDeviceId(bundleId, destination, csr);
await this.pushFileToiOSDevice( await this.pushFileToiOSDevice(
udid, udid,
appName, bundleId,
relativePathInsideApp, relativePathInsideApp,
filename, filename,
contents, contents,
); );
} }
console.debug('iOSCertificateProvider.deployOrStageFileForDevice -> done'); console.debug('[conn] Deploying file to device complete');
} }
private getRelativePathInAppContainer(absolutePath: string) { private getRelativePathInAppContainer(absolutePath: string) {
@@ -131,12 +129,12 @@ export default class iOSCertificateProvider extends CertificateProvider {
contents: string, contents: string,
): Promise<void> { ): Promise<void> {
const dir = await tmpDir({unsafeCleanup: true}); const dir = await tmpDir({unsafeCleanup: true});
const filePath = path.resolve(dir, filename); const src = path.resolve(dir, filename);
await fs.writeFile(filePath, contents); await fs.writeFile(src, contents);
await iosUtil.push( await iosUtil.push(
udid, udid,
filePath, src,
bundleId, bundleId,
destination, destination,
this.idbConfig.idbPath, this.idbConfig.idbPath,
@@ -149,69 +147,46 @@ export default class iOSCertificateProvider extends CertificateProvider {
bundleId: string, bundleId: string,
csr: string, csr: string,
): Promise<boolean> { ): Promise<boolean> {
const originalFile = this.getRelativePathInAppContainer( const src = this.getRelativePathInAppContainer(
path.resolve(directory, csrFileName), 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 { try {
await iosUtil.pull( await iosUtil.pull(deviceId, src, bundleId, dst, this.idbConfig.idbPath);
deviceId,
originalFile,
bundleId,
dir,
this.idbConfig.idbPath,
);
} catch (e) { } catch (e) {
console.warn( 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, e,
); );
await iosUtil.pull( await iosUtil.pull(
deviceId, deviceId,
originalFile, src,
bundleId, bundleId,
path.join(dir, csrFileName), path.join(dst, csrFileName),
this.idbConfig.idbPath, this.idbConfig.idbPath,
); );
console.info( 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) { if (items.length > 1) {
throw new Error('Conflict in temp dir'); throw new Error('Conflict in temporary dir');
} }
if (items.length === 0) { 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); const filename = items[0];
console.debug('Trying to read CSR from', copiedFile); const pulledFile = path.resolve(dst, filename);
const data = await fs.readFile(copiedFile);
console.debug(`[conn] Read CSR from '${pulledFile}'`);
const data = await fs.readFile(pulledFile);
const csrFromDevice = this.santitizeString(data.toString()); const csrFromDevice = this.santitizeString(data.toString());
return csrFromDevice === this.santitizeString(csr); return csrFromDevice === this.santitizeString(csr);
} }

View File

@@ -17,6 +17,7 @@ import {promisify} from 'util';
import child_process from 'child_process'; import child_process from 'child_process';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import {recorder} from '../../recorder';
const exec = promisify(child_process.exec); const exec = promisify(child_process.exec);
export type IdbConfig = { export type IdbConfig = {
@@ -69,12 +70,22 @@ async function safeExec(
async function queryTargetsWithXcode(): Promise<Array<DeviceTarget>> { async function queryTargetsWithXcode(): Promise<Array<DeviceTarget>> {
const cmd = 'xcrun xctrace list devices'; 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 { try {
const {stdout} = await safeExec(cmd); const {stdout} = await safeExec(cmd);
if (!stdout) { if (!stdout) {
recorder.event('cmd', {cmd, description, success: false, troubleshoot});
throw new Error('No output from command'); throw new Error('No output from command');
} }
recorder.event('cmd', {
cmd,
description,
success: true,
stdout: stdout.toString(),
});
return stdout return stdout
.toString() .toString()
.split('\n') .split('\n')
@@ -87,7 +98,13 @@ async function queryTargetsWithXcode(): Promise<Array<DeviceTarget>> {
return {udid, type: 'physical', name}; return {udid, type: 'physical', name};
}); });
} catch (e) { } catch (e) {
console.warn(`Failed to query devices using '${cmd}'`, e); recorder.event('cmd', {
cmd,
description,
success: false,
troubleshoot,
stderr: e.toString(),
});
return []; return [];
} }
} }
@@ -96,14 +113,33 @@ async function queryTargetsWithIdb(
idbPath: string, idbPath: string,
): Promise<Array<DeviceTarget>> { ): Promise<Array<DeviceTarget>> {
const cmd = `${idbPath} list-targets --json`; 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 { try {
const {stdout} = await safeExec(cmd); const {stdout} = await safeExec(cmd);
if (!stdout) { if (!stdout) {
recorder.event('cmd', {cmd, description, success: false, troubleshoot});
throw new Error('No output from command'); throw new Error('No output from command');
} }
recorder.event('cmd', {
cmd,
description,
success: true,
stdout: stdout.toString(),
});
return parseIdbTargets(stdout.toString()); return parseIdbTargets(stdout.toString());
} catch (e) { } catch (e) {
console.warn(`Failed to execute '${cmd}' for targets.`, e); recorder.event('cmd', {
cmd,
description,
success: false,
troubleshoot,
stderr: e.toString(),
});
return []; return [];
} }
} }
@@ -114,6 +150,7 @@ async function queryTargetsWithIdbCompanion(
): Promise<Array<DeviceTarget>> { ): Promise<Array<DeviceTarget>> {
if (await isAvailable(idbCompanionPath)) { if (await isAvailable(idbCompanionPath)) {
const cmd = `${idbCompanionPath} --list 1 --only device`; const cmd = `${idbCompanionPath} --list 1 --only device`;
recorder.rawLog(`Query devices with idb companion '${cmd}'`);
try { try {
const {stdout} = await safeExec(cmd); const {stdout} = await safeExec(cmd);
if (!stdout) { if (!stdout) {
@@ -122,18 +159,18 @@ async function queryTargetsWithIdbCompanion(
const devices = parseIdbTargets(stdout.toString()); const devices = parseIdbTargets(stdout.toString());
if (devices.length > 0 && !isPhysicalDeviceEnabled) { if (devices.length > 0 && !isPhysicalDeviceEnabled) {
console.warn( recorder.rawError(
`You are trying to connect Physical Device. `You are trying to connect Physical Device.
Please enable the toggle "Enable physical iOS device" from the setting screen.`, Please enable the toggle "Enable physical iOS device" from the setting screen.`,
); );
} }
return devices; return devices;
} catch (e) { } catch (e) {
console.warn(`Failed to execute '${cmd}' for targets:`, e); recorder.rawError(`Failed to query devices using '${cmd}'`, e);
return []; return [];
} }
} else { } else {
console.warn( recorder.rawError(
`Unable to locate idb_companion in '${idbCompanionPath}'. `Unable to locate idb_companion in '${idbCompanionPath}'.
Try running sudo yum install -y fb-idb`, Try running sudo yum install -y fb-idb`,
); );
@@ -177,6 +214,7 @@ async function idbDescribeTarget(
idbPath: string, idbPath: string,
): Promise<DeviceTarget | undefined> { ): Promise<DeviceTarget | undefined> {
const cmd = `${idbPath} describe --json`; const cmd = `${idbPath} describe --json`;
recorder.rawLog(`Describe target '${cmd}'`);
try { try {
const {stdout} = await safeExec(cmd); const {stdout} = await safeExec(cmd);
if (!stdout) { if (!stdout) {
@@ -184,7 +222,7 @@ async function idbDescribeTarget(
} }
return parseIdbTarget(stdout.toString()); return parseIdbTarget(stdout.toString());
} catch (e) { } 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; return undefined;
} }
} }
@@ -209,8 +247,7 @@ async function targets(
const isXcodeInstalled = await isXcodeDetected(); const isXcodeInstalled = await isXcodeDetected();
if (!isXcodeInstalled) { if (!isXcodeInstalled) {
if (!isPhysicalDeviceEnabled) { if (!isPhysicalDeviceEnabled) {
// TODO: Show a notification to enable the toggle or integrate Doctor to better suggest this advice. recorder.rawError(
console.warn(
'You are trying to connect a physical device. Please enable the toggle "Enable physical iOS device" from the setting screen.', '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); await memoize(checkIdbIsInstalled)(idbPath);
const push_ = async () => { const push_ = async () => {
const cmd = `${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`;
try { try {
await safeExec( recorder.rawLog(`Push file to device '${cmd}'`);
`${idbPath} file push --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, await safeExec(cmd);
); recorder.rawLog(`Successfully pushed file to device`);
} catch (e) { } catch (e) {
recorder.rawError(`Failed to push file to device`, e);
handleMissingIdb(e, idbPath); handleMissingIdb(e, idbPath);
throw e; throw e;
} }
@@ -266,11 +305,13 @@ async function pull(
await memoize(checkIdbIsInstalled)(idbPath); await memoize(checkIdbIsInstalled)(idbPath);
const pull_ = async () => { const pull_ = async () => {
const cmd = `${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`;
try { try {
await safeExec( recorder.rawLog(`Pull file from device '${cmd}'`);
`${idbPath} file pull --log ${IDB_LOG_LEVEL} --udid ${udid} --bundle-id ${bundleId} '${src}' '${dst}'`, await safeExec(cmd);
); recorder.rawLog(`Successfully pulled file from device`);
} catch (e) { } catch (e) {
recorder.rawError(`Failed to pull file from device`, e);
handleMissingIdb(e, idbPath); handleMissingIdb(e, idbPath);
handleMissingPermissions(e); handleMissingPermissions(e);
throw e; throw e;

View File

@@ -72,7 +72,8 @@ export function shouldShowiOSCrashNotification(
): boolean { ): boolean {
const appPath = legacy ? parsePathLegacy(content) : parsePathModern(content); const appPath = legacy ? parsePathLegacy(content) : parsePathModern(content);
if (!appPath || !appPath.includes(serial)) { 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 false;
} }
return true; return true;

View File

@@ -117,7 +117,7 @@ export class IOSDeviceManager {
if (currentDeviceIDs.has(udid)) { if (currentDeviceIDs.has(udid)) {
currentDeviceIDs.delete(udid); currentDeviceIDs.delete(udid);
} else { } else {
console.info(`[conn] detected new iOS device ${udid}`, activeDevice); console.info(`[conn] Detected new iOS device ${udid}`, activeDevice);
const iOSDevice = new IOSDevice( const iOSDevice = new IOSDevice(
this.flipperServer, this.flipperServer,
bridge, bridge,

View File

@@ -9,7 +9,7 @@
export {FlipperServerImpl} from './FlipperServerImpl'; export {FlipperServerImpl} from './FlipperServerImpl';
export {loadSettings} from './utils/settings'; export {loadSettings} from './utils/settings';
export * from './utils/tracker'; export * from './tracker';
export {loadLauncherSettings} from './utils/launcherSettings'; export {loadLauncherSettings} from './utils/launcherSettings';
export {loadProcessConfig} from './utils/processConfig'; export {loadProcessConfig} from './utils/processConfig';
export {getEnvironmentInfo} from './utils/environmentInfo'; export {getEnvironmentInfo} from './utils/environmentInfo';

View File

@@ -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 extends keyof ConnectionRecorderEvents>(
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};

View File

@@ -24,7 +24,7 @@ import {attachSocketServer} from './attachSocketServer';
import {FlipperServerImpl} from '../FlipperServerImpl'; import {FlipperServerImpl} from '../FlipperServerImpl';
import {FlipperServerCompanionEnv} from 'flipper-server-companion'; import {FlipperServerCompanionEnv} from 'flipper-server-companion';
import {validateAuthToken} from '../app-connectivity/certificate-exchange/certificate-utils'; import {validateAuthToken} from '../app-connectivity/certificate-exchange/certificate-utils';
import {tracker} from '../utils/tracker'; import {tracker} from '../tracker';
type Config = { type Config = {
port: number; port: number;

View File

@@ -10,6 +10,6 @@
import {Layout} from '../ui'; import {Layout} from '../ui';
import React from 'react'; import React from 'react';
export function ConnectionTroubleshootTools() { export function ConnectivityHub() {
return <Layout.Container grow>Connection Troubleshoot</Layout.Container>; return <Layout.Container grow>Connection Troubleshoot</Layout.Container>;
} }

View File

@@ -44,7 +44,7 @@ import {isFBEmployee} from '../utils/fbEmployee';
import {notification} from 'antd'; import {notification} from 'antd';
import isProduction from '../utils/isProduction'; import isProduction from '../utils/isProduction';
import {getRenderHostInstance} from 'flipper-frontend-core'; import {getRenderHostInstance} from 'flipper-frontend-core';
import {ConnectionTroubleshootTools} from '../chrome/ConnectionTroubleshootTools'; import {ConnectivityHub} from '../chrome/ConnectivityHub';
export type ToplevelNavItem = export type ToplevelNavItem =
| 'appinspect' | 'appinspect'
@@ -91,7 +91,7 @@ export function SandyApp() {
dispatch(setStaticView(FlipperDevTools)); dispatch(setStaticView(FlipperDevTools));
break; break;
case 'connectivity': case 'connectivity':
dispatch(setStaticView(ConnectionTroubleshootTools)); dispatch(setStaticView(ConnectivityHub));
break; break;
default: default:
} }