blob: 9fdc5a534cefffd68ffea8bcf45527123afeabd4 [file] [log] [blame]
Allen Webbdcf57fc2020-04-03 21:48:531#!/usr/bin/env python3
Mike Frysinger8b0fc372022-09-08 07:24:242# Copyright 2020 The ChromiumOS Authors
Allen Webbdcf57fc2020-04-03 21:48:533# 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 Hughes437705a2020-12-04 19:10:098Automates the process of forwarding specified USB devices to a target device.
Allen Webbdcf57fc2020-04-03 21:48:539This involves:
10
Tom Hughes437705a2020-12-04 19:10:0911 1) Loading the prerequisite kernel modules (both locally and on the target
Allen Webbdcf57fc2020-04-03 21:48:5312 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
19For example:
20 ./forward_usb_devices.py --log-level=debug -d test-device.local 1-3.1 1-3.2
21will forward two USB devices (the ones at bus ids 1-3.1 and 1-3.2) to the device
22with the hostname `test-device.local`.
Tom Hughes437705a2020-12-04 19:10:0923
24Requires usbip installed in the chroot:
25
26(chroot) $ sudo emerge usbip
27
28To list USB bus ids:
29
30(chroot) $ usbip list --local
31
32
Allen Webbdcf57fc2020-04-03 21:48:5333"""
34
35from __future__ import print_function
Jack Rosenthal8de609d2023-02-09 20:20:3536
Allen Webbdcf57fc2020-04-03 21:48:5337import contextlib
38import logging
39import os
40import shutil
41import signal
42import subprocess
43import sys
44import time
45
46from chromite.lib import commandline
47from chromite.lib import cros_build_lib
48from chromite.lib import osutils
49from chromite.lib import remote_access
50from chromite.lib import retry_util
51
52
Jack Rosenthal8de609d2023-02-09 20:20:3553HOST_MODULES = {"usbip-core", "usbip-host", "vhci-hcd"}
54CLIENT_MODULES = {"usbip-core", "vhci-hcd"}
Allen Webbdcf57fc2020-04-03 21:48:5355
Jack Rosenthal8de609d2023-02-09 20:20:3556KILL_COMMAND = "kill"
Allen Webbdcf57fc2020-04-03 21:48:5357
Jack Rosenthal8de609d2023-02-09 20:20:3558MODPROBE_COMMAND = "modprobe"
Allen Webbdcf57fc2020-04-03 21:48:5359
Jack Rosenthal8de609d2023-02-09 20:20:3560USBIP_PACKAGE = "usbip"
61USBIP_COMMAND = "usbip"
62USBIPD_COMMAND = "usbipd"
63USBIPD_PID_FILE = "/run/usbipd.pid"
Allen Webbdcf57fc2020-04-03 21:48:5364USBIPD_PORT = 3240
65
66RETRY_USBIPD_READ_PID = 10
67DELAY_USBIPD_READ_PID = 0.5
68
69
70def main(argv):
71 """Forward USB devices from the caller to the target device."""
Jack Rosenthal8de609d2023-02-09 20:20:3572 os.environ["PATH"] += ":/sbin:/usr/sbin"
Allen Webbdcf57fc2020-04-03 21:48:5373
74 opts = get_opts(argv)
Jack Rosenthal8de609d2023-02-09 20:20:3575 return forward_devices(
76 opts.device.hostname, opts.device.port, opts.usb_devices
77 )
Allen Webbdcf57fc2020-04-03 21:48:5378
79
80def 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 Rosenthal8de609d2023-02-09 20:20:3584 logging.debug("`%s` found in the chroot", USBIP_COMMAND)
Allen Webbdcf57fc2020-04-03 21:48:5385 else:
86 logging.error(
Jack Rosenthal8de609d2023-02-09 20:20:3587 "You need to emerge the `%s` package in the chroot: sudo emerge %s",
88 USBIP_PACKAGE,
89 USBIP_PACKAGE,
90 )
Allen Webbdcf57fc2020-04-03 21:48:5391
Jack Rosenthal8de609d2023-02-09 20:20:3592 logging.debug("Connecting to root@%s:%s`", hostname, port)
Allen Webbdcf57fc2020-04-03 21:48:5393 with contextlib.ExitStack() as stack:
94 device = stack.enter_context(
Jack Rosenthal8de609d2023-02-09 20:20:3595 remote_access.ChromiumOSDeviceHandler(
96 hostname=hostname, port=port, username="root"
97 )
98 )
Allen Webbdcf57fc2020-04-03 21:48:5399 if device.HasProgramInPath(USBIP_COMMAND):
Jack Rosenthal8de609d2023-02-09 20:20:35100 logging.debug("`%s` found on the device", USBIP_COMMAND)
Allen Webbdcf57fc2020-04-03 21:48:53101 else:
102 logging.error(
Jack Rosenthal8de609d2023-02-09 20:20:35103 "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 Webbdcf57fc2020-04-03 21:48:53110 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 Rosenthal8de609d2023-02-09 20:20:35125 logging.error("SSH tunnel is dead. Aborting.")
Allen Webbdcf57fc2020-04-03 21:48:53126 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 Rosenthal8de609d2023-02-09 20:20:35137 logging.info("Ready. Press Ctrl-C (SIGINT) to cleanup.")
Allen Webbdcf57fc2020-04-03 21:48:53138 while True:
139 time.sleep(60)
140 except KeyboardInterrupt:
141 pass
142
Jack Rosenthal8de609d2023-02-09 20:20:35143 logging.debug("Cleanup complete.")
Allen Webbdcf57fc2020-04-03 21:48:53144 return True
145
146
147def get_opts(argv):
148 """Parse the command-line options."""
149 parser = commandline.ArgumentParser(description=forward_devices.__doc__)
150 parser.add_argument(
Jack Rosenthal8de609d2023-02-09 20:20:35151 "-d",
152 "--device",
Allen Webbdcf57fc2020-04-03 21:48:53153 type=commandline.DeviceParser(commandline.DEVICE_SCHEME_SSH),
Jack Rosenthal8de609d2023-02-09 20:20:35154 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 Webbdcf57fc2020-04-03 21:48:53162 opts = parser.parse_args(argv)
163 opts.Freeze()
164 return opts
165
166
167def load_modules(device=None):
Tom Hughes437705a2020-12-04 19:10:09168 """Load prerequisite kernel modules.
Allen Webbdcf57fc2020-04-03 21:48:53169
170 The modules will first be loaded on the calling machine. If device is set,
Tom Hughes437705a2020-12-04 19:10:09171 the prerequisite kernel for the target device will also be loaded.
Allen Webbdcf57fc2020-04-03 21:48:53172 """
173 for module in HOST_MODULES:
174 try:
175 cros_build_lib.sudo_run([MODPROBE_COMMAND, module])
176 except cros_build_lib.RunCommandError:
Jack Rosenthal8de609d2023-02-09 20:20:35177 logging.error("Failed to load module on host: %s", module)
Allen Webbdcf57fc2020-04-03 21:48:53178 return False
Jack Rosenthal8de609d2023-02-09 20:20:35179 logging.debug("Loaded module on host: %s", module)
Allen Webbdcf57fc2020-04-03 21:48:53180
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 Rosenthal8de609d2023-02-09 20:20:35186 logging.error("Failed to load module on target: %s", module)
Allen Webbdcf57fc2020-04-03 21:48:53187 return False
Jack Rosenthal8de609d2023-02-09 20:20:35188 logging.debug("Loaded module on target: %s", module)
Allen Webbdcf57fc2020-04-03 21:48:53189 return True
190
191
192@contextlib.contextmanager
193def 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 Rosenthal8de609d2023-02-09 20:20:35203 [USBIPD_COMMAND, "-D", "-P%s" % USBIPD_PID_FILE]
204 )
Allen Webbdcf57fc2020-04-03 21:48:53205 except cros_build_lib.RunCommandError:
Jack Rosenthal8de609d2023-02-09 20:20:35206 logging.error("Failed to start: %s", USBIPD_COMMAND)
Allen Webbdcf57fc2020-04-03 21:48:53207 yield False
208 return
Jack Rosenthal8de609d2023-02-09 20:20:35209 logging.debug("Started on host: %s", USBIPD_COMMAND)
Allen Webbdcf57fc2020-04-03 21:48:53210
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 Rosenthal8de609d2023-02-09 20:20:35216 sleep=DELAY_USBIPD_READ_PID,
217 )
Allen Webbdcf57fc2020-04-03 21:48:53218
219 yield True
220
Jack Rosenthal8de609d2023-02-09 20:20:35221 logging.debug("Killing `usbipd` (%d).", pid)
Allen Webbdcf57fc2020-04-03 21:48:53222 cros_build_lib.sudo_run([KILL_COMMAND, str(pid)])
223
224
225@contextlib.contextmanager
226def 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 Rosenthal8de609d2023-02-09 20:20:35235 to_remote=[remote_access.PortForwardSpec(local_port=USBIPD_PORT)]
236 )
Allen Webbdcf57fc2020-04-03 21:48:53237
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 Rosenthal8de609d2023-02-09 20:20:35244 logging.debug("Stopping `usbip` tunnel.")
Allen Webbdcf57fc2020-04-03 21:48:53245 proc.terminate()
246 try:
247 proc.wait(timeout=10)
248 except subprocess.TimeoutExpired:
249 proc.kill()
250 proc.wait()
251
252
253@contextlib.contextmanager
254def 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 Rosenthal8de609d2023-02-09 20:20:35263 cros_build_lib.sudo_run([USBIP_COMMAND, "bind", "-b", busid])
Allen Webbdcf57fc2020-04-03 21:48:53264 except cros_build_lib.RunCommandError:
Jack Rosenthal8de609d2023-02-09 20:20:35265 logging.error("Failed to bind: %s", busid)
Allen Webbdcf57fc2020-04-03 21:48:53266 yield False
267 return
Jack Rosenthal8de609d2023-02-09 20:20:35268 logging.debug("Bound: %s", busid)
Allen Webbdcf57fc2020-04-03 21:48:53269
270 yield True
271
Jack Rosenthal8de609d2023-02-09 20:20:35272 logging.debug("unbinding: %s", busid)
273 cros_build_lib.sudo_run([USBIP_COMMAND, "unbind", "-b", busid])
Allen Webbdcf57fc2020-04-03 21:48:53274
275
276@contextlib.contextmanager
277def 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 Rosenthal8de609d2023-02-09 20:20:35286 device.run([USBIP_COMMAND, "attach", "-r", "localhost", "-b", busid])
Allen Webbdcf57fc2020-04-03 21:48:53287 except cros_build_lib.RunCommandError:
Jack Rosenthal8de609d2023-02-09 20:20:35288 logging.error("Failed to attach: %s", busid)
Allen Webbdcf57fc2020-04-03 21:48:53289 yield False
290 return
Jack Rosenthal8de609d2023-02-09 20:20:35291 logging.debug("Attached: %s", busid)
Allen Webbdcf57fc2020-04-03 21:48:53292
293 yield True
294
295 try:
Jack Rosenthal8de609d2023-02-09 20:20:35296 device.run([USBIP_COMMAND, "detach", "-p", str(port)])
297 logging.debug("Detached usbip port: %s", port)
Allen Webbdcf57fc2020-04-03 21:48:53298 except cros_build_lib.RunCommandError:
Jack Rosenthal8de609d2023-02-09 20:20:35299 logging.error("Failed to detach: %s", port)
Allen Webbdcf57fc2020-04-03 21:48:53300
301
Jack Rosenthal8de609d2023-02-09 20:20:35302if __name__ == "__main__":
Allen Webbdcf57fc2020-04-03 21:48:53303 sys.exit(main(sys.argv[1:]))