blob: f4e3ea91977eff829c8d1a0fd5250c566a3e3827 [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001import os
2import re
3import time
4import logging
5import posixpath
6import subprocess
7import tempfile
8import threading
9from collections import namedtuple
10
11from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
12from devlib.module import get_module
13from devlib.platform import Platform
14from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError
15from devlib.utils.ssh import SshConnection
16from devlib.utils.android import AdbConnection, AndroidProperties, adb_command, adb_disconnect
17from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists
18from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes
19from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
20
21
Sebastian Goscik040daab2016-02-23 10:27:45 +000022FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
Sebastian Goscik1890db72016-02-15 14:43:30 +000023ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010024 re.IGNORECASE)
25ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
26 r'\s+(?P<width>\d+)x(?P<height>\d+)')
27DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
28 re.MULTILINE)
29
30
31class Target(object):
32
33 conn_cls = None
34 path = None
35 os = None
36
37 default_modules = [
38 'hotplug',
39 'cpufreq',
40 'cpuidle',
41 'cgroups',
42 'hwmon',
43 ]
44
45 @property
46 def core_names(self):
47 return self.platform.core_names
48
49 @property
50 def core_clusters(self):
51 return self.platform.core_clusters
52
53 @property
54 def big_core(self):
55 return self.platform.big_core
56
57 @property
58 def little_core(self):
59 return self.platform.little_core
60
61 @property
62 def is_connected(self):
63 return self.conn is not None
64
65 @property
66 @memoized
67 def connected_as_root(self):
68 result = self.execute('id')
69 return 'uid=0(' in result
70
71 @property
72 @memoized
73 def is_rooted(self):
74 if self.connected_as_root:
75 return True
76 try:
77 self.execute('ls /', timeout=2, as_root=True)
78 return True
79 except (TargetError, TimeoutError):
80 return False
81
82 @property
83 @memoized
Javi Merino16d87c62016-06-23 14:55:19 +010084 def needs_su(self):
85 return not self.connected_as_root and self.is_rooted
86
87 @property
88 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010089 def kernel_version(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +000090 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010091
92 @property
93 def os_version(self): # pylint: disable=no-self-use
94 return {}
95
96 @property
97 def abi(self): # pylint: disable=no-self-use
98 return None
99
100 @property
101 @memoized
102 def cpuinfo(self):
103 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
104
105 @property
106 @memoized
107 def number_of_cpus(self):
108 num_cpus = 0
109 corere = re.compile(r'^\s*cpu\d+\s*$')
110 output = self.execute('ls /sys/devices/system/cpu')
111 for entry in output.split():
112 if corere.match(entry):
113 num_cpus += 1
114 return num_cpus
115
116 @property
117 @memoized
118 def config(self):
119 try:
120 return KernelConfig(self.execute('zcat /proc/config.gz'))
121 except TargetError:
122 for path in ['/boot/config', '/boot/config-$(uname -r)']:
123 try:
124 return KernelConfig(self.execute('cat {}'.format(path)))
125 except TargetError:
126 pass
127 return KernelConfig('')
128
129 @property
130 @memoized
131 def user(self):
132 return self.getenv('USER')
133
134 @property
135 def conn(self):
136 if self._connections:
137 tid = id(threading.current_thread())
138 if tid not in self._connections:
139 self._connections[tid] = self.get_connection()
140 return self._connections[tid]
141 else:
142 return None
143
144 def __init__(self,
145 connection_settings=None,
146 platform=None,
147 working_directory=None,
148 executables_directory=None,
149 connect=True,
150 modules=None,
151 load_default_modules=True,
152 shell_prompt=DEFAULT_SHELL_PROMPT,
153 ):
154 self.connection_settings = connection_settings or {}
155 self.platform = platform or Platform()
156 self.working_directory = working_directory
157 self.executables_directory = executables_directory
158 self.modules = modules or []
159 self.load_default_modules = load_default_modules
160 self.shell_prompt = shell_prompt
161 self.logger = logging.getLogger(self.__class__.__name__)
162 self._installed_binaries = {}
163 self._installed_modules = {}
164 self._cache = {}
165 self._connections = {}
166 self.busybox = None
167
168 if load_default_modules:
169 module_lists = [self.default_modules]
170 else:
171 module_lists = []
172 module_lists += [self.modules, self.platform.modules]
173 self.modules = merge_lists(*module_lists, duplicates='first')
174 self._update_modules('early')
175 if connect:
176 self.connect()
177
178 # connection and initialization
179
180 def connect(self, timeout=None):
181 self.platform.init_target_connection(self)
182 tid = id(threading.current_thread())
183 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000184 self._resolve_paths()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100185 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100186 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100187 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100188 if self.platform.big_core and self.load_default_modules:
189 self._install_module(get_module('bl'))
190
191 def disconnect(self):
192 for conn in self._connections.itervalues():
193 conn.close()
194 self._connections = {}
195
196 def get_connection(self, timeout=None):
197 if self.conn_cls is None:
198 raise NotImplementedError('conn_cls must be set by the subclass of Target')
199 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
200
201 def setup(self, executables=None):
202 self.execute('mkdir -p {}'.format(self.working_directory))
203 self.execute('mkdir -p {}'.format(self.executables_directory))
204 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000205
206 # Setup shutils script for the target
207 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
208 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
209 shell_path = '/bin/sh'
210 if self.os == 'android':
211 shell_path = '/system/bin/sh'
212 with open(shutils_ifile) as fh:
213 lines = fh.readlines()
214 with open(shutils_ofile, 'w') as ofile:
215 for line in lines:
216 line = line.replace("__DEVLIB_SHELL__", shell_path)
217 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
218 ofile.write(line)
219 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
220
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100221 for host_exe in (executables or []): # pylint: disable=superfluous-parens
222 self.install(host_exe)
223
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100224 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
225 self._update_modules('setup')
226
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100227 def reboot(self, hard=False, connect=True, timeout=180):
228 if hard:
229 if not self.has('hard_reset'):
230 raise TargetError('Hard reset not supported for this target.')
231 self.hard_reset() # pylint: disable=no-member
232 else:
233 if not self.is_connected:
234 message = 'Cannot reboot target becuase it is disconnected. ' +\
235 'Either connect() first, or specify hard=True ' +\
236 '(in which case, a hard_reset module must be installed)'
237 raise TargetError(message)
238 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000239 # Wait a fixed delay before starting polling to give the target time to
240 # shut down, otherwise, might create the connection while it's still shutting
241 # down resulting in subsequenct connection failing.
242 self.logger.debug('Waiting for target to power down...')
243 reset_delay = 20
244 time.sleep(reset_delay)
245 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100246 if self.has('boot'):
247 self.boot() # pylint: disable=no-member
248 if connect:
249 self.connect(timeout=timeout)
250
251 # file transfer
252
253 def push(self, source, dest, timeout=None):
254 return self.conn.push(source, dest, timeout=timeout)
255
256 def pull(self, source, dest, timeout=None):
257 return self.conn.pull(source, dest, timeout=timeout)
258
259 # execution
260
Patrick Bellasif2eac512015-11-27 16:35:57 +0000261 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
262 command = '{} {}'.format(self.shutils, command)
263 return self.conn.execute(command, timeout, check_exit_code, as_root)
264
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100265 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
266 return self.conn.execute(command, timeout, check_exit_code, as_root)
267
268 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
269 return self.conn.background(command, stdout, stderr, as_root)
270
271 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
272 as_root=False, timeout=30):
273 """
274 Executes the specified binary under the specified conditions.
275
276 :binary: binary to execute. Must be present and executable on the device.
277 :args: arguments to be passed to the binary. The can be either a list or
278 a string.
279 :in_directory: execute the binary in the specified directory. This must
280 be an absolute path.
281 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
282 case, it will be interpreted as the mask), a list of ``ints``, in which
283 case this will be interpreted as the list of cpus, or string, which
284 will be interpreted as a comma-separated list of cpu ranges, e.g.
285 ``"0,4-7"``.
286 :as_root: Specify whether the command should be run as root
287 :timeout: If the invocation does not terminate within this number of seconds,
288 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
289 invocation should not timeout.
290
291 """
292 command = binary
293 if args:
294 if isiterable(args):
295 args = ' '.join(args)
296 command = '{} {}'.format(command, args)
297 if on_cpus:
298 on_cpus = bitmask(on_cpus)
299 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
300 if in_directory:
301 command = 'cd {} && {}'.format(in_directory, command)
302 return self.execute(command, as_root=as_root, timeout=timeout)
303
304 def kick_off(self, command, as_root=False):
305 raise NotImplementedError()
306
307 # sysfs interaction
308
309 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100310 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100311 if kind:
312 return kind(output)
313 else:
314 return output
315
316 def read_int(self, path):
317 return self.read_value(path, kind=integer)
318
319 def read_bool(self, path):
320 return self.read_value(path, kind=boolean)
321
322 def write_value(self, path, value, verify=True):
323 value = str(value)
324 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
325 if verify:
326 output = self.read_value(path)
327 if not output == value:
328 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
329 raise TargetError(message)
330
331 def reset(self):
332 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100333 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100334 except (TargetError, TimeoutError, subprocess.CalledProcessError):
335 # on some targets "reboot" doesn't return gracefully
336 pass
337
338 def check_responsive(self):
339 try:
340 self.conn.execute('ls /', timeout=5)
341 except (TimeoutError, subprocess.CalledProcessError):
342 raise TargetNotRespondingError(self.conn.name)
343
344 # process management
345
346 def kill(self, pid, signal=None, as_root=False):
347 signal_string = '-s {}'.format(signal) if signal else ''
348 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
349
350 def killall(self, process_name, signal=None, as_root=False):
351 for pid in self.get_pids_of(process_name):
352 self.kill(pid, signal=signal, as_root=as_root)
353
354 def get_pids_of(self, process_name):
355 raise NotImplementedError()
356
357 def ps(self, **kwargs):
358 raise NotImplementedError()
359
360 # files
361
362 def file_exists(self, filepath):
363 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
364 return boolean(self.execute(command.format(filepath)).strip())
365
Sebastian Goscik33603c62016-02-15 15:07:19 +0000366 def directory_exists(self, filepath):
367 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
368 # output from ssh my contain part of the expression in the buffer,
369 # split out everything except the last word.
370 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
371
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100372 def list_file_systems(self):
373 output = self.execute('mount')
374 fstab = []
375 for line in output.split('\n'):
376 line = line.strip()
377 if not line:
378 continue
379 match = FSTAB_ENTRY_REGEX.search(line)
380 if match:
381 fstab.append(FstabEntry(match.group(1), match.group(2),
382 match.group(3), match.group(4),
383 None, None))
384 else: # assume pre-M Android
385 fstab.append(FstabEntry(*line.split()))
386 return fstab
387
388 def list_directory(self, path, as_root=False):
389 raise NotImplementedError()
390
391 def get_workpath(self, name):
392 return self.path.join(self.working_directory, name)
393
394 def tempfile(self, prefix='', suffix=''):
395 names = tempfile._get_candidate_names() # pylint: disable=W0212
396 for _ in xrange(tempfile.TMP_MAX):
397 name = names.next()
398 path = self.get_workpath(prefix + name + suffix)
399 if not self.file_exists(path):
400 return path
401 raise IOError('No usable temporary filename found')
402
403 def remove(self, path, as_root=False):
404 self.execute('rm -rf {}'.format(path), as_root=as_root)
405
406 # misc
407 def core_cpus(self, core):
408 return [i for i, c in enumerate(self.core_names) if c == core]
409
410 def list_online_cpus(self, core=None):
411 path = self.path.join('/sys/devices/system/cpu/online')
412 output = self.read_value(path)
413 all_online = ranges_to_list(output)
414 if core:
415 cpus = self.core_cpus(core)
416 if not cpus:
417 raise ValueError(core)
418 return [o for o in all_online if o in cpus]
419 else:
420 return all_online
421
422 def list_offline_cpus(self):
423 online = self.list_online_cpus()
424 return [c for c in xrange(self.number_of_cpus)
425 if c not in online]
426
427 def getenv(self, variable):
428 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
429
430 def capture_screen(self, filepath):
431 raise NotImplementedError()
432
433 def install(self, filepath, timeout=None, with_name=None):
434 raise NotImplementedError()
435
436 def uninstall(self, name):
437 raise NotImplementedError()
438
Sebastian Goscik84151f92016-02-15 15:09:27 +0000439 def get_installed(self, name, search_system_binaries=True):
440 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000441 if self.file_exists(self.executables_directory):
442 if name in self.list_directory(self.executables_directory):
443 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000444 # Fall back to binaries in PATH
445 if search_system_binaries:
446 for path in self.getenv('PATH').split(self.path.pathsep):
447 try:
448 if name in self.list_directory(path):
449 return self.path.join(path, name)
450 except TargetError:
451 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100452
453 which = get_installed
454
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000455 def install_if_needed(self, host_path, search_system_binaries=True):
456
457 binary_path = self.get_installed(os.path.split(host_path)[1],
458 search_system_binaries=search_system_binaries)
459 if not binary_path:
460 binary_path = self.install(host_path)
461 return binary_path
462
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100463 def is_installed(self, name):
464 return bool(self.get_installed(name))
465
466 def bin(self, name):
467 return self._installed_binaries.get(name, name)
468
469 def has(self, modname):
470 return hasattr(self, identifier(modname))
471
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000472 def lsmod(self):
473 lines = self.execute('lsmod').splitlines()
474 entries = []
475 for line in lines[1:]: # first line is the header
476 if not line.strip():
477 continue
478 parts = line.split()
479 name = parts[0]
480 size = int(parts[1])
481 use_count = int(parts[2])
482 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000483 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000484 else:
485 used_by = []
486 entries.append(LsmodEntry(name, size, use_count, used_by))
487 return entries
488
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000489 def insmod(self, path):
490 target_path = self.get_workpath(os.path.basename(path))
491 self.push(path, target_path)
492 self.execute('insmod {}'.format(target_path), as_root=True)
493
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100494
495 def extract(self, path, dest=None):
496 """
497 Extact the specified on-target file. The extraction method to be used
498 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
499 If ``dest`` is specified, it must be an existing directory on target;
500 the extracted contents will be placed there.
501
502 Note that, depending on the archive file format (and therfore the
503 extraction method used), the original archive file may or may not exist
504 after the extraction.
505
506 The return value is the path to the extracted contents. In case of
507 gunzip and bunzip2, this will be path to the extracted file; for tar
508 and uzip, this will be the directory with the extracted file(s)
509 (``dest`` if it was specified otherwise, the directory that cotained
510 the archive).
511
512 """
513 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
514 '.tgz', '.tbz', '.tbz2']:
515 if path.endswith(ending):
516 return self._extract_archive(path, 'tar xf {} -C {}', dest)
517
518 ext = self.path.splitext(path)[1]
519 if ext in ['.bz', '.bz2']:
520 return self._extract_file(path, 'bunzip2 -f {}', dest)
521 elif ext == '.gz':
522 return self._extract_file(path, 'gunzip -f {}', dest)
523 elif ext == '.zip':
524 return self._extract_archive(path, 'unzip {} -d {}', dest)
525 else:
526 raise ValueError('Unknown compression format: {}'.format(ext))
527
528 # internal methods
529
530 def _extract_archive(self, path, cmd, dest=None):
531 cmd = '{} ' + cmd # busybox
532 if dest:
533 extracted = dest
534 else:
535 extracted = self.path.dirname(path)
536 cmdtext = cmd.format(self.busybox, path, extracted)
537 self.execute(cmdtext)
538 return extracted
539
540 def _extract_file(self, path, cmd, dest=None):
541 cmd = '{} ' + cmd # busybox
542 cmdtext = cmd.format(self.busybox, path)
543 self.execute(cmdtext)
544 extracted = self.path.splitext(path)[0]
545 if dest:
546 self.execute('mv -f {} {}'.format(extracted, dest))
547 if dest.endswith('/'):
548 extracted = self.path.join(dest, self.path.basename(extracted))
549 else:
550 extracted = dest
551 return extracted
552
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100553 def _update_modules(self, stage):
554 for mod in self.modules:
555 if isinstance(mod, dict):
556 mod, params = mod.items()[0]
557 else:
558 params = {}
559 mod = get_module(mod)
560 if not mod.stage == stage:
561 continue
562 if mod.probe(self):
563 self._install_module(mod, **params)
564 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000565 msg = 'Module {} is not supported by the target'.format(mod.name)
566 if self.load_default_modules:
567 self.logger.debug(msg)
568 else:
569 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100570
571 def _install_module(self, mod, **params):
572 if mod.name not in self._installed_modules:
573 self.logger.debug('Installing module {}'.format(mod.name))
574 mod.install(self, **params)
575 self._installed_modules[mod.name] = mod
576 else:
577 self.logger.debug('Module {} is already installed.'.format(mod.name))
578
Sergei Trofimov961f9572015-11-18 17:32:26 +0000579 def _resolve_paths(self):
580 raise NotImplementedError()
581
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100582
583class LinuxTarget(Target):
584
585 conn_cls = SshConnection
586 path = posixpath
587 os = 'linux'
588
589 @property
590 @memoized
591 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100592 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100593 for abi, architectures in ABI_MAP.iteritems():
594 if value in architectures:
595 result = abi
596 break
597 else:
598 result = value
599 return result
600
601 @property
602 @memoized
603 def os_version(self):
604 os_version = {}
605 try:
606 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
607 version_files = self.execute(command, check_exit_code=False).strip().split()
608 for vf in version_files:
609 name = self.path.basename(vf)
610 output = self.read_value(vf)
611 os_version[name] = output.strip().replace('\n', ' ')
612 except TargetError:
613 raise
614 return os_version
615
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000616 @property
617 @memoized
618 # There is currently no better way to do this cross platform.
619 # ARM does not have dmidecode
620 def model(self):
621 if self.file_exists("/proc/device-tree/model"):
622 raw_model = self.execute("cat /proc/device-tree/model")
623 return '_'.join(raw_model.split()[:2])
624 return None
625
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100626 def connect(self, timeout=None):
627 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100628
629 def kick_off(self, command, as_root=False):
630 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
631 return self.conn.execute(command, as_root=as_root)
632
633 def get_pids_of(self, process_name):
634 """Returns a list of PIDs of all processes with the specified name."""
635 # result should be a column of PIDs with the first row as "PID" header
636 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
637 check_exit_code=False).strip().split()
638 if len(result) >= 2: # at least one row besides the header
639 return map(int, result[1:])
640 else:
641 return []
642
643 def ps(self, **kwargs):
644 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
645 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
646 lines.next() # header
647
648 result = []
649 for line in lines:
650 parts = re.split(r'\s+', line, maxsplit=8)
651 if parts and parts != ['']:
652 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
653
654 if not kwargs:
655 return result
656 else:
657 filtered_result = []
658 for entry in result:
659 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
660 filtered_result.append(entry)
661 return filtered_result
662
663 def list_directory(self, path, as_root=False):
664 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
665 return [x.strip() for x in contents.split('\n') if x.strip()]
666
667 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
668 destpath = self.path.join(self.executables_directory,
669 with_name and with_name or self.path.basename(filepath))
670 self.push(filepath, destpath)
671 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
672 self._installed_binaries[self.path.basename(destpath)] = destpath
673 return destpath
674
675 def uninstall(self, name):
676 path = self.path.join(self.executables_directory, name)
677 self.remove(path)
678
679 def capture_screen(self, filepath):
680 if not self.is_installed('scrot'):
681 self.logger.debug('Could not take screenshot as scrot is not installed.')
682 return
683 try:
684
685 tmpfile = self.tempfile()
686 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
687 self.pull(tmpfile, filepath)
688 self.remove(tmpfile)
689 except TargetError as e:
690 if "Can't open X dispay." not in e.message:
691 raise e
692 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
693 self.logger.debug('Could not take screenshot: {}'.format(message))
694
Sergei Trofimov961f9572015-11-18 17:32:26 +0000695 def _resolve_paths(self):
696 if self.working_directory is None:
697 if self.connected_as_root:
698 self.working_directory = '/root/devlib-target'
699 else:
700 self.working_directory = '/home/{}/devlib-target'.format(self.user)
701 if self.executables_directory is None:
702 self.executables_directory = self.path.join(self.working_directory, 'bin')
703
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100704
705class AndroidTarget(Target):
706
707 conn_cls = AdbConnection
708 path = posixpath
709 os = 'android'
710
711 @property
712 @memoized
713 def abi(self):
714 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
715
716 @property
717 @memoized
718 def os_version(self):
719 os_version = {}
720 for k, v in self.getprop().iteritems():
721 if k.startswith('ro.build.version'):
722 part = k.split('.')[-1]
723 os_version[part] = v
724 return os_version
725
726 @property
727 def adb_name(self):
728 return self.conn.device
729
730 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000731 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000732 def android_id(self):
733 """
734 Get the device's ANDROID_ID. Which is
735
736 "A 64-bit number (as a hex string) that is randomly generated when the user
737 first sets up the device and should remain constant for the lifetime of the
738 user's device."
739
740 .. note:: This will get reset on userdata erasure.
741
742 """
743 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
744 return output.split('value=')[-1]
745
746 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100747 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000748 def model(self):
749 try:
750 return self.getprop(prop='ro.product.device')
751 except KeyError:
752 return None
753
754 @property
755 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100756 def screen_resolution(self):
757 output = self.execute('dumpsys window')
758 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
759 if match:
760 return (int(match.group('width')),
761 int(match.group('height')))
762 else:
763 return (0, 0)
764
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000765 def __init__(self,
766 connection_settings=None,
767 platform=None,
768 working_directory=None,
769 executables_directory=None,
770 connect=True,
771 modules=None,
772 load_default_modules=True,
773 shell_prompt=DEFAULT_SHELL_PROMPT,
774 package_data_directory="/data/data",
775 external_storage_directory="/sdcard",
776 ):
777 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
778 platform=platform,
779 working_directory=working_directory,
780 executables_directory=executables_directory,
781 connect=connect,
782 modules=modules,
783 load_default_modules=load_default_modules,
784 shell_prompt=shell_prompt)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000785 self.package_data_directory = package_data_directory
786
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100787 def reset(self, fastboot=False): # pylint: disable=arguments-differ
788 try:
789 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100790 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100791 except (TargetError, TimeoutError, subprocess.CalledProcessError):
792 # on some targets "reboot" doesn't return gracefully
793 pass
794
795 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
796 start = time.time()
797 device = self.connection_settings.get('device')
798 if device and ':' in device:
799 # ADB does not automatically remove a network device from it's
800 # devices list when the connection is broken by the remote, so the
801 # adb connection may have gone "stale", resulting in adb blocking
802 # indefinitely when making calls to the device. To avoid this,
803 # always disconnect first.
804 adb_disconnect(device)
805 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100806
807 if check_boot_completed:
808 boot_completed = boolean(self.getprop('sys.boot_completed'))
809 while not boot_completed and timeout >= time.time() - start:
810 time.sleep(5)
811 boot_completed = boolean(self.getprop('sys.boot_completed'))
812 if not boot_completed:
813 raise TargetError('Connected but Android did not fully boot.')
814
815 def setup(self, executables=None):
816 super(AndroidTarget, self).setup(executables)
817 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
818
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100819 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100820 """
821 Like execute but closes adb session and returns immediately, leaving the command running on the
822 device (this is different from execute(background=True) which keeps adb connection open and returns
823 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100824 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100825 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100826 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100827 try:
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100828 command = 'cd {} && {} nohup {}'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100829 output = self.execute(command, timeout=1, as_root=as_root)
830 except TimeoutError:
831 pass
832 else:
833 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
834
835 def list_directory(self, path, as_root=False):
836 contents = self.execute('ls {}'.format(path), as_root=as_root)
837 return [x.strip() for x in contents.split('\n') if x.strip()]
838
839 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
840 ext = os.path.splitext(filepath)[1].lower()
841 if ext == '.apk':
842 return self.install_apk(filepath, timeout)
843 else:
844 return self.install_executable(filepath, with_name)
845
846 def uninstall(self, name):
847 if self.package_is_installed(name):
848 self.uninstall_package(name)
849 else:
850 self.uninstall_executable(name)
851
852 def get_pids_of(self, process_name):
853 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
854 if result and 'not found' not in result:
855 return [int(x.split()[1]) for x in result.split('\n')[1:]]
856 else:
857 return []
858
859 def ps(self, **kwargs):
860 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
861 lines.next() # header
862 result = []
863 for line in lines:
864 parts = line.split()
865 if parts:
866 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
867 if not kwargs:
868 return result
869 else:
870 filtered_result = []
871 for entry in result:
872 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
873 filtered_result.append(entry)
874 return filtered_result
875
876 def capture_screen(self, filepath):
877 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
878 self.execute('screencap -p {}'.format(on_device_file))
879 self.pull(on_device_file, filepath)
880 self.remove(on_device_file)
881
882 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
883 if not as_root:
884 self.conn.push(source, dest, timeout=timeout)
885 else:
886 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000887 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100888 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000889 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100890
891 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
892 if not as_root:
893 self.conn.pull(source, dest, timeout=timeout)
894 else:
895 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000896 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
897 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100898 self.conn.pull(device_tempfile, dest, timeout=timeout)
899
900 # Android-specific
901
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000902 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100903 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100904 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000905 if direction == "horizontal":
906 swipe_heigh = height * 2 // 3
907 start = 100
908 stop = width - start
909 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
910 if direction == "vertical":
911 swipe_middle = height / 2
912 swipe_heigh = height * 2 // 3
913 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
914 else:
915 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100916
917 def getprop(self, prop=None):
918 props = AndroidProperties(self.execute('getprop'))
919 if prop:
920 return props[prop]
921 return props
922
923 def is_installed(self, name):
924 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
925
926 def package_is_installed(self, package_name):
927 return package_name in self.list_packages()
928
929 def list_packages(self):
930 output = self.execute('pm list packages')
931 output = output.replace('package:', '')
932 return output.split()
933
934 def get_package_version(self, package):
935 output = self.execute('dumpsys package {}'.format(package))
936 for line in convert_new_lines(output).split('\n'):
937 if 'versionName' in line:
938 return line.split('=', 1)[1]
939 return None
940
941 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
942 ext = os.path.splitext(filepath)[1].lower()
943 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000944 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100945 else:
946 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
947
948 def install_executable(self, filepath, with_name=None):
949 self._ensure_executables_directory_is_writable()
950 executable_name = with_name or os.path.basename(filepath)
951 on_device_file = self.path.join(self.working_directory, executable_name)
952 on_device_executable = self.path.join(self.executables_directory, executable_name)
953 self.push(filepath, on_device_file)
954 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +0100955 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
956 self.remove(on_device_file, as_root=self.needs_su)
957 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100958 self._installed_binaries[executable_name] = on_device_executable
959 return on_device_executable
960
961 def uninstall_package(self, package):
962 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
963
964 def uninstall_executable(self, executable_name):
965 on_device_executable = self.path.join(self.executables_directory, executable_name)
966 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +0100967 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100968
969 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000970 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100971 filtstr = ' -s {}'.format(filter) if filter else ''
972 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
973 adb_command(self.adb_name, command, timeout=timeout)
974
975 def clear_logcat(self):
976 adb_command(self.adb_name, 'logcat -c', timeout=30)
977
978 def is_screen_on(self):
979 output = self.execute('dumpsys power')
980 match = ANDROID_SCREEN_STATE_REGEX.search(output)
981 if match:
982 return boolean(match.group(1))
983 else:
984 raise TargetError('Could not establish screen state.')
985
986 def ensure_screen_is_on(self):
987 if not self.is_screen_on():
988 self.execute('input keyevent 26')
989
Sergei Trofimov961f9572015-11-18 17:32:26 +0000990 def _resolve_paths(self):
991 if self.working_directory is None:
992 self.working_directory = '/data/local/tmp/devlib-target'
993 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
994 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +0000995 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +0000996
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100997 def _ensure_executables_directory_is_writable(self):
998 matched = []
999 for entry in self.list_file_systems():
1000 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1001 matched.append(entry)
1002 if matched:
1003 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1004 if 'rw' not in entry.options:
1005 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1006 entry.mount_point),
1007 as_root=True)
1008 else:
1009 message = 'Could not find mount point for executables directory {}'
1010 raise TargetError(message.format(self.executables_directory))
1011
1012
1013FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1014PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001015LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001016
1017
1018class Cpuinfo(object):
1019
1020 @property
1021 @memoized
1022 def architecture(self):
1023 for section in self.sections:
1024 if 'CPU architecture' in section:
1025 return section['CPU architecture']
1026 if 'architecture' in section:
1027 return section['architecture']
1028
1029 @property
1030 @memoized
1031 def cpu_names(self):
1032 cpu_names = []
1033 global_name = None
1034 for section in self.sections:
1035 if 'processor' in section:
1036 if 'CPU part' in section:
1037 cpu_names.append(_get_part_name(section))
1038 elif 'model name' in section:
1039 cpu_names.append(_get_model_name(section))
1040 else:
1041 cpu_names.append(None)
1042 elif 'CPU part' in section:
1043 global_name = _get_part_name(section)
1044 return [caseless_string(c or global_name) for c in cpu_names]
1045
1046 def __init__(self, text):
1047 self.sections = None
1048 self.text = None
1049 self.parse(text)
1050
1051 @memoized
1052 def get_cpu_features(self, cpuid=0):
1053 global_features = []
1054 for section in self.sections:
1055 if 'processor' in section:
1056 if int(section.get('processor')) != cpuid:
1057 continue
1058 if 'Features' in section:
1059 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001060 elif 'flags' in section:
1061 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001062 elif 'Features' in section:
1063 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001064 elif 'flags' in section:
1065 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001066 return global_features
1067
1068 def parse(self, text):
1069 self.sections = []
1070 current_section = {}
1071 self.text = text.strip()
1072 for line in self.text.split('\n'):
1073 line = line.strip()
1074 if line:
1075 key, value = line.split(':', 1)
1076 current_section[key.strip()] = value.strip()
1077 else: # not line
1078 self.sections.append(current_section)
1079 current_section = {}
1080 self.sections.append(current_section)
1081
1082 def __str__(self):
1083 return 'CpuInfo({})'.format(self.cpu_names)
1084
1085 __repr__ = __str__
1086
1087
1088class KernelVersion(object):
1089
1090 def __init__(self, version_string):
1091 if ' #' in version_string:
1092 release, version = version_string.split(' #')
1093 self.release = release
1094 self.version = version
1095 elif version_string.startswith('#'):
1096 self.release = ''
1097 self.version = version_string
1098 else:
1099 self.release = version_string
1100 self.version = ''
1101
1102 def __str__(self):
1103 return '{} {}'.format(self.release, self.version)
1104
1105 __repr__ = __str__
1106
1107
1108class KernelConfig(object):
1109
1110 not_set_regex = re.compile(r'# (\S+) is not set')
1111
1112 @staticmethod
1113 def get_config_name(name):
1114 name = name.upper()
1115 if not name.startswith('CONFIG_'):
1116 name = 'CONFIG_' + name
1117 return name
1118
1119 def iteritems(self):
1120 return self._config.iteritems()
1121
1122 def __init__(self, text):
1123 self.text = text
1124 self._config = {}
1125 for line in text.split('\n'):
1126 line = line.strip()
1127 if line.startswith('#'):
1128 match = self.not_set_regex.search(line)
1129 if match:
1130 self._config[match.group(1)] = 'n'
1131 elif '=' in line:
1132 name, value = line.split('=', 1)
1133 self._config[name.strip()] = value.strip()
1134
1135 def get(self, name):
1136 return self._config.get(self.get_config_name(name))
1137
1138 def like(self, name):
1139 regex = re.compile(name, re.I)
1140 result = {}
1141 for k, v in self._config.iteritems():
1142 if regex.search(k):
1143 result[k] = v
1144 return result
1145
1146 def is_enabled(self, name):
1147 return self.get(name) == 'y'
1148
1149 def is_module(self, name):
1150 return self.get(name) == 'm'
1151
1152 def is_not_set(self, name):
1153 return self.get(name) == 'n'
1154
1155 def has(self, name):
1156 return self.get(name) in ['m', 'y']
1157
1158
1159class LocalLinuxTarget(LinuxTarget):
1160
1161 conn_cls = LocalConnection
1162
Sergei Trofimov961f9572015-11-18 17:32:26 +00001163 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001164 if self.working_directory is None:
1165 self.working_directory = '/tmp'
1166 if self.executables_directory is None:
1167 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001168
1169
1170def _get_model_name(section):
1171 name_string = section['model name']
1172 parts = name_string.split('@')[0].strip().split()
1173 return ' '.join([p for p in parts
1174 if '(' not in p and p != 'CPU'])
1175
1176
1177def _get_part_name(section):
1178 implementer = section.get('CPU implementer', '0x0')
1179 part = section['CPU part']
1180 variant = section.get('CPU variant', '0x0')
1181 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1182 if name is None:
1183 name = '{}/{}/{}'.format(implementer, part, variant)
1184 return name