Fix pending connections for websocket browser connections

Summary:
Connections from VSCode and Kite would remain forever pending because they don't go through the secure connection handler. This diff fixes that. Also removed the separate event that existed for that, since registering a new client is already a 'success' signal, so it doesn't need a separate event.

It turned out that the VSCode pending connection is actually correct, as it never handles the `getPlugins` event, so apparently the handling is broken. Added timeouts to guard against that as well.

Applied several code simplications as well.

Introduced an explicit cert exchange medium 'NONE' so that in code it is a bit clearer where CSR negotiation is supposed to happen.

Changelog: Fixed an issue where Kite / Unity apps didn't connect anymore

Reviewed By: timur-valiev

Differential Revision: D30866301

fbshipit-source-id: 8bd214fd9eebcd9a7583f1b44ee283883002f62e
This commit is contained in:
Michel Weststrate
2021-09-10 07:01:41 -07:00
committed by Facebook GitHub Bot
parent d8f77db632
commit 80f48b444c
11 changed files with 56 additions and 75 deletions

View File

@@ -26,7 +26,12 @@ import {processMessagesLater} from './utils/messageQueue';
import {emitBytesReceived} from './dispatcher/tracking';
import {debounce} from 'lodash';
import {batch} from 'react-redux';
import {createState, _SandyPluginInstance, getFlipperLib} from 'flipper-plugin';
import {
createState,
_SandyPluginInstance,
getFlipperLib,
timeout,
} from 'flipper-plugin';
import {freeze} from 'immer';
import GK from './fb-stubs/GK';
import {message} from 'antd';
@@ -230,9 +235,10 @@ export default class Client extends EventEmitter {
// get the supported plugins
async loadPlugins(): Promise<Plugins> {
const {plugins} = await this.rawCall<{plugins: Plugins}>(
'getPlugins',
false,
const {plugins} = await timeout(
30 * 1000,
this.rawCall<{plugins: Plugins}>('getPlugins', false),
'Fetch plugin timeout for ' + this.id,
);
this.plugins = new Set(plugins);
return plugins;
@@ -307,10 +313,12 @@ export default class Client extends EventEmitter {
if (this.sdkVersion < 4) {
return [];
}
return this.rawCall<{plugins: PluginsArr}>(
'getBackgroundPlugins',
false,
).then((data) => data.plugins);
const data = await timeout(
30 * 1000,
this.rawCall<{plugins: PluginsArr}>('getBackgroundPlugins', false),
'Fetch background plugins timeout for ' + this.id,
);
return data.plugins;
}
// get the plugins, and update the UI

View File

@@ -70,7 +70,7 @@ export default async (store: Store, logger: Logger) => {
server.on('device-connected', (device) => {
logger.track('usage', 'register-device', {
os: 'Android',
os: device.os,
name: device.title,
serial: device.serial,
});

View File

@@ -14,7 +14,6 @@ import type BaseDevice from '../server/devices/BaseDevice';
import MacDevice from '../server/devices/desktop/MacDevice';
import type Client from '../Client';
import type {UninitializedClient} from '../server/UninitializedClient';
import {isEqual} from 'lodash';
import {performance} from 'perf_hooks';
import type {Actions, Store} from '.';
import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen';
@@ -73,11 +72,7 @@ type StateV2 = {
enabledPlugins: {[client: string]: string[]};
enabledDevicePlugins: Set<string>;
clients: Array<Client>;
uninitializedClients: Array<{
client: UninitializedClient;
deviceId?: string;
errorMessage?: string;
}>;
uninitializedClients: UninitializedClient[];
deepLinkPayload: unknown;
staticView: StaticView;
selectedAppPluginListRevision: number;
@@ -137,10 +132,6 @@ export type Action =
type: 'START_CLIENT_SETUP';
payload: UninitializedClient;
}
| {
type: 'FINISH_CLIENT_SETUP';
payload: {client: UninitializedClient; deviceId: string};
}
| {
type: 'SET_STATIC_VIEW';
payload: StaticView;
@@ -366,8 +357,8 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
clients: newClients,
uninitializedClients: state.uninitializedClients.filter((c) => {
return (
c.deviceId !== payload.query.device_id ||
c.client.appName !== payload.query.app
c.deviceName !== payload.query.device ||
c.appName !== payload.query.app
);
}),
});
@@ -402,23 +393,7 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
const {payload} = action;
return {
...state,
uninitializedClients: state.uninitializedClients
.filter((entry) => !isEqual(entry.client, payload))
.concat([{client: payload}])
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
};
}
case 'FINISH_CLIENT_SETUP': {
const {payload} = action;
return {
...state,
uninitializedClients: state.uninitializedClients
.map((c) =>
isEqual(c.client, payload.client)
? {...c, deviceId: payload.deviceId}
: c,
)
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
uninitializedClients: [...state.uninitializedClients, payload],
};
}
case 'REGISTER_PLUGINS': {

View File

@@ -223,8 +223,8 @@ function computeEntries(
Currently connecting...
</Menu.Item>,
...uninitializedClients.map((client) => (
<Menu.Item key={'connecting' + client.client.appName}>
{client.client.appName}
<Menu.Item key={'connecting' + client.appName}>
{`${client.appName} (${client.deviceName})`}
</Menu.Item>
)),
]);

View File

@@ -113,16 +113,6 @@ export class FlipperServer {
});
});
server.addListener(
'finish-client-setup',
(payload: {client: UninitializedClient; deviceId: string}) => {
this.store.dispatch({
type: 'FINISH_CLIENT_SETUP',
payload: payload,
});
},
);
server.addListener(
'client-setup-error',
({client, error}: {client: UninitializedClient; error: Error}) => {

View File

@@ -29,7 +29,7 @@ export type ClientCsrQuery = {
* ClientCsrQuery. It also adds medium information.
*/
export type SecureClientQuery = ClientQuery &
ClientCsrQuery & {medium: number | undefined};
ClientCsrQuery & {medium: 1 /*FS*/ | 2 /*WWW*/ | 3 /*NONE*/ | undefined};
/**
* Defines an interface for events triggered by a running server interacting

View File

@@ -34,6 +34,7 @@ import ServerAdapter, {
import {createBrowserServer, createServer} from './ServerFactory';
import {FlipperServer} from '../FlipperServer';
import {isTest} from '../../utils/isProduction';
import {timeout} from 'flipper-plugin';
type ClientInfo = {
connection: ClientConnection | null | undefined;
@@ -187,15 +188,15 @@ class ServerController extends EventEmitter implements ServerEventsListener {
const transformedMedium = transformCertificateExchangeMediumToType(
clientQuery.medium,
);
if (transformedMedium === 'WWW') {
this.store.dispatch({
type: 'REGISTER_DEVICE',
payload: new DummyDevice(
if (transformedMedium === 'WWW' || transformedMedium === 'NONE') {
this.flipperServer.registerDevice(
new DummyDevice(
clientQuery.device_id,
clientQuery.app + ' Server Exchanged',
clientQuery.app +
(transformedMedium === 'WWW' ? ' Server Exchanged' : ''),
clientQuery.os,
),
});
);
}
}
@@ -247,11 +248,6 @@ class ServerController extends EventEmitter implements ServerEventsListener {
});
}, 30 * 1000);
this.emit('finish-client-setup', {
client,
deviceId: response.deviceId,
});
resolve(response);
})
.catch((error) => {
@@ -285,8 +281,8 @@ class ServerController extends EventEmitter implements ServerEventsListener {
// otherwise, use given device_id
const {csr_path, csr} = csrQuery;
// For iOS we do not need to confirm the device id, as it never changes unlike android.
if (csr_path && csr && query.os != 'iOS') {
// For Android, device id might change
if (csr_path && csr && query.os === 'Android') {
const app_name = await this.certificateProvider.extractAppNameFromCSR(
csr,
);
@@ -312,6 +308,7 @@ class ServerController extends EventEmitter implements ServerEventsListener {
console.log(
`[conn] Matching device for ${query.app} on ${query.device_id}...`,
);
// TODO: grab device from flipperServer.devices instead of store
const device =
getDeviceBySerial(this.store.getState(), query.device_id) ??
(await findDeviceForConnection(this.store, query.app, query.device_id));
@@ -335,7 +332,11 @@ class ServerController extends EventEmitter implements ServerEventsListener {
`[conn] Initializing client ${query.app} on ${query.device_id}...`,
);
await client.init();
await timeout(
30 * 1000,
client.init(),
`[conn] Failed to initialize client ${query.app} on ${query.device_id} in a timely manner`,
);
connection.subscribeToEvents((status: ConnectionStatus) => {
if (

View File

@@ -281,8 +281,10 @@ class ServerWebSocket extends ServerWebSocketBase {
if (typeof query.medium === 'string') {
medium = parseInt(query.medium, 10);
}
return {...clientQuery, csr, csr_path, medium};
if (medium !== undefined && (medium < 1 || medium > 3)) {
throw new Error('Unsupported exchange medium: ' + medium);
}
return {...clientQuery, csr, csr_path, medium: medium as any};
}
}

View File

@@ -110,7 +110,7 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
plugins,
);
const extendedClientQuery = {...clientQuery, medium: 1};
const extendedClientQuery = {...clientQuery, medium: 3 as const};
extendedClientQuery.sdk_version = plugins == null ? 4 : 1;
console.log(
@@ -118,6 +118,7 @@ class ServerWebSocketBrowser extends ServerWebSocketBase {
);
let resolvedClient: Client | null = null;
this.listener.onSecureConnectionAttempt(extendedClientQuery);
const client: Promise<Client> = this.listener.onConnectionCreated(
extendedClientQuery,
clientConnection,

View File

@@ -18,12 +18,16 @@ import {ClientQuery} from '../../Client';
export function transformCertificateExchangeMediumToType(
medium: number | undefined,
): CertificateExchangeMedium {
if (medium == 1) {
switch (medium) {
case undefined:
case 1:
return 'FS_ACCESS';
} else if (medium == 2) {
case 2:
return 'WWW';
} else {
return 'FS_ACCESS';
case 3:
return 'NONE';
default:
throw new Error('Unknown Certificate exchange medium: ' + medium);
}
}

View File

@@ -30,7 +30,7 @@ import {timeout} from 'flipper-plugin';
import {v4 as uuid} from 'uuid';
import {isTest} from '../../utils/isProduction';
export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW';
export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW' | 'NONE';
const tmpFile = promisify(tmp.file) as (
options?: FileOptions,