Debugging Flipper Desktop App
How to deal with Flipper Plugin is unavailable

Building Great Apps with React Native
Search for a command to run...
How to deal with Flipper Plugin is unavailable

Building Great Apps with React Native
No comments yet. Be the first to comment.
Background If you've never heard of React Native EU, it's the largest React Native conference in Europe. Whether you're new to React Native or a guru, you can learn something or gain useful knowledge from the conference. Callstack hosts the conferenc...

Introduction Recently at work, my colleagues engaged in a discussion about whether to use React Native Modal or React Navigation Modal. In our tech stack, we already have both integrations, so there's no extra work required for setup. I never thought...

Intro Have you ever considered visualizing your React Navigation structure? If so, which tools do you utilize? Recently, I needed to construct a React Navigation tree for a presentation. So, I looked for different ways to show my React Navigation str...

Introduction If you missed Part 1, we discussed the keynote and Day 1's React Native EU agenda. In this post, we will delve into the remaining portions of the conference. Day 2 Hermes Hacking: Demystifying JavaScript Engines - by Radek Pietruszewski ...

Introduction React Native EU is an annual conference that brings together developers, industry leaders, and enthusiasts from around the world to share their knowledge, experience, and insights about the popular cross-platform framework, React Native....

While upgrading React Native project at work, one problem occurs to me is that I’m not able to use the Hermes debugger plugin on Flipper (Desktop App). Before the upgrade, my team has been using Chrome V8 for Android. This is where the exploration begins.

Below are my exploration flow
Is it due to React Native version and Flipper version mismatch? ⇒ ❌
Is it possible Flipper has a bug in the current version? ⇒ ❌
Look through the repo Github and did not find a relevant issue
Setting up a new pure React Native project with the specific version seems to work well with the Hermes debugger plugin
Is it due to some special package or setting on the current project? ⇒ ❌
Is it possible Metro server had worked differently? ⇒ ❌
While digging through the react-native-cli, did not find suspicious things
While launching the Metro sever on the pure React Native project, the Hermes debugger was showing
Since the above doesn’t seem to get me any further, I decide to explore on Flipper Desktop App more. But at least the problem has been nailed down as to Why Flipper couldn’t connect to the existing project’s Metro server?

To run Flipper Desktop App from the source
git clone <https://github.com/facebook/flipper.git>
cd flipper/desktop
yarn
yarn start
Let’s start from figure out where this screen comes from

First, this plugin is located at plugins/public/hermesdebuggerrn .
// /plugins/public/hermesdebuggerrn/package.json
{
"$schema": "<https://fbflipper.com/schemas/plugin-package/v2.json>",
"name": "flipper-plugin-hermesdebuggerrn",
"id": "Hermesdebuggerrn",
"pluginType": "device",
"supportedDevices": [
{
"os": "Metro",
"archived": false
}
] ...
}
Second, let’s look at the rendering part of the plugin
// plugins/public/hermesdebuggerrn/index.tsx
...
renderContent() {
const {error, selectedTarget, targets} = this.state;
console.log('Mark >>>>>>>>>>>>>>> ', error, selectedTarget, targets);
if (selectedTarget) {
...
return ( <ChromeDevTools ... />);
} else if (targets != null && targets.length === 0) {
return <LaunchScreen />;
} else if (targets != null && targets.length > 0) {
return <SelectScreen targets={targets} onSelect={this.handleSelect} />;
} else if (error != null) {
return <ErrorScreen error={error} />;
} else {
return null;
}
}
...
After adding the debug log, it doesn’t seem to show. So the issue should be coming from another place. By searching the substring of the error message, we were able to find the following.
// flipper-ui-core/src/utils/pluginUtils.tsx
export function computePluginLists(connections, plugins, device, metroDevice, client){
...
if (device) {
// find all device plugins that aren't part of the current device / metro
for (const p of plugins.devicePlugins.values()) {
if (!device.supportsPlugin(p) && !metroDevice?.supportsPlugin(p)) {
unavailablePlugins.push([
p.details,
`Device plugin '${getPluginTitle(
p.details,
)}' is not supported by the selected device '${device.title}' (${
device.os
})`,]);
}}...
}
...
}
So the origin of the error message has been found, let’s figure out how it got into this condition.
// flipper-ui-core/src/utils/pluginUtils.tsx
console.log('Device >>>> ', device, metroDevice);
if (!device.supportsPlugin(p) && !metroDevice?.supportsPlugin(p)) {
unavailablePlugins.push([...])
...
}


The reason that Hermes debugger is being pushed into the unavailablePlugins is that metroDevice was null.
Wonderful, we’ve nailed down the problem of metro Devices not being unavailable. Next, we just need to figure out why. Let’s find the caller for computePluginLists, to see what has been passed in for the metroDevice.

// flipper-ui-core/src/selectors/connections.tsx
export const getPluginLists = createSelector(
({
connections: {
enabledDevicePlugins,
enabledPlugins,
selectedAppPluginListRevision, // used only to invalidate cache
},
}) => ({
enabledDevicePlugins,
enabledPlugins,
selectedAppPluginListRevision,
}),
({
plugins: {
clientPlugins,
...
},
}) => ({
clientPlugins,
...
}),
getActiveDevice,
getMetroDevice,
getActiveClient,
computePluginLists,
);
To learn more about the createSelector
Exploring more on getMetroDevice
// flipper-ui-core/src/selectors/connections.tsx
export const getMetroDevice = createSelector(getDevices, (devices) => {
console.log('Mark >>>>>>>>', devices);
return (
devices.find((device) => **device.os === 'Metro' && !device.isArchived) ??
null
);
});

As we see from the log, there doesn’t exist any BaseDevice which device.os === 'Metro'.
Next, we need to have a look at the state
// flipper-ui-core/src/selectors/connections.tsx
const getDevices = (state: State) => state.connections.devices;
export const getMetroDevice = createSelector(**getDevices**, (devices) => {
return (
devices.find((device) => device.os === 'Metro' && !device.isArchived) ??
null
);
});
Let’s explore the reducer of connections
// flipper-ui-core/src/reducers/connections.tsx
export default (state: State = INITAL_STATE, action: Actions): State => {
switch (action.type) {
...
case 'REGISTER_DEVICE': {
const {payload} = action;
const newDevices = state.devices.slice();
...
return {
...state,
devices: newDevices,
selectedDevice: selectNewDevice ? payload : state.selectedDevice,
selectedAppId,
};
}
}
...
}
Next, let’s try to find where the Actions are being called
// flipper-ui-core/src/dispatcher/flipperServer.tsx
export function handleDeviceConnected(
server: FlipperServer,
store: Store,
logger: Logger,
deviceInfo: DeviceDescription,
) {
...
console.log('Device info >>>> ', server, deviceInfo);
const device = new BaseDevice(server, deviceInfo);
...
store.dispatch({
type: 'REGISTER_DEVICE',
payload: device,
});
}
Logs collect above

The caller of handleDeviceConnected would be connectFlipperServerToStore
// flipper-ui-core/src/dispatcher/flipperServer.tsx
export function connectFlipperServerToStore(
server: FlipperServer,
store: Store,
logger: Logger,
) {
...
server.on('device-connected', (deviceInfo) => {
**handleDeviceConnected**(server, store, logger, deviceInfo);
});
...
waitFor(store, (state) => state.plugins.initialized)
.then(() => server.exec('device-list'))
.then((devices) => {
// register all devices
devices.forEach((device) => {
**handleDeviceConnected**(server, store, logger, device);
});
})
...
}
We’ll need to find the server
The caller of connectFlipperServerToStore is the init in startFlipperDesktop.tsx
// flipper-ui-core/src/startFlipperDesktop.tsx
function init(flipperServer: FlipperServer) {
const settings = getRenderHostInstance().serverConfig.settings;
const store = getStore();
const logger = initLogger(store);
...
connectFlipperServerToStore(flipperServer, store, logger);
...
}
export async function startFlipperDesktop(flipperServer: FlipperServer) {
getRenderHostInstance(); // renderHost instance should be set at this point!
init(flipperServer);
}
To proceed, we will need to look at callers of startFlipperDesktop
// app/src/init.tsx
async function start() {
...
const electronIpcClient = new ElectronIpcClientRenderer();
const flipperServer: FlipperServer = await getFlipperServer**(
logger,
electronIpcClient,
);
const flipperServerConfig = await flipperServer.exec('get-config');
await initializeElectron(
flipperServer,
flipperServerConfig,
electronIpcClient,
);
...
require('flipper-ui-core').startFlipperDesktop(flipperServer);
await flipperServer.connect();
...
}
start().catch((e) => { ... });
Following, we should look into getFlipperServer
// app/src/init.tsx
async function getFlipperServer(
logger: Logger,
electronIpcClient: ElectronIpcClientRenderer,
): Promise<FlipperServer> {
...
const getEmbeddedServer = async () => {
const server = new FlipperServerImpl(
{
environmentInfo,
env: parseEnvironmentVariables(env),
gatekeepers: gatekeepers,
paths: {
appPath,
homePath: await electronIpcClient.send('getPath', 'home'),
execPath,
staticPath,
tempPath: await electronIpcClient.send('getPath', 'temp'),
desktopPath: await electronIpcClient.send('getPath', 'desktop'),
},
launcherSettings: await loadLauncherSettings(),
processConfig: loadProcessConfig(env),
settings,
validWebSocketOrigins:
constants.VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES,
},
logger,
keytar,
);
return server;
};
...
return getEmbeddedServer();
}
Dig deeper, we should look at the implementation of FlipperServerImpl
In the app/src/init.tsx called flipperServer.connect();
// flipper-server-core/src/FlipperServerImpl.tsx
export class FlipperServerImpl implements FlipperServer {
...
async connect() {
...
try {
await this.createFolders();
await this.server.init();
await this.pluginManager.start();
await this.startDeviceListeners();
this.setServerState('started');
} catch (e) {...}
}
...
}
More on the this.startDeviceListeners() part
// flipper-server-core/src/FlipperServerImpl.tsx
import metroDevice from './devices/metro/metroDeviceManager';
async startDeviceListeners() {
const asyncDeviceListenersPromises: Array<Promise<void>> = [];
if (this.config.settings.enableAndroid) {
...
}
if (this.config.settings.enableIOS) {
...
}
const asyncDeviceListeners = await Promise.all(
asyncDeviceListenersPromises,
);
this.disposers.push(
...asyncDeviceListeners,
metroDevice(this),
desktopDevice(this),
);
}
Deeper into metroDevice
// flipper-server-core/src/devices/metro/metroDeviceManager.tsx
export default (flipperServer: FlipperServerImpl) => {
let timeoutHandle: NodeJS.Timeout;
let ws: WebSocket | undefined;
let unregistered = false;
async function tryConnectToMetro() {
if (ws) {
return;
}
if (await **isMetroRunning**()) {
...
}
}
}
More in isMetroRunning
// flipper-server-core/src/devices/metro/metroDeviceManager.tsx
const METRO_MESSAGE = ['React Native packager is running', 'Metro is running'];
async function isMetroRunning(): Promise<boolean> {
console.log('Metro >>>>>> is Running');
return new Promise((resolve) => {
http
.get(METRO_URL, (resp) => {
let data = '';
resp
.on('data', (chunk) => {
data += chunk;
console.log('Metro >>>>>> data', data);
})
.on('end', () => {
const isMetro = METRO_MESSAGE.some((msg) => data.includes(msg));
console.log('Metro >>>>>> isMetro', isMetro);
resolve(isMetro);
});
})
.on('error', (err: any) => {
if (err.code !== 'ECONNREFUSED' && err.code !== 'ECONNRESET') {
console.error('Could not connect to METRO ' + err);
}
resolve(false);
});
});
}
Logs from about will get the below

So the reason why isMetro is being false is that the data part doesn’t contain any of the substrings.
// flipper-server-core/src/devices/metro/metroDeviceManager.tsx
const METRO_HOST = 'localhost';
const METRO_PORT = parseEnvironmentVariableAsNumber('METRO_SERVER_PORT', 8081);
const METRO_URL = `http://${METRO_HOST}:${METRO_PORT}`;
const METRO_MESSAGE = ['React Native packager is running', 'Metro is running'];
const isMetro = METRO_MESSAGE.some((msg) => data.includes(msg));
And since the current localhost:8081 will get the below
<!DOCTYPE html>
<html style="height:100%">
<head>
<title>OLIO</title>
</head>
<body style="height:100%">
<div id="root" style="display:flex;height:100%"></div>
<script type="text/javascript" src="/bundle.web.js"></script>
</body>
</html>
For some historical reason, we had an index.html as above in the root directory. Since it doesn’t contain any substring in METRO_MESSAGE. In this case isMetro being false.
To apply a quick fix to this would just add a comment to the index.html
<!DOCTYPE html>
<html style="height:100%">
<head>
<title>OLIO</title>
</head>
<body style="height:100%">
<!-- React Native packager is running. -->
<div id="root" style="display:flex;height:100%"></div>
<script type="text/javascript" src="/bundle.web.js"></script>
</body>
</html>
In the final step, add the comment (<!-- React Native packager is running. -->) ⇒ ✅

It’s quite a journey to explore the Flipper source code and found out that things are caused by something that you never had to imagine. It's common to see that Flipper plugin doesn't work and with the limited logs it provides, it could be tricky to figure out what the root cause was. As in this post, we explore how you could build Flipper from source and debug around. Hope this post built up some confidence while playing around with Flipper.