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.
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.
Please file an issue if you encounter bugs, or have questions or suggestions.
Prerequisites
This tutorial requires the following:
- Fuchsia source checkout and associated development environment.
Fuchsia device (physical or emulated) reachable via
ffx
that exposes the remote control service (RCS).When running
ffx target list
, the field underRCS
must readY
: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.)
Create a build target
Update a BUILD.gn
file to include a build target like the following:
import("//build/python/python_binary.gni")
assert(is_host)
python_binary("describe_host_example") {
testonly = true
main_source = "examples/describe_host.py"
deps = [
"//sdk/fidl/fuchsia.buildinfo:fuchsia.buildinfo_python",
"//sdk/fidl/fuchsia.developer.remotecontrol:fuchsia.developer.remotecontrol_python",
"//src/developer/ffx:host",
]
}
The ffx
tool enables the ffx
daemon to connect to our Fuchsia device. And
the FIDL dependencies make the necessary Python FIDL bindings available to the
script.
Write the script
First, our script needs to import the necessary modules:
import fidl_fuchsia_buildinfo as f_buildinfo
import fidl_fuchsia_developer_remotecontrol as f_remote_control
from fuchsia_controller_py import Context
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 the ffx
daemon and Fuchsia targets.
Next, our script needs to define a function (called describe_host
in this
example) to retrieve information from a target device:
# Return information about a Fuchsia target.
async def describe_host(target_ip: str) -> str:
ctx = Context(
target=target_ip,
)
remote_control_proxy = f_remote_control.RemoteControlClient(
ctx.connect_remote_control_proxy()
)
identify_host_response = (
# The `.response` in the next line is a bit confusing. The .unwrap() extracts the
# RemoteControlIdentifyHostResponse from the response field of the returned
# RemoteControlIdentifyHostResult, but RemoteControlIdentifyHostResponse also contains a
# response field. There are two nested response fields.
(await remote_control_proxy.identify_host())
.unwrap()
.response
)
build_info_proxy = f_buildinfo.ProviderClient(
ctx.connect_device_proxy(
"/core/build-info", f_buildinfo.ProviderMarker
)
)
buildinfo_response = await build_info_proxy.get_build_info()
return f"""
--- {target_ip} ---
nodename: {identify_host_response.nodename}
product_config: {buildinfo_response.build_info.product_config}
board_config: {buildinfo_response.build_info.board_config}
version: {buildinfo_response.build_info.version}
"""
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.
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:
import argparse
import asyncio
import fidl_fuchsia_buildinfo as f_buildinfo
import fidl_fuchsia_developer_remotecontrol as f_remote_control
from fuchsia_controller_py import Context
USAGE = "This script prints the result of RemoteControl.IdentifyHost on Fuchsia targets."
async def main() -> None:
params = argparse.ArgumentParser(usage=USAGE)
params.add_argument(
"target_ips",
help="Fuchsia target IP addresses",
nargs="+",
)
args = params.parse_args()
# Return information about a Fuchsia target.
async def describe_host(target_ip: str) -> str:
ctx = Context(
target=target_ip,
)
remote_control_proxy = f_remote_control.RemoteControlClient(
ctx.connect_remote_control_proxy()
)
identify_host_response = (
# The `.response` in the next line is a bit confusing. The .unwrap() extracts the
# RemoteControlIdentifyHostResponse from the response field of the returned
# RemoteControlIdentifyHostResult, but RemoteControlIdentifyHostResponse also contains a
# response field. There are two nested response fields.
(await remote_control_proxy.identify_host())
.unwrap()
.response
)
build_info_proxy = f_buildinfo.ProviderClient(
ctx.connect_device_proxy(
"/core/build-info", f_buildinfo.ProviderMarker
)
)
buildinfo_response = await build_info_proxy.get_build_info()
return f"""
--- {target_ip} ---
nodename: {identify_host_response.nodename}
product_config: {buildinfo_response.build_info.product_config}
board_config: {buildinfo_response.build_info.board_config}
version: {buildinfo_response.build_info.version}
"""
# Asynchronously await information from each Fuchsia target.
results = await asyncio.gather(*map(describe_host, args.target_ips))
print("Target Info Received:")
print("\n".join(results))
Build and run the script
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.
Reboot a device
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:
ch = self.device.ctx.connect_device_proxy(
"bootstrap/shutdown_shim", power_statecontrol.AdminMarker
)
admin = power_statecontrol.AdminClient(ch)
# Makes a coroutine to ensure that a PEER_CLOSED isn't received from attempting
# to write to the channel.
coro = admin.reboot(reason=power_statecontrol.RebootReason.USER_REQUEST)
try:
_LOGGER.info("Issuing reboot command")
await coro
except ZxStatus as status:
if status.raw() != ZxStatus.ZX_ERR_PEER_CLOSED:
raise status
_LOGGER.info("Device reboot command sent")
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.
A different approach, which results in less code, is to connect to the ffx
daemon's Target
protocol:
import fidl_fuchsia_developer_ffx as f_ffx
ch = ctx.connect_target_proxy()
target_proxy = f_ffx.TargetClient(ch)
await target_proxy.reboot(state=f_ffx.TargetRebootState.PRODUCT)
Run a component
You can use the RemoteControl
protocol to start a component, which involves
the following steps:
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)
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:
client, server = Channel.create()
file = io.FileClient(client)
params = feedback.GetSnapshotParameters(
# Two minutes of timeout time.
collection_timeout_per_data=(2 * 60 * 10**9),
response_channel=server.take(),
)
assert self.device.ctx is not None
ch = self.device.ctx.connect_device_proxy(
"/core/feedback", "fuchsia.feedback.DataProvider"
)
provider = feedback.DataProviderClient(ch)
await provider.get_snapshot(params=params)
attr_res = await file.get_attr()
asserts.assert_equal(attr_res.s, ZxStatus.ZX_OK)
data = bytearray()
while True:
response = (await file.read(count=io.MAX_BUF)).unwrap()
if not response.data:
break
data.extend(response.data)
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:
class TestEchoer(ffx.EchoServer):
def echo_string(
self, request: ffx.EchoEchoStringRequest
) -> ffx.EchoEchoStringResponse:
return ffx.EchoEchoStringResponse(response=request.value)
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
class TestEchoer(ffx.EchoServer):
def echo_string(
self, request: ffx.EchoEchoStringRequest
) -> ffx.EchoEchoStringResponse:
return ffx.EchoEchoStringResponse(response=request.value)
class TestCases(unittest.IsolatedAsyncioTestCase):
async def test_echoer_example(self):
(tx, rx) = Channel.create()
server = TestEchoer(rx)
client = ffx.EchoClient(tx)
server_task = asyncio.get_running_loop().create_task(server.serve())
res = await client.echo_string(value="foobar")
self.assertEqual(res.response, "foobar")
server_task.cancel()
There are a few things to note when implementing a server:
- Method definitions can either be
sync
orasync
. - 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
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.