| # Scripting remote interaction with a Fuchsia device |
| |
| This guide contains a tutorial for writing a script to remotely interact with a |
| Fuchsia device, various other example scripted interactions, and a primer on |
| implementing a FIDL server in Python on a host. |
| |
| The methods on this page were originally developed for end-to-end testing, but |
| you may also find them useful for scripting simple device interactions without |
| having to write an `ffx` plugin in Rust. |
| |
| ## Background |
| |
| The following technologies enable scripting remote interactions with a Fuchsia |
| device: |
| |
| - [Python FIDL bindings][python-fidl-bindings] |
| - [Fuchsia Controller][fuchsia-controller] |
| - [Overnet][overnet] |
| |
| Together, Fuchsia Controller and Overnet provide a transport between the host |
| running a Python script and the target Fuchsia device. And the Python FIDL |
| bindings, like FIDL bindings in other languages, facilitate sending and |
| receiving messages from components on the target device. |
| |
| You generally only need to interact with Python FIDL bindings after connecting |
| to your target device. Connecting to the target device, as you will see, is |
| done with the `Context` class from the `fuchsia_controller_py` module. |
| |
| Setting Overnet aside, the `libfuchsia_controller_internal.so` shared library |
| (which includes the ABI [header][fuchsia-controller-header-file]) is the core |
| library that drives scripted remote interactions. |
| |
| ## The Context object |
| |
| The `Context` object is the main entry point for all Fuchsia controller |
| interactions. It is responsible for creating and managing connections to Fuchsia |
| devices and is the primary method for getting access to FIDL handles. |
| |
| When constructed, the `Context` object can take a dictionary of configuration |
| key/value pairs, similar to the usage of [`ffx config`][ffx-config]. |
| |
| ```python |
| config = { "log.dir": "/path/to/logs", "log.level": "trace" } |
| ctx = Context(target="nodename", config=config) |
| ``` |
| |
| In most cases, a user must specify a target, which is the nodename of a Fuchsia |
| device. |
| |
| The most common way of using this object is to connect to a specific FIDL |
| protocol on a component running on a Fuchsia device. This is done by calling the |
| `connect_to_protocol` method, which takes a component moniker and a protocol |
| name. |
| |
| ## Tutorial |
| |
| This tutorial walks through writing a Python script that reads a list of |
| addresses and prints information about the Fuchsia device at each address. |
| |
| Note: Most FIDL interfaces should just work. However, some advanced APIs are not |
| supported, e.g., reading from or writing to a VMO. |
| |
| 1. [Prerequisites](#prerequisites) |
| 2. [Create a build target](#create-a-build-target) |
| 3. [Write the script](#write-the-script) |
| 4. [Build and run the script](#build-and-run-the-script) |
| |
| Please [file an issue][file-an-issue] if you encounter bugs, or have questions |
| or suggestions. |
| |
| ### Prerequisites {:.numbered} |
| |
| This tutorial requires the following: |
| |
| - [Fuchsia source checkout and associated development environment.][get-started] |
| - Fuchsia device (physical or emulated) reachable via `ffx` that exposes |
| the remote control service (RCS). |
| |
| When running `ffx target list`, the field under `RCS` must read `Y`: |
| |
| ```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].) |
| |
| ### Create a build target {:.numbered} |
| |
| Update a `BUILD.gn` file to include a build target like the following: |
| |
| ```none {:.devsite-disable-click-to-copy} |
| import("//build/python/python_binary.gni") |
| |
| assert(is_host) |
| |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/BUILD.gn" region_tag="describe_host_example_build_target" %} |
| ``` |
| |
| The `ffx` libraries enable connecting to our Fuchsia device. And |
| the FIDL dependencies make the necessary Python FIDL bindings available to the |
| script. |
| |
| ### Write the script {:.numbered} |
| |
| First, our script needs to import the necessary modules: |
| |
| ```py |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="required_import_block" %} |
| ``` |
| |
| The `fidl_fuchsia_developer_remotecontrol` and `fidl_fuchsia_buildinfo` Python |
| modules contains the Python FIDL bindings for the `fuchsia.developer.ffx` and |
| `fuchsia.buildinfo` FIDL libraries. |
| |
| The `Context` object provides connections to Fuchsia targets. |
| |
| Next, our script needs to define a function (called `describe_host` in this |
| example) to retrieve information from a target device: |
| |
| ```py |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="describe_host_function" %} |
| ``` |
| |
| This function instantiates a `Context` to connect to a Fuchsia device at |
| particular IP address, and then call the |
| `fuchsia.developer.remotecontrol/RemoteControl.IdentifyHost` and |
| `fuchsia.buildinfo/Provider.GetBuildInfo` methods to get information about the |
| target. |
| |
| Note: You might notice the component moniker `core/build-info` in the script. |
| See the [Finding component monikers][#finding-component-monikers] for how to |
| discover the component moniker for the Fuchsia component you wish to communicate |
| with. |
| |
| Note: The `.unwrap()` called on the result of the `IdentifyHost` call is a |
| helper method defined for FIDL result types. It either returns the response |
| contained in the result, if there is one, or raises an AssertionError if the |
| result contains a framework or domain error. (The `.response` in this example is |
| a little misleading because the returned `RemoteControlIdentifyHostResult` has a |
| response field with type `RemoteControlIdentifyHostResponse` that also contains |
| a response field. There are two nested response fields in the value returned by |
| `IdentifyHost`). |
| |
| Finally, you wrap this code with some Python boilerplate to read addresses and |
| print information for each target device. Thus, you arrive at the following |
| script: |
| |
| ```py |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/examples/describe_host.py" region_tag="full_code" %} |
| ``` |
| |
| ### Build and run the script {:.numbered} |
| |
| This example code lives in `//tools/fidl/fidlgen_python`, so you build it with |
| the following command on an `x64` host (after adding |
| `//tools/fidl/fidlgen_python:examples` to `host_labels` with `fx args`): |
| |
| ```posix-terminal |
| fx build --host //tools/fidl/fidlgen_python:describe_host_example |
| ``` |
| |
| Next, you use `ffx target list` to identify the address of our target device. |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| $ ffx target list |
| NAME SERIAL TYPE STATE ADDRS/IP RCS |
| fuchsia-emulator <unknown> core.x64 Product [127.0.0.1:34953] Y |
| ``` |
| |
| Then you run the script! |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| $ fx run-in-build-dir host_x64/obj/tools/fidl/fidlgen_python/describe_host_example.pyz '127.0.0.1:34953' |
| Target Info Received: |
| |
| --- 127.0.0.1:34953 --- |
| nodename: fuchsia-emulator |
| product_config: core |
| board_config: x64 |
| version: 2025-04-08T02:04:13+00:00 |
| ``` |
| |
| ## Finding component monikers |
| To communicate with a Fuchsia component, a script must know the component's |
| moniker in advance. A component moniker can be retrieved using `ffx`. For |
| example, the following `ffx` command will print that `core/build-info` exposes |
| the `fuchsia.buildinfo/Provider` capability: |
| |
| ```posix-terminal |
| ffx component capability fuchsia.buildinfo.Provider |
| ``` |
| |
| This command will print output similar to the following: |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| Declarations: |
| `core/build-info` declared capability `fuchsia.buildinfo.Provider` |
| |
| Exposes: |
| `core/build-info` exposed `fuchsia.buildinfo.Provider` from self to parent |
| |
| Offers: |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#cobalt` |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#remote-control` |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#sshd-host` |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#test_manager` |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#testing` |
| `core` offered `fuchsia.buildinfo.Provider` from child `#build-info` to child `#toolbox` |
| `core/sshd-host` offered `fuchsia.buildinfo.Provider` from parent to collection `#shell` |
| |
| Uses: |
| `core/remote-control` used `fuchsia.buildinfo.Provider` from parent |
| `core/sshd-host/shell:sshd-0` used `fuchsia.buildinfo.Provider` from parent |
| `core/cobalt` used `fuchsia.buildinfo.Provider` from parent |
| ``` |
| |
| ## Other examples |
| |
| This section demonstrates various other scripted interactions. |
| |
| Note: For more information on Python FIDL bindings, see this |
| [Python FIDL bindings][python-fidl-bindings] page. |
| |
| ### 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 |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="src/developer/ffx/lib/fuchsia-controller/end_to_end_tests/mobly/reboot_test.py" region_tag="reboot_example" %} |
| ``` |
| |
| However, a challenging part comes afterward when you 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. |
| |
| |
| |
| ### Run a component |
| |
| 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 |
| import fidl_fuchsia_developer_remotecontrol as f_remotecontrol |
| import fidl_fuchsia_sys2 as f_sys2 |
| ch = ctx.connect_to_remote_control_proxy() |
| remote_control_proxy = f_remotecontrol.RemoteControlClient(ch) |
| client, server = fuchsia_controller_py.Channel.create() |
| await remote_control_proxy.root_lifecycle_controller(server=server.take()) |
| lifecycle_ctrl = f_sys2.LifecycleControllerClient(client) |
| ``` |
| |
| 2. Attempt to start the instance of the component: |
| |
| ```py |
| import fidl_fuchsia_component as f_component |
| client, server = fuchsia_controller_py.Channel.create() |
| await lifecycle_ctrl.start_instance("some_moniker", server=server.take()) |
| binder = f_component.BinderClient(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 |
| |
| Getting a snapshot from a fuchsia device involves running a snapshot and binding |
| a `File` protocol for reading: |
| |
| ```py |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="src/developer/ffx/lib/fuchsia-controller/end_to_end_tests/mobly/target_identity_tests.py" region_tag="snapshot_example" %} |
| ``` |
| |
| ## Implementing a FIDL server on a host |
| |
| An important task for Fuchsia Controller (either for handling passed bindings or |
| for testing complex client side code) is to run a FIDL server. In this section, |
| you 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 |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="echo_server_impl" %} |
| ``` |
| |
| To make a proper implementation, you need to import the appropriate libraries. |
| As before, you will import `fidl_fuchsia_developer_ffx`. However, since you'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 unittest |
| import fidl_fuchsia_developer_ffx as ffx |
| from fuchsia_controller_py import Channel |
| |
| |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="echo_server_impl" %} |
| |
| |
| class TestCases(unittest.IsolatedAsyncioTestCase): |
| |
| async def test_echoer_example(self): |
| {% includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py" region_tag="use_echoer_example" %} |
| ``` |
| |
| 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. |
| |
| ### Common FIDL server code patterns |
| |
| Note: For more information on writing async Python code with Fuchsia Controller, |
| see this [async Python][async-python] page. |
| |
| In contrast to the simple `echo` server example above, this section covers |
| different types of server interactions. |
| |
| #### Creating a FIDL server class |
| |
| Let's work with the following FIDL protocol to make a server: |
| |
| ```fidl |
| library fuchsia.exampleserver; |
| |
| type SomeGenericError = flexible enum { |
| THIS = 1; |
| THAT = 2; |
| THOSE = 3; |
| }; |
| |
| closed protocol Example { |
| strict CheckFileExists(struct { |
| path string:255; |
| follow_symlinks bool; |
| }) -> (struct { |
| exists bool; |
| }) error SomeGenericError; |
| }; |
| ``` |
| |
| FIDL method names are derived by changing the method name from Camel case to |
| Lower snake case. So the method `CheckFileExists` in Python changes to |
| `check_file_exists`. |
| |
| The anonymous struct types is derived from the whole protocol name and |
| method. As a result, they can be quite verbose. The input method's input |
| parameter is defined as a type called `ExampleCheckFileExistsRequest`. And |
| the response is called `ExampleCheckFileExistsResponse`. |
| |
| Putting these together, the FIDL server implementation in Python looks like |
| below: |
| |
| ```py |
| import fidl_fuchsia_exampleserver as fe |
| |
| class ExampleServerImpl(fe.ExampleServer): |
| |
| def some_file_check_function(path: str) -> bool: |
| # Just pretend a real check happens here. |
| return True |
| |
| def check_file_exists(self, req: fe.ExampleCheckFileExistsRequest) -> fe.ExampleCheckFileExistsResponse: |
| return fe.ExampleCheckFileExistsResponse( |
| exists=ExampleServerImpl.some_file_check_function() |
| ) |
| ``` |
| |
| It is also possible to implement the methods as `async` without issues. |
| |
| In addition, returning an error requires wrapping the error in the FIDL |
| `DomainError` object, for example: |
| |
| ```py |
| import fidl_fuchsia_exampleserver as fe |
| |
| from fidl import DomainError |
| |
| class ExampleServerImpl(fe.ExampleServer): |
| |
| def check_file_exists(self, req: fe.ExampleCheckFileExistsRequests) -> fe.ExampleCheckFileExistsResponse | DomainError: |
| return DomainError(error=fe.SomeGenericError.THIS) |
| ``` |
| |
| #### Handling events |
| |
| Event handlers are written similarly to servers. Events are handled on the |
| client side of a channel, so passing a client is necessary to construct an event |
| handler. |
| |
| Let's start with the following FIDL code to build an example: |
| |
| ```fidl |
| library fuchsia.exampleserver; |
| |
| closed protocol Example { |
| strict -> OnFirst(struct { |
| message string:128; |
| }); |
| strict -> OnSecond(); |
| }; |
| ``` |
| |
| This FIDL example contains two different events that the event handler needs |
| to handle. Writing the simplest class that does nothing but print looks like |
| below: |
| |
| ```py |
| import fidl_fuchsia_exampleserver as fe |
| |
| class ExampleEventHandler(fe.ExampleEventHandler): |
| |
| def on_first(self, req: fe.ExampleOnFirstRequest): |
| print(f"Got a message on first: {req.message}") |
| |
| def on_second(self): |
| print(f"Got an 'on second' event") |
| ``` |
| |
| If you want to stop handling events without error, you can raise |
| `fidl.StopEventHandler`. |
| |
| An example of this event can be tested using some existing fidlgen_python |
| testing code. But first, make sure that the Fuchsia controller tests have been |
| added to the Fuchsia build settings, for example: |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| fx set ... --with-host //tools/fidl/fidlgen_python:tests |
| ``` |
| |
| With a protocol from `fuchsia.controller.test` (defined in |
| [`fuchsia_controller.test.fidl`][test-fidl]), you can write code that |
| uses the `ExampleEvents` protocol, for example: |
| |
| ```py |
| import asyncio |
| import fidl_fuchsia_controller_test as fct |
| |
| from fidl import StopEventHandler |
| from fuchsia_controller_py import Channel |
| |
| class ExampleEventHandler(fct.ExampleEventsEventHandler): |
| |
| def on_first(self, req: fct.ExampleEventsOnFirstRequest): |
| print(f"Got on-first event message: {req.message}") |
| |
| def on_second(self): |
| print(f"Got on-second event") |
| raise StopEventHandler |
| |
| async def main(): |
| client_chan, server_chan = Channel.create() |
| client = fct.ExampleEventsClient(client_chan) |
| server = fct.ExampleEventsServer(server_chan) |
| event_handler = ExampleEventHandler(client) |
| event_handler_task = asyncio.get_running_loop().create_task( |
| event_handler.serve() |
| ) |
| server.on_first(message="first message") |
| server.on_second() |
| server.on_complete() |
| await event_handler_task |
| |
| if __name__ == "__main__": |
| asyncio.run(main()) |
| ``` |
| |
| Then this can be run by completing the Python environment setup steps in the |
| [next section](#experiment-with-the-python-interpreter). When run, it prints |
| the following output and exits: |
| |
| ```sh {:.devsite-disable-click-to-copy} |
| Got on-first event message: first message |
| Got on-second event |
| ``` |
| |
| For more examples on server testing, see this |
| [`test_server_and_event_handler.py`][test-server-and-event-handler] file. |
| |
| <!-- Reference links --> |
| |
| [python-fidl-bindings]: /docs/development/tools/fuchsia-controller/fidl-bindings.md |
| [fuchsia-controller]: /src/developer/ffx/lib/fuchsia-controller/README.md |
| [overnet]: /src/connectivity/overnet/README.md |
| [fuchsia-controller-header-file]: /src/developer/ffx/lib/fuchsia-controller/cpp/fuchsia_controller_internal/fuchsia_controller.h |
| [file-an-issue]: 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 |
| [test-server-and-event-handler]: /tools/fidl/fidlgen_python/tests/test_server_and_event_handler.py |
| [test-fidl]: /src/developer/ffx/lib/fuchsia-controller/fidl/fuchsia_controller.test.fidl |
| [async-python]: /docs/development/tools/fuchsia-controller/async-python.md |
| [get-started]: /docs/get-started/README.md |
| [ffx-config]: /docs/development/tools/ffx/commands/config.md#runtime-configuration |