1import os 2import re 3import time 4import logging 5import posixpath 6import subprocess 7import tarfile 8import tempfile 9import threading 10from collections import namedtuple 11 12from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY 13from devlib.module import get_module 14from devlib.platform import Platform 15from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError 16from devlib.utils.ssh import SshConnection 17from devlib.utils.android import AdbConnection, AndroidProperties, adb_command, adb_disconnect 18from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists 19from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes 20from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string 21 22 23FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)') 24ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)', 25 re.IGNORECASE) 26ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)' 27 r'\s+(?P<width>\d+)x(?P<height>\d+)') 28DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ', 29 re.MULTILINE) 30KVERSION_REGEX =re.compile( 31 r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(.*-g(?P<sha1>[0-9a-fA-F]{7,}))?' 32) 33 34 35class Target(object): 36 37 path = None 38 os = None 39 40 default_modules = [ 41 'hotplug', 42 'cpufreq', 43 'cpuidle', 44 'cgroups', 45 'hwmon', 46 ] 47 48 @property 49 def core_names(self): 50 return self.platform.core_names 51 52 @property 53 def core_clusters(self): 54 return self.platform.core_clusters 55 56 @property 57 def big_core(self): 58 return self.platform.big_core 59 60 @property 61 def little_core(self): 62 return self.platform.little_core 63 64 @property 65 def is_connected(self): 66 return self.conn is not None 67 68 @property 69 def connected_as_root(self): 70 if self._connected_as_root is None: 71 result = self.execute('id') 72 self._connected_as_root = 'uid=0(' in result 73 return self._connected_as_root 74 75 @property 76 @memoized 77 def is_rooted(self): 78 if self.connected_as_root: 79 return True 80 try: 81 self.execute('ls /', timeout=2, as_root=True) 82 return True 83 except (TargetError, TimeoutError): 84 return False 85 86 @property 87 @memoized 88 def needs_su(self): 89 return not self.connected_as_root and self.is_rooted 90 91 @property 92 @memoized 93 def kernel_version(self): 94 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip()) 95 96 @property 97 def os_version(self): # pylint: disable=no-self-use 98 return {} 99 100 @property 101 def abi(self): # pylint: disable=no-self-use 102 return None 103 104 @property 105 @memoized 106 def cpuinfo(self): 107 return Cpuinfo(self.execute('cat /proc/cpuinfo')) 108 109 @property 110 @memoized 111 def number_of_cpus(self): 112 num_cpus = 0 113 corere = re.compile(r'^\s*cpu\d+\s*$') 114 output = self.execute('ls /sys/devices/system/cpu') 115 for entry in output.split(): 116 if corere.match(entry): 117 num_cpus += 1 118 return num_cpus 119 120 @property 121 @memoized 122 def config(self): 123 try: 124 return KernelConfig(self.execute('zcat /proc/config.gz')) 125 except TargetError: 126 for path in ['/boot/config', '/boot/config-$(uname -r)']: 127 try: 128 return KernelConfig(self.execute('cat {}'.format(path))) 129 except TargetError: 130 pass 131 return KernelConfig('') 132 133 @property 134 @memoized 135 def user(self): 136 return self.getenv('USER') 137 138 @property 139 def conn(self): 140 if self._connections: 141 tid = id(threading.current_thread()) 142 if tid not in self._connections: 143 self._connections[tid] = self.get_connection() 144 return self._connections[tid] 145 else: 146 return None 147 148 def __init__(self, 149 connection_settings=None, 150 platform=None, 151 working_directory=None, 152 executables_directory=None, 153 connect=True, 154 modules=None, 155 load_default_modules=True, 156 shell_prompt=DEFAULT_SHELL_PROMPT, 157 conn_cls=None, 158 ): 159 self._connected_as_root = None 160 self.connection_settings = connection_settings or {} 161 # Set self.platform: either it's given directly (by platform argument) 162 # or it's given in the connection_settings argument 163 # If neither, create default Platform() 164 if platform is None: 165 self.platform = self.connection_settings.get('platform', Platform()) 166 else: 167 self.platform = platform 168 # Check if the user hasn't given two different platforms 169 if 'platform' in self.connection_settings: 170 if connection_settings['platform'] is not platform: 171 raise TargetError('Platform specified in connection_settings ' 172 '({}) differs from that directly passed ' 173 '({})!)' 174 .format(connection_settings['platform'], 175 self.platform)) 176 self.connection_settings['platform'] = self.platform 177 self.working_directory = working_directory 178 self.executables_directory = executables_directory 179 self.modules = modules or [] 180 self.load_default_modules = load_default_modules 181 self.shell_prompt = shell_prompt 182 self.conn_cls = conn_cls 183 self.logger = logging.getLogger(self.__class__.__name__) 184 self._installed_binaries = {} 185 self._installed_modules = {} 186 self._cache = {} 187 self._connections = {} 188 self.busybox = None 189 190 if load_default_modules: 191 module_lists = [self.default_modules] 192 else: 193 module_lists = [] 194 module_lists += [self.modules, self.platform.modules] 195 self.modules = merge_lists(*module_lists, duplicates='first') 196 self._update_modules('early') 197 if connect: 198 self.connect() 199 200 # connection and initialization 201 202 def connect(self, timeout=None): 203 self.platform.init_target_connection(self) 204 tid = id(threading.current_thread()) 205 self._connections[tid] = self.get_connection(timeout=timeout) 206 self._resolve_paths() 207 self.busybox = self.get_installed('busybox') 208 self.platform.update_from_target(self) 209 self._update_modules('connected') 210 if self.platform.big_core and self.load_default_modules: 211 self._install_module(get_module('bl')) 212 213 def disconnect(self): 214 for conn in self._connections.itervalues(): 215 conn.close() 216 self._connections = {} 217 218 def get_connection(self, timeout=None): 219 if self.conn_cls == None: 220 raise ValueError('Connection class not specified on Target creation.') 221 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable 222 223 def setup(self, executables=None): 224 self.execute('mkdir -p {}'.format(self.working_directory)) 225 self.execute('mkdir -p {}'.format(self.executables_directory)) 226 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox')) 227 228 # Setup shutils script for the target 229 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in') 230 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils') 231 shell_path = '/bin/sh' 232 if self.os == 'android': 233 shell_path = '/system/bin/sh' 234 with open(shutils_ifile) as fh: 235 lines = fh.readlines() 236 with open(shutils_ofile, 'w') as ofile: 237 for line in lines: 238 line = line.replace("__DEVLIB_SHELL__", shell_path) 239 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox) 240 ofile.write(line) 241 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')) 242 243 for host_exe in (executables or []): # pylint: disable=superfluous-parens 244 self.install(host_exe) 245 246 # Check for platform dependent setup procedures 247 self.platform.setup(self) 248 249 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks) 250 self._update_modules('setup') 251 252 def reboot(self, hard=False, connect=True, timeout=180): 253 if hard: 254 if not self.has('hard_reset'): 255 raise TargetError('Hard reset not supported for this target.') 256 self.hard_reset() # pylint: disable=no-member 257 else: 258 if not self.is_connected: 259 message = 'Cannot reboot target becuase it is disconnected. ' +\ 260 'Either connect() first, or specify hard=True ' +\ 261 '(in which case, a hard_reset module must be installed)' 262 raise TargetError(message) 263 self.reset() 264 # Wait a fixed delay before starting polling to give the target time to 265 # shut down, otherwise, might create the connection while it's still shutting 266 # down resulting in subsequenct connection failing. 267 self.logger.debug('Waiting for target to power down...') 268 reset_delay = 20 269 time.sleep(reset_delay) 270 timeout = max(timeout - reset_delay, 10) 271 if self.has('boot'): 272 self.boot() # pylint: disable=no-member 273 self._connected_as_root = None 274 if connect: 275 self.connect(timeout=timeout) 276 277 # file transfer 278 279 def push(self, source, dest, timeout=None): 280 return self.conn.push(source, dest, timeout=timeout) 281 282 def pull(self, source, dest, timeout=None): 283 return self.conn.pull(source, dest, timeout=timeout) 284 285 def get_directory(self, source_dir, dest): 286 """ Pull a directory from the device, after compressing dir """ 287 # Create all file names 288 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.') 289 # Host location of dir 290 outdir = os.path.join(dest, tar_file_name) 291 # Host location of archive 292 tar_file_name = '{}.tar'.format(tar_file_name) 293 tempfile = os.path.join(dest, tar_file_name) 294 295 # Does the folder exist? 296 self.execute('ls -la {}'.format(source_dir)) 297 # Try compressing the folder 298 try: 299 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name, 300 source_dir)) 301 except TargetError: 302 self.logger.debug('Failed to run tar command on target! ' \ 303 'Not pulling directory {}'.format(source_dir)) 304 # Pull the file 305 os.mkdir(outdir) 306 self.pull(tar_file_name, tempfile ) 307 # Decompress 308 f = tarfile.open(tempfile, 'r') 309 f.extractall(outdir) 310 os.remove(tempfile) 311 312 # execution 313 314 def execute(self, command, timeout=None, check_exit_code=True, as_root=False): 315 return self.conn.execute(command, timeout, check_exit_code, as_root) 316 317 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False): 318 return self.conn.background(command, stdout, stderr, as_root) 319 320 def invoke(self, binary, args=None, in_directory=None, on_cpus=None, 321 as_root=False, timeout=30): 322 """ 323 Executes the specified binary under the specified conditions. 324 325 :binary: binary to execute. Must be present and executable on the device. 326 :args: arguments to be passed to the binary. The can be either a list or 327 a string. 328 :in_directory: execute the binary in the specified directory. This must 329 be an absolute path. 330 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which 331 case, it will be interpreted as the mask), a list of ``ints``, in which 332 case this will be interpreted as the list of cpus, or string, which 333 will be interpreted as a comma-separated list of cpu ranges, e.g. 334 ``"0,4-7"``. 335 :as_root: Specify whether the command should be run as root 336 :timeout: If the invocation does not terminate within this number of seconds, 337 a ``TimeoutError`` exception will be raised. Set to ``None`` if the 338 invocation should not timeout. 339 340 :returns: output of command. 341 """ 342 command = binary 343 if args: 344 if isiterable(args): 345 args = ' '.join(args) 346 command = '{} {}'.format(command, args) 347 if on_cpus: 348 on_cpus = bitmask(on_cpus) 349 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command) 350 if in_directory: 351 command = 'cd {} && {}'.format(in_directory, command) 352 return self.execute(command, as_root=as_root, timeout=timeout) 353 354 def kick_off(self, command, as_root=False): 355 raise NotImplementedError() 356 357 # sysfs interaction 358 359 def read_value(self, path, kind=None): 360 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103 361 if kind: 362 return kind(output) 363 else: 364 return output 365 366 def read_int(self, path): 367 return self.read_value(path, kind=integer) 368 369 def read_bool(self, path): 370 return self.read_value(path, kind=boolean) 371 372 def write_value(self, path, value, verify=True): 373 value = str(value) 374 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True) 375 if verify: 376 output = self.read_value(path) 377 if not output == value: 378 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output) 379 raise TargetError(message) 380 381 def reset(self): 382 try: 383 self.execute('reboot', as_root=self.needs_su, timeout=2) 384 except (TargetError, TimeoutError, subprocess.CalledProcessError): 385 # on some targets "reboot" doesn't return gracefully 386 pass 387 self._connected_as_root = None 388 389 def check_responsive(self): 390 try: 391 self.conn.execute('ls /', timeout=5) 392 except (TimeoutError, subprocess.CalledProcessError): 393 raise TargetNotRespondingError(self.conn.name) 394 395 # process management 396 397 def kill(self, pid, signal=None, as_root=False): 398 signal_string = '-s {}'.format(signal) if signal else '' 399 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root) 400 401 def killall(self, process_name, signal=None, as_root=False): 402 for pid in self.get_pids_of(process_name): 403 try: 404 self.kill(pid, signal=signal, as_root=as_root) 405 except TargetError: 406 pass 407 408 def get_pids_of(self, process_name): 409 raise NotImplementedError() 410 411 def ps(self, **kwargs): 412 raise NotImplementedError() 413 414 # files 415 416 def file_exists(self, filepath): 417 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi' 418 output = self.execute(command.format(filepath), as_root=self.is_rooted) 419 return boolean(output.strip()) 420 421 def directory_exists(self, filepath): 422 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath)) 423 # output from ssh my contain part of the expression in the buffer, 424 # split out everything except the last word. 425 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member 426 427 def list_file_systems(self): 428 output = self.execute('mount') 429 fstab = [] 430 for line in output.split('\n'): 431 line = line.strip() 432 if not line: 433 continue 434 match = FSTAB_ENTRY_REGEX.search(line) 435 if match: 436 fstab.append(FstabEntry(match.group(1), match.group(2), 437 match.group(3), match.group(4), 438 None, None)) 439 else: # assume pre-M Android 440 fstab.append(FstabEntry(*line.split())) 441 return fstab 442 443 def list_directory(self, path, as_root=False): 444 raise NotImplementedError() 445 446 def get_workpath(self, name): 447 return self.path.join(self.working_directory, name) 448 449 def tempfile(self, prefix='', suffix=''): 450 names = tempfile._get_candidate_names() # pylint: disable=W0212 451 for _ in xrange(tempfile.TMP_MAX): 452 name = names.next() 453 path = self.get_workpath(prefix + name + suffix) 454 if not self.file_exists(path): 455 return path 456 raise IOError('No usable temporary filename found') 457 458 def remove(self, path, as_root=False): 459 self.execute('rm -rf {}'.format(path), as_root=as_root) 460 461 # misc 462 def core_cpus(self, core): 463 return [i for i, c in enumerate(self.core_names) if c == core] 464 465 def list_online_cpus(self, core=None): 466 path = self.path.join('/sys/devices/system/cpu/online') 467 output = self.read_value(path) 468 all_online = ranges_to_list(output) 469 if core: 470 cpus = self.core_cpus(core) 471 if not cpus: 472 raise ValueError(core) 473 return [o for o in all_online if o in cpus] 474 else: 475 return all_online 476 477 def list_offline_cpus(self): 478 online = self.list_online_cpus() 479 return [c for c in xrange(self.number_of_cpus) 480 if c not in online] 481 482 def getenv(self, variable): 483 return self.execute('echo ${}'.format(variable)).rstrip('\r\n') 484 485 def capture_screen(self, filepath): 486 raise NotImplementedError() 487 488 def install(self, filepath, timeout=None, with_name=None): 489 raise NotImplementedError() 490 491 def uninstall(self, name): 492 raise NotImplementedError() 493 494 def get_installed(self, name, search_system_binaries=True): 495 # Check user installed binaries first 496 if self.file_exists(self.executables_directory): 497 if name in self.list_directory(self.executables_directory): 498 return self.path.join(self.executables_directory, name) 499 # Fall back to binaries in PATH 500 if search_system_binaries: 501 for path in self.getenv('PATH').split(self.path.pathsep): 502 try: 503 if name in self.list_directory(path): 504 return self.path.join(path, name) 505 except TargetError: 506 pass # directory does not exist or no executable premssions 507 508 which = get_installed 509 510 def install_if_needed(self, host_path, search_system_binaries=True): 511 512 binary_path = self.get_installed(os.path.split(host_path)[1], 513 search_system_binaries=search_system_binaries) 514 if not binary_path: 515 binary_path = self.install(host_path) 516 return binary_path 517 518 def is_installed(self, name): 519 return bool(self.get_installed(name)) 520 521 def bin(self, name): 522 return self._installed_binaries.get(name, name) 523 524 def has(self, modname): 525 return hasattr(self, identifier(modname)) 526 527 def lsmod(self): 528 lines = self.execute('lsmod').splitlines() 529 entries = [] 530 for line in lines[1:]: # first line is the header 531 if not line.strip(): 532 continue 533 parts = line.split() 534 name = parts[0] 535 size = int(parts[1]) 536 use_count = int(parts[2]) 537 if len(parts) > 3: 538 used_by = ''.join(parts[3:]).split(',') 539 else: 540 used_by = [] 541 entries.append(LsmodEntry(name, size, use_count, used_by)) 542 return entries 543 544 def insmod(self, path): 545 target_path = self.get_workpath(os.path.basename(path)) 546 self.push(path, target_path) 547 self.execute('insmod {}'.format(target_path), as_root=True) 548 549 550 def extract(self, path, dest=None): 551 """ 552 Extact the specified on-target file. The extraction method to be used 553 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension. 554 If ``dest`` is specified, it must be an existing directory on target; 555 the extracted contents will be placed there. 556 557 Note that, depending on the archive file format (and therfore the 558 extraction method used), the original archive file may or may not exist 559 after the extraction. 560 561 The return value is the path to the extracted contents. In case of 562 gunzip and bunzip2, this will be path to the extracted file; for tar 563 and uzip, this will be the directory with the extracted file(s) 564 (``dest`` if it was specified otherwise, the directory that cotained 565 the archive). 566 567 """ 568 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2', 569 '.tgz', '.tbz', '.tbz2']: 570 if path.endswith(ending): 571 return self._extract_archive(path, 'tar xf {} -C {}', dest) 572 573 ext = self.path.splitext(path)[1] 574 if ext in ['.bz', '.bz2']: 575 return self._extract_file(path, 'bunzip2 -f {}', dest) 576 elif ext == '.gz': 577 return self._extract_file(path, 'gunzip -f {}', dest) 578 elif ext == '.zip': 579 return self._extract_archive(path, 'unzip {} -d {}', dest) 580 else: 581 raise ValueError('Unknown compression format: {}'.format(ext)) 582 583 def sleep(self, duration): 584 timeout = duration + 10 585 self.execute('sleep {}'.format(duration), timeout=timeout) 586 587 # internal methods 588 589 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False): 590 command = '{} {}'.format(self.shutils, command) 591 return self.conn.execute(command, timeout, check_exit_code, as_root) 592 593 def _extract_archive(self, path, cmd, dest=None): 594 cmd = '{} ' + cmd # busybox 595 if dest: 596 extracted = dest 597 else: 598 extracted = self.path.dirname(path) 599 cmdtext = cmd.format(self.busybox, path, extracted) 600 self.execute(cmdtext) 601 return extracted 602 603 def _extract_file(self, path, cmd, dest=None): 604 cmd = '{} ' + cmd # busybox 605 cmdtext = cmd.format(self.busybox, path) 606 self.execute(cmdtext) 607 extracted = self.path.splitext(path)[0] 608 if dest: 609 self.execute('mv -f {} {}'.format(extracted, dest)) 610 if dest.endswith('/'): 611 extracted = self.path.join(dest, self.path.basename(extracted)) 612 else: 613 extracted = dest 614 return extracted 615 616 def _update_modules(self, stage): 617 for mod in self.modules: 618 if isinstance(mod, dict): 619 mod, params = mod.items()[0] 620 else: 621 params = {} 622 mod = get_module(mod) 623 if not mod.stage == stage: 624 continue 625 if mod.probe(self): 626 self._install_module(mod, **params) 627 else: 628 msg = 'Module {} is not supported by the target'.format(mod.name) 629 if self.load_default_modules: 630 self.logger.debug(msg) 631 else: 632 self.logger.warning(msg) 633 634 def _install_module(self, mod, **params): 635 if mod.name not in self._installed_modules: 636 self.logger.debug('Installing module {}'.format(mod.name)) 637 mod.install(self, **params) 638 self._installed_modules[mod.name] = mod 639 else: 640 self.logger.debug('Module {} is already installed.'.format(mod.name)) 641 642 def _resolve_paths(self): 643 raise NotImplementedError() 644 645 646class LinuxTarget(Target): 647 648 path = posixpath 649 os = 'linux' 650 651 @property 652 @memoized 653 def abi(self): 654 value = self.execute('uname -m').strip() 655 for abi, architectures in ABI_MAP.iteritems(): 656 if value in architectures: 657 result = abi 658 break 659 else: 660 result = value 661 return result 662 663 @property 664 @memoized 665 def os_version(self): 666 os_version = {} 667 try: 668 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null' 669 version_files = self.execute(command, check_exit_code=False).strip().split() 670 for vf in version_files: 671 name = self.path.basename(vf) 672 output = self.read_value(vf) 673 os_version[name] = output.strip().replace('\n', ' ') 674 except TargetError: 675 raise 676 return os_version 677 678 @property 679 @memoized 680 # There is currently no better way to do this cross platform. 681 # ARM does not have dmidecode 682 def model(self): 683 if self.file_exists("/proc/device-tree/model"): 684 raw_model = self.execute("cat /proc/device-tree/model") 685 return '_'.join(raw_model.split()[:2]) 686 return None 687 688 def __init__(self, 689 connection_settings=None, 690 platform=None, 691 working_directory=None, 692 executables_directory=None, 693 connect=True, 694 modules=None, 695 load_default_modules=True, 696 shell_prompt=DEFAULT_SHELL_PROMPT, 697 conn_cls=SshConnection, 698 ): 699 super(LinuxTarget, self).__init__(connection_settings=connection_settings, 700 platform=platform, 701 working_directory=working_directory, 702 executables_directory=executables_directory, 703 connect=connect, 704 modules=modules, 705 load_default_modules=load_default_modules, 706 shell_prompt=shell_prompt, 707 conn_cls=conn_cls) 708 709 def connect(self, timeout=None): 710 super(LinuxTarget, self).connect(timeout=timeout) 711 712 def kick_off(self, command, as_root=False): 713 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command)) 714 return self.conn.execute(command, as_root=as_root) 715 716 def get_pids_of(self, process_name): 717 """Returns a list of PIDs of all processes with the specified name.""" 718 # result should be a column of PIDs with the first row as "PID" header 719 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA 720 check_exit_code=False).strip().split() 721 if len(result) >= 2: # at least one row besides the header 722 return map(int, result[1:]) 723 else: 724 return [] 725 726 def ps(self, **kwargs): 727 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname' 728 lines = iter(convert_new_lines(self.execute(command)).split('\n')) 729 lines.next() # header 730 731 result = [] 732 for line in lines: 733 parts = re.split(r'\s+', line, maxsplit=8) 734 if parts and parts != ['']: 735 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) 736 737 if not kwargs: 738 return result 739 else: 740 filtered_result = [] 741 for entry in result: 742 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()): 743 filtered_result.append(entry) 744 return filtered_result 745 746 def list_directory(self, path, as_root=False): 747 contents = self.execute('ls -1 {}'.format(path), as_root=as_root) 748 return [x.strip() for x in contents.split('\n') if x.strip()] 749 750 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221 751 destpath = self.path.join(self.executables_directory, 752 with_name and with_name or self.path.basename(filepath)) 753 self.push(filepath, destpath) 754 self.execute('chmod a+x {}'.format(destpath), timeout=timeout) 755 self._installed_binaries[self.path.basename(destpath)] = destpath 756 return destpath 757 758 def uninstall(self, name): 759 path = self.path.join(self.executables_directory, name) 760 self.remove(path) 761 762 def capture_screen(self, filepath): 763 if not self.is_installed('scrot'): 764 self.logger.debug('Could not take screenshot as scrot is not installed.') 765 return 766 try: 767 768 tmpfile = self.tempfile() 769 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile)) 770 self.pull(tmpfile, filepath) 771 self.remove(tmpfile) 772 except TargetError as e: 773 if "Can't open X dispay." not in e.message: 774 raise e 775 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member 776 self.logger.debug('Could not take screenshot: {}'.format(message)) 777 778 def _resolve_paths(self): 779 if self.working_directory is None: 780 if self.connected_as_root: 781 self.working_directory = '/root/devlib-target' 782 else: 783 self.working_directory = '/home/{}/devlib-target'.format(self.user) 784 if self.executables_directory is None: 785 self.executables_directory = self.path.join(self.working_directory, 'bin') 786 787 788class AndroidTarget(Target): 789 790 path = posixpath 791 os = 'android' 792 ls_command = '' 793 794 @property 795 @memoized 796 def abi(self): 797 return self.getprop()['ro.product.cpu.abi'].split('-')[0] 798 799 @property 800 @memoized 801 def os_version(self): 802 os_version = {} 803 for k, v in self.getprop().iteritems(): 804 if k.startswith('ro.build.version'): 805 part = k.split('.')[-1] 806 os_version[part] = v 807 return os_version 808 809 @property 810 def adb_name(self): 811 return self.conn.device 812 813 @property 814 @memoized 815 def android_id(self): 816 """ 817 Get the device's ANDROID_ID. Which is 818 819 "A 64-bit number (as a hex string) that is randomly generated when the user 820 first sets up the device and should remain constant for the lifetime of the 821 user's device." 822 823 .. note:: This will get reset on userdata erasure. 824 825 """ 826 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip() 827 return output.split('value=')[-1] 828 829 @property 830 @memoized 831 def model(self): 832 try: 833 return self.getprop(prop='ro.product.device') 834 except KeyError: 835 return None 836 837 @property 838 @memoized 839 def screen_resolution(self): 840 output = self.execute('dumpsys window') 841 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output) 842 if match: 843 return (int(match.group('width')), 844 int(match.group('height'))) 845 else: 846 return (0, 0) 847 848 def __init__(self, 849 connection_settings=None, 850 platform=None, 851 working_directory=None, 852 executables_directory=None, 853 connect=True, 854 modules=None, 855 load_default_modules=True, 856 shell_prompt=DEFAULT_SHELL_PROMPT, 857 conn_cls=AdbConnection, 858 package_data_directory="/data/data", 859 ): 860 super(AndroidTarget, self).__init__(connection_settings=connection_settings, 861 platform=platform, 862 working_directory=working_directory, 863 executables_directory=executables_directory, 864 connect=connect, 865 modules=modules, 866 load_default_modules=load_default_modules, 867 shell_prompt=shell_prompt, 868 conn_cls=conn_cls) 869 self.package_data_directory = package_data_directory 870 871 def reset(self, fastboot=False): # pylint: disable=arguments-differ 872 try: 873 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''), 874 as_root=self.needs_su, timeout=2) 875 except (TargetError, TimeoutError, subprocess.CalledProcessError): 876 # on some targets "reboot" doesn't return gracefully 877 pass 878 self._connected_as_root = None 879 880 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ 881 start = time.time() 882 device = self.connection_settings.get('device') 883 if device and ':' in device: 884 # ADB does not automatically remove a network device from it's 885 # devices list when the connection is broken by the remote, so the 886 # adb connection may have gone "stale", resulting in adb blocking 887 # indefinitely when making calls to the device. To avoid this, 888 # always disconnect first. 889 adb_disconnect(device) 890 super(AndroidTarget, self).connect(timeout=timeout) 891 892 if check_boot_completed: 893 boot_completed = boolean(self.getprop('sys.boot_completed')) 894 while not boot_completed and timeout >= time.time() - start: 895 time.sleep(5) 896 boot_completed = boolean(self.getprop('sys.boot_completed')) 897 if not boot_completed: 898 raise TargetError('Connected but Android did not fully boot.') 899 900 def setup(self, executables=None): 901 super(AndroidTarget, self).setup(executables) 902 self.execute('mkdir -p {}'.format(self._file_transfer_cache)) 903 904 def kick_off(self, command, as_root=None): 905 """ 906 Like execute but closes adb session and returns immediately, leaving the command running on the 907 device (this is different from execute(background=True) which keeps adb connection open and returns 908 a subprocess object). 909 """ 910 if as_root is None: 911 as_root = self.needs_su 912 try: 913 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command) 914 output = self.execute(command, timeout=1, as_root=as_root) 915 except TimeoutError: 916 pass 917 918 def __setup_list_directory(self): 919 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe 920 # AOSP 7.0 as well, the ls command was changed. 921 # Previous versions default to a single column listing, which is nice and easy to parse. 922 # Newer versions default to a multi-column listing, which is not, but it does support 923 # a '-1' option to get into single column mode. Older versions do not support this option 924 # so we try the new version, and if it fails we use the old version. 925 self.ls_command = 'ls -1' 926 try: 927 self.execute('ls -1 {}'.format(self.working_directory), as_root=False) 928 except TargetError: 929 self.ls_command = 'ls' 930 931 def list_directory(self, path, as_root=False): 932 if self.ls_command == '': 933 self.__setup_list_directory() 934 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root) 935 return [x.strip() for x in contents.split('\n') if x.strip()] 936 937 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221 938 ext = os.path.splitext(filepath)[1].lower() 939 if ext == '.apk': 940 return self.install_apk(filepath, timeout) 941 else: 942 return self.install_executable(filepath, with_name) 943 944 def uninstall(self, name): 945 if self.package_is_installed(name): 946 self.uninstall_package(name) 947 else: 948 self.uninstall_executable(name) 949 950 def get_pids_of(self, process_name): 951 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip() 952 if result and 'not found' not in result: 953 return [int(x.split()[1]) for x in result.split('\n')[1:]] 954 else: 955 return [] 956 957 def ps(self, **kwargs): 958 lines = iter(convert_new_lines(self.execute('ps')).split('\n')) 959 lines.next() # header 960 result = [] 961 for line in lines: 962 parts = line.split(None, 8) 963 if parts: 964 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:]))) 965 if not kwargs: 966 return result 967 else: 968 filtered_result = [] 969 for entry in result: 970 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()): 971 filtered_result.append(entry) 972 return filtered_result 973 974 def capture_screen(self, filepath): 975 on_device_file = self.path.join(self.working_directory, 'screen_capture.png') 976 self.execute('screencap -p {}'.format(on_device_file)) 977 self.pull(on_device_file, filepath) 978 self.remove(on_device_file) 979 980 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ 981 if not as_root: 982 self.conn.push(source, dest, timeout=timeout) 983 else: 984 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep)) 985 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile))) 986 self.conn.push(source, device_tempfile, timeout=timeout) 987 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True) 988 989 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ 990 if not as_root: 991 self.conn.pull(source, dest, timeout=timeout) 992 else: 993 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep)) 994 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile))) 995 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True) 996 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True) 997 self.conn.pull(device_tempfile, dest, timeout=timeout) 998 999 # Android-specific 1000 1001 def swipe_to_unlock(self, direction="horizontal"): 1002 width, height = self.screen_resolution 1003 command = 'input swipe {} {} {} {}' 1004 if direction == "horizontal": 1005 swipe_heigh = height * 2 // 3 1006 start = 100 1007 stop = width - start 1008 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh)) 1009 if direction == "vertical": 1010 swipe_middle = height / 2 1011 swipe_heigh = height * 2 // 3 1012 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0)) 1013 else: 1014 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock)) 1015 1016 def getprop(self, prop=None): 1017 props = AndroidProperties(self.execute('getprop')) 1018 if prop: 1019 return props[prop] 1020 return props 1021 1022 def is_installed(self, name): 1023 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name) 1024 1025 def package_is_installed(self, package_name): 1026 return package_name in self.list_packages() 1027 1028 def list_packages(self): 1029 output = self.execute('pm list packages') 1030 output = output.replace('package:', '') 1031 return output.split() 1032 1033 def get_package_version(self, package): 1034 output = self.execute('dumpsys package {}'.format(package)) 1035 for line in convert_new_lines(output).split('\n'): 1036 if 'versionName' in line: 1037 return line.split('=', 1)[1] 1038 return None 1039 1040 def get_sdk_version(self): 1041 try: 1042 return int(self.getprop('ro.build.version.sdk')) 1043 except (ValueError, TypeError): 1044 return None 1045 1046 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221 1047 ext = os.path.splitext(filepath)[1].lower() 1048 if ext == '.apk': 1049 flags = [] 1050 if replace: 1051 flags.append('-r') # Replace existing APK 1052 if allow_downgrade: 1053 flags.append('-d') # Install the APK even if a newer version is already installed 1054 if self.get_sdk_version() >= 23: 1055 flags.append('-g') # Grant all runtime permissions 1056 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags))) 1057 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout) 1058 else: 1059 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath)) 1060 1061 def install_executable(self, filepath, with_name=None): 1062 self._ensure_executables_directory_is_writable() 1063 executable_name = with_name or os.path.basename(filepath) 1064 on_device_file = self.path.join(self.working_directory, executable_name) 1065 on_device_executable = self.path.join(self.executables_directory, executable_name) 1066 self.push(filepath, on_device_file) 1067 if on_device_file != on_device_executable: 1068 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su) 1069 self.remove(on_device_file, as_root=self.needs_su) 1070 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su) 1071 self._installed_binaries[executable_name] = on_device_executable 1072 return on_device_executable 1073 1074 def uninstall_package(self, package): 1075 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30) 1076 1077 def uninstall_executable(self, executable_name): 1078 on_device_executable = self.path.join(self.executables_directory, executable_name) 1079 self._ensure_executables_directory_is_writable() 1080 self.remove(on_device_executable, as_root=self.needs_su) 1081 1082 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin 1083 op = '>>' if append else '>' 1084 filtstr = ' -s {}'.format(filter) if filter else '' 1085 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath) 1086 adb_command(self.adb_name, command, timeout=timeout) 1087 1088 def clear_logcat(self): 1089 adb_command(self.adb_name, 'logcat -c', timeout=30) 1090 1091 def adb_reboot_bootloader(self, timeout=30): 1092 adb_command(self.adb_name, 'reboot-bootloader', timeout) 1093 1094 def adb_root(self, enable=True, force=False): 1095 if enable: 1096 if self._connected_as_root and not force: 1097 return 1098 adb_command(self.adb_name, 'root', timeout=30) 1099 self._connected_as_root = True 1100 return 1101 adb_command(self.adb_name, 'unroot', timeout=30) 1102 self._connected_as_root = False 1103 1104 def is_screen_on(self): 1105 output = self.execute('dumpsys power') 1106 match = ANDROID_SCREEN_STATE_REGEX.search(output) 1107 if match: 1108 return boolean(match.group(1)) 1109 else: 1110 raise TargetError('Could not establish screen state.') 1111 1112 def ensure_screen_is_on(self): 1113 if not self.is_screen_on(): 1114 self.execute('input keyevent 26') 1115 1116 def ensure_screen_is_off(self): 1117 if self.is_screen_on(): 1118 self.execute('input keyevent 26') 1119 1120 def homescreen(self): 1121 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME') 1122 1123 def _resolve_paths(self): 1124 if self.working_directory is None: 1125 self.working_directory = '/data/local/tmp/devlib-target' 1126 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache') 1127 if self.executables_directory is None: 1128 self.executables_directory = '/data/local/tmp/bin' 1129 1130 def _ensure_executables_directory_is_writable(self): 1131 matched = [] 1132 for entry in self.list_file_systems(): 1133 if self.executables_directory.rstrip('/').startswith(entry.mount_point): 1134 matched.append(entry) 1135 if matched: 1136 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1] 1137 if 'rw' not in entry.options: 1138 self.execute('mount -o rw,remount {} {}'.format(entry.device, 1139 entry.mount_point), 1140 as_root=True) 1141 else: 1142 message = 'Could not find mount point for executables directory {}' 1143 raise TargetError(message.format(self.executables_directory)) 1144 1145 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled' 1146 1147 @property 1148 def charging_enabled(self): 1149 """ 1150 Whether drawing power to charge the battery is enabled 1151 1152 Not all devices have the ability to enable/disable battery charging 1153 (e.g. because they don't have a battery). In that case, 1154 ``charging_enabled`` is None. 1155 """ 1156 if not self.file_exists(self._charging_enabled_path): 1157 return None 1158 return self.read_bool(self._charging_enabled_path) 1159 1160 @charging_enabled.setter 1161 def charging_enabled(self, enabled): 1162 """ 1163 Enable/disable drawing power to charge the battery 1164 1165 Not all devices have this facility. In that case, do nothing. 1166 """ 1167 if not self.file_exists(self._charging_enabled_path): 1168 return 1169 self.write_value(self._charging_enabled_path, int(bool(enabled))) 1170 1171FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num']) 1172PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name') 1173LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by']) 1174 1175 1176class Cpuinfo(object): 1177 1178 @property 1179 @memoized 1180 def architecture(self): 1181 for section in self.sections: 1182 if 'CPU architecture' in section: 1183 return section['CPU architecture'] 1184 if 'architecture' in section: 1185 return section['architecture'] 1186 1187 @property 1188 @memoized 1189 def cpu_names(self): 1190 cpu_names = [] 1191 global_name = None 1192 for section in self.sections: 1193 if 'processor' in section: 1194 if 'CPU part' in section: 1195 cpu_names.append(_get_part_name(section)) 1196 elif 'model name' in section: 1197 cpu_names.append(_get_model_name(section)) 1198 else: 1199 cpu_names.append(None) 1200 elif 'CPU part' in section: 1201 global_name = _get_part_name(section) 1202 return [caseless_string(c or global_name) for c in cpu_names] 1203 1204 def __init__(self, text): 1205 self.sections = None 1206 self.text = None 1207 self.parse(text) 1208 1209 @memoized 1210 def get_cpu_features(self, cpuid=0): 1211 global_features = [] 1212 for section in self.sections: 1213 if 'processor' in section: 1214 if int(section.get('processor')) != cpuid: 1215 continue 1216 if 'Features' in section: 1217 return section.get('Features').split() 1218 elif 'flags' in section: 1219 return section.get('flags').split() 1220 elif 'Features' in section: 1221 global_features = section.get('Features').split() 1222 elif 'flags' in section: 1223 global_features = section.get('flags').split() 1224 return global_features 1225 1226 def parse(self, text): 1227 self.sections = [] 1228 current_section = {} 1229 self.text = text.strip() 1230 for line in self.text.split('\n'): 1231 line = line.strip() 1232 if line: 1233 key, value = line.split(':', 1) 1234 current_section[key.strip()] = value.strip() 1235 else: # not line 1236 self.sections.append(current_section) 1237 current_section = {} 1238 self.sections.append(current_section) 1239 1240 def __str__(self): 1241 return 'CpuInfo({})'.format(self.cpu_names) 1242 1243 __repr__ = __str__ 1244 1245 1246class KernelVersion(object): 1247 """ 1248 Class representing the version of a target kernel 1249 1250 Not expected to work for very old (pre-3.0) kernel version numbers. 1251 1252 :ivar release: Version number/revision string. Typical output of 1253 ``uname -r`` 1254 :type release: str 1255 :ivar version: Extra version info (aside from ``release``) reported by 1256 ``uname`` 1257 :type version: str 1258 :ivar version_number: Main version number (e.g. 3 for Linux 3.18) 1259 :type version_number: int 1260 :ivar major: Major version number (e.g. 18 for Linux 3.18) 1261 :type major: int 1262 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May 1263 be None 1264 :type minor: int 1265 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None. 1266 :type rc: int 1267 :ivar sha1: Kernel git revision hash, if available (otherwise None) 1268 :type sha1: str 1269 1270 :ivar parts: Tuple of version number components. Can be used for 1271 lexicographically comparing kernel versions. 1272 :type parts: tuple(int) 1273 """ 1274 def __init__(self, version_string): 1275 if ' #' in version_string: 1276 release, version = version_string.split(' #') 1277 self.release = release 1278 self.version = version 1279 elif version_string.startswith('#'): 1280 self.release = '' 1281 self.version = version_string 1282 else: 1283 self.release = version_string 1284 self.version = '' 1285 1286 self.version_number = None 1287 self.major = None 1288 self.minor = None 1289 self.sha1 = None 1290 self.rc = None 1291 match = KVERSION_REGEX.match(version_string) 1292 if match: 1293 groups = match.groupdict() 1294 self.version_number = int(groups['version']) 1295 self.major = int(groups['major']) 1296 if groups['minor'] is not None: 1297 self.minor = int(groups['minor']) 1298 if groups['rc'] is not None: 1299 self.rc = int(groups['rc']) 1300 if groups['sha1'] is not None: 1301 self.sha1 = match.group('sha1') 1302 1303 self.parts = (self.version_number, self.major, self.minor) 1304 1305 def __str__(self): 1306 return '{} {}'.format(self.release, self.version) 1307 1308 __repr__ = __str__ 1309 1310 1311class KernelConfig(object): 1312 1313 not_set_regex = re.compile(r'# (\S+) is not set') 1314 1315 @staticmethod 1316 def get_config_name(name): 1317 name = name.upper() 1318 if not name.startswith('CONFIG_'): 1319 name = 'CONFIG_' + name 1320 return name 1321 1322 def iteritems(self): 1323 return self._config.iteritems() 1324 1325 def __init__(self, text): 1326 self.text = text 1327 self._config = {} 1328 for line in text.split('\n'): 1329 line = line.strip() 1330 if line.startswith('#'): 1331 match = self.not_set_regex.search(line) 1332 if match: 1333 self._config[match.group(1)] = 'n' 1334 elif '=' in line: 1335 name, value = line.split('=', 1) 1336 self._config[name.strip()] = value.strip() 1337 1338 def get(self, name): 1339 return self._config.get(self.get_config_name(name)) 1340 1341 def like(self, name): 1342 regex = re.compile(name, re.I) 1343 result = {} 1344 for k, v in self._config.iteritems(): 1345 if regex.search(k): 1346 result[k] = v 1347 return result 1348 1349 def is_enabled(self, name): 1350 return self.get(name) == 'y' 1351 1352 def is_module(self, name): 1353 return self.get(name) == 'm' 1354 1355 def is_not_set(self, name): 1356 return self.get(name) == 'n' 1357 1358 def has(self, name): 1359 return self.get(name) in ['m', 'y'] 1360 1361 1362class LocalLinuxTarget(LinuxTarget): 1363 1364 def __init__(self, 1365 connection_settings=None, 1366 platform=None, 1367 working_directory=None, 1368 executables_directory=None, 1369 connect=True, 1370 modules=None, 1371 load_default_modules=True, 1372 shell_prompt=DEFAULT_SHELL_PROMPT, 1373 conn_cls=LocalConnection, 1374 ): 1375 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings, 1376 platform=platform, 1377 working_directory=working_directory, 1378 executables_directory=executables_directory, 1379 connect=connect, 1380 modules=modules, 1381 load_default_modules=load_default_modules, 1382 shell_prompt=shell_prompt, 1383 conn_cls=conn_cls) 1384 1385 def _resolve_paths(self): 1386 if self.working_directory is None: 1387 self.working_directory = '/tmp' 1388 if self.executables_directory is None: 1389 self.executables_directory = '/tmp' 1390 1391 1392def _get_model_name(section): 1393 name_string = section['model name'] 1394 parts = name_string.split('@')[0].strip().split() 1395 return ' '.join([p for p in parts 1396 if '(' not in p and p != 'CPU']) 1397 1398 1399def _get_part_name(section): 1400 implementer = section.get('CPU implementer', '0x0') 1401 part = section['CPU part'] 1402 variant = section.get('CPU variant', '0x0') 1403 name = get_cpu_name(*map(integer, [implementer, part, variant])) 1404 if name is None: 1405 name = '{}/{}/{}'.format(implementer, part, variant) 1406 return name 1407