| # Fuchsia Controller tutorial |
| |
| This tutorial walks through the steps on how to write a simple script that uses |
| Fuchsia Controller (`fuchsia-controller`) in the Fuchsia source checkout |
| (`fuchsia.git`) setup. |
| |
| Fuchsia Controller consists of a set of libraries that allow users to connect |
| to a Fuchsia device and interact with the device using FIDL. Fuchsia Controller |
| was initially created for testing. But it is also useful for creating scriptable |
| code that interacts with FIDL interfaces on a Fuchsia device. For instance, |
| users may use Fuchsia Controller to write a script that performs simple device |
| interactions without having to write an `ffx` plugin in Rust. |
| |
| The main two parts of Fuchsia Controller are: |
| |
| - The `fuchsia-controller.so` file (which includes a |
| [header][fuchsia-controller-header-file] for the ABI) |
| - Higher level language bindings (which are built on top of the `.so` file |
| using the ABI) |
| |
| Currently, Fuchsia Controller's higher level language bindings are written |
| in Python only. |
| |
| The quickest way to use Fuchsia Controller is to write a Python script that |
| uses the `fuchsia-controller` code. In the Fuchsia source checkout setup, |
| you can build your Python binary into a `.pyz` file, which can then be |
| executed from the `out` directory (for instance, `$FUCHSIA_DIR/out/default`). |
| |
| To write your first Fuchsia Controller script, the steps are: |
| |
| 1. [Prerequisites](#prerequisites). |
| 1. [Update dependencies in BUILD.gn](#update-dependencies-in-buildgn). |
| 1. [Write your first program](#write-your-first-program). |
| 1. [Communicate with a Fuchsia device](#communicate-with-a-fuchsia-device). |
| 1. [Implement a FIDL server](#implement-a-fidl-server). |
| |
| If you run into bugs or have questions or suggestions, please |
| [file a bug][file-a-bug]. |
| |
| ## Prerequisites {:.numbered} |
| |
| This tutorial requires the following prerequisite items: |
| |
| - You need to use the Fuchsia source checkout (`fuchsia.git`) development |
| environment. |
| |
| - You need a Fuchsia device running. This can either be a physical device |
| or an emulator. |
| |
| - This device must have a connection to `ffx` and have the remote control |
| service (RCS) connected properly. |
| |
| If running `ffx target list`, the field under `RCS` must read `Y`, |
| for example: |
| |
| ```none {:.devsite-disable-click-to-copy} |
| NAME SERIAL TYPE STATE ADDRS/IP RCS |
| fuchsia-emulator <unknown> Unknown Product [fe80::5054:ff:fe63:5e7a%4] Y |
| ``` |
| |
| (For more information, see |
| [Interacting with target devices][interact-with-target-devices].) |
| |
| - To start the Fuchsia emulator with networking enabled but without graphical |
| user interface support, run `ffx emu start --headless`. (For more |
| information, see [Start the Fuchsia emulator][femu-guide].) |
| |
| - Your device must be running a `core` [product][product-config] at a minumim. |
| |
| ## Update dependencies in BUILD.gn {:#update-dependencies-in-buildgn .numbered} |
| |
| Update a `BUILD.gn` file to include the following dependencies: |
| |
| ```none {:.devsite-disable-click-to-copy} |
| import("//build/python/python_binary.gni") |
| |
| assert(is_host) |
| |
| python_binary("your_binary") { |
| main_source = "path/to/your/main.py" |
| deps = [ |
| "//src/developer/ffx:host", |
| "//src/developer/ffx/lib/fuchsia-controller:fidl_bindings", |
| ] |
| } |
| ``` |
| |
| The `fidl_bindings` rule includes the necessary Python and `.so` binding code. |
| The `ffx` tool must also be included to enable the `ffx` daemon to connect to |
| your Fuchsia device. |
| |
| ## Write your first program {:#write-your-first-program .numbered} |
| |
| In this section, we create a simple program that doesn't yet connect to a |
| Fuchsia device, but connect to the `ffx` daemon to verify that the device is |
| up and running. To do this, we leverage the existing `ffx` FIDL libraries for |
| interacting with the daemon, which is defined in `//sdk/fidl/fuchsia.developer.ffx`. |
| |
| ### Include FIDL dependencies {:#include-fidl-dependencies .numbered} |
| |
| Fuchsia Controller uses the FIDL Intermediate Representation (FIDL IR) to |
| generate its FIDL bindings at runtime. So you need to include the following |
| dependency in your `BUILD.gn` for the `fidlc` target to create these FIDL |
| bindings: |
| |
| ```gn |
| "//sdk/fidl/fuchsia.developer.ffx:fuchsia.developer.ffx_compile_fidlc($fidl_toolchain)" |
| ``` |
| |
| If you're writing a test, you need to include the host test data (which will |
| allow infra tests to run correctly, given they need access to the IR on test |
| runners as well), for example: |
| |
| ```gn |
| "//sdk/fidl/fuchsia.developer.ffx:fuchsia.developer.ffx_host_test_data" |
| ``` |
| |
| Including the host test data rule will also include the FIDL IR, so no need |
| to include both dependencies. |
| |
| ### Add the Python import block {:#add-the-python-imnport-block .numbered} |
| |
| Once all dependencies are all included, we can add the following libraries |
| in the Python main file: |
| |
| ```py |
| from fuchsia_controller_py import Context, IsolateDir |
| import fidl.fuchsia_developer_ffx as ffx |
| import asyncio |
| ``` |
| |
| The sections below cover each library in this code block. |
| |
| #### Context and IsolateDir |
| |
| ```py |
| from fuchsia_controller_py import Context, IsolateDir |
| ``` |
| |
| The first line includes a `Context` object, which provides the context from |
| which a user might run an `ffx` command. Plus, you can do much more with this |
| object because it also provides connections the following: |
| |
| - The `ffx` daemon |
| - A Fuchsia target |
| |
| The `IsolateDir` object is related to `ffx` isolation, which refers to |
| running the `ffx` daemon in a way that all its metadata (for instance, config |
| values) is contained under a specific directory. Isolation is primarily |
| intended for preventing pollution of `ffx`'s state as well as setting up less |
| active device discovery defaults (which can cause issues when running `ffx` in |
| testing infrastructure). |
| |
| `IsolateDir` is optional for general purpose commands, but is required if you |
| intend to use your program for testing. An `IsolateDir` object creates (and |
| points to) a directory that allows an isolated `ffx` daemon instance to run. |
| (For more information on `ffx` isolation, see |
| [Integration testing][integration-testing].) |
| |
| An `IsolateDir` object needs to be passed to a `Context` object during |
| initialization. An `IsolateDir` object may also be shared among `Context` |
| objects. The cleanup of an `IsolateDir` object, which also results in |
| the shutdown of the `ffx` daemon, occurs once the object is garbage |
| collected. |
| |
| #### FIDL IR |
| |
| ```py |
| import fidl.fuchsia_developer_ffx as ffx |
| ``` |
| |
| The second line comes from the FIDL IR code written in the previous section |
| above. The part written after `fidl.` (for instance, `fuchsia_developer_ffx`) |
| requires that the FIDL IR exists for the `fuchsia.developer.ffx` library. |
| This is the case for any FIDL import line. Importing |
| `fidl.example_fuchsia_library` requires that the FIDL IR for a library |
| named `example.fuchsia.library` has been generated. Using the `as` keyword |
| makes this library easy to use. |
| |
| This `fuchsia.developer.ffx` library includes all the structures expected |
| from FIDL bindings, which is covered later in this tutorial. |
| |
| #### asyncio |
| |
| ```py |
| import asyncio |
| ``` |
| |
| The objects generated from FIDL IR use asynchronous bindings, which requires |
| use of the `asyncio` library. In this tutorial, we use the echo protocol |
| defined in [`echo.fidl`][echo-fidl]. |
| |
| ### Write the main implementation {:.numbered} |
| |
| Beyond the boilerplate of `async_main` and `main`, we're primarily interested |
| in the `echo_func` definition: |
| |
| ```py |
| async def echo_func(): |
| isolate = IsolateDir() |
| config = {"sdk.root": "."} |
| ctx = Context(config=config, isolate_dir=isolate) |
| echo_proxy = ffx.Echo.Client(ctx.connect_daemon_protocol(ffx.Echo.MARKER)) |
| echo_string = "foobar" |
| print(f"Sending string for echo: {echo_string}") |
| result = await echo_proxy.echo_string(value="foobar") |
| print(f"Received result: {result.response}") |
| |
| |
| async def async_main(): |
| await echo_func() |
| |
| |
| def main(): |
| asyncio.run(async_main()) |
| |
| |
| if __name__ == "__main__": |
| main() |
| ``` |
| |
| The `config` object created and passed to the `Context` object is necessary |
| because of the isolation in use. When it's no longer applicable to use |
| isolation with `ffx`'s default config (by default `ffx` knows where to find |
| the SDK in the Fuchsia source checkout setup), any config values that you |
| wish to use must be supplied to the `Context` object. |
| |
| ### Run the code {:.numbered} |
| |
| Before we can run the code, we must build it first. The `BUILD.gn` file |
| may look similar to the following: |
| |
| ```gn |
| import("//build/python/python_binary.gni") |
| |
| assert(is_host) |
| |
| python_binary("example_echo") { |
| main_source = "main.py" |
| deps = [ |
| "//src/developer/ffx:host", |
| "//src/developer/ffx/lib/fuchsia-controller:fidl_bindings", |
| "//sdk/fidl/fuchsia.developer.ffx:fuchsia.developer.ffx_compile_fidlc($fidl_toolchain)", |
| ] |
| } |
| ``` |
| |
| Let's say this `BUILD.gn` is in the `src/developer/example_py_thing` |
| directory. Then with the correct `fx set` in place, you can build |
| this code using the host target. If your host is `x64`, the build |
| command may look like: |
| |
| ```posix-terminal |
| fx build host_x64/obj/src/developer/example_py_thing/example_echo.pyz |
| ``` |
| |
| One the build is complete, you can find the code in the `out` |
| directory (to be precise, `out/default` by default). And you can run |
| the `.pyz` file directly from that directory. It is important to use |
| the full path from your `out/default` directory so that the `pyz` file |
| can locate and open the appropriate `.so` files, for example: |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| $ cd $FUCHSIA_DIR/out/default |
| $ ./host_x64/obj/src/developer/example_py_thing/example_echo.pyz |
| Sending string for echo: foobar |
| Received result: foobar |
| $ |
| ``` |
| |
| ## Communicate with a Fuchsia device {:#communicate-with-a-fuchsia-device .numbered} |
| |
| If the code builds and runs so far, we can start writing code that speaks |
| to Fuchsia devices through FIDL interfaces. Most code is similar, but |
| there are some subtle differences to cover in this section. |
| |
| ### Get build information {:.numbered} |
| |
| We can start simple by getting a device's build information. |
| |
| To start, we need to include dependencies for the build info FIDL protocols: |
| |
| ```gn |
| "//sdk/fidl/fuchsia.buildinfo:fuchsia.buildinfo_compile_fidlc($fidl_toolchain)" |
| ``` |
| |
| We then need to write code for getting a proxy from a Fuchsia device. |
| Currently, this is done by connecting to the build info moniker (though |
| this is due to change soon): |
| |
| ```py |
| isolate = IsolateDir() |
| config = {"sdk.root": "."} |
| target = "foo-target-emu" # Replace with the target nodename. |
| ctx = Context(config=config, isolate_dir=isolate, target=target) |
| build_info_proxy = fuchsia_buildinfo.Provider.Client( |
| ctx.connect_device_proxy("/core/build-info", fuchsia_buildinfo.Provider.MARKER)) |
| build_info = await build_info_proxy.get_build_info() |
| print(f"{target} build info: {build_info}") |
| ``` |
| |
| If you were to run the above code, it would print something like below: |
| |
| ```sh {:.devsite-disalbe-click-to-copy} |
| foo-target-emu build info: ProviderGetBuildInfoResponse(build_info=BuildInfo(product_config='core', board_config='qemu-x64', version='2023-08-18T23:28:37+00:00', latest_commit_date='2023-08-18T23:28:37+00:00')) |
| ``` |
| |
| If you were to continue this, you could create something akin to the |
| `ffx target show` command: |
| |
| ```py |
| results = await asyncio.gather( |
| build_info_proxy.get_build_info(), |
| board_proxy.get_info(), |
| device_proxy.get_info(), |
| ... |
| ) |
| ``` |
| |
| Since each invocation to a FIDL method returns a co-routine, they can be |
| launched as tasks and awaited in parallel, as you would expect with other |
| FIDL bindings. |
| |
| ### Reboot a device {:.numbered} |
| |
| There's more than one way to reboot a device. One approach to reboot a |
| device is to connect to a component running the |
| `fuchsia.hardware.power.statecontrol/Admin` protocol, which can be found |
| under `/bootstrap/shutdown_shim`. |
| |
| With this approach, the protocol is expected to exit mid-execution of the |
| method with a `PEER_CLOSED` error: |
| |
| ```py |
| ch = ctx.connect_device_proxy("/bootstrap/shutdown_shim", |
| fuchsia_hardware_power_statecontrol.Admin.MARKER) |
| reboot_proxy = fuchsia_hardware_power_statecontrol.Admin.Client(ch) |
| try: |
| await reboot_proxy.reboot(reason=fuchsia_hardware_power_statecontrol.RebootReason.USER_REQUEST) |
| except fuchsia_controller_py.ZxStatus as status: |
| zx_status = status.args[0] |
| if zx_status != fuchsia_controller_py.ZxStatus.ZX_PEER_CLOSED: |
| raise status |
| ``` |
| |
| However, a challenging part comes afterward when we need to determine |
| whether or not the device has come back online. This is usually done by |
| attempting to connect to a protocol (usually the `RemoteControl` protocol) |
| until a timeout is reached. |
| |
| A different approach, which results in less code, is to connect to the |
| `ffx` daemon's `Target` protocol: |
| |
| ```py |
| ch = ctx.connect_target_proxy() |
| target_proxy = fuchsia_developer_ffx.Target.Client(ch) |
| await target_proxy.reboot(state=fuchsia_developer_ffx.TargetRebootState.PRODUCT) |
| ``` |
| |
| ### Run a component {:.numbered} |
| |
| Note: This section may be subject to change depending on the development |
| in the component framework. |
| |
| You can use the `RemoteControl` protocol to start a component, which involves |
| the following steps: |
| |
| 1. Connect to the lifecycle controller: |
| |
| ```py |
| ch = ctx.connect_to_remote_control_proxy() |
| remote_control = fuchsia_developer_remotecontrol.RemoteControl.Client(ch) |
| client, server = fuchsia_controller_py.Channel.create() |
| await remote_control.root_lifecycle_controller(server=server.take()) |
| lifecycle_ctrl = fuchsia_sys2.LifecycleController.Client(client) |
| ``` |
| |
| 2. Attempt to start the instance of the component: |
| |
| ```py |
| client, server = fuchsia_controller_py.Channel.create() |
| await lifecycle_ctrl.start_instance("some_moniker", server=server.take()) |
| binder = fuchsia_component.Binder.Client(client) |
| ``` |
| |
| The `binder` object lets the user know whether or not the component |
| remains connected. However, it has no methods. Support to determine |
| whether the component has become unbound (using the binder protocol) |
| is not yet implemented. |
| |
| ### Get a snapshot {:.numbered} |
| |
| Getting a snapshot from a fuchsia device involves running a snapshot and |
| binding a `File` protocol for reading: |
| |
| ```py |
| client, server = fuchsia_controller_py.Channel.create() |
| file = fuchsia_io.File.Client(client) |
| params = fuchsia_feedback.GetSnapshotParameters( |
| # This is 120 seconds calculated from nanoseconds. |
| collection_timeout_per_data=(2 * 60 * 10**9), |
| response_channel=server.take()) |
| ch = ctx.connect_device_proxy("/core/feedback", "fuchsia.feedback.DataProvider") |
| provider = fuchsia_feedback.DataProvider.Client(ch) |
| await provider.get_snapshot(params) |
| |
| data = bytearray() |
| while True: |
| response = await file.read(count=fuchsia_io.MAX_BUF) |
| if not response.data: |
| break |
| data.extend(response.data) |
| ``` |
| |
| ## Implement a FIDL server {:#implement-a-fidl-server .numbered} |
| |
| An important task for Fuchsia Controller (either for handling passed bindings |
| or for testing complex client side code) is to run a FIDL server. For all |
| FIDL protocols covered in this tutorial, there is a client that accepts |
| a channel. For this, you need to use the `Server` class. |
| |
| In this section, we return to the `echo` example and implement an `echo` server. |
| The functions you need to override are derived from the FIDL file definition. So |
| the `echo` server (using the `ffx` protocol) would look like below: |
| |
| ```py |
| class EchoServerIimpl(ffx.Echo.Server): |
| |
| def echo_string( |
| self, |
| request: ffx.EchoEchoStringRequest |
| ) -> ffx.EchoEchoStringResponse: |
| echo_value = request.value |
| result = ffx.EchoEchoStringResponse(response=echo_value) |
| return result |
| ``` |
| |
| To make a proper implementation, you need to import the appropriate libraries. |
| As before, we will import `fidl.fuchsia_developer_ffx`. However, since we're |
| going to run an `echo` server, the quickest way to test this server is to use |
| a `Channel` object from the `fuchsia_controller_py` library: |
| |
| ```py |
| import fidl.fuchsia_developer_ffx as ffx |
| from fuchsia_controller_py import Channel |
| ``` |
| |
| This `Channel` object behaves similarly to the ones in other languages. |
| The following code is a simple program that utilizes the `echo` server: |
| |
| ```py |
| import asyncio |
| import fidl.fuchsia_developer_ffx as ffx |
| from fuchsia_controller_py import Channel |
| |
| |
| class EchoServerImpl(ffx.Echo.Server): |
| |
| def echo_string( |
| self, |
| request: ffx.EchoEchoStringRequest |
| ) -> ffx.EchoEchoStringResponse: |
| echo_value = request.value |
| result = ffx.EchoEchoStringResponse(response=echo_value) |
| return result |
| |
| |
| async def async_main(): |
| client, server = Channel.create() |
| echo_proxy = ffx.Echo.Client(client) |
| echo_server = EchoServerImpl(server) |
| loop = asyncio.get_running_loop() |
| echo_server_task = loop.create_task(echo_server.serve()) |
| sent_string = "foobar" |
| echo_result = await echo_proxy.echo_string(value=sent_string) |
| print(f"Echo: sent {sent_string}, received {echo_result}") |
| |
| |
| def main(): |
| asyncio.run(async_main()) |
| |
| |
| if __name__ == "__main__": |
| main() |
| ``` |
| |
| Running this program prints output similar to the following: |
| |
| ```sh {.devsite-disable-click-to-copy} |
| Echo: sent foobar, received EchoEchoStringResponse(response='foobar') |
| ``` |
| |
| There are a few things to note when implementing a server: |
| |
| * Method definitions can either be `sync` or `async`. |
| * The `serve()` task will process requests and call the necessary method in |
| the server implementation until either the task is completed or the |
| underlying channel object is closed. |
| * If an exception occurs when the serving task is running, the client |
| channel receives a `PEER_CLOSED` error. Then you must check the result |
| of the serving task. |
| * Unlike Rust's async code, when creating an async task, you must keep |
| the returned object until you're done with it. Otherwise, the task may |
| be garbage collected and canceled. |
| |
| <!-- Reference links --> |
| |
| [fuchsia-controller-header-file]: /src/developer/ffx/lib/fuchsia-controller/cpp/abi/fuchsia_controller.h |
| [file-a-bug]: https://issuetracker.google.com/issues/new?component=1378581&template=1840403 |
| [interact-with-target-devices]: /docs/development/tools/ffx/getting-started.md#interacting_with_target_devices |
| [femu-guide]: /docs/get-started/set_up_femu.md |
| [product-config]: /docs/development/build/build_system/boards_and_products.md |
| [integration-testing]: /docs/development/tools/ffx/development/integration_testing/README.md |
| [echo-fidl]: /sdk/fidl/fuchsia.developer.ffx/echo.fidl |