| OLD | NEW |
| 1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import atexit | 5 import atexit |
| 6 import hashlib | 6 import hashlib |
| 7 import logging | 7 import logging |
| 8 import os | 8 import os |
| 9 import os.path | 9 import os.path |
| 10 import random | 10 import random |
| (...skipping 12 matching lines...) Expand all Loading... |
| 23 | 23 |
| 24 # Tags used by mojo shell Java logging. | 24 # Tags used by mojo shell Java logging. |
| 25 _LOGCAT_JAVA_TAGS = [ | 25 _LOGCAT_JAVA_TAGS = [ |
| 26 'AndroidHandler', | 26 'AndroidHandler', |
| 27 'MojoFileHelper', | 27 'MojoFileHelper', |
| 28 'MojoMain', | 28 'MojoMain', |
| 29 'MojoShellActivity', | 29 'MojoShellActivity', |
| 30 'MojoShellApplication', | 30 'MojoShellApplication', |
| 31 ] | 31 ] |
| 32 | 32 |
| 33 # Tags used by native logging reflected in the logcat. | |
| 34 _LOGCAT_NATIVE_TAGS = [ | |
| 35 'chromium', | |
| 36 'sky', | |
| 37 ] | |
| 38 | |
| 39 _MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell' | 33 _MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell' |
| 40 | 34 |
| 41 # Used to parse the output of `adb devices`. | 35 # Used to parse the output of `adb devices`. |
| 42 _ADB_DEVICES_HEADER = 'List of devices attached' | 36 _ADB_DEVICES_HEADER = 'List of devices attached' |
| 43 | 37 |
| 44 | 38 |
| 45 _logger = logging.getLogger() | 39 _logger = logging.getLogger() |
| 46 | 40 |
| 47 | 41 |
| 48 def _exit_if_needed(process): | 42 def _exit_if_needed(process): |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 93 adb_path: Path to adb, optional if adb is in PATH. | 87 adb_path: Path to adb, optional if adb is in PATH. |
| 94 target_device: Device to run on, if multiple devices are connected. | 88 target_device: Device to run on, if multiple devices are connected. |
| 95 logcat_tags: Comma-separated list of additional logcat tags to use. | 89 logcat_tags: Comma-separated list of additional logcat tags to use. |
| 96 """ | 90 """ |
| 97 | 91 |
| 98 def __init__(self, adb_path="adb", target_device=None, logcat_tags=None, | 92 def __init__(self, adb_path="adb", target_device=None, logcat_tags=None, |
| 99 verbose=False): | 93 verbose=False): |
| 100 self.adb_path = adb_path | 94 self.adb_path = adb_path |
| 101 self.target_device = target_device | 95 self.target_device = target_device |
| 102 self.stop_shell_registered = False | 96 self.stop_shell_registered = False |
| 103 self.adb_running_as_root = None | |
| 104 self.additional_logcat_tags = logcat_tags | 97 self.additional_logcat_tags = logcat_tags |
| 105 self.verbose_stdout = sys.stdout if verbose else open(os.devnull, 'w') | 98 self.verbose_stdout = sys.stdout if verbose else open(os.devnull, 'w') |
| 106 self.verbose_stderr = sys.stderr if verbose else self.verbose_stdout | 99 self.verbose_stderr = sys.stderr if verbose else self.verbose_stdout |
| 107 | 100 |
| 108 def _adb_command(self, args): | 101 def _adb_command(self, args): |
| 109 """Forms an adb command from the given arguments, prepending the adb path | 102 """Forms an adb command from the given arguments, prepending the adb path |
| 110 and adding a target device specifier, if needed. | 103 and adding a target device specifier, if needed. |
| 111 """ | 104 """ |
| 112 adb_command = [self.adb_path] | 105 adb_command = [self.adb_path] |
| 113 if self.target_device: | 106 if self.target_device: |
| 114 adb_command.extend(['-s', self.target_device]) | 107 adb_command.extend(['-s', self.target_device]) |
| 115 adb_command.extend(args) | 108 adb_command.extend(args) |
| 116 return adb_command | 109 return adb_command |
| 117 | 110 |
| 118 def _read_fifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): | 111 def _read_fifo(self, fifo_path, pipe, on_fifo_closed, max_attempts=5): |
| 119 """Reads |fifo_path| on the device and write the contents to |pipe|. | 112 """Reads |fifo_path| on the device and write the contents to |pipe|. |
| 120 | 113 |
| 121 Calls |on_fifo_closed| when the fifo is closed. This method will try to find | 114 Calls |on_fifo_closed| when the fifo is closed. This method will try to find |
| 122 the path up to |max_attempts|, waiting 1 second between each attempt. If it | 115 the path up to |max_attempts|, waiting 1 second between each attempt. If it |
| 123 cannot find |fifo_path|, a exception will be raised. | 116 cannot find |fifo_path|, a exception will be raised. |
| 124 """ | 117 """ |
| 125 fifo_command = self._adb_command( | 118 fifo_command = self._adb_command( |
| 126 ['shell', 'test -e "%s"; echo $?' % fifo_path]) | 119 ['shell', 'run-as', _MOJO_SHELL_PACKAGE_NAME, 'ls', fifo_path]) |
| 127 | 120 |
| 128 def _run(): | 121 def _run(): |
| 129 def _wait_for_fifo(): | 122 def _wait_for_fifo(): |
| 130 for _ in xrange(max_attempts): | 123 for _ in xrange(max_attempts): |
| 131 if subprocess.check_output(fifo_command)[0] == '0': | 124 output = subprocess.check_output(fifo_command).strip() |
| 125 if output == fifo_path: |
| 132 return | 126 return |
| 133 time.sleep(1) | 127 time.sleep(1) |
| 134 if on_fifo_closed: | 128 if on_fifo_closed: |
| 135 on_fifo_closed() | 129 on_fifo_closed() |
| 136 raise Exception("Unable to find fifo.") | 130 raise Exception("Unable to find fifo.") |
| 137 _wait_for_fifo() | 131 _wait_for_fifo() |
| 138 stdout_cat = subprocess.Popen( | 132 stdout_cat = subprocess.Popen( |
| 139 self._adb_command(['shell', 'cat', fifo_path]), stdout=pipe) | 133 self._adb_command(['shell', 'run-as', _MOJO_SHELL_PACKAGE_NAME, |
| 134 'cat', fifo_path]), stdout=pipe) |
| 140 atexit.register(_exit_if_needed, stdout_cat) | 135 atexit.register(_exit_if_needed, stdout_cat) |
| 141 stdout_cat.wait() | 136 stdout_cat.wait() |
| 142 if on_fifo_closed: | 137 if on_fifo_closed: |
| 143 on_fifo_closed() | 138 on_fifo_closed() |
| 144 | 139 |
| 145 thread = threading.Thread(target=_run, name="StdoutRedirector") | 140 thread = threading.Thread(target=_run, name="StdoutRedirector") |
| 146 thread.start() | 141 thread.start() |
| 147 | 142 |
| 148 def _find_available_device_port(self): | 143 def _find_available_device_port(self): |
| 149 netstat_output = subprocess.check_output( | 144 netstat_output = subprocess.check_output( |
| 150 self._adb_command(['shell', 'netstat'])) | 145 self._adb_command(['shell', 'netstat'])) |
| 151 return _find_available_port(netstat_output) | 146 return _find_available_port(netstat_output) |
| 152 | 147 |
| 153 def _forward_device_port_to_host(self, device_port, host_port): | 148 def _forward_device_port_to_host(self, device_port, host_port): |
| 154 """Maps the device port to the host port. If |device_port| is 0, a random | 149 """Maps the device port to the host port. If |device_port| is 0, a random |
| 155 available port is chosen. | 150 available port is chosen. |
| 156 | 151 |
| 157 Returns: | 152 Returns: |
| 158 The device port. | 153 The device port. |
| 159 """ | 154 """ |
| 160 assert host_port | 155 assert host_port |
| 161 # Root is not required for `adb forward` (hence we don't check the return | |
| 162 # value), but if we can run adb as root, we have to do it now, because | |
| 163 # restarting adbd as root clears any port mappings. See | |
| 164 # https://github.com/domokit/devtools/issues/20. | |
| 165 self._run_adb_as_root() | |
| 166 | 156 |
| 167 if device_port == 0: | 157 if device_port == 0: |
| 168 # TODO(ppi): Should we have a retry loop to handle the unlikely races? | 158 # TODO(ppi): Should we have a retry loop to handle the unlikely races? |
| 169 device_port = self._find_available_device_port() | 159 device_port = self._find_available_device_port() |
| 170 subprocess.check_call(self._adb_command([ | 160 subprocess.check_call(self._adb_command([ |
| 171 "reverse", "tcp:%d" % device_port, "tcp:%d" % host_port])) | 161 "reverse", "tcp:%d" % device_port, "tcp:%d" % host_port])) |
| 172 | 162 |
| 173 def _unmap_port(): | 163 def _unmap_port(): |
| 174 unmap_command = self._adb_command([ | 164 unmap_command = self._adb_command([ |
| 175 "reverse", "--remove", "tcp:%d" % device_port]) | 165 "reverse", "--remove", "tcp:%d" % device_port]) |
| 176 subprocess.Popen(unmap_command) | 166 subprocess.Popen(unmap_command) |
| 177 atexit.register(_unmap_port) | 167 atexit.register(_unmap_port) |
| 178 return device_port | 168 return device_port |
| 179 | 169 |
| 180 def _forward_host_port_to_device(self, host_port, device_port): | 170 def _forward_host_port_to_device(self, host_port, device_port): |
| 181 """Maps the host port to the device port. If |host_port| is 0, a random | 171 """Maps the host port to the device port. If |host_port| is 0, a random |
| 182 available port is chosen. | 172 available port is chosen. |
| 183 | 173 |
| 184 Returns: | 174 Returns: |
| 185 The host port. | 175 The host port. |
| 186 """ | 176 """ |
| 187 assert device_port | 177 assert device_port |
| 188 self._run_adb_as_root() | |
| 189 | 178 |
| 190 if host_port == 0: | 179 if host_port == 0: |
| 191 # TODO(ppi): Should we have a retry loop to handle the unlikely races? | 180 # TODO(ppi): Should we have a retry loop to handle the unlikely races? |
| 192 host_port = _find_available_host_port() | 181 host_port = _find_available_host_port() |
| 193 subprocess.check_call(self._adb_command([ | 182 subprocess.check_call(self._adb_command([ |
| 194 "forward", 'tcp:%d' % host_port, 'tcp:%d' % device_port])) | 183 "forward", 'tcp:%d' % host_port, 'tcp:%d' % device_port])) |
| 195 | 184 |
| 196 def _unmap_port(): | 185 def _unmap_port(): |
| 197 unmap_command = self._adb_command([ | 186 unmap_command = self._adb_command([ |
| 198 "forward", "--remove", "tcp:%d" % device_port]) | 187 "forward", "--remove", "tcp:%d" % device_port]) |
| 199 subprocess.Popen(unmap_command) | 188 subprocess.Popen(unmap_command) |
| 200 atexit.register(_unmap_port) | 189 atexit.register(_unmap_port) |
| 201 return host_port | 190 return host_port |
| 202 | 191 |
| 203 def _run_adb_as_root(self): | |
| 204 if self.adb_running_as_root is not None: | |
| 205 return self.adb_running_as_root | |
| 206 | |
| 207 if ('cannot run as root' not in subprocess.check_output( | |
| 208 self._adb_command(['root']))): | |
| 209 # Wait for adbd to restart. | |
| 210 subprocess.check_call( | |
| 211 self._adb_command(['wait-for-device']), | |
| 212 stdout=self.verbose_stdout, stderr=self.verbose_stderr) | |
| 213 self.adb_running_as_root = True | |
| 214 else: | |
| 215 self.adb_running_as_root = False | |
| 216 | |
| 217 return self.adb_running_as_root | |
| 218 | |
| 219 def _is_shell_package_installed(self): | 192 def _is_shell_package_installed(self): |
| 220 # Adb should print one line if the package is installed and return empty | 193 # Adb should print one line if the package is installed and return empty |
| 221 # string otherwise. | 194 # string otherwise. |
| 222 return len(subprocess.check_output(self._adb_command([ | 195 return len(subprocess.check_output(self._adb_command([ |
| 223 'shell', 'pm', 'list', 'packages', _MOJO_SHELL_PACKAGE_NAME]))) > 0 | 196 'shell', 'pm', 'list', 'packages', _MOJO_SHELL_PACKAGE_NAME]))) > 0 |
| 224 | 197 |
| 225 @staticmethod | 198 @staticmethod |
| 226 def get_tmp_dir_path(): | 199 def get_tmp_dir_path(): |
| 227 """Returns a path to a cache directory owned by the shell where temporary | 200 """Returns a path to a cache directory owned by the shell where temporary |
| 228 files can be stored. | 201 files can be stored. |
| 229 """ | 202 """ |
| 230 return '/data/data/%s/cache/tmp/' % _MOJO_SHELL_PACKAGE_NAME | 203 return '/data/data/%s/cache/tmp/' % _MOJO_SHELL_PACKAGE_NAME |
| 231 | 204 |
| 232 def pull_file(self, device_path, destination_path, remove_original=False): | 205 def pull_file(self, device_path, destination_path, remove_original=False): |
| 233 """Copies or moves the specified file on the device to the host.""" | 206 """Copies or moves the specified file on the device to the host.""" |
| 234 subprocess.check_call(self._adb_command([ | 207 subprocess.check_call(self._adb_command([ |
| 235 'pull', device_path, destination_path])) | 208 'pull', device_path, destination_path])) |
| 236 if remove_original: | 209 if remove_original: |
| 237 subprocess.check_call(self._adb_command([ | 210 subprocess.check_call(self._adb_command([ |
| 238 'shell', 'rm', device_path])) | 211 'shell', 'rm', device_path])) |
| 239 | 212 |
| 240 def check_device(self, require_root=False): | 213 def check_device(self): |
| 241 """Verifies if the device configuration allows adb to run. | 214 """Verifies if the device configuration allows adb to run. |
| 242 | 215 |
| 243 If a target device was indicated in the constructor, it checks that the | 216 If a target device was indicated in the constructor, it checks that the |
| 244 device is available. Otherwise, it checks that there is exactly one | 217 device is available. Otherwise, it checks that there is exactly one |
| 245 available device. | 218 available device. |
| 246 | 219 |
| 247 Returns: | 220 Returns: |
| 248 A tuple of (result, msg). |result| is True iff if the device is correctly | 221 A tuple of (result, msg). |result| is True iff if the device is correctly |
| 249 configured and False otherwise. |msg| is the reason for failure if | 222 configured and False otherwise. |msg| is the reason for failure if |
| 250 |result| is False and None otherwise. | 223 |result| is False and None otherwise. |
| (...skipping 13 matching lines...) Expand all Loading... |
| 264 return False, ('Cannot connect to the selected device, status: ' + | 237 return False, ('Cannot connect to the selected device, status: ' + |
| 265 devices[self.target_device]) | 238 devices[self.target_device]) |
| 266 | 239 |
| 267 if len(devices) > 1: | 240 if len(devices) > 1: |
| 268 return False, ('More than one device connected and target device not ' | 241 return False, ('More than one device connected and target device not ' |
| 269 'specified.') | 242 'specified.') |
| 270 | 243 |
| 271 if not devices.itervalues().next() == 'device': | 244 if not devices.itervalues().next() == 'device': |
| 272 return False, 'Connected device is not available.' | 245 return False, 'Connected device is not available.' |
| 273 | 246 |
| 274 if require_root and not self._run_adb_as_root(): | |
| 275 return False, 'Cannot run on an unrooted device.' | |
| 276 | |
| 277 return True, None | 247 return True, None |
| 278 | 248 |
| 279 def install_apk(self, shell_apk_path): | 249 def install_apk(self, shell_apk_path): |
| 280 """Installs the apk on the device. | 250 """Installs the apk on the device. |
| 281 | 251 |
| 282 This method computes checksum of the APK and skips the installation if the | 252 This method computes checksum of the APK and skips the installation if the |
| 283 fingerprint matches the one saved on the device upon the previous | 253 fingerprint matches the one saved on the device upon the previous |
| 284 installation. | 254 installation. |
| 285 | 255 |
| 286 Args: | 256 Args: |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 330 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % _MOJO_SHELL_PACKAGE_NAME | 300 STDOUT_PIPE = "/data/data/%s/stdout.fifo" % _MOJO_SHELL_PACKAGE_NAME |
| 331 | 301 |
| 332 cmd = self._adb_command(['shell', 'am', 'start', | 302 cmd = self._adb_command(['shell', 'am', 'start', |
| 333 '-S', | 303 '-S', |
| 334 '-a', 'android.intent.action.VIEW', | 304 '-a', 'android.intent.action.VIEW', |
| 335 '-n', '%s/.MojoShellActivity' % | 305 '-n', '%s/.MojoShellActivity' % |
| 336 _MOJO_SHELL_PACKAGE_NAME]) | 306 _MOJO_SHELL_PACKAGE_NAME]) |
| 337 | 307 |
| 338 parameters = [] | 308 parameters = [] |
| 339 if stdout or on_application_stop: | 309 if stdout or on_application_stop: |
| 340 # We need to run as root to access the fifo file we use for stdout | 310 # Remove any leftover fifo file after the previous run. |
| 341 # redirection. | 311 subprocess.check_call(self._adb_command( |
| 342 if self._run_adb_as_root(): | 312 ['shell', 'run-as', _MOJO_SHELL_PACKAGE_NAME, |
| 343 # Remove any leftover fifo file after the previous run. | 313 'rm', '-f', STDOUT_PIPE])) |
| 344 subprocess.check_call(self._adb_command( | |
| 345 ['shell', 'rm', '-f', STDOUT_PIPE])) | |
| 346 | 314 |
| 347 parameters.append('--fifo-path=%s' % STDOUT_PIPE) | 315 parameters.append('--fifo-path=%s' % STDOUT_PIPE) |
| 348 self._read_fifo(STDOUT_PIPE, stdout, on_application_stop) | 316 self._read_fifo(STDOUT_PIPE, stdout, on_application_stop) |
| 349 else: | |
| 350 _logger.warning("Running without root access, full stdout of the " | |
| 351 "shell won't be available.") | |
| 352 parameters.extend(arguments) | 317 parameters.extend(arguments) |
| 353 | 318 |
| 354 if parameters: | 319 if parameters: |
| 355 device_filename = ( | 320 device_filename = ( |
| 356 '/sdcard/%s/args_%s' % (_MOJO_SHELL_PACKAGE_NAME, str(uuid.uuid4()))) | 321 '/sdcard/%s/args_%s' % (_MOJO_SHELL_PACKAGE_NAME, str(uuid.uuid4()))) |
| 357 with tempfile.NamedTemporaryFile(delete=False) as temp: | 322 with tempfile.NamedTemporaryFile(delete=False) as temp: |
| 358 try: | 323 try: |
| 359 for parameter in parameters: | 324 for parameter in parameters: |
| 360 temp.write(parameter) | 325 temp.write(parameter) |
| 361 temp.write('\n') | 326 temp.write('\n') |
| (...skipping 13 matching lines...) Expand all Loading... |
| 375 """Stops the mojo shell.""" | 340 """Stops the mojo shell.""" |
| 376 subprocess.check_call(self._adb_command(['shell', | 341 subprocess.check_call(self._adb_command(['shell', |
| 377 'am', | 342 'am', |
| 378 'force-stop', | 343 'force-stop', |
| 379 _MOJO_SHELL_PACKAGE_NAME])) | 344 _MOJO_SHELL_PACKAGE_NAME])) |
| 380 | 345 |
| 381 def clean_logs(self): | 346 def clean_logs(self): |
| 382 """Cleans the logs on the device.""" | 347 """Cleans the logs on the device.""" |
| 383 subprocess.check_call(self._adb_command(['logcat', '-c'])) | 348 subprocess.check_call(self._adb_command(['logcat', '-c'])) |
| 384 | 349 |
| 385 def show_logs(self, include_native_logs=True): | 350 def show_logs(self): |
| 386 """Displays the log for the mojo shell. | 351 """Displays the log for the mojo shell. |
| 387 | 352 |
| 388 Returns: | 353 Returns: |
| 389 The process responsible for reading the logs. | 354 The process responsible for reading the logs. |
| 390 """ | 355 """ |
| 391 tags = _LOGCAT_JAVA_TAGS | 356 tags = _LOGCAT_JAVA_TAGS |
| 392 if include_native_logs: | |
| 393 tags.extend(_LOGCAT_NATIVE_TAGS) | |
| 394 if self.additional_logcat_tags is not None: | 357 if self.additional_logcat_tags is not None: |
| 395 tags.extend(self.additional_logcat_tags.split(",")) | 358 tags.extend(self.additional_logcat_tags.split(",")) |
| 396 logcat = subprocess.Popen( | 359 logcat = subprocess.Popen( |
| 397 self._adb_command(['logcat', '-s', ' '.join(tags)]), | 360 self._adb_command(['logcat', '-s', ' '.join(tags)]), |
| 398 stdout=sys.stdout) | 361 stdout=sys.stdout) |
| 399 atexit.register(_exit_if_needed, logcat) | 362 atexit.register(_exit_if_needed, logcat) |
| 400 return logcat | 363 return logcat |
| 401 | 364 |
| 402 def forward_observatory_ports(self): | 365 def forward_observatory_ports(self): |
| 403 """Forwards the ports used by the dart observatories to the host machine. | 366 """Forwards the ports used by the dart observatories to the host machine. |
| (...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 438 | 401 |
| 439 @overrides(Shell) | 402 @overrides(Shell) |
| 440 def forward_host_port_to_shell(self, host_port): | 403 def forward_host_port_to_shell(self, host_port): |
| 441 self._forward_host_port_to_device(host_port, host_port) | 404 self._forward_host_port_to_device(host_port, host_port) |
| 442 | 405 |
| 443 @overrides(Shell) | 406 @overrides(Shell) |
| 444 def run(self, arguments): | 407 def run(self, arguments): |
| 445 self.clean_logs() | 408 self.clean_logs() |
| 446 self.forward_observatory_ports() | 409 self.forward_observatory_ports() |
| 447 | 410 |
| 448 # If we are running as root, don't carry over the native logs from logcat - | 411 p = self.show_logs(); |
| 449 # we will have these in the stdout. | |
| 450 p = self.show_logs(include_native_logs=(not self._run_adb_as_root())) | |
| 451 self.start_shell(arguments, sys.stdout, p.terminate) | 412 self.start_shell(arguments, sys.stdout, p.terminate) |
| 452 p.wait() | 413 p.wait() |
| 453 return None | 414 return None |
| 454 | 415 |
| 455 @overrides(Shell) | 416 @overrides(Shell) |
| 456 def run_and_get_output(self, arguments, timeout=None): | 417 def run_and_get_output(self, arguments, timeout=None): |
| 457 class Results: | 418 class Results: |
| 458 """Workaround for Python scoping rules that prevent assigning to variables | 419 """Workaround for Python scoping rules that prevent assigning to variables |
| 459 from the outer scope. | 420 from the outer scope. |
| 460 """ | 421 """ |
| 461 output = None | 422 output = None |
| 462 | 423 |
| 463 def do_run(): | 424 def do_run(): |
| 464 (r, w) = os.pipe() | 425 (r, w) = os.pipe() |
| 465 with os.fdopen(r, "r") as rf: | 426 with os.fdopen(r, "r") as rf: |
| 466 with os.fdopen(w, "w") as wf: | 427 with os.fdopen(w, "w") as wf: |
| 467 self.start_shell(arguments, wf, wf.close) | 428 self.start_shell(arguments, wf, wf.close) |
| 468 Results.output = rf.read() | 429 Results.output = rf.read() |
| 469 | 430 |
| 470 run_thread = threading.Thread(target=do_run) | 431 run_thread = threading.Thread(target=do_run) |
| 471 run_thread.start() | 432 run_thread.start() |
| 472 run_thread.join(timeout) | 433 run_thread.join(timeout) |
| 473 | 434 |
| 474 if run_thread.is_alive(): | 435 if run_thread.is_alive(): |
| 475 self.stop_shell() | 436 self.stop_shell() |
| 476 return None, Results.output, True | 437 return None, Results.output, True |
| 477 return None, Results.output, False | 438 return None, Results.output, False |
| OLD | NEW |