Wrap plugin calls into isConnected guards

Summary:
Previously, plugins could relatively safely use `client.call` at any moment in time to fetch some information from / to the client. Except for some raise conditions there was generally speaking a connection available.

With this stack it becomes possible to interact with plugins even after an app (unexpectedly) disconnected, which makes Flipper a lot more versatile, especially when it comes to inspect crashes post mortem. (For more explanation see the second diff in this stack)

However, this means that it is no longer safe to assume there is always a connection available. For that reason `client.isConnected` has been introduced to safeguard against that.

This diff introduces guards on all user interactions that are not super explicitly triggered by the user to avoid a lot of errors being generated. This is mostly in `init()` blocks and implicit user events like selecting or hovering rows.

Explicit user interactions like pressing buttons are not guarded by this diff, as ideally failure to communicate with the client should be communicated back to the user more explicitly rather than failing silently. The next diff will introduce a fallback mechanism where a popup is shown in case those calls aren't guarded.

Fixed a few key warnings while at it.

Reviewed By: nikoant

Differential Revision: D26275604

fbshipit-source-id: 5630066cdd9541e448a6dd1f8a21861b5d751ced
This commit is contained in:
Michel Weststrate
2021-02-09 04:12:09 -08:00
committed by Facebook GitHub Bot
parent 43c68c0e7c
commit 60cfcb54a1
9 changed files with 66 additions and 47 deletions

View File

@@ -354,6 +354,10 @@ export default function createTableNativePlugin(id: string, title: string) {
getTableMetadata = () => {
if (!this.props.persistedState.tableMetadata) {
if (!this.client.isConnected) {
this.setState({error: 'Application disconnected'});
return;
}
this.client
.call('getMetadata')
.then((metadata) => {

View File

@@ -150,19 +150,7 @@ function renderStatusMessage(
isAppConnected: boolean,
): React.ReactNode {
if (!activeDevice) {
return (
<Layout.Horizontal gap center>
<ExclamationCircleOutlined style={{color: theme.warningColor}} />
<Text
type="secondary"
style={{
textTransform: 'uppercase',
fontSize: '0.8em',
}}>
Device disconnected
</Text>
</Layout.Horizontal>
);
return;
}
return !isDeviceConnected ? (
activeDevice.isArchived ? (

View File

@@ -108,11 +108,15 @@ export class InspectorSidebar extends Component<Props, State> {
}
checkIfConsoleIsEnabled() {
this.props.client
.call('isConsoleEnabled')
.then((result: {isEnabled: boolean}) => {
this.setState({isConsoleEnabled: result.isEnabled});
});
if (this.props.client.isConnected) {
this.props.client
.call('isConsoleEnabled')
.then((result: {isEnabled: boolean}) => {
this.setState({isConsoleEnabled: result.isEnabled});
});
} else {
this.setState({isConsoleEnabled: false});
}
}
render() {

View File

@@ -879,19 +879,21 @@ export default class DatabasesPlugin extends FlipperPlugin<
}
init() {
this.databaseClient = new DatabaseClient(this.client);
this.databaseClient.getDatabases({}).then((databases) => {
this.dispatchAction({
type: 'UpdateDatabases',
databases,
if (this.client.isConnected) {
this.databaseClient = new DatabaseClient(this.client);
this.databaseClient.getDatabases({}).then((databases) => {
this.dispatchAction({
type: 'UpdateDatabases',
databases,
});
});
});
this.dispatchAction({
type: 'UpdateFavorites',
favorites: JSON.parse(
localStorage.getItem('plugin-database-favorites-sql-queries') || '[]',
),
});
this.dispatchAction({
type: 'UpdateFavorites',
favorites: JSON.parse(
localStorage.getItem('plugin-database-favorites-sql-queries') || '[]',
),
});
}
}
onDataClicked = () => {

View File

@@ -87,13 +87,15 @@ export default class Example extends FlipperPlugin<State, any, PersistedState> {
* Call a method of the mobile counterpart, to display a message.
*/
sendMessage() {
this.client
.call('displayMessage', {message: this.state.message || 'Weeeee!'})
.then((_params: DisplayMessageResponse) => {
this.setState({
prompt: 'Nice',
if (this.client.isConnected) {
this.client
.call('displayMessage', {message: this.state.message || 'Weeeee!'})
.then((_params: DisplayMessageResponse) => {
this.setState({
prompt: 'Nice',
});
});
});
}
}
render() {

View File

@@ -284,13 +284,17 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
init() {
debugLog('init()');
this.updateCaches('init');
this.client.subscribe(
'debug_overlay_event',
(event: FrescoDebugOverlayEvent) => {
this.setState({isDebugOverlayEnabled: event.enabled});
},
);
if (this.client.isConnected) {
this.updateCaches('init');
this.client.subscribe(
'debug_overlay_event',
(event: FrescoDebugOverlayEvent) => {
this.setState({isDebugOverlayEnabled: event.enabled});
},
);
} else {
debugLog(`not connected)`);
}
this.imagePool = new ImagePool(this.getImage, (images: ImagesMap) =>
this.props.setPersistedState({imagesMap: images}),
);
@@ -378,6 +382,10 @@ export default class FlipperImagesPlugin extends FlipperPlugin<
};
getImage = (imageId: string) => {
if (!this.client.isConnected) {
debugLog(`Cannot fetch image ${imageId}: disconnected`);
return;
}
debugLog('<- getImage requested for ' + imageId);
this.client.call('getImage', {imageId}).then((image: ImageData) => {
debugLog('-> getImage ' + imageId + ' returned');

View File

@@ -113,13 +113,18 @@ export default class Inspector extends Component<Props, State> {
{
label: 'Focus',
click: (id: ElementID) => {
this.props.client.call('onRequestAXFocus', {id});
if (this.props.client.isConnected) {
this.props.client.call('onRequestAXFocus', {id});
}
},
},
]
: [];
componentDidMount() {
if (!this.props.client.isConnected) {
return;
}
this.props.client.call(this.call().GET_ROOT).then((root: Element) => {
this.props.setPersistedState({
[this.props.ax ? 'rootAXElement' : 'rootElement']: root.id,
@@ -309,7 +314,7 @@ export default class Inspector extends Component<Props, State> {
ids: Array<ElementID> = [],
options: GetNodesOptions,
): Promise<Array<Element>> {
if (ids.length > 0) {
if (ids.length > 0 && this.props.client.isConnected) {
const {forAccessibilityEvent} = options;
const {
elements,
@@ -404,6 +409,9 @@ export default class Inspector extends Component<Props, State> {
});
onElementHovered = debounce((key: ElementID | null | undefined) => {
if (!this.props.client.isConnected) {
return;
}
this.props.client.call(this.call().SET_HIGHLIGHTED, {
id: key,
isAlignmentMode: this.props.inAlignmentMode,

View File

@@ -320,8 +320,8 @@ export default class LayoutPlugin extends FlipperPlugin<
: this.client;
}
onToggleAlignmentMode = () => {
if (this.client.isConnected) {
if (this.state.selectedElement) {
if (this.state.selectedElement) {
if (this.client.isConnected) {
this.client.send('setHighlighted', {
id: this.state.selectedElement,
inAlignmentMode: !this.state.inAlignmentMode,

View File

@@ -72,6 +72,9 @@ export default class SandboxView extends FlipperPlugin<
});
init() {
if (!this.client.isConnected) {
return;
}
this.client.call('getSandbox', {}).then((results: Array<Sandbox>) => {
this.setState({sandboxes: results});
});