Files
flipper/src/reducers/connections.js
Benjamin Elo 1a0ee24b1a Mac devices excluded from default devices
Summary:
Bug Summary:

When running Flipper on Mac, the Mac device is always first to register with Flipper and thus is always selected as the default connected device when first launching. This blocks the beautiful tutorial screen from appearing for new users.

Fix:

When registering new devices in the redux store, a new check has been added that maintains a blacklist of devices that cannot be selected as default. I have added Mac to start. At the same time, this fix preserves userPreferredDevices so that if the user has selected Mac as a device in the past, then next time Flipper is opened, even blacklisted devices will be displayed.

Making this fix uncovered a race condition, between the redux state being rehydrated and the devices being registered. I have potentially fixed this via a callback function in persistStore.

Reviewed By: passy

Differential Revision: D16048371

fbshipit-source-id: 79580b30e8a3b077dac1ac15131266e59646253f
2019-06-28 03:46:34 -07:00

390 lines
10 KiB
JavaScript

/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type BaseDevice from '../devices/BaseDevice';
import MacDevice from '../devices/MacDevice';
import type Client from '../Client';
import type {UninitializedClient} from '../UninitializedClient';
import {isEqual} from 'lodash';
import iosUtil from '../fb-stubs/iOSContainerUtility';
// $FlowFixMe perf_hooks is a new API in node
import {performance} from 'perf_hooks';
export type State = {|
devices: Array<BaseDevice>,
androidEmulators: Array<string>,
selectedDevice: ?BaseDevice,
selectedPlugin: ?string,
selectedApp: ?string,
userPreferredDevice: ?string,
userPreferredPlugin: ?string,
userPreferredApp: ?string,
error: ?string,
clients: Array<Client>,
uninitializedClients: Array<{
client: UninitializedClient,
deviceId?: string,
errorMessage?: string,
}>,
deepLinkPayload: ?string,
|};
export type Action =
| {
type: 'UNREGISTER_DEVICES',
payload: Set<string>,
}
| {
type: 'REGISTER_DEVICE',
payload: BaseDevice,
}
| {
type: 'REGISTER_ANDROID_EMULATORS',
payload: Array<string>,
}
| {
type: 'SELECT_DEVICE',
payload: BaseDevice,
}
| {
type: 'SELECT_PLUGIN',
payload: {|
selectedPlugin: ?string,
selectedApp: ?string,
deepLinkPayload: ?string,
|},
}
| {
type: 'SELECT_USER_PREFERRED_PLUGIN',
payload: string,
}
| {
type: 'SERVER_ERROR',
payload: ?string,
}
| {
type: 'NEW_CLIENT',
payload: Client,
}
| {
type: 'NEW_CLIENT_SANITY_CHECK',
payload: Client,
}
| {
type: 'CLIENT_REMOVED',
payload: string,
}
| {
type: 'PREFER_DEVICE',
payload: string,
}
| {
type: 'START_CLIENT_SETUP',
payload: UninitializedClient,
}
| {
type: 'FINISH_CLIENT_SETUP',
payload: {client: UninitializedClient, deviceId: string},
}
| {
type: 'CLIENT_SETUP_ERROR',
payload: {client: UninitializedClient, error: Error},
};
const DEFAULT_PLUGIN = 'DeviceLogs';
const DEFAULT_DEVICE_BLACKLIST = [MacDevice];
const INITAL_STATE: State = {
devices: [],
androidEmulators: [],
selectedDevice: null,
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
userPreferredDevice: null,
userPreferredPlugin: null,
userPreferredApp: null,
error: null,
clients: [],
uninitializedClients: [],
deepLinkPayload: null,
};
const reducer = (state: State = INITAL_STATE, action: Action): State => {
switch (action.type) {
case 'SELECT_DEVICE': {
const {payload} = action;
return {
...state,
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
selectedDevice: payload,
userPreferredDevice: payload.title,
};
}
case 'REGISTER_ANDROID_EMULATORS': {
const {payload} = action;
return {
...state,
androidEmulators: payload,
};
}
case 'REGISTER_DEVICE': {
const {payload} = action;
const devices = state.devices.concat(payload);
let {selectedDevice, selectedPlugin} = state;
// select the default plugin
let selection = {
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
};
let canBeDefaultDevice = !DEFAULT_DEVICE_BLACKLIST.some(
blacklistedDevice => payload instanceof blacklistedDevice,
);
if (!selectedDevice && canBeDefaultDevice) {
selectedDevice = payload;
if (selectedPlugin) {
// We already had a plugin selected, but no device. This is happening
// when the Client connected before the Device.
selection = {};
}
} else if (payload.title === state.userPreferredDevice) {
selectedDevice = payload;
} else {
// We didn't select the newly connected device, so we don't want to
// change the plugin.
selection = {};
}
return {
...state,
devices,
// select device if none was selected before
selectedDevice,
...selection,
};
}
case 'UNREGISTER_DEVICES': {
const {payload} = action;
const {selectedDevice} = state;
let selectedDeviceWasRemoved = false;
const devices = state.devices.filter((device: BaseDevice) => {
if (payload.has(device.serial)) {
if (selectedDevice === device) {
// removed device is the selected
selectedDeviceWasRemoved = true;
}
return false;
} else {
return true;
}
});
let selection = {};
if (selectedDeviceWasRemoved) {
selection = {
selectedDevice: devices[devices.length - 1],
selectedApp: null,
selectedPlugin: DEFAULT_PLUGIN,
};
}
return {
...state,
devices,
...selection,
};
}
case 'SELECT_PLUGIN': {
const {payload} = action;
const {selectedPlugin, selectedApp} = payload;
if (selectedPlugin) {
performance.mark(`activePlugin-${selectedPlugin}`);
}
return {
...state,
...payload,
userPreferredApp: selectedApp || state.userPreferredApp,
userPreferredPlugin: selectedPlugin,
};
}
case 'SELECT_USER_PREFERRED_PLUGIN': {
const {payload} = action;
return {...state, userPreferredPlugin: payload};
}
case 'NEW_CLIENT': {
const {payload} = action;
const {userPreferredApp, userPreferredPlugin} = state;
let {selectedApp, selectedPlugin} = state;
if (
userPreferredApp &&
userPreferredPlugin &&
payload.id === userPreferredApp &&
payload.plugins.includes(userPreferredPlugin)
) {
// user preferred client did reconnect, so let's select it
selectedApp = userPreferredApp;
selectedPlugin = userPreferredPlugin;
}
return {
...state,
clients: state.clients.concat(payload),
uninitializedClients: state.uninitializedClients.filter(c => {
return (
c.deviceId !== payload.query.device_id ||
c.client.appName !== payload.query.app
);
}),
selectedApp,
selectedPlugin,
};
}
case 'NEW_CLIENT_SANITY_CHECK': {
const {payload} = action;
// Check for clients initialised when there is no matching device
const clientIsStillConnected = state.clients.filter(
client => client.id == payload.query.device_id,
);
if (clientIsStillConnected) {
const matchingDeviceForClient = state.devices.filter(
device => payload.query.device_id === device.serial,
);
if (matchingDeviceForClient.length === 0) {
console.error(
`Client initialised for non-displayed device: ${payload.id}`,
);
}
}
return state;
}
case 'CLIENT_REMOVED': {
const {payload} = action;
let selected = {};
if (state.selectedApp === payload) {
selected.selectedApp = null;
selected.selectedPlugin = DEFAULT_PLUGIN;
}
return {
...state,
...selected,
clients: state.clients.filter(
(client: Client) => client.id !== payload,
),
};
}
case 'PREFER_DEVICE': {
const {payload: userPreferredDevice} = action;
return {...state, userPreferredDevice};
}
case 'SERVER_ERROR': {
const {payload} = action;
return {...state, error: payload};
}
case 'START_CLIENT_SETUP': {
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)),
};
}
case 'CLIENT_SETUP_ERROR': {
const {payload} = action;
const errorMessage =
payload.error instanceof Error ? payload.error.message : payload.error;
console.error(
`Client setup error: ${errorMessage} while setting up client: ${
payload.client.os
}:${payload.client.deviceName}:${payload.client.appName}`,
);
return {
...state,
uninitializedClients: state.uninitializedClients
.map(c =>
isEqual(c.client, payload.client)
? {...c, errorMessage: errorMessage}
: c,
)
.sort((a, b) => a.client.appName.localeCompare(b.client.appName)),
error: `Client setup error: ${errorMessage}`,
};
}
default:
return state;
}
};
export default (state: State = INITAL_STATE, action: Action): State => {
const nextState = reducer(state, action);
if (nextState.selectedDevice) {
const {selectedDevice} = nextState;
const deviceNotSupportedError = 'iOS Devices are not yet supported';
const error =
selectedDevice.os === 'iOS' &&
selectedDevice.deviceType === 'physical' &&
!iosUtil.isAvailable()
? deviceNotSupportedError
: null;
if (nextState.error === deviceNotSupportedError) {
nextState.error = error;
} else {
nextState.error = error || nextState.error;
}
}
return nextState;
};
export const selectDevice = (payload: BaseDevice): Action => ({
type: 'SELECT_DEVICE',
payload,
});
export const preferDevice = (payload: string): Action => ({
type: 'PREFER_DEVICE',
payload,
});
export const selectPlugin = (payload: {|
selectedPlugin: ?string,
selectedApp?: ?string,
deepLinkPayload: ?string,
|}): Action => ({
type: 'SELECT_PLUGIN',
payload,
});
export const userPreferredPlugin = (payload: string): Action => ({
type: 'SELECT_USER_PREFERRED_PLUGIN',
payload,
});