def _KillHostLocked(self): """Kills the forwarder process running on the host. Note that the global lock must be acquired before calling this method. """ logger.info('Killing host_forwarder.') try: kill_cmd = [self._host_forwarder_path, '--kill-server'] (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code != 0: logger.warning('Forwarder unable to shut down:\n%s', output) kill_cmd = ['pkill', '-9', 'host_forwarder'] (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code == -9: # pkill can exit with -9, seemingly in cases where the process it's # asked to kill dies sometime during pkill running. In this case, # re-running should result in pkill succeeding. logging.warning( 'pkilling host forwarder returned -9, retrying through strace. ' 'Output: %s', output) exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['strace', '-f', '-s', '256'] + kill_cmd, Forwarder._TIMEOUT) if exit_code in (0, 1): # pkill exits with a 0 if it was able to signal at least one process. # pkill exits with a 1 if it wasn't able to signal a process because # no matching process existed. We're ok with either. return _, ps_output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['ps', 'aux'], Forwarder._TIMEOUT) host_forwarder_lines = [ line for line in ps_output.splitlines() if 'host_forwarder' in line ] if host_forwarder_lines: logger.error('Remaining host_forwarder processes:\n %s', '\n '.join(host_forwarder_lines)) else: logger.error('No remaining host_forwarder processes?') _DumpHostLog() error_msg = textwrap.dedent("""\ `{kill_cmd}` failed to kill host_forwarder. exit_code: {exit_code} output: {output} """) raise HostForwarderError( error_msg.format(kill_cmd=' '.join(kill_cmd), exit_code=str(exit_code), output='\n'.join( ' %s' % l for l in output.splitlines()))) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(kill_cmd), e.output))
def _RunSingleTest(self, test): self._test_instance.WriteBuildBotJson(self._output_dir) timeout = self._tests[test].get('timeout', self._timeout) cmd = self._CreateCmd(test) cwd = os.path.abspath(host_paths.DIR_SOURCE_ROOT) self._LogTest(test, cmd, timeout) try: start_time = time.time() with contextlib_ext.Optional(trace_event.trace(test), self._env.trace_output): exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( cmd, timeout, cwd=cwd, shell=True) end_time = time.time() chart_json_output = self._test_instance.ReadChartjsonOutput( self._output_dir) if exit_code == 0: result_type = base_test_result.ResultType.PASS else: result_type = base_test_result.ResultType.FAIL except cmd_helper.TimeoutError as e: end_time = time.time() exit_code = -1 output = e.output chart_json_output = '' result_type = base_test_result.ResultType.TIMEOUT return self._ProcessTestResult(test, cmd, start_time, end_time, exit_code, output, chart_json_output, result_type)
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None, check_error=True, cpu_affinity=None): # pylint: disable=no-member try: status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity), timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()) except OSError as e: if e.errno in (errno.ENOENT, errno.ENOEXEC): raise device_errors.NoAdbError(msg=str(e)) else: raise if status != 0: raise device_errors.AdbCommandFailedError(args, output, status, device_serial) # This catches some errors, including when the device drops offline; # unfortunately adb is very inconsistent with error reporting so many # command failures present differently. if check_error and output.startswith('error:'): raise device_errors.AdbCommandFailedError(args, output) return output
def _UnmapDevicePortLocked(device_port, device): """Internal method used by UnmapDevicePort(). Note that the global lock must be acquired before calling this method. """ instance = Forwarder._GetInstanceLocked(None) serial = str(device) serial_with_port = (serial, device_port) if serial_with_port not in instance._device_to_host_port_map: logger.error('Trying to unmap non-forwarded port %d', device_port) return host_port = instance._device_to_host_port_map[serial_with_port] del instance._device_to_host_port_map[serial_with_port] del instance._host_to_device_port_map[host_port] unmap_cmd = [ instance._host_forwarder_path, '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=%s' % serial, '--unmap', str(device_port) ] try: (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( unmap_cmd, Forwarder._TIMEOUT) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(unmap_cmd), e.output)) if exit_code != 0: logger.error( '`%s` exited with %d:\n%s', ' '.join(unmap_cmd), exit_code, '\n'.join(output) if isinstance(output, list) else output)
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None, check_error=True, cpu_affinity=None, ensure_logs_on_timeout=True): timeout = timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime() if ensure_logs_on_timeout and timeout: timeout = 0.95 * timeout try: status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity), timeout, env=cls._ADB_ENV) except OSError as e: if e.errno in (errno.ENOENT, errno.ENOEXEC): raise device_errors.NoAdbError(msg=str(e)) else: raise # Best effort to catch errors from adb; unfortunately adb is very # inconsistent with error reporting so many command failures present # differently. if status != 0 or (check_error and output.startswith('error:')): not_found_m = _DEVICE_NOT_FOUND_RE.match(output) device_waiting_m = _WAITING_FOR_DEVICE_RE.match(output) if (device_waiting_m is not None or (not_found_m is not None and not_found_m.group('serial') == device_serial)): raise device_errors.DeviceUnreachableError(device_serial) else: raise device_errors.AdbCommandFailedError( args, output, status, device_serial) return output
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None, check_error=True, cpu_affinity=None): # pylint: disable=no-member try: status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity), timeout_retry.CurrentTimeoutThreadGroup().GetRemainingTime()) except OSError as e: if e.errno in (errno.ENOENT, errno.ENOEXEC): raise device_errors.NoAdbError(msg=str(e)) else: raise # Best effort to catch errors from adb; unfortunately adb is very # inconsistent with error reporting so many command failures present # differently. if status != 0 or (check_error and output.startswith('error:')): m = _DEVICE_NOT_FOUND_RE.match(output) if m is not None and m.group('serial') == device_serial: raise device_errors.DeviceUnreachableError(device_serial) else: raise device_errors.AdbCommandFailedError( args, output, status, device_serial) return output
def _KillHostLocked(self): """Kills the forwarder process running on the host. Note that the global lock must be acquired before calling this method. """ logger.info('Killing host_forwarder.') try: kill_cmd = [self._host_forwarder_path, '--kill-server'] (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code != 0: logger.warning('Forwarder unable to shut down:\n%s', output) kill_cmd = ['pkill', '-9', 'host_forwarder'] (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code != 0: _, ps_output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['ps', 'aux'], Forwarder._TIMEOUT) host_forwarder_lines = [ line for line in ps_output.splitlines() if 'host_forwarder' in line ] if host_forwarder_lines: logger.error( 'Remaining host_forwarder processes:\n %s', '\n '.join(host_forwarder_lines)) else: logger.error('No remaining host_forwarder processes?') _DumpHostLog() error_msg = textwrap.dedent("""\ `{kill_cmd}` failed to kill host_forwarder. exit_code: {exit_code} """) raise HostForwarderError( error_msg.format(kill_cmd=' '.join(kill_cmd), exit_code=str(exit_code))) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(kill_cmd), e.output))
def _KillHostLocked(self): """Kills the forwarder process running on the host. Note that the global lock must be acquired before calling this method. """ logger.info('Killing host_forwarder.') try: kill_cmd = [self._host_forwarder_path, '--kill-server'] (exit_code, _o) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code != 0: kill_cmd = ['pkill', '-9', 'host_forwarder'] (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( kill_cmd, Forwarder._TIMEOUT) if exit_code != 0: raise HostForwarderError('%s exited with %d:\n%s' % (self._host_forwarder_path, exit_code, '\n'.join(output))) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(kill_cmd), e.output))
def _RunAdbCmd(cls, args, timeout=None, retries=None, device_serial=None, check_error=True, cpu_affinity=None, additional_env=None): if timeout: remaining = timeout_retry.CurrentTimeoutThreadGroup( ).GetRemainingTime() if remaining: # Use a slightly smaller timeout than remaining time to ensure that we # have time to collect output from the command. timeout = 0.95 * remaining else: timeout = None env = cls._ADB_ENV.copy() if additional_env: env.update(additional_env) try: adb_cmd = cls._BuildAdbCmd(args, device_serial, cpu_affinity=cpu_affinity) status, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( adb_cmd, timeout, env=env) except OSError as e: if e.errno in (errno.ENOENT, errno.ENOEXEC): raise device_errors.NoAdbError(msg=str(e)) else: raise except cmd_helper.TimeoutError: logger.error('Timeout on adb command: %r', adb_cmd) raise # Best effort to catch errors from adb; unfortunately adb is very # inconsistent with error reporting so many command failures present # differently. if status != 0 or (check_error and output.startswith('error:')): not_found_m = _DEVICE_NOT_FOUND_RE.search(output) device_waiting_m = _WAITING_FOR_DEVICE_RE.match(output) if (device_waiting_m is not None or (not_found_m is not None and not_found_m.group('serial') == device_serial)): raise device_errors.DeviceUnreachableError(device_serial) else: raise device_errors.AdbCommandFailedError( args, output, status, device_serial) return output
def lsusb(): """Call lsusb and return the parsed output.""" _, lsusb_list_output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['lsusb'], timeout=10) devices = [] for line in lsusb_list_output.splitlines(): m = _LSUSB_BUS_DEVICE_RE.match(line) if m: bus_num = m.group(1) dev_num = m.group(2) try: devices.append(_lsusbv_on_device(bus_num, dev_num)) except cmd_helper.TimeoutError: # Will be blacklisted if it is in expected device file, but times out. logger.info('lsusb -v %s:%s timed out.', bus_num, dev_num) return devices
def UnmapAllDevicePorts(device): """Unmaps all the previously forwarded ports for the provided device. Args: device: A DeviceUtils instance. port_pairs: A list of tuples (device_port, host_port) to unmap. """ with _FileLock(Forwarder._LOCK_PATH): instance = Forwarder._GetInstanceLocked(None) unmap_all_cmd = [ instance._host_forwarder_path, '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=%s' % device.serial, '--unmap-all' ] try: exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( unmap_all_cmd, Forwarder._TIMEOUT) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(unmap_all_cmd), e.output)) if exit_code != 0: error_msg = [ '`%s` exited with %d' % (' '.join(unmap_all_cmd), exit_code) ] if isinstance(output, list): error_msg += output else: error_msg += [output] raise HostForwarderError('\n'.join(error_msg)) # Clean out any entries from the device & host map. device_map = instance._device_to_host_port_map host_map = instance._host_to_device_port_map for device_serial_and_port, host_port in device_map.items(): device_serial = device_serial_and_port[0] if device_serial == device.serial: del device_map[device_serial_and_port] del host_map[host_port] # Kill the device forwarder. tool = base_tool.BaseTool() instance._KillDeviceLocked(device, tool)
def _lsusbv_on_device(bus_id, dev_id): """Calls lsusb -v on device.""" _, raw_output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['lsusb', '-v', '-s', '%s:%s' % (bus_id, dev_id)], timeout=10) device = {'bus': bus_id, 'device': dev_id} depth_stack = [device] # This builds a nested dict -- a tree, basically -- that corresponds # to the lsusb output. It looks first for a line containing # # "Bus <bus number> Device <device number>: ..." # # and uses that to create the root node. It then parses all remaining # lines as a tree, with the indentation level determining the # depth of the new node. # # This expects two kinds of lines: # - "groups", which take the form # "<Group name>:" # and typically have children, and # - "entries", which take the form # "<entry name> <entry value> <possible entry description>" # and typically do not have children (but can). # # This maintains a stack containing all current ancestor nodes in # order to add new nodes to the proper place in the tree. # The stack is added to when a new node is parsed. Nodes are removed # from the stack when they are either at the same indentation level as # or a deeper indentation level than the current line. # # e.g. the following lsusb output: # # Bus 123 Device 456: School bus # Device Descriptor: # bDeviceClass 5 Actual School Bus # Configuration Descriptor: # bLength 20 Rows # # would produce the following dict: # # { # 'bus': 123, # 'device': 456, # 'desc': 'School bus', # 'Device Descriptor': { # 'bDeviceClass': { # '_value': '5', # '_desc': 'Actual School Bus', # }, # 'Configuration Descriptor': { # 'bLength': { # '_value': '20', # '_desc': 'Rows', # }, # }, # } # } for line in raw_output.splitlines(): # Ignore blank lines. if not line: continue # Filter out error mesage about opening device. if _COULDNT_OPEN_ERROR_RE.match(line): continue # Find start of device information. m = _LSUSB_BUS_DEVICE_RE.match(line) if m: if m.group(1) != bus_id: logger.warning('Expected bus_id value: %r, seen %r', bus_id, m.group(1)) if m.group(2) != dev_id: logger.warning('Expected dev_id value: %r, seen %r', dev_id, m.group(2)) device['desc'] = m.group(3) continue # Skip any lines that aren't indented, as they're not part of the # device descriptor. indent_match = _INDENTATION_RE.match(line) if not indent_match: continue # Determine the indentation depth. depth = 1 + len(indent_match.group(1)) / 2 if depth > len(depth_stack): logger.error('lsusb parsing error: unexpected indentation: "%s"', line) continue # Pop everything off the depth stack that isn't a parent of # this element. while depth < len(depth_stack): depth_stack.pop() cur = depth_stack[-1] m = _LSUSB_GROUP_RE.match(line) if m: new_group = {} cur[m.group(1)] = new_group depth_stack.append(new_group) continue m = _LSUSB_ENTRY_RE.match(line) if m: new_entry = { '_value': m.group(2), '_desc': m.group(3), } cur[m.group(1)] = new_entry depth_stack.append(new_entry) continue logger.error('lsusb parsing error: unrecognized line: "%s"', line) return device
def _lsusbv_on_device(bus_id, dev_id): """Calls lsusb -v on device.""" _, raw_output = cmd_helper.GetCmdStatusAndOutputWithTimeout( ['lsusb', '-v', '-s', '%s:%s' % (bus_id, dev_id)], timeout=10) device = {'bus': bus_id, 'device': dev_id} depth_stack = [device] # TODO(jbudorick): Add documentation for parsing. for line in raw_output.splitlines(): # Ignore blank lines. if not line: continue # Filter out error mesage about opening device. if _COULDNT_OPEN_ERROR_RE.match(line): continue # Find start of device information. m = _LSUSB_BUS_DEVICE_RE.match(line) if m: if m.group(1) != bus_id: logging.warning( 'Expected bus_id value: %r, seen %r', bus_id, m.group(1)) if m.group(2) != dev_id: logging.warning( 'Expected dev_id value: %r, seen %r', dev_id, m.group(2)) continue indent_match = _INDENTATION_RE.match(line) if not indent_match: continue depth = 1 + len(indent_match.group(1)) / 2 if depth > len(depth_stack): logging.error( 'lsusb parsing error: unexpected indentation: "%s"', line) continue while depth < len(depth_stack): depth_stack.pop() cur = depth_stack[-1] m = _LSUSB_GROUP_RE.match(line) if m: new_group = {} cur[m.group(1)] = new_group depth_stack.append(new_group) continue m = _LSUSB_ENTRY_RE.match(line) if m: new_entry = { '_value': m.group(2), '_desc': m.group(3), } cur[m.group(1)] = new_entry depth_stack.append(new_entry) continue logging.error('lsusb parsing error: unrecognized line: "%s"', line) return device
def _LaunchPerfTest(self, test_name): """Runs a perf test. Args: test_name: the name of the test to be executed. Returns: A tuple containing (Output, base_test_result.ResultType) """ if not self._CheckDeviceAffinity(test_name): return '', base_test_result.ResultType.PASS try: logging.warning('Unmapping device ports') forwarder.Forwarder.UnmapAllDevicePorts(self.device) self.device.RestartAdbd() except Exception as e: # pylint: disable=broad-except logging.error('Exception when tearing down device %s', e) test_config = self._tests['steps'][test_name] cmd = ('%s --device %s' % (test_config['cmd'], self.device_serial)) if (self._options.collect_chartjson_data or test_config.get('archive_output_dir')): self._output_dir = tempfile.mkdtemp() self._WriteBuildBotJson() cmd = cmd + ' --output-dir=%s' % self._output_dir logging.info( 'temperature: %s (0.1 C)', str(self._device_battery.GetBatteryInfo().get('temperature'))) if self._options.max_battery_temp: self._device_battery.LetBatteryCoolToTemperature( self._options.max_battery_temp) logging.info('Charge level: %s%%', str(self._device_battery.GetBatteryInfo().get('level'))) if self._options.min_battery_level: self._device_battery.ChargeDeviceToLevel( self._options.min_battery_level) logging.info('%s : %s', test_name, cmd) start_time = time.time() timeout = test_config.get('timeout', 3600) if self._options.no_timeout: timeout = None logging.info('Timeout for %s test: %s', test_name, timeout) full_cmd = cmd if self._options.dry_run: full_cmd = 'echo %s' % cmd logfile = sys.stdout archive_bytes = None if self._options.single_step: # Just print a heart-beat so that the outer buildbot scripts won't timeout # without response. logfile = _HeartBeatLogger() cwd = os.path.abspath(constants.DIR_SOURCE_ROOT) if full_cmd.startswith('src/'): cwd = os.path.abspath( os.path.join(constants.DIR_SOURCE_ROOT, os.pardir)) try: exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( full_cmd, timeout, cwd=cwd, shell=True, logfile=logfile) json_output = self._ReadChartjsonOutput() if test_config.get('archive_output_dir'): archive_bytes = self._ArchiveOutputDir() except cmd_helper.TimeoutError as e: exit_code = -1 output = e.output json_output = '' finally: self._CleanupOutputDirectory() if self._options.single_step: logfile.stop() end_time = time.time() if exit_code is None: exit_code = -1 logging.info('%s : exit_code=%d in %d secs at %s', test_name, exit_code, end_time - start_time, self.device_serial) if exit_code == 0: result_type = base_test_result.ResultType.PASS else: result_type = base_test_result.ResultType.FAIL # Since perf tests use device affinity, give the device a chance to # recover if it is offline after a failure. Otherwise, the master sharder # will remove it from the pool and future tests on this device will fail. try: self.device.WaitUntilFullyBooted(timeout=120) except device_errors.CommandTimeoutError as e: logging.error('Device failed to return after %s: %s', test_name, e) actual_exit_code = exit_code if test_name in self._flaky_tests: # The exit_code is used at the second stage when printing the # test output. If the test is flaky, force to "0" to get that step green # whilst still gathering data to the perf dashboards. # The result_type is used by the test_dispatcher to retry the test. exit_code = 0 persisted_result = { 'name': test_name, 'output': [output], 'chartjson': json_output, 'archive_bytes': archive_bytes, 'exit_code': exit_code, 'actual_exit_code': actual_exit_code, 'result_type': result_type, 'start_time': start_time, 'end_time': end_time, 'total_time': end_time - start_time, 'device': self.device_serial, 'cmd': cmd, } self._SaveResult(persisted_result) return (output, result_type)
with _FileLock(Forwarder._LOCK_PATH): instance = Forwarder._GetInstanceLocked(tool) instance._InitDeviceLocked(presentation.device, tool) device_serial = str(presentation.device) map_arg_lists = [ ['--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=' + device_serial, '--map', str(device_port), str(host_port)] for device_port, host_port in port_pairs] logger.info('Forwarding using commands: %s', map_arg_lists) for map_arg_list in map_arg_lists: try: map_cmd = [instance._host_forwarder_path] + map_arg_list (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( map_cmd, Forwarder._TIMEOUT) except cmd_helper.TimeoutError as e: raise HostForwarderError( '`%s` timed out:\n%s' % (' '.join(map_cmd), e.output)) except OSError as e: if e.errno == 2: raise HostForwarderError( 'Unable to start host forwarder. ' 'Make sure you have built host_forwarder.') else: raise if exit_code != 0: try: instance._KillDeviceLocked(presentation.device, tool) except device_errors.CommandFailedError: # We don't want the failure to kill the presentation.device forwarder to # supersede the original failure to map.
def Map(port_pairs, device, tool=None): """Runs the forwarder. Args: port_pairs: A list of tuples (device_port, host_port) to forward. Note that you can specify 0 as a device_port, in which case a port will by dynamically assigned on the device. You can get the number of the assigned port using the DevicePortForHostPort method. device: A DeviceUtils instance. tool: Tool class to use to get wrapper, if necessary, for executing the forwarder (see valgrind_tools.py). Raises: Exception on failure to forward the port. """ if not tool: tool = base_tool.BaseTool() with _FileLock(Forwarder._LOCK_PATH): instance = Forwarder._GetInstanceLocked(tool) instance._InitDeviceLocked(device, tool) device_serial = str(device) map_arg_lists = [[ '--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(), '--serial-id=' + device_serial, '--map', str(device_port), str(host_port) ] for device_port, host_port in port_pairs] logger.info('Forwarding using commands: %s', map_arg_lists) for map_arg_list in map_arg_lists: try: map_cmd = [instance._host_forwarder_path] + map_arg_list (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout( map_cmd, Forwarder._TIMEOUT) except cmd_helper.TimeoutError as e: raise HostForwarderError('`%s` timed out:\n%s' % (' '.join(map_cmd), e.output)) except OSError as e: if e.errno == 2: raise HostForwarderError( 'Unable to start host forwarder. ' 'Make sure you have built host_forwarder.') else: raise if exit_code != 0: try: instance._KillDeviceLocked(device, tool) except (device_errors.CommandFailedError, device_errors.DeviceUnreachableError): # We don't want the failure to kill the device forwarder to # supersede the original failure to map. logger.warning( 'Failed to kill the device forwarder after map failure: %s', str(e)) _LogMapFailureDiagnostics(device) formatted_output = ('\n'.join(output) if isinstance( output, list) else output) raise HostForwarderError( '`%s` exited with %d:\n%s' % (' '.join(map_cmd), exit_code, formatted_output)) tokens = output.split(':') if len(tokens) != 2: raise HostForwarderError( 'Unexpected host forwarder output "%s", ' 'expected "device_port:host_port"' % output) device_port = int(tokens[0]) host_port = int(tokens[1]) serial_with_port = (device_serial, device_port) instance._device_to_host_port_map[serial_with_port] = host_port instance._host_to_device_port_map[host_port] = serial_with_port logger.info('Forwarding device port: %d to host port: %d.', device_port, host_port)