Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Mike Frysinger | 8b0fc37 | 2022-09-08 07:24:24 | [diff] [blame] | 2 | # Copyright 2020 The ChromiumOS Authors |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
| 6 | """Forward USB devices from the caller to the target device. |
| 7 | |
Tom Hughes | 437705a | 2020-12-04 19:10:09 | [diff] [blame] | 8 | Automates the process of forwarding specified USB devices to a target device. |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 9 | This involves: |
| 10 | |
Tom Hughes | 437705a | 2020-12-04 19:10:09 | [diff] [blame] | 11 | 1) Loading the prerequisite kernel modules (both locally and on the target |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 12 | device). |
| 13 | 2) Running and cleaning up `usbipd`. |
| 14 | 3) Setting up a SSH tunnel for the `usbipd` TCP port. |
| 15 | 4) Bind the devices to the usbip kernel driver. |
| 16 | 5) Attach the devices to the target. |
| 17 | 6) Clean up on exit so that the USB devices will function again locally. |
| 18 | |
| 19 | For example: |
| 20 | ./forward_usb_devices.py --log-level=debug -d test-device.local 1-3.1 1-3.2 |
| 21 | will forward two USB devices (the ones at bus ids 1-3.1 and 1-3.2) to the device |
| 22 | with the hostname `test-device.local`. |
Tom Hughes | 437705a | 2020-12-04 19:10:09 | [diff] [blame] | 23 | |
| 24 | Requires usbip installed in the chroot: |
| 25 | |
| 26 | (chroot) $ sudo emerge usbip |
| 27 | |
| 28 | To list USB bus ids: |
| 29 | |
| 30 | (chroot) $ usbip list --local |
| 31 | |
| 32 | |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 33 | """ |
| 34 | |
| 35 | from __future__ import print_function |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 36 | |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 37 | import contextlib |
| 38 | import logging |
| 39 | import os |
| 40 | import shutil |
| 41 | import signal |
| 42 | import subprocess |
| 43 | import sys |
| 44 | import time |
| 45 | |
| 46 | from chromite.lib import commandline |
| 47 | from chromite.lib import cros_build_lib |
| 48 | from chromite.lib import osutils |
| 49 | from chromite.lib import remote_access |
| 50 | from chromite.lib import retry_util |
| 51 | |
| 52 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 53 | HOST_MODULES = {"usbip-core", "usbip-host", "vhci-hcd"} |
| 54 | CLIENT_MODULES = {"usbip-core", "vhci-hcd"} |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 55 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 56 | KILL_COMMAND = "kill" |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 57 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 58 | MODPROBE_COMMAND = "modprobe" |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 59 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 60 | USBIP_PACKAGE = "usbip" |
| 61 | USBIP_COMMAND = "usbip" |
| 62 | USBIPD_COMMAND = "usbipd" |
| 63 | USBIPD_PID_FILE = "/run/usbipd.pid" |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 64 | USBIPD_PORT = 3240 |
| 65 | |
| 66 | RETRY_USBIPD_READ_PID = 10 |
| 67 | DELAY_USBIPD_READ_PID = 0.5 |
| 68 | |
| 69 | |
| 70 | def main(argv): |
| 71 | """Forward USB devices from the caller to the target device.""" |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 72 | os.environ["PATH"] += ":/sbin:/usr/sbin" |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 73 | |
| 74 | opts = get_opts(argv) |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 75 | return forward_devices( |
| 76 | opts.device.hostname, opts.device.port, opts.usb_devices |
| 77 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 78 | |
| 79 | |
| 80 | def forward_devices(hostname, port, usb_devices): |
| 81 | """Forward USB devices from the caller to the target device.""" |
| 82 | |
| 83 | if shutil.which(USBIP_COMMAND) is not None: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 84 | logging.debug("`%s` found in the chroot", USBIP_COMMAND) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 85 | else: |
| 86 | logging.error( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 87 | "You need to emerge the `%s` package in the chroot: sudo emerge %s", |
| 88 | USBIP_PACKAGE, |
| 89 | USBIP_PACKAGE, |
| 90 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 91 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 92 | logging.debug("Connecting to root@%s:%s`", hostname, port) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 93 | with contextlib.ExitStack() as stack: |
| 94 | device = stack.enter_context( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 95 | remote_access.ChromiumOSDeviceHandler( |
| 96 | hostname=hostname, port=port, username="root" |
| 97 | ) |
| 98 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 99 | if device.HasProgramInPath(USBIP_COMMAND): |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 100 | logging.debug("`%s` found on the device", USBIP_COMMAND) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 101 | else: |
| 102 | logging.error( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 103 | "You need to emerge and deploy the `%s` package to the test " |
| 104 | "device: emerge-${{BOARD}} %s && cros deploy " |
| 105 | "--board=${{BOARD}} %s", |
| 106 | USBIP_PACKAGE, |
| 107 | USBIP_PACKAGE, |
| 108 | USBIP_PACKAGE, |
| 109 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 110 | return False |
| 111 | |
| 112 | tunnel_is_alive = stack.enter_context(setup_usbip_tunnel(device)) |
| 113 | |
| 114 | if not load_modules(device=device): |
| 115 | return False |
| 116 | |
| 117 | if not stack.enter_context(start_usbipd()): |
| 118 | return False |
| 119 | |
| 120 | for busid in usb_devices: |
| 121 | if not stack.enter_context(bind_usb_device(busid)): |
| 122 | return False |
| 123 | |
| 124 | if not tunnel_is_alive(): |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 125 | logging.error("SSH tunnel is dead. Aborting.") |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 126 | return False |
| 127 | |
| 128 | for i, busid in enumerate(usb_devices): |
| 129 | if not stack.enter_context(attach_usb_device(device, busid, i)): |
| 130 | return False |
| 131 | |
| 132 | # Catch SIGINT, SIGTERM, and SIGHUP. |
| 133 | try: |
| 134 | signal.signal(signal.SIGINT, signal.default_int_handler) |
| 135 | signal.signal(signal.SIGTERM, signal.default_int_handler) |
| 136 | signal.signal(signal.SIGHUP, signal.default_int_handler) |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 137 | logging.info("Ready. Press Ctrl-C (SIGINT) to cleanup.") |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 138 | while True: |
| 139 | time.sleep(60) |
| 140 | except KeyboardInterrupt: |
| 141 | pass |
| 142 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 143 | logging.debug("Cleanup complete.") |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 144 | return True |
| 145 | |
| 146 | |
| 147 | def get_opts(argv): |
| 148 | """Parse the command-line options.""" |
| 149 | parser = commandline.ArgumentParser(description=forward_devices.__doc__) |
| 150 | parser.add_argument( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 151 | "-d", |
| 152 | "--device", |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 153 | type=commandline.DeviceParser(commandline.DEVICE_SCHEME_SSH), |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 154 | help="The target device to forward the USB devices to " |
| 155 | "(hostname[:port]).", |
| 156 | ) |
| 157 | parser.add_argument( |
| 158 | "usb_devices", |
| 159 | nargs="+", |
| 160 | help="Bus identifiers of USB devices to forward", |
| 161 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 162 | opts = parser.parse_args(argv) |
| 163 | opts.Freeze() |
| 164 | return opts |
| 165 | |
| 166 | |
| 167 | def load_modules(device=None): |
Tom Hughes | 437705a | 2020-12-04 19:10:09 | [diff] [blame] | 168 | """Load prerequisite kernel modules. |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 169 | |
| 170 | The modules will first be loaded on the calling machine. If device is set, |
Tom Hughes | 437705a | 2020-12-04 19:10:09 | [diff] [blame] | 171 | the prerequisite kernel for the target device will also be loaded. |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 172 | """ |
| 173 | for module in HOST_MODULES: |
| 174 | try: |
| 175 | cros_build_lib.sudo_run([MODPROBE_COMMAND, module]) |
| 176 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 177 | logging.error("Failed to load module on host: %s", module) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 178 | return False |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 179 | logging.debug("Loaded module on host: %s", module) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 180 | |
| 181 | if device is not None: |
| 182 | for module in CLIENT_MODULES: |
| 183 | try: |
| 184 | device.run([MODPROBE_COMMAND, module]) |
| 185 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 186 | logging.error("Failed to load module on target: %s", module) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 187 | return False |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 188 | logging.debug("Loaded module on target: %s", module) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 189 | return True |
| 190 | |
| 191 | |
| 192 | @contextlib.contextmanager |
| 193 | def start_usbipd(): |
| 194 | """Starts the `usbipd` daemon in the background. |
| 195 | |
| 196 | On cleanup kills the daemon. |
| 197 | |
| 198 | Returns: |
| 199 | False on failure. |
| 200 | """ |
| 201 | try: |
| 202 | cros_build_lib.sudo_run( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 203 | [USBIPD_COMMAND, "-D", "-P%s" % USBIPD_PID_FILE] |
| 204 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 205 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 206 | logging.error("Failed to start: %s", USBIPD_COMMAND) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 207 | yield False |
| 208 | return |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 209 | logging.debug("Started on host: %s", USBIPD_COMMAND) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 210 | |
| 211 | # Give the daemon a chance to write the PID file. |
| 212 | pid = retry_util.GenericRetry( |
| 213 | handler=lambda e: isinstance(e, FileNotFoundError), |
| 214 | max_retry=RETRY_USBIPD_READ_PID, |
| 215 | functor=lambda: int(osutils.ReadFile(USBIPD_PID_FILE).strip()), |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 216 | sleep=DELAY_USBIPD_READ_PID, |
| 217 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 218 | |
| 219 | yield True |
| 220 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 221 | logging.debug("Killing `usbipd` (%d).", pid) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 222 | cros_build_lib.sudo_run([KILL_COMMAND, str(pid)]) |
| 223 | |
| 224 | |
| 225 | @contextlib.contextmanager |
| 226 | def setup_usbip_tunnel(device): |
| 227 | """Tunnels the `usbip` port over SSH to the target device. |
| 228 | |
| 229 | On cleanup tears down the tunnel by killing the tunnel process. |
| 230 | |
| 231 | Returns: |
| 232 | A callback to check if the tunnel is still alive. |
| 233 | """ |
| 234 | proc = device.GetAgent().CreateTunnel( |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 235 | to_remote=[remote_access.PortForwardSpec(local_port=USBIPD_PORT)] |
| 236 | ) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 237 | |
| 238 | def alive(): |
| 239 | """Returns `True` if the SSH tunnel process is still alive.""" |
| 240 | return proc.poll() is None |
| 241 | |
| 242 | yield alive |
| 243 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 244 | logging.debug("Stopping `usbip` tunnel.") |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 245 | proc.terminate() |
| 246 | try: |
| 247 | proc.wait(timeout=10) |
| 248 | except subprocess.TimeoutExpired: |
| 249 | proc.kill() |
| 250 | proc.wait() |
| 251 | |
| 252 | |
| 253 | @contextlib.contextmanager |
| 254 | def bind_usb_device(busid): |
| 255 | """Binds the USB device at `busid` to usbip driver so it can be exported. |
| 256 | |
| 257 | On cleanup unbinds the usb device. |
| 258 | |
| 259 | Returns: |
| 260 | False on failure. |
| 261 | """ |
| 262 | try: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 263 | cros_build_lib.sudo_run([USBIP_COMMAND, "bind", "-b", busid]) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 264 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 265 | logging.error("Failed to bind: %s", busid) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 266 | yield False |
| 267 | return |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 268 | logging.debug("Bound: %s", busid) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 269 | |
| 270 | yield True |
| 271 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 272 | logging.debug("unbinding: %s", busid) |
| 273 | cros_build_lib.sudo_run([USBIP_COMMAND, "unbind", "-b", busid]) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 274 | |
| 275 | |
| 276 | @contextlib.contextmanager |
| 277 | def attach_usb_device(device, busid, port): |
| 278 | """Attaches the specified busid using `usbip`. |
| 279 | |
| 280 | On cleanup detaches the USB device at the specified `usbip` port number. |
| 281 | |
| 282 | Returns: |
| 283 | False on failure. |
| 284 | """ |
| 285 | try: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 286 | device.run([USBIP_COMMAND, "attach", "-r", "localhost", "-b", busid]) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 287 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 288 | logging.error("Failed to attach: %s", busid) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 289 | yield False |
| 290 | return |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 291 | logging.debug("Attached: %s", busid) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 292 | |
| 293 | yield True |
| 294 | |
| 295 | try: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 296 | device.run([USBIP_COMMAND, "detach", "-p", str(port)]) |
| 297 | logging.debug("Detached usbip port: %s", port) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 298 | except cros_build_lib.RunCommandError: |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 299 | logging.error("Failed to detach: %s", port) |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 300 | |
| 301 | |
Jack Rosenthal | 8de609d | 2023-02-09 20:20:35 | [diff] [blame] | 302 | if __name__ == "__main__": |
Allen Webb | dcf57fc | 2020-04-03 21:48:53 | [diff] [blame] | 303 | sys.exit(main(sys.argv[1:])) |