blob: 78697cac86726fdb08c5833c94d5d458541056b7 [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
22FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (\S+) 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
84 def kernel_version(self):
85 return KernelVersion(self.execute('uname -r -v').strip())
86
87 @property
88 def os_version(self): # pylint: disable=no-self-use
89 return {}
90
91 @property
92 def abi(self): # pylint: disable=no-self-use
93 return None
94
95 @property
96 @memoized
97 def cpuinfo(self):
98 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
99
100 @property
101 @memoized
102 def number_of_cpus(self):
103 num_cpus = 0
104 corere = re.compile(r'^\s*cpu\d+\s*$')
105 output = self.execute('ls /sys/devices/system/cpu')
106 for entry in output.split():
107 if corere.match(entry):
108 num_cpus += 1
109 return num_cpus
110
111 @property
112 @memoized
113 def config(self):
114 try:
115 return KernelConfig(self.execute('zcat /proc/config.gz'))
116 except TargetError:
117 for path in ['/boot/config', '/boot/config-$(uname -r)']:
118 try:
119 return KernelConfig(self.execute('cat {}'.format(path)))
120 except TargetError:
121 pass
122 return KernelConfig('')
123
124 @property
125 @memoized
126 def user(self):
127 return self.getenv('USER')
128
129 @property
130 def conn(self):
131 if self._connections:
132 tid = id(threading.current_thread())
133 if tid not in self._connections:
134 self._connections[tid] = self.get_connection()
135 return self._connections[tid]
136 else:
137 return None
138
139 def __init__(self,
140 connection_settings=None,
141 platform=None,
142 working_directory=None,
143 executables_directory=None,
144 connect=True,
145 modules=None,
146 load_default_modules=True,
147 shell_prompt=DEFAULT_SHELL_PROMPT,
148 ):
149 self.connection_settings = connection_settings or {}
150 self.platform = platform or Platform()
151 self.working_directory = working_directory
152 self.executables_directory = executables_directory
153 self.modules = modules or []
154 self.load_default_modules = load_default_modules
155 self.shell_prompt = shell_prompt
156 self.logger = logging.getLogger(self.__class__.__name__)
157 self._installed_binaries = {}
158 self._installed_modules = {}
159 self._cache = {}
160 self._connections = {}
161 self.busybox = None
162
163 if load_default_modules:
164 module_lists = [self.default_modules]
165 else:
166 module_lists = []
167 module_lists += [self.modules, self.platform.modules]
168 self.modules = merge_lists(*module_lists, duplicates='first')
169 self._update_modules('early')
170 if connect:
171 self.connect()
172
173 # connection and initialization
174
175 def connect(self, timeout=None):
176 self.platform.init_target_connection(self)
177 tid = id(threading.current_thread())
178 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000179 self._resolve_paths()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100180 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100181 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100182 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100183 if self.platform.big_core and self.load_default_modules:
184 self._install_module(get_module('bl'))
185
186 def disconnect(self):
187 for conn in self._connections.itervalues():
188 conn.close()
189 self._connections = {}
190
191 def get_connection(self, timeout=None):
192 if self.conn_cls is None:
193 raise NotImplementedError('conn_cls must be set by the subclass of Target')
194 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
195
196 def setup(self, executables=None):
197 self.execute('mkdir -p {}'.format(self.working_directory))
198 self.execute('mkdir -p {}'.format(self.executables_directory))
199 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000200
201 # Setup shutils script for the target
202 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
203 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
204 shell_path = '/bin/sh'
205 if self.os == 'android':
206 shell_path = '/system/bin/sh'
207 with open(shutils_ifile) as fh:
208 lines = fh.readlines()
209 with open(shutils_ofile, 'w') as ofile:
210 for line in lines:
211 line = line.replace("__DEVLIB_SHELL__", shell_path)
212 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
213 ofile.write(line)
214 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
215
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100216 for host_exe in (executables or []): # pylint: disable=superfluous-parens
217 self.install(host_exe)
218
219 def reboot(self, hard=False, connect=True, timeout=180):
220 if hard:
221 if not self.has('hard_reset'):
222 raise TargetError('Hard reset not supported for this target.')
223 self.hard_reset() # pylint: disable=no-member
224 else:
225 if not self.is_connected:
226 message = 'Cannot reboot target becuase it is disconnected. ' +\
227 'Either connect() first, or specify hard=True ' +\
228 '(in which case, a hard_reset module must be installed)'
229 raise TargetError(message)
230 self.reset()
231 if self.has('boot'):
232 self.boot() # pylint: disable=no-member
233 if connect:
234 self.connect(timeout=timeout)
235
236 # file transfer
237
238 def push(self, source, dest, timeout=None):
239 return self.conn.push(source, dest, timeout=timeout)
240
241 def pull(self, source, dest, timeout=None):
242 return self.conn.pull(source, dest, timeout=timeout)
243
244 # execution
245
Patrick Bellasif2eac512015-11-27 16:35:57 +0000246 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
247 command = '{} {}'.format(self.shutils, command)
248 return self.conn.execute(command, timeout, check_exit_code, as_root)
249
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100250 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
251 return self.conn.execute(command, timeout, check_exit_code, as_root)
252
253 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
254 return self.conn.background(command, stdout, stderr, as_root)
255
256 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
257 as_root=False, timeout=30):
258 """
259 Executes the specified binary under the specified conditions.
260
261 :binary: binary to execute. Must be present and executable on the device.
262 :args: arguments to be passed to the binary. The can be either a list or
263 a string.
264 :in_directory: execute the binary in the specified directory. This must
265 be an absolute path.
266 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
267 case, it will be interpreted as the mask), a list of ``ints``, in which
268 case this will be interpreted as the list of cpus, or string, which
269 will be interpreted as a comma-separated list of cpu ranges, e.g.
270 ``"0,4-7"``.
271 :as_root: Specify whether the command should be run as root
272 :timeout: If the invocation does not terminate within this number of seconds,
273 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
274 invocation should not timeout.
275
276 """
277 command = binary
278 if args:
279 if isiterable(args):
280 args = ' '.join(args)
281 command = '{} {}'.format(command, args)
282 if on_cpus:
283 on_cpus = bitmask(on_cpus)
284 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
285 if in_directory:
286 command = 'cd {} && {}'.format(in_directory, command)
287 return self.execute(command, as_root=as_root, timeout=timeout)
288
289 def kick_off(self, command, as_root=False):
290 raise NotImplementedError()
291
292 # sysfs interaction
293
294 def read_value(self, path, kind=None):
295 output = self.execute('cat \'{}\''.format(path), as_root=self.is_rooted).strip() # pylint: disable=E1103
296 if kind:
297 return kind(output)
298 else:
299 return output
300
301 def read_int(self, path):
302 return self.read_value(path, kind=integer)
303
304 def read_bool(self, path):
305 return self.read_value(path, kind=boolean)
306
307 def write_value(self, path, value, verify=True):
308 value = str(value)
309 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
310 if verify:
311 output = self.read_value(path)
312 if not output == value:
313 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
314 raise TargetError(message)
315
316 def reset(self):
317 try:
318 self.execute('reboot', as_root=self.is_rooted, timeout=2)
319 except (TargetError, TimeoutError, subprocess.CalledProcessError):
320 # on some targets "reboot" doesn't return gracefully
321 pass
322
323 def check_responsive(self):
324 try:
325 self.conn.execute('ls /', timeout=5)
326 except (TimeoutError, subprocess.CalledProcessError):
327 raise TargetNotRespondingError(self.conn.name)
328
329 # process management
330
331 def kill(self, pid, signal=None, as_root=False):
332 signal_string = '-s {}'.format(signal) if signal else ''
333 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
334
335 def killall(self, process_name, signal=None, as_root=False):
336 for pid in self.get_pids_of(process_name):
337 self.kill(pid, signal=signal, as_root=as_root)
338
339 def get_pids_of(self, process_name):
340 raise NotImplementedError()
341
342 def ps(self, **kwargs):
343 raise NotImplementedError()
344
345 # files
346
347 def file_exists(self, filepath):
348 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
349 return boolean(self.execute(command.format(filepath)).strip())
350
Sebastian Goscik33603c62016-02-15 15:07:19 +0000351 def directory_exists(self, filepath):
352 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
353 # output from ssh my contain part of the expression in the buffer,
354 # split out everything except the last word.
355 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
356
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100357 def list_file_systems(self):
358 output = self.execute('mount')
359 fstab = []
360 for line in output.split('\n'):
361 line = line.strip()
362 if not line:
363 continue
364 match = FSTAB_ENTRY_REGEX.search(line)
365 if match:
366 fstab.append(FstabEntry(match.group(1), match.group(2),
367 match.group(3), match.group(4),
368 None, None))
369 else: # assume pre-M Android
370 fstab.append(FstabEntry(*line.split()))
371 return fstab
372
373 def list_directory(self, path, as_root=False):
374 raise NotImplementedError()
375
376 def get_workpath(self, name):
377 return self.path.join(self.working_directory, name)
378
379 def tempfile(self, prefix='', suffix=''):
380 names = tempfile._get_candidate_names() # pylint: disable=W0212
381 for _ in xrange(tempfile.TMP_MAX):
382 name = names.next()
383 path = self.get_workpath(prefix + name + suffix)
384 if not self.file_exists(path):
385 return path
386 raise IOError('No usable temporary filename found')
387
388 def remove(self, path, as_root=False):
389 self.execute('rm -rf {}'.format(path), as_root=as_root)
390
391 # misc
392 def core_cpus(self, core):
393 return [i for i, c in enumerate(self.core_names) if c == core]
394
395 def list_online_cpus(self, core=None):
396 path = self.path.join('/sys/devices/system/cpu/online')
397 output = self.read_value(path)
398 all_online = ranges_to_list(output)
399 if core:
400 cpus = self.core_cpus(core)
401 if not cpus:
402 raise ValueError(core)
403 return [o for o in all_online if o in cpus]
404 else:
405 return all_online
406
407 def list_offline_cpus(self):
408 online = self.list_online_cpus()
409 return [c for c in xrange(self.number_of_cpus)
410 if c not in online]
411
412 def getenv(self, variable):
413 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
414
415 def capture_screen(self, filepath):
416 raise NotImplementedError()
417
418 def install(self, filepath, timeout=None, with_name=None):
419 raise NotImplementedError()
420
421 def uninstall(self, name):
422 raise NotImplementedError()
423
Sebastian Goscik84151f92016-02-15 15:09:27 +0000424 def get_installed(self, name, search_system_binaries=True):
425 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000426 if self.file_exists(self.executables_directory):
427 if name in self.list_directory(self.executables_directory):
428 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000429 # Fall back to binaries in PATH
430 if search_system_binaries:
431 for path in self.getenv('PATH').split(self.path.pathsep):
432 try:
433 if name in self.list_directory(path):
434 return self.path.join(path, name)
435 except TargetError:
436 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100437
438 which = get_installed
439
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000440 def install_if_needed(self, host_path, search_system_binaries=True):
441
442 binary_path = self.get_installed(os.path.split(host_path)[1],
443 search_system_binaries=search_system_binaries)
444 if not binary_path:
445 binary_path = self.install(host_path)
446 return binary_path
447
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100448 def is_installed(self, name):
449 return bool(self.get_installed(name))
450
451 def bin(self, name):
452 return self._installed_binaries.get(name, name)
453
454 def has(self, modname):
455 return hasattr(self, identifier(modname))
456
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000457 def lsmod(self):
458 lines = self.execute('lsmod').splitlines()
459 entries = []
460 for line in lines[1:]: # first line is the header
461 if not line.strip():
462 continue
463 parts = line.split()
464 name = parts[0]
465 size = int(parts[1])
466 use_count = int(parts[2])
467 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000468 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000469 else:
470 used_by = []
471 entries.append(LsmodEntry(name, size, use_count, used_by))
472 return entries
473
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000474 def insmod(self, path):
475 target_path = self.get_workpath(os.path.basename(path))
476 self.push(path, target_path)
477 self.execute('insmod {}'.format(target_path), as_root=True)
478
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100479 def _update_modules(self, stage):
480 for mod in self.modules:
481 if isinstance(mod, dict):
482 mod, params = mod.items()[0]
483 else:
484 params = {}
485 mod = get_module(mod)
486 if not mod.stage == stage:
487 continue
488 if mod.probe(self):
489 self._install_module(mod, **params)
490 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000491 msg = 'Module {} is not supported by the target'.format(mod.name)
492 if self.load_default_modules:
493 self.logger.debug(msg)
494 else:
495 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100496
497 def _install_module(self, mod, **params):
498 if mod.name not in self._installed_modules:
499 self.logger.debug('Installing module {}'.format(mod.name))
500 mod.install(self, **params)
501 self._installed_modules[mod.name] = mod
502 else:
503 self.logger.debug('Module {} is already installed.'.format(mod.name))
504
Sergei Trofimov961f9572015-11-18 17:32:26 +0000505 def _resolve_paths(self):
506 raise NotImplementedError()
507
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100508
509class LinuxTarget(Target):
510
511 conn_cls = SshConnection
512 path = posixpath
513 os = 'linux'
514
515 @property
516 @memoized
517 def abi(self):
518 value = self.execute('uname -m').strip()
519 for abi, architectures in ABI_MAP.iteritems():
520 if value in architectures:
521 result = abi
522 break
523 else:
524 result = value
525 return result
526
527 @property
528 @memoized
529 def os_version(self):
530 os_version = {}
531 try:
532 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
533 version_files = self.execute(command, check_exit_code=False).strip().split()
534 for vf in version_files:
535 name = self.path.basename(vf)
536 output = self.read_value(vf)
537 os_version[name] = output.strip().replace('\n', ' ')
538 except TargetError:
539 raise
540 return os_version
541
542 def connect(self, timeout=None):
543 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100544
545 def kick_off(self, command, as_root=False):
546 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
547 return self.conn.execute(command, as_root=as_root)
548
549 def get_pids_of(self, process_name):
550 """Returns a list of PIDs of all processes with the specified name."""
551 # result should be a column of PIDs with the first row as "PID" header
552 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
553 check_exit_code=False).strip().split()
554 if len(result) >= 2: # at least one row besides the header
555 return map(int, result[1:])
556 else:
557 return []
558
559 def ps(self, **kwargs):
560 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
561 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
562 lines.next() # header
563
564 result = []
565 for line in lines:
566 parts = re.split(r'\s+', line, maxsplit=8)
567 if parts and parts != ['']:
568 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
569
570 if not kwargs:
571 return result
572 else:
573 filtered_result = []
574 for entry in result:
575 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
576 filtered_result.append(entry)
577 return filtered_result
578
579 def list_directory(self, path, as_root=False):
580 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
581 return [x.strip() for x in contents.split('\n') if x.strip()]
582
583 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
584 destpath = self.path.join(self.executables_directory,
585 with_name and with_name or self.path.basename(filepath))
586 self.push(filepath, destpath)
587 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
588 self._installed_binaries[self.path.basename(destpath)] = destpath
589 return destpath
590
591 def uninstall(self, name):
592 path = self.path.join(self.executables_directory, name)
593 self.remove(path)
594
595 def capture_screen(self, filepath):
596 if not self.is_installed('scrot'):
597 self.logger.debug('Could not take screenshot as scrot is not installed.')
598 return
599 try:
600
601 tmpfile = self.tempfile()
602 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
603 self.pull(tmpfile, filepath)
604 self.remove(tmpfile)
605 except TargetError as e:
606 if "Can't open X dispay." not in e.message:
607 raise e
608 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
609 self.logger.debug('Could not take screenshot: {}'.format(message))
610
Sergei Trofimov961f9572015-11-18 17:32:26 +0000611 def _resolve_paths(self):
612 if self.working_directory is None:
613 if self.connected_as_root:
614 self.working_directory = '/root/devlib-target'
615 else:
616 self.working_directory = '/home/{}/devlib-target'.format(self.user)
617 if self.executables_directory is None:
618 self.executables_directory = self.path.join(self.working_directory, 'bin')
619
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100620
621class AndroidTarget(Target):
622
623 conn_cls = AdbConnection
624 path = posixpath
625 os = 'android'
626
627 @property
628 @memoized
629 def abi(self):
630 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
631
632 @property
633 @memoized
634 def os_version(self):
635 os_version = {}
636 for k, v in self.getprop().iteritems():
637 if k.startswith('ro.build.version'):
638 part = k.split('.')[-1]
639 os_version[part] = v
640 return os_version
641
642 @property
643 def adb_name(self):
644 return self.conn.device
645
646 @property
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000647 def android_id(self):
648 """
649 Get the device's ANDROID_ID. Which is
650
651 "A 64-bit number (as a hex string) that is randomly generated when the user
652 first sets up the device and should remain constant for the lifetime of the
653 user's device."
654
655 .. note:: This will get reset on userdata erasure.
656
657 """
658 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
659 return output.split('value=')[-1]
660
661 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100662 @memoized
663 def screen_resolution(self):
664 output = self.execute('dumpsys window')
665 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
666 if match:
667 return (int(match.group('width')),
668 int(match.group('height')))
669 else:
670 return (0, 0)
671
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100672 def reset(self, fastboot=False): # pylint: disable=arguments-differ
673 try:
674 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
675 as_root=self.is_rooted, timeout=2)
676 except (TargetError, TimeoutError, subprocess.CalledProcessError):
677 # on some targets "reboot" doesn't return gracefully
678 pass
679
680 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
681 start = time.time()
682 device = self.connection_settings.get('device')
683 if device and ':' in device:
684 # ADB does not automatically remove a network device from it's
685 # devices list when the connection is broken by the remote, so the
686 # adb connection may have gone "stale", resulting in adb blocking
687 # indefinitely when making calls to the device. To avoid this,
688 # always disconnect first.
689 adb_disconnect(device)
690 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100691
692 if check_boot_completed:
693 boot_completed = boolean(self.getprop('sys.boot_completed'))
694 while not boot_completed and timeout >= time.time() - start:
695 time.sleep(5)
696 boot_completed = boolean(self.getprop('sys.boot_completed'))
697 if not boot_completed:
698 raise TargetError('Connected but Android did not fully boot.')
699
700 def setup(self, executables=None):
701 super(AndroidTarget, self).setup(executables)
702 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
703
704 def kick_off(self, command, as_root=False):
705 """
706 Like execute but closes adb session and returns immediately, leaving the command running on the
707 device (this is different from execute(background=True) which keeps adb connection open and returns
708 a subprocess object).
709
710 .. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.
711
712 """
713 if not self.is_rooted:
714 raise TargetError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.')
715 try:
716 command = 'cd {} && {} nohup {}'.format(self.working_directory, self.bin('busybox'), command)
717 output = self.execute(command, timeout=1, as_root=as_root)
718 except TimeoutError:
719 pass
720 else:
721 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
722
723 def list_directory(self, path, as_root=False):
724 contents = self.execute('ls {}'.format(path), as_root=as_root)
725 return [x.strip() for x in contents.split('\n') if x.strip()]
726
727 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
728 ext = os.path.splitext(filepath)[1].lower()
729 if ext == '.apk':
730 return self.install_apk(filepath, timeout)
731 else:
732 return self.install_executable(filepath, with_name)
733
734 def uninstall(self, name):
735 if self.package_is_installed(name):
736 self.uninstall_package(name)
737 else:
738 self.uninstall_executable(name)
739
740 def get_pids_of(self, process_name):
741 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
742 if result and 'not found' not in result:
743 return [int(x.split()[1]) for x in result.split('\n')[1:]]
744 else:
745 return []
746
747 def ps(self, **kwargs):
748 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
749 lines.next() # header
750 result = []
751 for line in lines:
752 parts = line.split()
753 if parts:
754 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
755 if not kwargs:
756 return result
757 else:
758 filtered_result = []
759 for entry in result:
760 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
761 filtered_result.append(entry)
762 return filtered_result
763
764 def capture_screen(self, filepath):
765 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
766 self.execute('screencap -p {}'.format(on_device_file))
767 self.pull(on_device_file, filepath)
768 self.remove(on_device_file)
769
770 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
771 if not as_root:
772 self.conn.push(source, dest, timeout=timeout)
773 else:
774 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000775 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100776 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000777 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100778
779 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
780 if not as_root:
781 self.conn.pull(source, dest, timeout=timeout)
782 else:
783 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000784 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
785 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100786 self.conn.pull(device_tempfile, dest, timeout=timeout)
787
788 # Android-specific
789
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000790 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100791 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100792 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000793 if direction == "horizontal":
794 swipe_heigh = height * 2 // 3
795 start = 100
796 stop = width - start
797 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
798 if direction == "vertical":
799 swipe_middle = height / 2
800 swipe_heigh = height * 2 // 3
801 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
802 else:
803 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100804
805 def getprop(self, prop=None):
806 props = AndroidProperties(self.execute('getprop'))
807 if prop:
808 return props[prop]
809 return props
810
811 def is_installed(self, name):
812 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
813
814 def package_is_installed(self, package_name):
815 return package_name in self.list_packages()
816
817 def list_packages(self):
818 output = self.execute('pm list packages')
819 output = output.replace('package:', '')
820 return output.split()
821
822 def get_package_version(self, package):
823 output = self.execute('dumpsys package {}'.format(package))
824 for line in convert_new_lines(output).split('\n'):
825 if 'versionName' in line:
826 return line.split('=', 1)[1]
827 return None
828
829 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
830 ext = os.path.splitext(filepath)[1].lower()
831 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000832 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100833 else:
834 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
835
836 def install_executable(self, filepath, with_name=None):
837 self._ensure_executables_directory_is_writable()
838 executable_name = with_name or os.path.basename(filepath)
839 on_device_file = self.path.join(self.working_directory, executable_name)
840 on_device_executable = self.path.join(self.executables_directory, executable_name)
841 self.push(filepath, on_device_file)
842 if on_device_file != on_device_executable:
843 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
844 self.remove(on_device_file, as_root=self.is_rooted)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000845 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.is_rooted)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100846 self._installed_binaries[executable_name] = on_device_executable
847 return on_device_executable
848
849 def uninstall_package(self, package):
850 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
851
852 def uninstall_executable(self, executable_name):
853 on_device_executable = self.path.join(self.executables_directory, executable_name)
854 self._ensure_executables_directory_is_writable()
855 self.remove(on_device_executable, as_root=self.is_rooted)
856
857 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000858 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100859 filtstr = ' -s {}'.format(filter) if filter else ''
860 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
861 adb_command(self.adb_name, command, timeout=timeout)
862
863 def clear_logcat(self):
864 adb_command(self.adb_name, 'logcat -c', timeout=30)
865
866 def is_screen_on(self):
867 output = self.execute('dumpsys power')
868 match = ANDROID_SCREEN_STATE_REGEX.search(output)
869 if match:
870 return boolean(match.group(1))
871 else:
872 raise TargetError('Could not establish screen state.')
873
874 def ensure_screen_is_on(self):
875 if not self.is_screen_on():
876 self.execute('input keyevent 26')
877
Sergei Trofimov961f9572015-11-18 17:32:26 +0000878 def _resolve_paths(self):
879 if self.working_directory is None:
880 self.working_directory = '/data/local/tmp/devlib-target'
881 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
882 if self.executables_directory is None:
883 self.executables_directory = self.path.join(self.working_directory, 'bin')
884
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100885 def _ensure_executables_directory_is_writable(self):
886 matched = []
887 for entry in self.list_file_systems():
888 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
889 matched.append(entry)
890 if matched:
891 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
892 if 'rw' not in entry.options:
893 self.execute('mount -o rw,remount {} {}'.format(entry.device,
894 entry.mount_point),
895 as_root=True)
896 else:
897 message = 'Could not find mount point for executables directory {}'
898 raise TargetError(message.format(self.executables_directory))
899
900
901FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
902PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000903LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100904
905
906class Cpuinfo(object):
907
908 @property
909 @memoized
910 def architecture(self):
911 for section in self.sections:
912 if 'CPU architecture' in section:
913 return section['CPU architecture']
914 if 'architecture' in section:
915 return section['architecture']
916
917 @property
918 @memoized
919 def cpu_names(self):
920 cpu_names = []
921 global_name = None
922 for section in self.sections:
923 if 'processor' in section:
924 if 'CPU part' in section:
925 cpu_names.append(_get_part_name(section))
926 elif 'model name' in section:
927 cpu_names.append(_get_model_name(section))
928 else:
929 cpu_names.append(None)
930 elif 'CPU part' in section:
931 global_name = _get_part_name(section)
932 return [caseless_string(c or global_name) for c in cpu_names]
933
934 def __init__(self, text):
935 self.sections = None
936 self.text = None
937 self.parse(text)
938
939 @memoized
940 def get_cpu_features(self, cpuid=0):
941 global_features = []
942 for section in self.sections:
943 if 'processor' in section:
944 if int(section.get('processor')) != cpuid:
945 continue
946 if 'Features' in section:
947 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +0000948 elif 'flags' in section:
949 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100950 elif 'Features' in section:
951 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +0000952 elif 'flags' in section:
953 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100954 return global_features
955
956 def parse(self, text):
957 self.sections = []
958 current_section = {}
959 self.text = text.strip()
960 for line in self.text.split('\n'):
961 line = line.strip()
962 if line:
963 key, value = line.split(':', 1)
964 current_section[key.strip()] = value.strip()
965 else: # not line
966 self.sections.append(current_section)
967 current_section = {}
968 self.sections.append(current_section)
969
970 def __str__(self):
971 return 'CpuInfo({})'.format(self.cpu_names)
972
973 __repr__ = __str__
974
975
976class KernelVersion(object):
977
978 def __init__(self, version_string):
979 if ' #' in version_string:
980 release, version = version_string.split(' #')
981 self.release = release
982 self.version = version
983 elif version_string.startswith('#'):
984 self.release = ''
985 self.version = version_string
986 else:
987 self.release = version_string
988 self.version = ''
989
990 def __str__(self):
991 return '{} {}'.format(self.release, self.version)
992
993 __repr__ = __str__
994
995
996class KernelConfig(object):
997
998 not_set_regex = re.compile(r'# (\S+) is not set')
999
1000 @staticmethod
1001 def get_config_name(name):
1002 name = name.upper()
1003 if not name.startswith('CONFIG_'):
1004 name = 'CONFIG_' + name
1005 return name
1006
1007 def iteritems(self):
1008 return self._config.iteritems()
1009
1010 def __init__(self, text):
1011 self.text = text
1012 self._config = {}
1013 for line in text.split('\n'):
1014 line = line.strip()
1015 if line.startswith('#'):
1016 match = self.not_set_regex.search(line)
1017 if match:
1018 self._config[match.group(1)] = 'n'
1019 elif '=' in line:
1020 name, value = line.split('=', 1)
1021 self._config[name.strip()] = value.strip()
1022
1023 def get(self, name):
1024 return self._config.get(self.get_config_name(name))
1025
1026 def like(self, name):
1027 regex = re.compile(name, re.I)
1028 result = {}
1029 for k, v in self._config.iteritems():
1030 if regex.search(k):
1031 result[k] = v
1032 return result
1033
1034 def is_enabled(self, name):
1035 return self.get(name) == 'y'
1036
1037 def is_module(self, name):
1038 return self.get(name) == 'm'
1039
1040 def is_not_set(self, name):
1041 return self.get(name) == 'n'
1042
1043 def has(self, name):
1044 return self.get(name) in ['m', 'y']
1045
1046
1047class LocalLinuxTarget(LinuxTarget):
1048
1049 conn_cls = LocalConnection
1050
Sergei Trofimov961f9572015-11-18 17:32:26 +00001051 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001052 if self.working_directory is None:
1053 self.working_directory = '/tmp'
1054 if self.executables_directory is None:
1055 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001056
1057
1058def _get_model_name(section):
1059 name_string = section['model name']
1060 parts = name_string.split('@')[0].strip().split()
1061 return ' '.join([p for p in parts
1062 if '(' not in p and p != 'CPU'])
1063
1064
1065def _get_part_name(section):
1066 implementer = section.get('CPU implementer', '0x0')
1067 part = section['CPU part']
1068 variant = section.get('CPU variant', '0x0')
1069 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1070 if name is None:
1071 name = '{}/{}/{}'.format(implementer, part, variant)
1072 return name