blob: 0dbf345c3e443c7b4aecde85d23a694f4f905a9a [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 Trofimov4e6afe92015-10-09 09:30:04 +0100494 def _update_modules(self, stage):
495 for mod in self.modules:
496 if isinstance(mod, dict):
497 mod, params = mod.items()[0]
498 else:
499 params = {}
500 mod = get_module(mod)
501 if not mod.stage == stage:
502 continue
503 if mod.probe(self):
504 self._install_module(mod, **params)
505 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000506 msg = 'Module {} is not supported by the target'.format(mod.name)
507 if self.load_default_modules:
508 self.logger.debug(msg)
509 else:
510 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100511
512 def _install_module(self, mod, **params):
513 if mod.name not in self._installed_modules:
514 self.logger.debug('Installing module {}'.format(mod.name))
515 mod.install(self, **params)
516 self._installed_modules[mod.name] = mod
517 else:
518 self.logger.debug('Module {} is already installed.'.format(mod.name))
519
Sergei Trofimov961f9572015-11-18 17:32:26 +0000520 def _resolve_paths(self):
521 raise NotImplementedError()
522
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100523
524class LinuxTarget(Target):
525
526 conn_cls = SshConnection
527 path = posixpath
528 os = 'linux'
529
530 @property
531 @memoized
532 def abi(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +0000533 value = self.execute('{} uname -m'.format(self.busybox)).strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100534 for abi, architectures in ABI_MAP.iteritems():
535 if value in architectures:
536 result = abi
537 break
538 else:
539 result = value
540 return result
541
542 @property
543 @memoized
544 def os_version(self):
545 os_version = {}
546 try:
547 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
548 version_files = self.execute(command, check_exit_code=False).strip().split()
549 for vf in version_files:
550 name = self.path.basename(vf)
551 output = self.read_value(vf)
552 os_version[name] = output.strip().replace('\n', ' ')
553 except TargetError:
554 raise
555 return os_version
556
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000557 @property
558 @memoized
559 # There is currently no better way to do this cross platform.
560 # ARM does not have dmidecode
561 def model(self):
562 if self.file_exists("/proc/device-tree/model"):
563 raw_model = self.execute("cat /proc/device-tree/model")
564 return '_'.join(raw_model.split()[:2])
565 return None
566
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100567 def connect(self, timeout=None):
568 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100569
570 def kick_off(self, command, as_root=False):
571 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
572 return self.conn.execute(command, as_root=as_root)
573
574 def get_pids_of(self, process_name):
575 """Returns a list of PIDs of all processes with the specified name."""
576 # result should be a column of PIDs with the first row as "PID" header
577 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
578 check_exit_code=False).strip().split()
579 if len(result) >= 2: # at least one row besides the header
580 return map(int, result[1:])
581 else:
582 return []
583
584 def ps(self, **kwargs):
585 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
586 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
587 lines.next() # header
588
589 result = []
590 for line in lines:
591 parts = re.split(r'\s+', line, maxsplit=8)
592 if parts and parts != ['']:
593 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
594
595 if not kwargs:
596 return result
597 else:
598 filtered_result = []
599 for entry in result:
600 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
601 filtered_result.append(entry)
602 return filtered_result
603
604 def list_directory(self, path, as_root=False):
605 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
606 return [x.strip() for x in contents.split('\n') if x.strip()]
607
608 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
609 destpath = self.path.join(self.executables_directory,
610 with_name and with_name or self.path.basename(filepath))
611 self.push(filepath, destpath)
612 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
613 self._installed_binaries[self.path.basename(destpath)] = destpath
614 return destpath
615
616 def uninstall(self, name):
617 path = self.path.join(self.executables_directory, name)
618 self.remove(path)
619
620 def capture_screen(self, filepath):
621 if not self.is_installed('scrot'):
622 self.logger.debug('Could not take screenshot as scrot is not installed.')
623 return
624 try:
625
626 tmpfile = self.tempfile()
627 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
628 self.pull(tmpfile, filepath)
629 self.remove(tmpfile)
630 except TargetError as e:
631 if "Can't open X dispay." not in e.message:
632 raise e
633 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
634 self.logger.debug('Could not take screenshot: {}'.format(message))
635
Sergei Trofimov961f9572015-11-18 17:32:26 +0000636 def _resolve_paths(self):
637 if self.working_directory is None:
638 if self.connected_as_root:
639 self.working_directory = '/root/devlib-target'
640 else:
641 self.working_directory = '/home/{}/devlib-target'.format(self.user)
642 if self.executables_directory is None:
643 self.executables_directory = self.path.join(self.working_directory, 'bin')
644
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100645
646class AndroidTarget(Target):
647
648 conn_cls = AdbConnection
649 path = posixpath
650 os = 'android'
651
652 @property
653 @memoized
654 def abi(self):
655 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
656
657 @property
658 @memoized
659 def os_version(self):
660 os_version = {}
661 for k, v in self.getprop().iteritems():
662 if k.startswith('ro.build.version'):
663 part = k.split('.')[-1]
664 os_version[part] = v
665 return os_version
666
667 @property
668 def adb_name(self):
669 return self.conn.device
670
671 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000672 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000673 def android_id(self):
674 """
675 Get the device's ANDROID_ID. Which is
676
677 "A 64-bit number (as a hex string) that is randomly generated when the user
678 first sets up the device and should remain constant for the lifetime of the
679 user's device."
680
681 .. note:: This will get reset on userdata erasure.
682
683 """
684 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
685 return output.split('value=')[-1]
686
687 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100688 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000689 def model(self):
690 try:
691 return self.getprop(prop='ro.product.device')
692 except KeyError:
693 return None
694
695 @property
696 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100697 def screen_resolution(self):
698 output = self.execute('dumpsys window')
699 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
700 if match:
701 return (int(match.group('width')),
702 int(match.group('height')))
703 else:
704 return (0, 0)
705
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000706 def __init__(self,
707 connection_settings=None,
708 platform=None,
709 working_directory=None,
710 executables_directory=None,
711 connect=True,
712 modules=None,
713 load_default_modules=True,
714 shell_prompt=DEFAULT_SHELL_PROMPT,
715 package_data_directory="/data/data",
716 external_storage_directory="/sdcard",
717 ):
718 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
719 platform=platform,
720 working_directory=working_directory,
721 executables_directory=executables_directory,
722 connect=connect,
723 modules=modules,
724 load_default_modules=load_default_modules,
725 shell_prompt=shell_prompt)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000726 self.package_data_directory = package_data_directory
727
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100728 def reset(self, fastboot=False): # pylint: disable=arguments-differ
729 try:
730 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100731 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100732 except (TargetError, TimeoutError, subprocess.CalledProcessError):
733 # on some targets "reboot" doesn't return gracefully
734 pass
735
736 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
737 start = time.time()
738 device = self.connection_settings.get('device')
739 if device and ':' in device:
740 # ADB does not automatically remove a network device from it's
741 # devices list when the connection is broken by the remote, so the
742 # adb connection may have gone "stale", resulting in adb blocking
743 # indefinitely when making calls to the device. To avoid this,
744 # always disconnect first.
745 adb_disconnect(device)
746 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100747
748 if check_boot_completed:
749 boot_completed = boolean(self.getprop('sys.boot_completed'))
750 while not boot_completed and timeout >= time.time() - start:
751 time.sleep(5)
752 boot_completed = boolean(self.getprop('sys.boot_completed'))
753 if not boot_completed:
754 raise TargetError('Connected but Android did not fully boot.')
755
756 def setup(self, executables=None):
757 super(AndroidTarget, self).setup(executables)
758 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
759
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100760 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100761 """
762 Like execute but closes adb session and returns immediately, leaving the command running on the
763 device (this is different from execute(background=True) which keeps adb connection open and returns
764 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100765 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100766 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100767 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100768 try:
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100769 command = 'cd {} && {} nohup {}'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100770 output = self.execute(command, timeout=1, as_root=as_root)
771 except TimeoutError:
772 pass
773 else:
774 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
775
776 def list_directory(self, path, as_root=False):
777 contents = self.execute('ls {}'.format(path), as_root=as_root)
778 return [x.strip() for x in contents.split('\n') if x.strip()]
779
780 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
781 ext = os.path.splitext(filepath)[1].lower()
782 if ext == '.apk':
783 return self.install_apk(filepath, timeout)
784 else:
785 return self.install_executable(filepath, with_name)
786
787 def uninstall(self, name):
788 if self.package_is_installed(name):
789 self.uninstall_package(name)
790 else:
791 self.uninstall_executable(name)
792
793 def get_pids_of(self, process_name):
794 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
795 if result and 'not found' not in result:
796 return [int(x.split()[1]) for x in result.split('\n')[1:]]
797 else:
798 return []
799
800 def ps(self, **kwargs):
801 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
802 lines.next() # header
803 result = []
804 for line in lines:
805 parts = line.split()
806 if parts:
807 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
808 if not kwargs:
809 return result
810 else:
811 filtered_result = []
812 for entry in result:
813 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
814 filtered_result.append(entry)
815 return filtered_result
816
817 def capture_screen(self, filepath):
818 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
819 self.execute('screencap -p {}'.format(on_device_file))
820 self.pull(on_device_file, filepath)
821 self.remove(on_device_file)
822
823 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
824 if not as_root:
825 self.conn.push(source, dest, timeout=timeout)
826 else:
827 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000828 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100829 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000830 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100831
832 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
833 if not as_root:
834 self.conn.pull(source, dest, timeout=timeout)
835 else:
836 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000837 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
838 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100839 self.conn.pull(device_tempfile, dest, timeout=timeout)
840
841 # Android-specific
842
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000843 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100844 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100845 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000846 if direction == "horizontal":
847 swipe_heigh = height * 2 // 3
848 start = 100
849 stop = width - start
850 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
851 if direction == "vertical":
852 swipe_middle = height / 2
853 swipe_heigh = height * 2 // 3
854 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
855 else:
856 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100857
858 def getprop(self, prop=None):
859 props = AndroidProperties(self.execute('getprop'))
860 if prop:
861 return props[prop]
862 return props
863
864 def is_installed(self, name):
865 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
866
867 def package_is_installed(self, package_name):
868 return package_name in self.list_packages()
869
870 def list_packages(self):
871 output = self.execute('pm list packages')
872 output = output.replace('package:', '')
873 return output.split()
874
875 def get_package_version(self, package):
876 output = self.execute('dumpsys package {}'.format(package))
877 for line in convert_new_lines(output).split('\n'):
878 if 'versionName' in line:
879 return line.split('=', 1)[1]
880 return None
881
882 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
883 ext = os.path.splitext(filepath)[1].lower()
884 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000885 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100886 else:
887 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
888
889 def install_executable(self, filepath, with_name=None):
890 self._ensure_executables_directory_is_writable()
891 executable_name = with_name or os.path.basename(filepath)
892 on_device_file = self.path.join(self.working_directory, executable_name)
893 on_device_executable = self.path.join(self.executables_directory, executable_name)
894 self.push(filepath, on_device_file)
895 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +0100896 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
897 self.remove(on_device_file, as_root=self.needs_su)
898 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100899 self._installed_binaries[executable_name] = on_device_executable
900 return on_device_executable
901
902 def uninstall_package(self, package):
903 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
904
905 def uninstall_executable(self, executable_name):
906 on_device_executable = self.path.join(self.executables_directory, executable_name)
907 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +0100908 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100909
910 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000911 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100912 filtstr = ' -s {}'.format(filter) if filter else ''
913 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
914 adb_command(self.adb_name, command, timeout=timeout)
915
916 def clear_logcat(self):
917 adb_command(self.adb_name, 'logcat -c', timeout=30)
918
919 def is_screen_on(self):
920 output = self.execute('dumpsys power')
921 match = ANDROID_SCREEN_STATE_REGEX.search(output)
922 if match:
923 return boolean(match.group(1))
924 else:
925 raise TargetError('Could not establish screen state.')
926
927 def ensure_screen_is_on(self):
928 if not self.is_screen_on():
929 self.execute('input keyevent 26')
930
Sergei Trofimov961f9572015-11-18 17:32:26 +0000931 def _resolve_paths(self):
932 if self.working_directory is None:
933 self.working_directory = '/data/local/tmp/devlib-target'
934 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
935 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +0000936 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +0000937
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100938 def _ensure_executables_directory_is_writable(self):
939 matched = []
940 for entry in self.list_file_systems():
941 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
942 matched.append(entry)
943 if matched:
944 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
945 if 'rw' not in entry.options:
946 self.execute('mount -o rw,remount {} {}'.format(entry.device,
947 entry.mount_point),
948 as_root=True)
949 else:
950 message = 'Could not find mount point for executables directory {}'
951 raise TargetError(message.format(self.executables_directory))
952
953
954FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
955PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000956LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100957
958
959class Cpuinfo(object):
960
961 @property
962 @memoized
963 def architecture(self):
964 for section in self.sections:
965 if 'CPU architecture' in section:
966 return section['CPU architecture']
967 if 'architecture' in section:
968 return section['architecture']
969
970 @property
971 @memoized
972 def cpu_names(self):
973 cpu_names = []
974 global_name = None
975 for section in self.sections:
976 if 'processor' in section:
977 if 'CPU part' in section:
978 cpu_names.append(_get_part_name(section))
979 elif 'model name' in section:
980 cpu_names.append(_get_model_name(section))
981 else:
982 cpu_names.append(None)
983 elif 'CPU part' in section:
984 global_name = _get_part_name(section)
985 return [caseless_string(c or global_name) for c in cpu_names]
986
987 def __init__(self, text):
988 self.sections = None
989 self.text = None
990 self.parse(text)
991
992 @memoized
993 def get_cpu_features(self, cpuid=0):
994 global_features = []
995 for section in self.sections:
996 if 'processor' in section:
997 if int(section.get('processor')) != cpuid:
998 continue
999 if 'Features' in section:
1000 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001001 elif 'flags' in section:
1002 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001003 elif 'Features' in section:
1004 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001005 elif 'flags' in section:
1006 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001007 return global_features
1008
1009 def parse(self, text):
1010 self.sections = []
1011 current_section = {}
1012 self.text = text.strip()
1013 for line in self.text.split('\n'):
1014 line = line.strip()
1015 if line:
1016 key, value = line.split(':', 1)
1017 current_section[key.strip()] = value.strip()
1018 else: # not line
1019 self.sections.append(current_section)
1020 current_section = {}
1021 self.sections.append(current_section)
1022
1023 def __str__(self):
1024 return 'CpuInfo({})'.format(self.cpu_names)
1025
1026 __repr__ = __str__
1027
1028
1029class KernelVersion(object):
1030
1031 def __init__(self, version_string):
1032 if ' #' in version_string:
1033 release, version = version_string.split(' #')
1034 self.release = release
1035 self.version = version
1036 elif version_string.startswith('#'):
1037 self.release = ''
1038 self.version = version_string
1039 else:
1040 self.release = version_string
1041 self.version = ''
1042
1043 def __str__(self):
1044 return '{} {}'.format(self.release, self.version)
1045
1046 __repr__ = __str__
1047
1048
1049class KernelConfig(object):
1050
1051 not_set_regex = re.compile(r'# (\S+) is not set')
1052
1053 @staticmethod
1054 def get_config_name(name):
1055 name = name.upper()
1056 if not name.startswith('CONFIG_'):
1057 name = 'CONFIG_' + name
1058 return name
1059
1060 def iteritems(self):
1061 return self._config.iteritems()
1062
1063 def __init__(self, text):
1064 self.text = text
1065 self._config = {}
1066 for line in text.split('\n'):
1067 line = line.strip()
1068 if line.startswith('#'):
1069 match = self.not_set_regex.search(line)
1070 if match:
1071 self._config[match.group(1)] = 'n'
1072 elif '=' in line:
1073 name, value = line.split('=', 1)
1074 self._config[name.strip()] = value.strip()
1075
1076 def get(self, name):
1077 return self._config.get(self.get_config_name(name))
1078
1079 def like(self, name):
1080 regex = re.compile(name, re.I)
1081 result = {}
1082 for k, v in self._config.iteritems():
1083 if regex.search(k):
1084 result[k] = v
1085 return result
1086
1087 def is_enabled(self, name):
1088 return self.get(name) == 'y'
1089
1090 def is_module(self, name):
1091 return self.get(name) == 'm'
1092
1093 def is_not_set(self, name):
1094 return self.get(name) == 'n'
1095
1096 def has(self, name):
1097 return self.get(name) in ['m', 'y']
1098
1099
1100class LocalLinuxTarget(LinuxTarget):
1101
1102 conn_cls = LocalConnection
1103
Sergei Trofimov961f9572015-11-18 17:32:26 +00001104 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001105 if self.working_directory is None:
1106 self.working_directory = '/tmp'
1107 if self.executables_directory is None:
1108 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001109
1110
1111def _get_model_name(section):
1112 name_string = section['model name']
1113 parts = name_string.split('@')[0].strip().split()
1114 return ' '.join([p for p in parts
1115 if '(' not in p and p != 'CPU'])
1116
1117
1118def _get_part_name(section):
1119 implementer = section.get('CPU implementer', '0x0')
1120 part = section['CPU part']
1121 variant = section.get('CPU variant', '0x0')
1122 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1123 if name is None:
1124 name = '{}/{}/{}'.format(implementer, part, variant)
1125 return name