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:

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

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
  2. Create a build target
  3. Write the script
  4. Build and run the script

Please file an issue if you encounter bugs, or have questions or suggestions.

Prerequisites {:.numbered}

This tutorial requires the following:

Create a build target {:.numbered}

Update a BUILD.gn file to include a build target like the following:

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:

{% 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:

{% 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:

{% 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):

fx build --host //tools/fidl/fidlgen_python:describe_host_example

Next, you use ffx target list to identify the address of our target device.

$ 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!

$ 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:

ffx component capability fuchsia.buildinfo.Provider

This command will print output similar to the following:

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 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:

{% 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:

    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:

    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:

{% 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:

{% 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:

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:

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 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:

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:

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:

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:

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:

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:

fx set ... --with-host //tools/fidl/fidlgen_python:tests

With a protocol from fuchsia.controller.test (defined in fuchsia_controller.test.fidl), you can write code that uses the ExampleEvents protocol, for example:

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. When run, it prints the following output and exits:

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