Several minor connection improvements

Summary:
This diff

1. Fixes an reported issue where a concurrent running standalone React Devtools results in Flipper not connecting with a clear message
2. Improves the Error UI so that it is more clear what is happening
3. Introduces a Retry button that can be smashed to retrigger the whole initialization sequence, including retrying to grab the port needed for the connection

Reviewed By: jknoxville

Differential Revision: D20439911

fbshipit-source-id: f37a47f1000dd3b049dae8503856f6015cd422ab
This commit is contained in:
Michel Weststrate
2020-03-13 07:27:59 -07:00
committed by Facebook GitHub Bot
parent 1d04fc3e34
commit 406f21bc4e

View File

@@ -13,11 +13,14 @@ import {
AndroidDevice, AndroidDevice,
styled, styled,
View, View,
Toolbar,
MetroDevice, MetroDevice,
ReduxState, ReduxState,
connect, connect,
Device, Device,
CenteredView,
RoundedSection,
Text,
Button,
} from 'flipper'; } from 'flipper';
import React, {useEffect} from 'react'; import React, {useEffect} from 'react';
import getPort from 'get-port'; import getPort from 'get-port';
@@ -88,6 +91,13 @@ const GrabMetroDevice = connect<
const SUPPORTED_OCULUS_DEVICE_TYPES = ['quest', 'go', 'pacific']; const SUPPORTED_OCULUS_DEVICE_TYPES = ['quest', 'go', 'pacific'];
enum ConnectionStatus {
Initializing = 'Initializing...',
WaitingForReload = 'Waiting for connection from device...',
Connected = 'Connected',
Error = 'Error',
}
export default class ReactDevTools extends FlipperDevicePlugin< export default class ReactDevTools extends FlipperDevicePlugin<
{ {
status: string; status: string;
@@ -101,7 +111,7 @@ export default class ReactDevTools extends FlipperDevicePlugin<
pollHandle?: NodeJS.Timeout; pollHandle?: NodeJS.Timeout;
containerRef: React.RefObject<HTMLDivElement> = React.createRef(); containerRef: React.RefObject<HTMLDivElement> = React.createRef();
triedToAutoConnect = false; connectionStatus: ConnectionStatus = ConnectionStatus.Initializing;
metroDevice?: MetroDevice; metroDevice?: MetroDevice;
isMounted = true; isMounted = true;
@@ -110,23 +120,7 @@ export default class ReactDevTools extends FlipperDevicePlugin<
}; };
componentDidMount() { componentDidMount() {
let devToolsNode = findDevToolsNode(); this.bootDevTools();
if (!devToolsNode) {
devToolsNode = createDevToolsNode();
this.initializeDevTools(devToolsNode);
} else {
this.setStatus(
'DevTools have been initialized, waiting for connection...',
);
if (devToolsNode.innerHTML) {
this.setStatus(CONNECTED);
} else {
this.startPollForConnection();
}
}
attachDevTools(this.containerRef?.current!, devToolsNode);
this.startPollForConnection();
} }
componentWillUnmount() { componentWillUnmount() {
@@ -138,7 +132,8 @@ export default class ReactDevTools extends FlipperDevicePlugin<
devToolsNode && detachDevTools(devToolsNode); devToolsNode && detachDevTools(devToolsNode);
} }
setStatus(status: string) { setStatus(connectionStatus: ConnectionStatus, status: string) {
this.connectionStatus = connectionStatus;
if (!this.isMounted) { if (!this.isMounted) {
return; return;
} }
@@ -149,42 +144,84 @@ export default class ReactDevTools extends FlipperDevicePlugin<
} }
} }
startPollForConnection() { devtoolsHaveStarted() {
this.pollHandle = setTimeout(() => { return !!findDevToolsNode()?.innerHTML;
if (!this.isMounted) {
return false;
} }
if (findDevToolsNode()?.innerHTML) {
this.setStatus(CONNECTED); bootDevTools() {
let devToolsNode = findDevToolsNode();
if (!devToolsNode) {
devToolsNode = createDevToolsNode();
}
this.initializeDevTools(devToolsNode);
this.setStatus(
ConnectionStatus.Initializing,
'DevTools have been initialized, waiting for connection...',
);
if (this.devtoolsHaveStarted()) {
this.setStatus(ConnectionStatus.Connected, CONNECTED);
} else { } else {
if (!this.triedToAutoConnect) {
this.triedToAutoConnect = true;
this.setStatus(
"The DevTools didn't connect yet. Please open the DevMenu in the React Native app, or Reload it to connect",
);
if (this.metroDevice && this.metroDevice.ws) {
this.setStatus(
"Sending 'reload' to the Metro to force the DevTools to connect...",
);
this.metroDevice?.sendCommand('reload');
}
}
this.startPollForConnection(); this.startPollForConnection();
} }
}, 3000);
attachDevTools(this.containerRef?.current!, devToolsNode);
this.startPollForConnection();
}
startPollForConnection(delay = 3000) {
this.pollHandle = setTimeout(() => {
switch (true) {
// Closed already, ignore
case !this.isMounted:
return;
// Found DevTools!
case this.devtoolsHaveStarted():
this.setStatus(ConnectionStatus.Connected, CONNECTED);
return;
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
// prettier-ignore
case this.connectionStatus === ConnectionStatus.Initializing && !!this.metroDevice?.ws:
this.setStatus(
ConnectionStatus.WaitingForReload,
"Sending 'reload' to Metro to force the DevTools to connect...",
);
this.metroDevice!.sendCommand('reload');
this.startPollForConnection(10000);
return;
// Waiting for initial connection, but no WS bridge available
case this.connectionStatus === ConnectionStatus.Initializing:
this.setStatus(
ConnectionStatus.WaitingForReload,
"The DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect",
);
this.startPollForConnection(10000);
return;
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
case this.connectionStatus === ConnectionStatus.WaitingForReload:
this.setStatus(
ConnectionStatus.WaitingForReload,
"The DevTools didn't connect yet. Please verify your React Native app is in development mode, and that no other instance of the React DevTools are attached to the app already.",
);
this.startPollForConnection();
return;
}
}, delay);
} }
async initializeDevTools(devToolsNode: HTMLElement) { async initializeDevTools(devToolsNode: HTMLElement) {
try { try {
this.setStatus('Waiting for port 8097'); this.setStatus(ConnectionStatus.Initializing, 'Waiting for port 8097');
const port = await getPort({port: 8097}); // default port for dev tools const port = await getPort({port: 8097}); // default port for dev tools
this.setStatus('Starting DevTools server on ' + port); this.setStatus(
ConnectionStatus.Initializing,
'Starting DevTools server on ' + port,
);
ReactDevToolsStandalone.setContentDOMNode(devToolsNode) ReactDevToolsStandalone.setContentDOMNode(devToolsNode)
.setStatusListener(status => { .setStatusListener(status => {
this.setStatus(status); this.setStatus(ConnectionStatus.Initializing, status);
}) })
.startServer(port); .startServer(port);
this.setStatus('Waiting for device'); this.setStatus(ConnectionStatus.Initializing, 'Waiting for device');
const device = this.device; const device = this.device;
if (device) { if (device) {
@@ -192,22 +229,26 @@ export default class ReactDevTools extends FlipperDevicePlugin<
device.deviceType === 'physical' || device.deviceType === 'physical' ||
SUPPORTED_OCULUS_DEVICE_TYPES.includes(device.title.toLowerCase()) SUPPORTED_OCULUS_DEVICE_TYPES.includes(device.title.toLowerCase())
) { ) {
this.setStatus(`Setting up reverse port mapping: ${port}:${port}`); this.setStatus(
ConnectionStatus.Initializing,
`Setting up reverse port mapping: ${port}:${port}`,
);
(device as AndroidDevice).reverse([port, port]); (device as AndroidDevice).reverse([port, port]);
} }
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
this.setStatus('Failed to initialize DevTools: ' + e); this.setStatus(
ConnectionStatus.Error,
'Failed to initialize DevTools: ' + e,
);
} }
} }
render() { render() {
return ( return (
<View grow> <View grow>
{this.state.status !== CONNECTED ? ( {!this.devtoolsHaveStarted() ? this.renderStatus() : null}
<Toolbar>{this.state.status}</Toolbar>
) : null}
<Container ref={this.containerRef} /> <Container ref={this.containerRef} />
<GrabMetroDevice <GrabMetroDevice
onHasDevice={device => { onHasDevice={device => {
@@ -217,4 +258,26 @@ export default class ReactDevTools extends FlipperDevicePlugin<
</View> </View>
); );
} }
renderStatus() {
return (
<CenteredView>
<RoundedSection title={this.connectionStatus}>
<Text>{this.state.status}</Text>
{(this.connectionStatus === ConnectionStatus.WaitingForReload &&
this.metroDevice?.ws) ||
this.connectionStatus === ConnectionStatus.Error ? (
<Button
style={{width: 200, margin: '10px auto 0 auto'}}
onClick={() => {
this.metroDevice?.sendCommand('reload');
this.bootDevTools();
}}>
Retry
</Button>
) : null}
</RoundedSection>
</CenteredView>
);
}
} }