• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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