xref: /aosp_15_r20/external/angle/build/fuchsia/test/test_server.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1*8975f5c5SAndroid Build Coastguard Worker# Copyright 2022 The Chromium Authors
2*8975f5c5SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
3*8975f5c5SAndroid Build Coastguard Worker# found in the LICENSE file.
4*8975f5c5SAndroid Build Coastguard Worker"""Test server set up."""
5*8975f5c5SAndroid Build Coastguard Worker
6*8975f5c5SAndroid Build Coastguard Workerimport logging
7*8975f5c5SAndroid Build Coastguard Workerimport os
8*8975f5c5SAndroid Build Coastguard Workerimport sys
9*8975f5c5SAndroid Build Coastguard Workerimport subprocess
10*8975f5c5SAndroid Build Coastguard Worker
11*8975f5c5SAndroid Build Coastguard Workerfrom typing import List, Optional, Tuple
12*8975f5c5SAndroid Build Coastguard Worker
13*8975f5c5SAndroid Build Coastguard Workerfrom common import DIR_SRC_ROOT, get_free_local_port, get_ssh_address
14*8975f5c5SAndroid Build Coastguard Workerfrom compatible_utils import get_ssh_prefix
15*8975f5c5SAndroid Build Coastguard Worker
16*8975f5c5SAndroid Build Coastguard Workersys.path.append(os.path.join(DIR_SRC_ROOT, 'build', 'util', 'lib', 'common'))
17*8975f5c5SAndroid Build Coastguard Worker# pylint: disable=import-error,wrong-import-position
18*8975f5c5SAndroid Build Coastguard Workerimport chrome_test_server_spawner
19*8975f5c5SAndroid Build Coastguard Worker# pylint: enable=import-error,wrong-import-position
20*8975f5c5SAndroid Build Coastguard Worker
21*8975f5c5SAndroid Build Coastguard Worker
22*8975f5c5SAndroid Build Coastguard Workerdef _run_ssh_tunnel(target_addr: str, command: str,
23*8975f5c5SAndroid Build Coastguard Worker                    port_maps: List[str]) -> subprocess.CompletedProcess:
24*8975f5c5SAndroid Build Coastguard Worker    assert port_maps
25*8975f5c5SAndroid Build Coastguard Worker
26*8975f5c5SAndroid Build Coastguard Worker    ssh_prefix = get_ssh_prefix(target_addr)
27*8975f5c5SAndroid Build Coastguard Worker
28*8975f5c5SAndroid Build Coastguard Worker    # Allow a tunnel / control path to be established for the first time.
29*8975f5c5SAndroid Build Coastguard Worker    # The sshconfig https://crsrc.org/c/build/fuchsia/test/sshconfig used here
30*8975f5c5SAndroid Build Coastguard Worker    # persists the connection.
31*8975f5c5SAndroid Build Coastguard Worker    subprocess.run(ssh_prefix + ['echo', 'true'], check=True)
32*8975f5c5SAndroid Build Coastguard Worker
33*8975f5c5SAndroid Build Coastguard Worker    forward_proc = subprocess.run(
34*8975f5c5SAndroid Build Coastguard Worker        ssh_prefix + [
35*8975f5c5SAndroid Build Coastguard Worker            '-O',
36*8975f5c5SAndroid Build Coastguard Worker            command,  # Send SSH mux control signal.
37*8975f5c5SAndroid Build Coastguard Worker            '-NT'  # Don't execute command; don't allocate terminal.
38*8975f5c5SAndroid Build Coastguard Worker        ] + port_maps,
39*8975f5c5SAndroid Build Coastguard Worker        capture_output=True,
40*8975f5c5SAndroid Build Coastguard Worker        check=True,
41*8975f5c5SAndroid Build Coastguard Worker        text=True)
42*8975f5c5SAndroid Build Coastguard Worker    return forward_proc
43*8975f5c5SAndroid Build Coastguard Worker
44*8975f5c5SAndroid Build Coastguard Worker
45*8975f5c5SAndroid Build Coastguard Workerdef _forward_command(fuchsia_port: int, host_port: int,
46*8975f5c5SAndroid Build Coastguard Worker                     port_forwarding: bool) -> List[str]:
47*8975f5c5SAndroid Build Coastguard Worker    max_port = 65535
48*8975f5c5SAndroid Build Coastguard Worker    assert fuchsia_port is not None and 0 <= fuchsia_port <= max_port
49*8975f5c5SAndroid Build Coastguard Worker    assert host_port is not None and 0 < host_port <= max_port
50*8975f5c5SAndroid Build Coastguard Worker    if port_forwarding:
51*8975f5c5SAndroid Build Coastguard Worker        return ['-R', f'{fuchsia_port}:localhost:{host_port}']
52*8975f5c5SAndroid Build Coastguard Worker    assert fuchsia_port != 0
53*8975f5c5SAndroid Build Coastguard Worker    return ['-L', f'{host_port}:localhost:{fuchsia_port}']
54*8975f5c5SAndroid Build Coastguard Worker
55*8975f5c5SAndroid Build Coastguard Worker
56*8975f5c5SAndroid Build Coastguard Workerdef _forward_commands(ports: List[Tuple[int, int]],
57*8975f5c5SAndroid Build Coastguard Worker                      port_forwarding: bool) -> List[str]:
58*8975f5c5SAndroid Build Coastguard Worker    assert ports
59*8975f5c5SAndroid Build Coastguard Worker    forward_cmd = []
60*8975f5c5SAndroid Build Coastguard Worker    for port in ports:
61*8975f5c5SAndroid Build Coastguard Worker        assert port is not None
62*8975f5c5SAndroid Build Coastguard Worker        forward_cmd.extend(_forward_command(port[0], port[1], port_forwarding))
63*8975f5c5SAndroid Build Coastguard Worker    return forward_cmd
64*8975f5c5SAndroid Build Coastguard Worker
65*8975f5c5SAndroid Build Coastguard Worker
66*8975f5c5SAndroid Build Coastguard Workerdef ports_forward(target_addr: str,
67*8975f5c5SAndroid Build Coastguard Worker                  ports: List[Tuple[int, int]]) -> subprocess.CompletedProcess:
68*8975f5c5SAndroid Build Coastguard Worker    """Establishes a port forwarding SSH task to forward ports from the host to
69*8975f5c5SAndroid Build Coastguard Worker    the fuchsia endpoints specified by tuples of port numbers in format of
70*8975f5c5SAndroid Build Coastguard Worker    [fuchsia-port, host-port]. Setting fuchsia-port to 0 would allow the fuchsia
71*8975f5c5SAndroid Build Coastguard Worker    selecting a free port; host-port shouldn't be 0.
72*8975f5c5SAndroid Build Coastguard Worker
73*8975f5c5SAndroid Build Coastguard Worker    Blocks until port forwarding is established.
74*8975f5c5SAndroid Build Coastguard Worker
75*8975f5c5SAndroid Build Coastguard Worker    Returns the CompletedProcess of the SSH task."""
76*8975f5c5SAndroid Build Coastguard Worker    return _run_ssh_tunnel(target_addr, 'forward',
77*8975f5c5SAndroid Build Coastguard Worker                           _forward_commands(ports, True))
78*8975f5c5SAndroid Build Coastguard Worker
79*8975f5c5SAndroid Build Coastguard Worker
80*8975f5c5SAndroid Build Coastguard Workerdef ports_backward(
81*8975f5c5SAndroid Build Coastguard Worker        target_addr: str,
82*8975f5c5SAndroid Build Coastguard Worker        ports: List[Tuple[int, int]]) -> subprocess.CompletedProcess:
83*8975f5c5SAndroid Build Coastguard Worker    """Establishes a reverse port forwarding SSH task to forward ports from the
84*8975f5c5SAndroid Build Coastguard Worker    fuchsia to the host endpoints specified by tuples of port numbers in format
85*8975f5c5SAndroid Build Coastguard Worker    of [fuchsia-port, host-port]. Both host-port and fuchsia-port shouldn't be
86*8975f5c5SAndroid Build Coastguard Worker    0.
87*8975f5c5SAndroid Build Coastguard Worker
88*8975f5c5SAndroid Build Coastguard Worker    Blocks until port forwarding is established.
89*8975f5c5SAndroid Build Coastguard Worker
90*8975f5c5SAndroid Build Coastguard Worker    Returns the CompletedProcess of the SSH task."""
91*8975f5c5SAndroid Build Coastguard Worker    return _run_ssh_tunnel(target_addr, 'forward',
92*8975f5c5SAndroid Build Coastguard Worker                           _forward_commands(ports, False))
93*8975f5c5SAndroid Build Coastguard Worker
94*8975f5c5SAndroid Build Coastguard Worker
95*8975f5c5SAndroid Build Coastguard Workerdef port_forward(target_addr: str, host_port: int) -> int:
96*8975f5c5SAndroid Build Coastguard Worker    """Establishes a port forwarding SSH task to a host TCP endpoint at port
97*8975f5c5SAndroid Build Coastguard Worker    |host_port|. Blocks until port forwarding is established.
98*8975f5c5SAndroid Build Coastguard Worker
99*8975f5c5SAndroid Build Coastguard Worker    Returns the fuchsia port number."""
100*8975f5c5SAndroid Build Coastguard Worker
101*8975f5c5SAndroid Build Coastguard Worker    forward_proc = ports_forward(target_addr, [(0, host_port)])
102*8975f5c5SAndroid Build Coastguard Worker    parsed_port = int(forward_proc.stdout.splitlines()[0].strip())
103*8975f5c5SAndroid Build Coastguard Worker    logging.debug('Port forwarding established (local=%d, device=%d)',
104*8975f5c5SAndroid Build Coastguard Worker                  host_port, parsed_port)
105*8975f5c5SAndroid Build Coastguard Worker    return parsed_port
106*8975f5c5SAndroid Build Coastguard Worker
107*8975f5c5SAndroid Build Coastguard Worker
108*8975f5c5SAndroid Build Coastguard Workerdef port_backward(target_addr: str,
109*8975f5c5SAndroid Build Coastguard Worker                  fuchsia_port: int,
110*8975f5c5SAndroid Build Coastguard Worker                  host_port: int = 0) -> int:
111*8975f5c5SAndroid Build Coastguard Worker    """Establishes a reverse port forwarding SSH task to a fuchsia TCP endpoint
112*8975f5c5SAndroid Build Coastguard Worker    at port |fuchsia_port| from the host at port |host_port|. If |host_port| is
113*8975f5c5SAndroid Build Coastguard Worker    None or 0, a local free port will be selected.
114*8975f5c5SAndroid Build Coastguard Worker    Blocks until reverse port forwarding is established.
115*8975f5c5SAndroid Build Coastguard Worker
116*8975f5c5SAndroid Build Coastguard Worker    Returns the local port number."""
117*8975f5c5SAndroid Build Coastguard Worker
118*8975f5c5SAndroid Build Coastguard Worker    if not host_port:
119*8975f5c5SAndroid Build Coastguard Worker        host_port = get_free_local_port()
120*8975f5c5SAndroid Build Coastguard Worker    ports_backward(target_addr, [(fuchsia_port, host_port)])
121*8975f5c5SAndroid Build Coastguard Worker    logging.debug('Reverse port forwarding established (local=%d, device=%d)',
122*8975f5c5SAndroid Build Coastguard Worker                  host_port, fuchsia_port)
123*8975f5c5SAndroid Build Coastguard Worker    return host_port
124*8975f5c5SAndroid Build Coastguard Worker
125*8975f5c5SAndroid Build Coastguard Worker
126*8975f5c5SAndroid Build Coastguard Workerdef cancel_port_forwarding(target_addr: str, fuchsia_port: int, host_port: int,
127*8975f5c5SAndroid Build Coastguard Worker                           port_forwarding: bool) -> None:
128*8975f5c5SAndroid Build Coastguard Worker    """Cancels an existing port forwarding, if port_forwarding is false, it will
129*8975f5c5SAndroid Build Coastguard Worker    be treated as reverse port forwarding.
130*8975f5c5SAndroid Build Coastguard Worker    Note, the ports passing in here need to exactly match the ports used to
131*8975f5c5SAndroid Build Coastguard Worker    setup the port forwarding, i.e. if ports_forward([0, 8080]) was issued, even
132*8975f5c5SAndroid Build Coastguard Worker    it returned an allocated port, cancel_port_forwarding(..., 0, 8080, ...)
133*8975f5c5SAndroid Build Coastguard Worker    should still be used to cancel the port forwarding."""
134*8975f5c5SAndroid Build Coastguard Worker    _run_ssh_tunnel(target_addr, 'cancel',
135*8975f5c5SAndroid Build Coastguard Worker                    _forward_command(fuchsia_port, host_port, port_forwarding))
136*8975f5c5SAndroid Build Coastguard Worker
137*8975f5c5SAndroid Build Coastguard Worker
138*8975f5c5SAndroid Build Coastguard Worker# Disable pylint errors since the subclass is not from this directory.
139*8975f5c5SAndroid Build Coastguard Worker# pylint: disable=invalid-name,missing-function-docstring
140*8975f5c5SAndroid Build Coastguard Workerclass SSHPortForwarder(chrome_test_server_spawner.PortForwarder):
141*8975f5c5SAndroid Build Coastguard Worker    """Implementation of chrome_test_server_spawner.PortForwarder that uses
142*8975f5c5SAndroid Build Coastguard Worker    SSH's remote port forwarding feature to forward ports."""
143*8975f5c5SAndroid Build Coastguard Worker
144*8975f5c5SAndroid Build Coastguard Worker    def __init__(self, target_addr: str) -> None:
145*8975f5c5SAndroid Build Coastguard Worker        self._target_addr = target_addr
146*8975f5c5SAndroid Build Coastguard Worker
147*8975f5c5SAndroid Build Coastguard Worker        # Maps the host (server) port to the device port number.
148*8975f5c5SAndroid Build Coastguard Worker        self._port_mapping = {}
149*8975f5c5SAndroid Build Coastguard Worker
150*8975f5c5SAndroid Build Coastguard Worker    def Map(self, port_pairs: List[Tuple[int, int]]) -> None:
151*8975f5c5SAndroid Build Coastguard Worker        for p in port_pairs:
152*8975f5c5SAndroid Build Coastguard Worker            fuchsia_port, host_port = p
153*8975f5c5SAndroid Build Coastguard Worker            assert fuchsia_port == 0, \
154*8975f5c5SAndroid Build Coastguard Worker                'Port forwarding with a fixed fuchsia-port is unsupported yet.'
155*8975f5c5SAndroid Build Coastguard Worker            self._port_mapping[host_port] = \
156*8975f5c5SAndroid Build Coastguard Worker                port_forward(self._target_addr, host_port)
157*8975f5c5SAndroid Build Coastguard Worker
158*8975f5c5SAndroid Build Coastguard Worker    def GetDevicePortForHostPort(self, host_port: int) -> int:
159*8975f5c5SAndroid Build Coastguard Worker        return self._port_mapping[host_port]
160*8975f5c5SAndroid Build Coastguard Worker
161*8975f5c5SAndroid Build Coastguard Worker    def Unmap(self, device_port: int) -> None:
162*8975f5c5SAndroid Build Coastguard Worker        for host_port, fuchsia_port in self._port_mapping.items():
163*8975f5c5SAndroid Build Coastguard Worker            if fuchsia_port == device_port:
164*8975f5c5SAndroid Build Coastguard Worker                cancel_port_forwarding(self._target_addr, 0, host_port, True)
165*8975f5c5SAndroid Build Coastguard Worker                del self._port_mapping[host_port]
166*8975f5c5SAndroid Build Coastguard Worker                return
167*8975f5c5SAndroid Build Coastguard Worker
168*8975f5c5SAndroid Build Coastguard Worker        raise Exception('Unmap called for unknown port: %d' % device_port)
169*8975f5c5SAndroid Build Coastguard Worker
170*8975f5c5SAndroid Build Coastguard Worker
171*8975f5c5SAndroid Build Coastguard Worker# pylint: enable=invalid-name,missing-function-docstring
172*8975f5c5SAndroid Build Coastguard Worker
173*8975f5c5SAndroid Build Coastguard Worker
174*8975f5c5SAndroid Build Coastguard Workerdef setup_test_server(target_id: Optional[str], test_concurrency: int)\
175*8975f5c5SAndroid Build Coastguard Worker         -> Tuple[chrome_test_server_spawner.SpawningServer, str]:
176*8975f5c5SAndroid Build Coastguard Worker    """Provisions a test server and configures |target_id| to use it.
177*8975f5c5SAndroid Build Coastguard Worker
178*8975f5c5SAndroid Build Coastguard Worker    Args:
179*8975f5c5SAndroid Build Coastguard Worker        target_id: The target to which port forwarding to the test server will
180*8975f5c5SAndroid Build Coastguard Worker            be established.
181*8975f5c5SAndroid Build Coastguard Worker        test_concurrency: The number of parallel test jobs that will be run.
182*8975f5c5SAndroid Build Coastguard Worker
183*8975f5c5SAndroid Build Coastguard Worker    Returns a tuple of a SpawningServer object and the local url to use on
184*8975f5c5SAndroid Build Coastguard Worker    |target_id| to reach the test server."""
185*8975f5c5SAndroid Build Coastguard Worker
186*8975f5c5SAndroid Build Coastguard Worker    logging.debug('Starting test server.')
187*8975f5c5SAndroid Build Coastguard Worker
188*8975f5c5SAndroid Build Coastguard Worker    target_addr = get_ssh_address(target_id)
189*8975f5c5SAndroid Build Coastguard Worker
190*8975f5c5SAndroid Build Coastguard Worker    # The TestLauncher can launch more jobs than the limit specified with
191*8975f5c5SAndroid Build Coastguard Worker    # --test-launcher-jobs so the max number of spawned test servers is set to
192*8975f5c5SAndroid Build Coastguard Worker    # twice that limit here. See https://crbug.com/913156#c19.
193*8975f5c5SAndroid Build Coastguard Worker    spawning_server = chrome_test_server_spawner.SpawningServer(
194*8975f5c5SAndroid Build Coastguard Worker        0, SSHPortForwarder(target_addr), test_concurrency * 2)
195*8975f5c5SAndroid Build Coastguard Worker
196*8975f5c5SAndroid Build Coastguard Worker    forwarded_port = port_forward(target_addr, spawning_server.server_port)
197*8975f5c5SAndroid Build Coastguard Worker    spawning_server.Start()
198*8975f5c5SAndroid Build Coastguard Worker
199*8975f5c5SAndroid Build Coastguard Worker    logging.debug('Test server listening for connections (port=%d)',
200*8975f5c5SAndroid Build Coastguard Worker                  spawning_server.server_port)
201*8975f5c5SAndroid Build Coastguard Worker    logging.debug('Forwarded port is %d', forwarded_port)
202*8975f5c5SAndroid Build Coastguard Worker
203*8975f5c5SAndroid Build Coastguard Worker    return (spawning_server, 'http://localhost:%d' % forwarded_port)
204