Blog post "Headless Flipper - what it means for plugin developers"
Reviewed By: passy, antonk52 Differential Revision: D36480939 fbshipit-source-id: dacbdb1af4382e9b52de79403e6aea9a0c740b1c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
34579ae266
commit
29eae00309
337
website/blog/2022-05-20-preparing-for-headless-flipper.md
Normal file
337
website/blog/2022-05-20-preparing-for-headless-flipper.md
Normal file
@@ -0,0 +1,337 @@
|
||||
---
|
||||
title: Headless Flipper - what it means for plugin developers
|
||||
author: Andrey Goncharov
|
||||
author_title: Software Engineer
|
||||
author_url: https://github.com/aigoncharov
|
||||
author_image_url: https://github.com/aigoncharov.png
|
||||
tags: [flipper, headless, plugins]
|
||||
description:
|
||||
Flipper is moving from an Electron monolith to a headless Node.js app with a
|
||||
web UI. It reshapes how we think about plugins and what plugins can do. We
|
||||
talk about what changes and how to prepare our plugins for the migration.
|
||||
image: /img/preparing-for-headless-flipper.jpg
|
||||
hide_table_of_contents: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
We know Flipper as an Electron desktop app that serves mobile developers as
|
||||
their debugging companion. Thousands of people use Flipper every day to tinker
|
||||
with their app and get to the bottom of tricky problems.
|
||||
|
||||
As announced in the previous
|
||||
[roadmap post](https://fbflipper.com/blog/2021/10/14/roadmap/), we are committed
|
||||
to amplifying how Flipper could improve the quality of our software. We want
|
||||
take Flipper beyond its current role as a complementary debugging tool, provide
|
||||
a powerful API, and allow using Flipper in more than just the GUI context (we
|
||||
call it "headless mode"). Imagine talking to your mobile device (or anything
|
||||
else that runs Flipper Client) from your terminal. Imagine deploying Flipper
|
||||
remotely in the cloud and interacting with it from your laptop. Imagine using
|
||||
your favorite plugins for automated testing.
|
||||
|
||||
In this post we cover:
|
||||
|
||||
- How Flipper changes to facilitate the headless mode
|
||||
- How it affects plugins
|
||||
- A migration guide
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
## How Flipper changes
|
||||
|
||||
Let us take a look at how it works today as an Electron app.
|
||||
|
||||

|
||||
|
||||
Here is what happens:
|
||||
|
||||
1. Flipper starts as an Electron application.
|
||||
1. WebSocket server starts.
|
||||
2. Device discovery starts via adb/idb/metro.
|
||||
3. Electron shows a web view with Flipper UI (React).
|
||||
4. Flipper UI queries the device discovery service for a list of devices.
|
||||
2. At this point, Flipper can already run
|
||||
["device" plugins](https://fbflipper.com/docs/extending/desktop-plugin-structure/#creating-a-device-plugin).
|
||||
These plugins do not receive a connection to a running app. They talk to the
|
||||
device via adb/idb/metro.
|
||||
3. An app starts on the device.
|
||||
4. Flipper Client embedded in the app connects to the WebSocket server.
|
||||
5. Flipper updates the list of known clients and reflects it in the UI.
|
||||
6. Now Flipper can run
|
||||
["client" plugins](https://fbflipper.com/docs/extending/desktop-plugin-structure/#creating-a-client-plugin).
|
||||
7. Client plugins talk to the device application over the WebSocket connection.
|
||||
|
||||
> You can start Flipper Electron with `yarn start` from the `/desktop` folder.
|
||||
|
||||
Here is how Flipper Headless works.
|
||||
|
||||

|
||||
|
||||
1. Flipper starts as a Node.js application.
|
||||
1. WebSocket server starts.
|
||||
2. Device discovery starts via adb/idb/metro.
|
||||
3. Web server starts.
|
||||
4. It serves Flipper UI to a browser.
|
||||
5. Flipper UI connects to the WebSocket server.
|
||||
6. Flipper UI sends a message over the WebSocket connection to query the
|
||||
device discovery service for a list of devices.
|
||||
2. At this point, Flipper can already run
|
||||
["device" plugins](https://fbflipper.com/docs/extending/desktop-plugin-structure/#creating-a-device-plugin).
|
||||
These plugins do not receive a connection to a running app. They talk to the
|
||||
device via adb/idb/metro.
|
||||
3. An app starts on the device.
|
||||
4. Flipper Client embedded in the app connects to the WebSocket server.
|
||||
5. Flipper updates the list of known clients. It sends a message over the
|
||||
WebSocket connection to Flipper UI with the information about the new device.
|
||||
6. Now Flipper can run
|
||||
["client" plugins](https://fbflipper.com/docs/extending/desktop-plugin-structure/#creating-a-client-plugin).
|
||||
7. Client plugins talk to the device application over the WebSocket bridge - the
|
||||
connection from Flipper UI to Flipper WebSocket server piped to the
|
||||
connection from the device application to the Flipper WebSocket server.
|
||||
|
||||
> You can start Flipper Electron with `yarn flipper-server` from the `/desktop`
|
||||
> folder.
|
||||
|
||||
## How it affects plugins
|
||||
|
||||
Plugins are hosted by Flipper UI. When it was a part of the Electron app, there
|
||||
was no problem. Plugins could access any Node.js APIs thanks to Electron magic.
|
||||
There were no constraints on what plugins could do. After making Flipper UI a
|
||||
proper web app running in a browser, we limited what plugins can do. They no
|
||||
longer can access the network stack or the file system because there are no
|
||||
corresponding browser APIs. Yet, we want to keep Flipper flexible and provide as
|
||||
much freedom to plugin developers as possible. Moreover, we could not leave the
|
||||
existing plugins without a clear migration path.
|
||||
|
||||

|
||||
|
||||
Since we already have a WebSocket connection between Flipper UI and Flipper
|
||||
Server, we can model almost any request-response and even stream-based Node.js
|
||||
APIs over it. So far, we exposed a curated subset of them:
|
||||
|
||||
- child_process
|
||||
- exec
|
||||
- fs (and [fs-extra](https://www.npmjs.com/package/fs-extra))
|
||||
- constants
|
||||
- access
|
||||
- pathExists
|
||||
- unlink
|
||||
- mkdir
|
||||
- rm
|
||||
- copyFile
|
||||
- stat
|
||||
- readlink
|
||||
- readFile
|
||||
- writeFile
|
||||
|
||||
We also provided a way to
|
||||
[download a file](https://github.com/facebook/flipper/blob/0f038218f893d86e91714cd91eed8e37d756386c/desktop/flipper-plugin/src/plugin/FlipperLib.tsx#L83)
|
||||
or send requests to the
|
||||
[internal infrastructure](https://github.com/facebook/flipper/blob/0f038218f893d86e91714cd91eed8e37d756386c/desktop/flipper-plugin/src/plugin/FlipperLib.tsx#L186).
|
||||
|
||||
> Please, find the complete list of available APIs on
|
||||
> [GitHub](https://github.com/facebook/flipper/blob/0f038218f893d86e91714cd91eed8e37d756386c/desktop/flipper-plugin/src/plugin/FlipperLib.tsx#L95).
|
||||
> [Here are Node.js API abstractions](https://github.com/facebook/flipper/blob/0f038218f893d86e91714cd91eed8e37d756386c/desktop/flipper-plugin/src/plugin/FlipperLib.tsx#L47)
|
||||
> specifically.
|
||||
|
||||
As you might have noticed, all exposed APIs are of the request-response nature.
|
||||
They assume a short finite controlled lifespan. Yet, some plugins start
|
||||
additional web servers or spawn long-living child processes. To control their
|
||||
lifetime we need to track them on Flipper Server side and stop them whenever
|
||||
Flipper UI disconnects. Say hello to a new experimental feature - Flipper Server
|
||||
Add-ons!
|
||||
|
||||

|
||||
|
||||
Now, every flipper plugin could have "server add-on" meta-information. Whenever
|
||||
a Flipper plugin that has a corresponding Server Add-on starts, it sends a
|
||||
command to Flipper Server to start its Server Add-on counterpart. Flipper plugin
|
||||
that lives in a browser inside of Flipper UI talks to its server add-on over the
|
||||
WebSocket connection. Whenever a user disables a plugin, Flipper sends a command
|
||||
to Flipper Server to stop the add-on. At the same time, if Flipper UI crashes or
|
||||
the user just closes the tab, Flipper Server can kill the server add-on on its
|
||||
own.
|
||||
|
||||
Flipper plugin can talk to its server add-on companion (see
|
||||
`onServerAddOnMessage`, `onServerAddOnUnhandledMessage`, `sendToServerAddOn` in
|
||||
[the docs](https://fbflipper.com/docs/extending/flipper-plugin/#pluginclient))
|
||||
and act whenever it starts or stops (see `onServerAddOnStart`,
|
||||
`onServerAddOnStop` in
|
||||
[the docs](https://fbflipper.com/docs/extending/flipper-plugin/#pluginclient)).
|
||||
|
||||
Say, you wrote an ultimate library to find primes. You were cautious of the
|
||||
resource consumption, so you did it in Rust. You created a CLI interface for
|
||||
your new shiny library. Now, you want your Flipper plugin to use it. It takes a
|
||||
long time to find a prime and you want to keep track of the progress. You could
|
||||
use `getFlipperLib().remoteServerContext.childProcess.exec`, but it is not
|
||||
flexible enough to monitor progress updates that your CLI sends to stdout. Here
|
||||
is how you could approach it:
|
||||
|
||||
```tsx
|
||||
// contract.tsx
|
||||
export interface ServerAddOnEvents {
|
||||
// Server add-on sends "progress" events with the progress updates
|
||||
progress: number;
|
||||
}
|
||||
export interface ServerAddOnMethods {
|
||||
// Client plugin send "findPrime" messages to the server add-on to start finding primes
|
||||
findPrime: () => Promise<number>;
|
||||
}
|
||||
|
||||
// index.tsx (your plugin)
|
||||
import {usePlugin, useValue, createState, PluginClient} from 'flipper-plugin';
|
||||
import {ServerAddOnEvents, ServerAddOnMethods} from './contract';
|
||||
|
||||
export const plugin = (
|
||||
client: PluginClient<{}, {}, ServerAddOnEvents, ServerAddOnMethods>,
|
||||
) => {
|
||||
const prime = createState<number | null>(null);
|
||||
const progress = createState<number>(0);
|
||||
|
||||
client.onServerAddOnStart(async () => {
|
||||
const newPrime = await client.sendToServerAddOn('findPrime');
|
||||
prime.set(newPrime);
|
||||
});
|
||||
|
||||
client.onServerAddOnStart(() => {
|
||||
client.onServerAddOnMessage('progress', progressUpdate => {
|
||||
progress.set(progressUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
prime,
|
||||
progress,
|
||||
};
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const pluginInstance = usePlugin(plugin);
|
||||
const prime = useValue(pluginInstance.prime);
|
||||
const progress = useValue(pluginInstance.progress);
|
||||
|
||||
return <div>{prime ?? `Calculating (${progress}%) done...`}</div>;
|
||||
};
|
||||
|
||||
// serverAddOn.tsx
|
||||
import {ServerAddOn} from 'flipper-plugin';
|
||||
import {exec, ChildProcess} from 'child_process';
|
||||
import {ServerAddOnEvents, ServerAddOnMethods} from './contract';
|
||||
|
||||
const serverAddOn: ServerAddOn<ServerAddOnEvents, ServerAddOnMethods> =
|
||||
async connection => {
|
||||
let findPrimeChildProcess: ChildProcess | undefined;
|
||||
|
||||
connection.receive('findPrime', () => {
|
||||
if (findPrimeChildProcess) {
|
||||
// Allow only one findPrime request at a time. Finding primes is expensive!
|
||||
throw new Error('Too many requests!');
|
||||
}
|
||||
|
||||
// Start our awesome Rust lib
|
||||
findPrimeChildProcess = exec('/find-prime-cli', {shell: true});
|
||||
|
||||
// Return a Promise that resolves when a prime is found.
|
||||
// Flipper will serialize the value the promise is resolved with and send it oer the wire.
|
||||
return new Promise(resolve => {
|
||||
// Listen to stdout of the lib for the progress updates and, eventually, the prime
|
||||
findPrimeChildProcess.stdout.on('data', data => {
|
||||
// Say, data is a stringified JSON
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.type === 'progress') {
|
||||
connection.send('progress', parsed.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow new requests to find new primes
|
||||
findPrimeChildProcess = undefined;
|
||||
// If it is not a progress update, then a prime is found.
|
||||
resolve(parsed.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default serverAddOn;
|
||||
```
|
||||
|
||||
## Migration guide
|
||||
|
||||
1. Examine your plugins for Node.js APIs. Replace them with
|
||||
`getFlipperLib().remoteServerContext.*` calls.
|
||||
|
||||
```tsx
|
||||
// before
|
||||
import {mkdir} from 'fs/promises';
|
||||
|
||||
export const plugin = () => {
|
||||
const myAwesomeFn = async () => {
|
||||
await mkdir('/universe/dagobah');
|
||||
};
|
||||
|
||||
return {
|
||||
myAwesomeFn,
|
||||
};
|
||||
};
|
||||
|
||||
// after
|
||||
import {getFlipperLib} from 'flipper-plugin';
|
||||
|
||||
export const plugin = () => {
|
||||
const myAwesomeFn = async () => {
|
||||
await getFlipperLib().remoteServerContext.mkdir('/universe/dagobah');
|
||||
};
|
||||
|
||||
return {
|
||||
myAwesomeFn,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
2. If your plugin uses network stack of spawns a subprocess, consider creating a
|
||||
Server Add-on.
|
||||
|
||||
1. In your plugin's folder create a new file - `serverAddOn.tsx`
|
||||
2. In your plugin's package.json add fields `serverAddOn` and
|
||||
`flipperBundlerEntryServerAddOn`
|
||||
|
||||
```js
|
||||
{
|
||||
// ...
|
||||
"serverAddOn": "dist/serverAddOn.js",
|
||||
"flipperBundlerEntryServerAddOn": "serverAddOn.tsx",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
3. Move your Node.js API calls to `serverAddOn.tsx`
|
||||
|
||||
3. Verify your plugin works in a browser environment.
|
||||
1. Clone [Flipper repo](https://github.com/facebook/flipper).
|
||||
2. Navigate to the `desktop` folder.
|
||||
3. In your terminal run `yarn`.
|
||||
4. Run `yarn flipper-server`.
|
||||
5. Load your plugin and make sure it works.
|
||||
|
||||
## P.S. Flipper needs you!
|
||||
|
||||
Flipper is maintained by a small team at Meta, yet is serving over a hundred
|
||||
plugins and dozens of different targets. Our team's goal is to support Flipper
|
||||
as a plugin-based platform for which we maintain the infrastructure. We don't
|
||||
typically invest in individual plugins, but we do love plugin improvements.
|
||||
|
||||
For that reason, we've marked many requests in the issue tracker as
|
||||
[PR Welcome](https://github.com/facebook/flipper/issues?q=is%3Aissue+is%3Aopen+label%3A%22PR+welcome%22).
|
||||
Contributing changes should be as simple as cloning the
|
||||
[repository](https://github.com/facebook/flipper) and running
|
||||
`yarn && yarn start` in the `desktop/` folder.
|
||||
|
||||
Investing in debugging tools, both generic ones or just for specific apps, will
|
||||
benefit iteration speed. And we hope Flipper will make it as hassle free as
|
||||
possible to create your debugging tools. For an overview of Flipper for React
|
||||
Native, and why and how to build your own plugins, we recommend checking out the
|
||||
[Flipper: The Extensible DevTool Platform for React Native](https://youtu.be/WltZTn3ODW4)
|
||||
talk.
|
||||
|
||||
Happy debugging!
|
||||
BIN
website/static/img/flipper-add-on.jpg
Normal file
BIN
website/static/img/flipper-add-on.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
website/static/img/flipper-arch-electron.jpg
Normal file
BIN
website/static/img/flipper-arch-electron.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
website/static/img/flipper-arch-headless.jpg
Normal file
BIN
website/static/img/flipper-arch-headless.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
BIN
website/static/img/flipper-node-apis.jpg
Normal file
BIN
website/static/img/flipper-node-apis.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
BIN
website/static/img/preparing-for-headless-flipper.jpg
Normal file
BIN
website/static/img/preparing-for-headless-flipper.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Reference in New Issue
Block a user