Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
331 changes: 331 additions & 0 deletions docs/source/reference/package-apis/drivers/android.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a symlink to packages/jumpstarter-driver-android/README.md

Large diffs are not rendered by default.

16 changes: 13 additions & 3 deletions docs/source/reference/package-apis/drivers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ function:
Drivers that control the power state and basic operation of devices:

* **[Power](power.md)** (`jumpstarter-driver-power`) - Power control for devices
* **[Android](android.md)** (`jumpstarter-driver-android`) -
Android device control over ADB
* **[Raspberry Pi](raspberrypi.md)** (`jumpstarter-driver-raspberrypi`) -
Raspberry Pi hardware control
* **[Yepkit](yepkit.md)** (`jumpstarter-driver-yepkit`) - Yepkit hardware
Expand Down Expand Up @@ -56,6 +58,16 @@ Drivers that handle media streams:
* **[UStreamer](ustreamer.md)** (`jumpstarter-driver-ustreamer`) - Video
streaming functionality

### Virtualization Drivers

Drivers for running virtual machines and systems:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

running or connecting to?


* **[QEMU](qemu.md)** (`jumpstarter-driver-qemu`) - QEMU virtualization platform
* **[Corellium](corellium.md)** (`jumpstarter-driver-corellium`) - Corellium
virtualization platform
* **[Android](android.md)** (`jumpstarter-driver-android`) -
Android Virtual Device (AVD) emulator

### Debug and Programming Drivers

Drivers for debugging and programming devices:
Expand All @@ -64,9 +76,6 @@ Drivers for debugging and programming devices:
programming tools
* **[Probe-RS](probe-rs.md)** (`jumpstarter-driver-probe-rs`) - Debugging probe
support
* **[QEMU](qemu.md)** (`jumpstarter-driver-qemu`) - QEMU virtualization platform
* **[Corellium](corellium.md)** (`jumpstarter-driver-corellium`) - Corellium
virtualization platform
* **[U-Boot](uboot.md)** (`jumpstarter-driver-uboot`) - Universal Bootloader
interface

Expand All @@ -78,6 +87,7 @@ General-purpose utility drivers:

```{toctree}
:hidden:
android.md
can.md
corellium.md
dutlink.md
Expand Down
1 change: 1 addition & 0 deletions packages/jumpstarter-all/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies = [
"jumpstarter-cli-admin",
"jumpstarter-cli-common",
"jumpstarter-cli-driver",
"jumpstarter-driver-android",
"jumpstarter-driver-can",
"jumpstarter-driver-composite",
"jumpstarter-driver-corellium",
Expand Down
331 changes: 331 additions & 0 deletions packages/jumpstarter-driver-android/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import errno
import os
import socket
import subprocess
import sys
from contextlib import contextmanager
from threading import Event
from typing import Generator

import adbutils
import click
from jumpstarter_driver_composite.client import CompositeClient
from jumpstarter_driver_network.adapters import TcpPortforwardAdapter

from jumpstarter.client import DriverClient


class AndroidClient(CompositeClient):
"""Generic Android client for controlling Android devices/emulators."""

pass


class AdbClientBase(DriverClient):
"""
Base class for ADB clients. This class provides a context manager to
create an ADB client and forward the ADB server address and port.
"""

def _check_port_in_use(self, host: str, port: int) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is unused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be removed after some experimentation, not reliable across platforms.

# Check if port is already bound
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.bind((host, port))
except socket.error as e:
if e.errno == errno.EADDRINUSE:
return True
finally:
sock.close()
return False

@contextmanager
def forward_adb(self, host: str, port: int) -> Generator[str, None, None]:
"""
Port-forward remote ADB server to local host and port.
If the port is already bound, yields the existing address instead.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet implemented?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this was an attempt to fix port issues, should be removed.

Args:
host (str): The local host to forward to.
port (int): The local port to forward to.
Yields:
str: The address of the forwarded ADB server.
"""
with TcpPortforwardAdapter(
client=self,
local_host=host,
local_port=port,
) as addr:
yield addr

@contextmanager
def adb_client(self, host: str = "127.0.0.1", port: int = 5038) -> Generator[adbutils.AdbClient, None, None]:
"""
Context manager to get an `adbutils.AdbClient`.
Args:
host (str): The local host to forward to.
port (int): The local port to forward to.
Yields:
adbutils.AdbClient: The `adbutils.AdbClient` instance.
"""
with self.forward_adb(host, port) as addr:
client = adbutils.AdbClient(host=addr[0], port=int(addr[1]))
yield client


class AdbClient(AdbClientBase):
"""ADB client for interacting with Android devices."""

def cli(self):
@click.command(context_settings={"ignore_unknown_options": True})
@click.option("host", "-H", default="127.0.0.1", show_default=True, help="Local adb host to forward to.")
@click.option("port", "-P", type=int, default=5038, show_default=True, help="Local adb port to forward to.")
@click.option("-a", is_flag=True, hidden=True)
@click.option("-d", is_flag=True, hidden=True)
@click.option("-e", is_flag=True, hidden=True)
@click.option("-L", hidden=True)
@click.option("--one-device", hidden=True)
@click.option(
"--adb",
default="adb",
show_default=True,
help="Path to the ADB executable",
)
@click.argument("args", nargs=-1)
def adb(
host: str,
port: int,
adb: str,
a: bool,
d: bool,
e: bool,
l: str, # noqa: E741
one_device: str,
args: tuple[str, ...],
):
"""
Run adb using a local adb binary against the remote adb server.
This command is a wrapper around the adb command-line tool. It allows you to run regular adb commands
with an automatically forwarded adb server running on your Jumpstarter exporter.
When executing this command, the exporter adb daemon is forwarded to a local port. The
adb server address and port are automatically set in the environment variables ANDROID_ADB_SERVER_ADDRESS
and ANDROID_ADB_SERVER_PORT, respectively. This configures your local adb client to communicate with the
remote adb server.
Most command line arguments and commands are passed directly to the adb CLI. However, some
arguments and commands are not supported by the Jumpstarter adb client. These options include:
-a, -d, -e, -L, --one-device.
The following adb commands are also not supported in remote adb environments: connect, disconnect,
reconnect, nodaemon, pair
When running start-server or kill-server, Jumpstarter will start or kill the adb server on the exporter.
Use the forward-adb command to forward the adb server address and port to a local port manually.
"""
# Throw exception for all unsupported arguments
if any([a, d, e, l, one_device]):
raise click.UsageError(
"ADB options -a, -d, -e, -L, and --one-device are not supported by the Jumpstarter ADB client"
)
# Check for unsupported server management commands
unsupported_commands = [
"connect",
"disconnect",
"reconnect",
"nodaemon",
"pair",
]
for arg in args:
if arg in unsupported_commands:
raise click.UsageError(f"The adb command '{arg}' is not supported by the Jumpstarter ADB client")

if "start-server" in args:
remote_port = int(self.call("start_server"))
click.echo(f"Remote adb server started on remote port exporter:{remote_port}")
return 0
elif "kill-server" in args:
remote_port = int(self.call("kill_server"))
click.echo(f"Remote adb server killed on remote port exporter:{remote_port}")
return 0
elif "forward-adb" in args:
# Port is available, proceed with forwarding
with self.forward_adb(host, port) as addr:
click.echo(f"Remote adb server forwarded to {addr[0]}:{addr[1]}")
Event().wait()

# Forward the ADB server address and port and call ADB executable with args
with self.forward_adb(host, port) as addr:
env = os.environ | {
"ANDROID_ADB_SERVER_ADDRESS": addr[0],
"ANDROID_ADB_SERVER_PORT": str(addr[1]),
}
cmd = [adb, *args]
process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
return process.wait()

Comment on lines +162 to +171
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use proper subprocess management for resource cleanup.

The subprocess should be managed properly to ensure cleanup even if an exception occurs.

 # Forward the ADB server address and port and call ADB executable with args
 with self.forward_adb(host, port) as addr:
     env = os.environ | {
         "ANDROID_ADB_SERVER_ADDRESS": addr[0],
         "ANDROID_ADB_SERVER_PORT": str(addr[1]),
     }
     cmd = [adb, *args]
-    process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
-    return process.wait()
+    with subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env) as process:
+        return process.wait()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Forward the ADB server address and port and call ADB executable with args
with self.forward_adb(host, port) as addr:
env = os.environ | {
"ANDROID_ADB_SERVER_ADDRESS": addr[0],
"ANDROID_ADB_SERVER_PORT": str(addr[1]),
}
cmd = [adb, *args]
process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
return process.wait()
# Forward the ADB server address and port and call ADB executable with args
with self.forward_adb(host, port) as addr:
env = os.environ | {
"ANDROID_ADB_SERVER_ADDRESS": addr[0],
"ANDROID_ADB_SERVER_PORT": str(addr[1]),
}
cmd = [adb, *args]
with subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env) as process:
return process.wait()
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 169-169: Line too long (111/100)

(C0301)


[refactor] 169-169: Consider using 'with' for resource-allocating operations

(R1732)

🤖 Prompt for AI Agents
In packages/jumpstarter-driver-android/jumpstarter_driver_android/client.py
around lines 162 to 171, the subprocess.Popen call is not managed with a context
that ensures proper cleanup on exceptions. Replace the current subprocess.Popen
usage with a context manager (using subprocess.Popen as a context) or ensure the
process is properly terminated and waited on in a finally block to guarantee
resource cleanup even if an error occurs.

return adb


class ScrcpyClient(AdbClientBase):
"""Scrcpy client for controlling Android devices remotely."""

def cli(self):
@click.command(context_settings={"ignore_unknown_options": True})
@click.option("host", "-H", default="127.0.0.1", show_default=True, help="Local adb host to forward to.")
@click.option("port", "-P", type=int, default=5038, show_default=True, help="Local adb port to forward to.")
@click.option(
"--scrcpy",
default="scrcpy",
show_default=True,
help="Path to the scrcpy executable",
)
@click.argument("args", nargs=-1)
def scrcpy(
host: str,
port: int,
scrcpy: str,
args: tuple[str, ...],
):
"""
Run scrcpy using a local executable against the remote adb server.
This command is a wrapper around the scrcpy command-line tool. It allows you to run scrcpy
against a remote Android device through an ADB server tunneled via Jumpstarter.
When executing this command, the adb server address and port are forwarded to the local scrcpy executable.
The adb server socket path is set in the environment variable ADB_SERVER_SOCKET, allowing scrcpy to
communicate with the remote adb server.
Most command line arguments are passed directly to the scrcpy executable.
"""
# Unsupported scrcpy arguments that depend on direct adb server management
unsupported_args = [
"--connect",
"-c",
"--serial",
"-s",
"--select-usb",
"--select-tcpip",
]

for arg in args:
for unsupported in unsupported_args:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or print a warning and ignore the argument? Then the same scrcpy invocation can be used with or without jumpstarter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These arguments don't really work when using remote scrcpy which itself is also an adb wrapper 🙄. I can switch this to warnings instead.

if arg.startswith(unsupported):
raise click.UsageError(
f"Scrcpy argument '{unsupported}' is not supported by the Jumpstarter scrcpy client"
)

# Forward the ADB server address and port and call scrcpy executable with args
with self.forward_adb(host, port) as addr:
# Scrcpy uses ADB_SERVER_SOCKET environment variable
socket_path = f"tcp:{addr[0]}:{addr[1]}"
env = os.environ | {
"ADB_SERVER_SOCKET": socket_path,
}
cmd = [scrcpy, *args]
process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
return process.wait()

Comment on lines +224 to +234
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use proper subprocess management for resource cleanup.

Similar to the AdbClient, the subprocess should be properly managed.

 # Forward the ADB server address and port and call scrcpy executable with args
 with self.forward_adb(host, port) as addr:
     # Scrcpy uses ADB_SERVER_SOCKET environment variable
     socket_path = f"tcp:{addr[0]}:{addr[1]}"
     env = os.environ | {
         "ADB_SERVER_SOCKET": socket_path,
     }
     cmd = [scrcpy, *args]
-    process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
-    return process.wait()
+    with subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env) as process:
+        return process.wait()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Forward the ADB server address and port and call scrcpy executable with args
with self.forward_adb(host, port) as addr:
# Scrcpy uses ADB_SERVER_SOCKET environment variable
socket_path = f"tcp:{addr[0]}:{addr[1]}"
env = os.environ | {
"ADB_SERVER_SOCKET": socket_path,
}
cmd = [scrcpy, *args]
process = subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env)
return process.wait()
# Forward the ADB server address and port and call scrcpy executable with args
with self.forward_adb(host, port) as addr:
# Scrcpy uses ADB_SERVER_SOCKET environment variable
socket_path = f"tcp:{addr[0]}:{addr[1]}"
env = os.environ | {
"ADB_SERVER_SOCKET": socket_path,
}
cmd = [scrcpy, *args]
with subprocess.Popen(cmd, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, env=env) as process:
return process.wait()
🧰 Tools
🪛 Pylint (3.3.7)

[convention] 232-232: Line too long (111/100)

(C0301)


[refactor] 232-232: Consider using 'with' for resource-allocating operations

(R1732)

🤖 Prompt for AI Agents
In packages/jumpstarter-driver-android/jumpstarter_driver_android/client.py
around lines 224 to 234, the subprocess.Popen call is not properly managed,
which can lead to resource leaks. Replace the subprocess.Popen and
process.wait() with a subprocess.run call using the same command, stdin, stdout,
stderr, and env parameters to ensure the subprocess is properly waited on and
resources are cleaned up automatically.

return scrcpy
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .adb import AdbServer
from .emulator import AndroidEmulator, AndroidEmulatorPower
from .options import AdbOptions, EmulatorOptions
from .scrcpy import Scrcpy

__all__ = [
"AdbServer",
"AndroidEmulator",
"AndroidEmulatorPower",
"AdbOptions",
"EmulatorOptions",
"Scrcpy",
]
Loading