Fix device cleanup

Summary:
Fixes https://github.com/facebook/flipper/issues/1989

We had some self healing side effect that would destroy devices when registering a new device with the same serial, if they weren't yet. Redux isn't too happy about that, causing the attached crash.

Instead introduced a utility to destroy devices, and log an error if the device life cycle isn't respected by the device implementations, rather than crashing we will now just waste some memory.

Changelog: Fix a crash when disconnecting metro devices

Reviewed By: passy

Differential Revision: D26749214

fbshipit-source-id: 4c185ac521d44c1337fac8a9145440123b8b784c
This commit is contained in:
Michel Weststrate
2021-03-02 03:57:02 -08:00
committed by Facebook GitHub Bot
parent 4a71a5abd1
commit 224ec4d5d6
9 changed files with 49 additions and 42 deletions

View File

@@ -17,6 +17,7 @@ import {
PluginClient, PluginClient,
} from 'flipper-plugin'; } from 'flipper-plugin';
import {registerNewClient} from '../dispatcher/server'; import {registerNewClient} from '../dispatcher/server';
import {destroyDevice} from '../reducers/connections';
test('Devices can disconnect', async () => { test('Devices can disconnect', async () => {
const deviceplugin = new _SandyPluginDefinition( const deviceplugin = new _SandyPluginDefinition(
@@ -89,7 +90,9 @@ test('New device with same serial removes & cleans the old one', async () => {
}, },
}, },
); );
const {device, store} = await createMockFlipperWithPlugin(deviceplugin); const {device, store, logger} = await createMockFlipperWithPlugin(
deviceplugin,
);
const instance = device.sandyPluginStates.get(deviceplugin.id)!; const instance = device.sandyPluginStates.get(deviceplugin.id)!;
@@ -98,6 +101,10 @@ test('New device with same serial removes & cleans the old one', async () => {
expect(instance.instanceApi.destroy).toBeCalledTimes(0); expect(instance.instanceApi.destroy).toBeCalledTimes(0);
expect(store.getState().connections.devices).toEqual([device]); expect(store.getState().connections.devices).toEqual([device]);
// calling destroy explicitly defeats the point of this test a bit,
// but we now print an error rather than proactively destroying the device,
// see https://github.com/facebook/flipper/issues/1989
destroyDevice(store, logger, device.serial);
// submit a new device with same serial // submit a new device with same serial
const device2 = new BaseDevice( const device2 = new BaseDevice(
device.serial, device.serial,

View File

@@ -20,6 +20,7 @@ import {promisify} from 'util';
import {ServerPorts} from '../reducers/application'; import {ServerPorts} from '../reducers/application';
import {Client as ADBClient} from 'adbkit'; import {Client as ADBClient} from 'adbkit';
import {addErrorNotification} from '../reducers/notifications'; import {addErrorNotification} from '../reducers/notifications';
import {destroyDevice} from '../reducers/connections';
function createDevice( function createDevice(
adbClient: ADBClient, adbClient: ADBClient,
@@ -235,9 +236,8 @@ export default (store: Store, logger: Logger) => {
) )
.map((device) => device.serial); .map((device) => device.serial);
store.dispatch({ reconnectedDevices.forEach((serial) => {
type: 'UNREGISTER_DEVICES', destroyDevice(store, logger, serial);
payload: new Set(reconnectedDevices),
}); });
androidDevice.loadDevicePlugins( androidDevice.loadDevicePlugins(

View File

@@ -21,6 +21,7 @@ import IOSDevice from '../devices/IOSDevice';
import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice'; import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
import {addErrorNotification} from '../reducers/notifications'; import {addErrorNotification} from '../reducers/notifications';
import {getStaticPath} from '../utils/pathUtils'; import {getStaticPath} from '../utils/pathUtils';
import {destroyDevice} from '../reducers/connections';
type iOSSimulatorDevice = { type iOSSimulatorDevice = {
state: 'Booted' | 'Shutdown' | 'Shutting Down'; state: 'Booted' | 'Shutdown' | 'Shutting Down';
@@ -127,6 +128,8 @@ function processDevices(
if (currentDeviceIDs.has(udid)) { if (currentDeviceIDs.has(udid)) {
currentDeviceIDs.delete(udid); currentDeviceIDs.delete(udid);
} else { } else {
// clean up offline device
destroyDevice(store, logger, udid);
logger.track('usage', 'register-device', { logger.track('usage', 'register-device', {
os: 'iOS', os: 'iOS',
type: type, type: type,

View File

@@ -13,6 +13,7 @@ import {registerDeviceCallbackOnPlugins} from '../utils/onRegisterDevice';
import MetroDevice from '../devices/MetroDevice'; import MetroDevice from '../devices/MetroDevice';
import http from 'http'; import http from 'http';
import {addErrorNotification} from '../reducers/notifications'; import {addErrorNotification} from '../reducers/notifications';
import {destroyDevice} from '../reducers/connections';
const METRO_PORT = 8081; const METRO_PORT = 8081;
const METRO_HOST = 'localhost'; const METRO_HOST = 'localhost';
@@ -75,18 +76,6 @@ export async function registerMetroDevice(
); );
} }
async function unregisterDevices(store: Store, logger: Logger) {
logger.track('usage', 'unregister-device', {
os: 'Metro',
serial: METRO_URL,
});
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: new Set([METRO_URL]),
});
}
export default (store: Store, logger: Logger) => { export default (store: Store, logger: Logger) => {
let timeoutHandle: NodeJS.Timeout; let timeoutHandle: NodeJS.Timeout;
let ws: WebSocket | undefined; let ws: WebSocket | undefined;
@@ -111,7 +100,7 @@ export default (store: Store, logger: Logger) => {
unregistered = true; unregistered = true;
clearTimeout(guard); clearTimeout(guard);
ws = undefined; ws = undefined;
unregisterDevices(store, logger); destroyDevice(store, logger, METRO_URL);
scheduleNext(); scheduleNext();
} }
}; };

View File

@@ -9,7 +9,7 @@
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin'; import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {Store} from '../../'; import {Store} from '../../';
import {selectPlugin} from '../../reducers/connections'; import {destroyDevice, selectPlugin} from '../../reducers/connections';
import { import {
_SandyPluginDefinition, _SandyPluginDefinition,
_SandyDevicePluginInstance, _SandyDevicePluginInstance,
@@ -87,15 +87,12 @@ test('it should initialize device sandy plugins', async () => {
}); });
test('it should cleanup if device is removed', async () => { test('it should cleanup if device is removed', async () => {
const {device, store} = await createMockFlipperWithPlugin(TestPlugin); const {device, store, logger} = await createMockFlipperWithPlugin(TestPlugin);
const pluginInstance = device.sandyPluginStates.get(TestPlugin.id)!; const pluginInstance = device.sandyPluginStates.get(TestPlugin.id)!;
expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0); expect(pluginInstance.instanceApi.destroyStub).toHaveBeenCalledTimes(0);
// close device // close device
store.dispatch({ destroyDevice(store, logger, device.serial);
type: 'UNREGISTER_DEVICES',
payload: new Set([device.serial]),
});
expect( expect(
(pluginInstance.instanceApi as PluginApi).destroyStub, (pluginInstance.instanceApi as PluginApi).destroyStub,
).toHaveBeenCalledTimes(1); ).toHaveBeenCalledTimes(1);

View File

@@ -16,7 +16,7 @@ import type Client from '../Client';
import type {UninitializedClient} from '../UninitializedClient'; import type {UninitializedClient} from '../UninitializedClient';
import {isEqual} from 'lodash'; import {isEqual} from 'lodash';
import {performance} from 'perf_hooks'; import {performance} from 'perf_hooks';
import type {Actions} from '.'; import type {Actions, Store} from '.';
import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen'; import {WelcomeScreenStaticView} from '../sandy-chrome/WelcomeScreen';
import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils'; import {getPluginKey, isDevicePluginDefinition} from '../utils/pluginUtils';
import {deconstructClientId} from '../utils/clientUtils'; import {deconstructClientId} from '../utils/clientUtils';
@@ -231,10 +231,9 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
(device) => device.serial === payload.serial, (device) => device.serial === payload.serial,
); );
if (existing !== -1) { if (existing !== -1) {
console.debug( console.warn(
`Got a new device instance for already existing serial ${payload.serial}`, `Got a new device instance for already existing serial ${payload.serial}`,
); );
state.devices[existing].destroy();
newDevices[existing] = payload; newDevices[existing] = payload;
} else { } else {
newDevices.push(payload); newDevices.push(payload);
@@ -255,7 +254,11 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
if (!deviceSerials.has(device.serial)) { if (!deviceSerials.has(device.serial)) {
return true; return true;
} else { } else {
device.destroy(); if (device.connected.get()) {
console.warn(
'Tried to unregister a device before it was destroyed',
);
}
return false; return false;
} }
}); });
@@ -654,3 +657,20 @@ export function isPluginEnabled(
const enabledAppPlugins = enabledPlugins[appInfo.app]; const enabledAppPlugins = enabledPlugins[appInfo.app];
return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1; return enabledAppPlugins && enabledAppPlugins.indexOf(pluginId) > -1;
} }
export function destroyDevice(store: Store, logger: Logger, serial: string) {
const device = store
.getState()
.connections.devices.find((device) => device.serial === serial);
if (device) {
device.destroy();
logger.track('usage', 'unregister-device', {
os: device.os,
serial,
});
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: new Set([serial]),
});
}
}

View File

@@ -40,6 +40,7 @@ import {initSelfInpector} from './utils/self-inspection/selfInspectionUtils';
import ClientDevice from './devices/ClientDevice'; import ClientDevice from './devices/ClientDevice';
import BaseDevice from './devices/BaseDevice'; import BaseDevice from './devices/BaseDevice';
import {sideEffect} from './utils/sideEffect'; import {sideEffect} from './utils/sideEffect';
import {destroyDevice} from './reducers/connections';
type ClientInfo = { type ClientInfo = {
connection: FlipperClientConnection<any, any> | null | undefined; connection: FlipperClientConnection<any, any> | null | undefined;
@@ -206,10 +207,7 @@ class Server extends EventEmitter {
Object.values(clients).map((p) => Object.values(clients).map((p) =>
p.then((c) => this.removeConnection(c.id)), p.then((c) => this.removeConnection(c.id)),
); );
this.store.dispatch({ destroyDevice(this.store, this.logger, deviceId);
type: 'UNREGISTER_DEVICES',
payload: new Set([deviceId]),
});
}; };
ws.on('message', (rawMessage: any) => { ws.on('message', (rawMessage: any) => {

View File

@@ -18,6 +18,7 @@ import {Payload, ConnectionStatus, ISubscriber} from 'rsocket-types';
import {Flowable, Single} from 'rsocket-flowable'; import {Flowable, Single} from 'rsocket-flowable';
import Server from '../../server'; import Server from '../../server';
import {buildClientId} from '../clientUtils'; import {buildClientId} from '../clientUtils';
import {destroyDevice} from '../../reducers/connections';
const connections: Map<number, JSClientFlipperConnection<any>> = new Map(); const connections: Map<number, JSClientFlipperConnection<any>> = new Map();
@@ -83,12 +84,7 @@ export function initJsEmulatorIPC(
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') { if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
console.debug(`Device disconnected ${client.id}`, 'server'); console.debug(`Device disconnected ${client.id}`, 'server');
flipperServer.removeConnection(client.id); flipperServer.removeConnection(client.id);
const toUnregister = new Set<string>(); destroyDevice(store, logger, jsDeviceId(windowId));
toUnregister.add(jsDeviceId(windowId));
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: toUnregister,
});
connections.delete(windowId); connections.delete(windowId);
availablePlugins.delete(windowId); availablePlugins.delete(windowId);
} }

View File

@@ -16,6 +16,7 @@ import Server from '../../server';
import {buildClientId} from '../clientUtils'; import {buildClientId} from '../clientUtils';
import {selfInspectionClient} from './selfInspectionClient'; import {selfInspectionClient} from './selfInspectionClient';
import {flipperMessagesClientPlugin} from './plugins/FlipperMessagesClientPlugin'; import {flipperMessagesClientPlugin} from './plugins/FlipperMessagesClientPlugin';
import {destroyDevice} from '../../reducers/connections';
export function initSelfInpector( export function initSelfInpector(
store: Store, store: Store,
@@ -69,11 +70,7 @@ export function initSelfInpector(
if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') { if (payload.kind == 'ERROR' || payload.kind == 'CLOSED') {
console.debug(`Device disconnected ${client.id}`, 'server'); console.debug(`Device disconnected ${client.id}`, 'server');
flipperServer.removeConnection(client.id); flipperServer.removeConnection(client.id);
const toUnregister = new Set<string>(); destroyDevice(store, logger, client.id);
store.dispatch({
type: 'UNREGISTER_DEVICES',
payload: toUnregister,
});
} }
}, },
onSubscribe(subscription) { onSubscribe(subscription) {