# Debugging Flipper Desktop App

## Background

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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679554481273/c7bbe201-abc6-454d-9c51-94bf04683bb7.png align="center")

## Explore Flow

Below are my exploration flow

* Is it due to React Native version and Flipper version mismatch? ⇒ ❌
    
    * Things are expected
        
* 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? ⇒ ❌
    
    * Comparing package-lock file between the pure React Native project, related packages are using the same version
        
* 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?***

## Flipper (Desktop App)

### How Flipper does connect?

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679554775679/f38f0513-6680-4da6-884e-005638a3499c.png align="center")

### Run from Source

To run Flipper Desktop App from the source

```bash
git clone <https://github.com/facebook/flipper.git>
cd flipper/desktop
yarn
yarn start
```

### Debugging

Let’s start from figure out where this screen comes from

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679554849695/3ad171cd-c77e-477d-9c0c-9e36ad4967e9.png align="left")

First, this plugin is located at `plugins/public/hermesdebuggerrn` .

```json
// /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

```typescript
// 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.

```typescript
// 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.

```typescript
// flipper-ui-core/src/utils/pluginUtils.tsx
console.log('Device >>>> ', device, metroDevice);
if (!device.supportsPlugin(p) && !metroDevice?.supportsPlugin(p)) {
  unavailablePlugins.push([...])
 ...
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679554989167/cf34c861-b56d-434d-8747-2b0d3e3d80ee.png align="left")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555007415/cbae33bb-94cf-4987-b943-55ad254d8290.png align="left")

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`.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555057076/65d9e724-e385-4fc9-81b0-2530022f4b51.png align="center")

```typescript
// 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](https://github.com/reduxjs/reselect#createselectorinputselectors--inputselectors-resultfunc-selectoroptions)

Exploring more on `getMetroDevice`

```typescript
// 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
  );
});
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555277745/6c111dc0-0c1f-4d99-ad04-d2b6f04923b5.png align="center")

As we see from the log, there doesn’t exist any BaseDevice which `device.os === 'Metro'`.

### More about the Metro Device part

Next, we need to have a look at the **state**

```typescript
// 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

```typescript
// 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

```typescript
// 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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555434311/b1fda2ea-1802-475c-b67c-4b4b87fc378f.png align="center")

The caller of `handleDeviceConnected` would be `connectFlipperServerToStore`

```typescript
// 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*

```jsx
// 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`

```typescript
// 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**

```typescript
// 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();`

```typescript
// 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

```typescript
// 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**

```typescript
// 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**

```typescript
// 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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555575300/b9903783-98f0-468b-b4f2-ade58509053f.png align="left")

So the reason why isMetro is being false is that the data part doesn’t contain any of the substrings.

```typescript
// 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**](http://localhost:8081) will get the below

```html
<!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.

## How to Fix it

To apply a quick fix to this would just add a comment to the index.html

```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. -->`) ⇒ ✅

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1679555630299/12f901a4-3f57-4bed-950b-d0ecfc0d1754.png align="center")

## Conclusion

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.
