blob: 5ec27631a686ed5ec3598a79fe79910a61c8ccf0 [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
84 def kernel_version(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +000085 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010086
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
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100219 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
220 self._update_modules('setup')
221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100222 def reboot(self, hard=False, connect=True, timeout=180):
223 if hard:
224 if not self.has('hard_reset'):
225 raise TargetError('Hard reset not supported for this target.')
226 self.hard_reset() # pylint: disable=no-member
227 else:
228 if not self.is_connected:
229 message = 'Cannot reboot target becuase it is disconnected. ' +\
230 'Either connect() first, or specify hard=True ' +\
231 '(in which case, a hard_reset module must be installed)'
232 raise TargetError(message)
233 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000234 # Wait a fixed delay before starting polling to give the target time to
235 # shut down, otherwise, might create the connection while it's still shutting
236 # down resulting in subsequenct connection failing.
237 self.logger.debug('Waiting for target to power down...')
238 reset_delay = 20
239 time.sleep(reset_delay)
240 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100241 if self.has('boot'):
242 self.boot() # pylint: disable=no-member
243 if connect:
244 self.connect(timeout=timeout)
245
246 # file transfer
247
248 def push(self, source, dest, timeout=None):
249 return self.conn.push(source, dest, timeout=timeout)
250
251 def pull(self, source, dest, timeout=None):
252 return self.conn.pull(source, dest, timeout=timeout)
253
254 # execution
255
Patrick Bellasif2eac512015-11-27 16:35:57 +0000256 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
257 command = '{} {}'.format(self.shutils, command)
258 return self.conn.execute(command, timeout, check_exit_code, as_root)
259
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100260 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
261 return self.conn.execute(command, timeout, check_exit_code, as_root)
262
263 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
264 return self.conn.background(command, stdout, stderr, as_root)
265
266 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
267 as_root=False, timeout=30):
268 """
269 Executes the specified binary under the specified conditions.
270
271 :binary: binary to execute. Must be present and executable on the device.
272 :args: arguments to be passed to the binary. The can be either a list or
273 a string.
274 :in_directory: execute the binary in the specified directory. This must
275 be an absolute path.
276 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
277 case, it will be interpreted as the mask), a list of ``ints``, in which
278 case this will be interpreted as the list of cpus, or string, which
279 will be interpreted as a comma-separated list of cpu ranges, e.g.
280 ``"0,4-7"``.
281 :as_root: Specify whether the command should be run as root
282 :timeout: If the invocation does not terminate within this number of seconds,
283 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
284 invocation should not timeout.
285
286 """
287 command = binary
288 if args:
289 if isiterable(args):
290 args = ' '.join(args)
291 command = '{} {}'.format(command, args)
292 if on_cpus:
293 on_cpus = bitmask(on_cpus)
294 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
295 if in_directory:
296 command = 'cd {} && {}'.format(in_directory, command)
297 return self.execute(command, as_root=as_root, timeout=timeout)
298
299 def kick_off(self, command, as_root=False):
300 raise NotImplementedError()
301
302 # sysfs interaction
303
304 def read_value(self, path, kind=None):
305 output = self.execute('cat \'{}\''.format(path), as_root=self.is_rooted).strip() # pylint: disable=E1103
306 if kind:
307 return kind(output)
308 else:
309 return output
310
311 def read_int(self, path):
312 return self.read_value(path, kind=integer)
313
314 def read_bool(self, path):
315 return self.read_value(path, kind=boolean)
316
317 def write_value(self, path, value, verify=True):
318 value = str(value)
319 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
320 if verify:
321 output = self.read_value(path)
322 if not output == value:
323 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
324 raise TargetError(message)
325
326 def reset(self):
327 try:
328 self.execute('reboot', as_root=self.is_rooted, timeout=2)
329 except (TargetError, TimeoutError, subprocess.CalledProcessError):
330 # on some targets "reboot" doesn't return gracefully
331 pass
332
333 def check_responsive(self):
334 try:
335 self.conn.execute('ls /', timeout=5)
336 except (TimeoutError, subprocess.CalledProcessError):
337 raise TargetNotRespondingError(self.conn.name)
338
339 # process management
340
341 def kill(self, pid, signal=None, as_root=False):
342 signal_string = '-s {}'.format(signal) if signal else ''
343 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
344
345 def killall(self, process_name, signal=None, as_root=False):
346 for pid in self.get_pids_of(process_name):
347 self.kill(pid, signal=signal, as_root=as_root)
348
349 def get_pids_of(self, process_name):
350 raise NotImplementedError()
351
352 def ps(self, **kwargs):
353 raise NotImplementedError()
354
355 # files
356
357 def file_exists(self, filepath):
358 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
359 return boolean(self.execute(command.format(filepath)).strip())
360
Sebastian Goscik33603c62016-02-15 15:07:19 +0000361 def directory_exists(self, filepath):
362 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
363 # output from ssh my contain part of the expression in the buffer,
364 # split out everything except the last word.
365 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
366
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100367 def list_file_systems(self):
368 output = self.execute('mount')
369 fstab = []
370 for line in output.split('\n'):
371 line = line.strip()
372 if not line:
373 continue
374 match = FSTAB_ENTRY_REGEX.search(line)
375 if match:
376 fstab.append(FstabEntry(match.group(1), match.group(2),
377 match.group(3), match.group(4),
378 None, None))
379 else: # assume pre-M Android
380 fstab.append(FstabEntry(*line.split()))
381 return fstab
382
383 def list_directory(self, path, as_root=False):
384 raise NotImplementedError()
385
386 def get_workpath(self, name):
387 return self.path.join(self.working_directory, name)
388
389 def tempfile(self, prefix='', suffix=''):
390 names = tempfile._get_candidate_names() # pylint: disable=W0212
391 for _ in xrange(tempfile.TMP_MAX):
392 name = names.next()
393 path = self.get_workpath(prefix + name + suffix)
394 if not self.file_exists(path):
395 return path
396 raise IOError('No usable temporary filename found')
397
398 def remove(self, path, as_root=False):
399 self.execute('rm -rf {}'.format(path), as_root=as_root)
400
401 # misc
402 def core_cpus(self, core):
403 return [i for i, c in enumerate(self.core_names) if c == core]
404
405 def list_online_cpus(self, core=None):
406 path = self.path.join('/sys/devices/system/cpu/online')
407 output = self.read_value(path)
408 all_online = ranges_to_list(output)
409 if core:
410 cpus = self.core_cpus(core)
411 if not cpus:
412 raise ValueError(core)
413 return [o for o in all_online if o in cpus]
414 else:
415 return all_online
416
417 def list_offline_cpus(self):
418 online = self.list_online_cpus()
419 return [c for c in xrange(self.number_of_cpus)
420 if c not in online]
421
422 def getenv(self, variable):
423 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
424
425 def capture_screen(self, filepath):
426 raise NotImplementedError()
427
428 def install(self, filepath, timeout=None, with_name=None):
429 raise NotImplementedError()
430
431 def uninstall(self, name):
432 raise NotImplementedError()
433
Sebastian Goscik84151f92016-02-15 15:09:27 +0000434 def get_installed(self, name, search_system_binaries=True):
435 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000436 if self.file_exists(self.executables_directory):
437 if name in self.list_directory(self.executables_directory):
438 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000439 # Fall back to binaries in PATH
440 if search_system_binaries:
441 for path in self.getenv('PATH').split(self.path.pathsep):
442 try:
443 if name in self.list_directory(path):
444 return self.path.join(path, name)
445 except TargetError:
446 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100447
448 which = get_installed
449
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000450 def install_if_needed(self, host_path, search_system_binaries=True):
451
452 binary_path = self.get_installed(os.path.split(host_path)[1],
453 search_system_binaries=search_system_binaries)
454 if not binary_path:
455 binary_path = self.install(host_path)
456 return binary_path
457
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100458 def is_installed(self, name):
459 return bool(self.get_installed(name))
460
461 def bin(self, name):
462 return self._installed_binaries.get(name, name)
463
464 def has(self, modname):
465 return hasattr(self, identifier(modname))
466
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000467 def lsmod(self):
468 lines = self.execute('lsmod').splitlines()
469 entries = []
470 for line in lines[1:]: # first line is the header
471 if not line.strip():
472 continue
473 parts = line.split()
474 name = parts[0]
475 size = int(parts[1])
476 use_count = int(parts[2])
477 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000478 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000479 else:
480 used_by = []
481 entries.append(LsmodEntry(name, size, use_count, used_by))
482 return entries
483
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000484 def insmod(self, path):
485 target_path = self.get_workpath(os.path.basename(path))
486 self.push(path, target_path)
487 self.execute('insmod {}'.format(target_path), as_root=True)
488
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100489 def _update_modules(self, stage):
490 for mod in self.modules:
491 if isinstance(mod, dict):
492 mod, params = mod.items()[0]
493 else:
494 params = {}
495 mod = get_module(mod)
496 if not mod.stage == stage:
497 continue
498 if mod.probe(self):
499 self._install_module(mod, **params)
500 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000501 msg = 'Module {} is not supported by the target'.format(mod.name)
502 if self.load_default_modules:
503 self.logger.debug(msg)
504 else:
505 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100506
507 def _install_module(self, mod, **params):
508 if mod.name not in self._installed_modules:
509 self.logger.debug('Installing module {}'.format(mod.name))
510 mod.install(self, **params)
511 self._installed_modules[mod.name] = mod
512 else:
513 self.logger.debug('Module {} is already installed.'.format(mod.name))
514
Sergei Trofimov961f9572015-11-18 17:32:26 +0000515 def _resolve_paths(self):
516 raise NotImplementedError()
517
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100518
519class LinuxTarget(Target):
520
521 conn_cls = SshConnection
522 path = posixpath
523 os = 'linux'
524
525 @property
526 @memoized
527 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100528 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100529 for abi, architectures in ABI_MAP.iteritems():
530 if value in architectures:
531 result = abi
532 break
533 else:
534 result = value
535 return result
536
537 @property
538 @memoized
539 def os_version(self):
540 os_version = {}
541 try:
542 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
543 version_files = self.execute(command, check_exit_code=False).strip().split()
544 for vf in version_files:
545 name = self.path.basename(vf)
546 output = self.read_value(vf)
547 os_version[name] = output.strip().replace('\n', ' ')
548 except TargetError:
549 raise
550 return os_version
551
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000552 @property
553 @memoized
554 # There is currently no better way to do this cross platform.
555 # ARM does not have dmidecode
556 def model(self):
557 if self.file_exists("/proc/device-tree/model"):
558 raw_model = self.execute("cat /proc/device-tree/model")
559 return '_'.join(raw_model.split()[:2])
560 return None
561
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100562 def connect(self, timeout=None):
563 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100564
565 def kick_off(self, command, as_root=False):
566 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
567 return self.conn.execute(command, as_root=as_root)
568
569 def get_pids_of(self, process_name):
570 """Returns a list of PIDs of all processes with the specified name."""
571 # result should be a column of PIDs with the first row as "PID" header
572 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
573 check_exit_code=False).strip().split()
574 if len(result) >= 2: # at least one row besides the header
575 return map(int, result[1:])
576 else:
577 return []
578
579 def ps(self, **kwargs):
580 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
581 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
582 lines.next() # header
583
584 result = []
585 for line in lines:
586 parts = re.split(r'\s+', line, maxsplit=8)
587 if parts and parts != ['']:
588 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
589
590 if not kwargs:
591 return result
592 else:
593 filtered_result = []
594 for entry in result:
595 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
596 filtered_result.append(entry)
597 return filtered_result
598
599 def list_directory(self, path, as_root=False):
600 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
601 return [x.strip() for x in contents.split('\n') if x.strip()]
602
603 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
604 destpath = self.path.join(self.executables_directory,
605 with_name and with_name or self.path.basename(filepath))
606 self.push(filepath, destpath)
607 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
608 self._installed_binaries[self.path.basename(destpath)] = destpath
609 return destpath
610
611 def uninstall(self, name):
612 path = self.path.join(self.executables_directory, name)
613 self.remove(path)
614
615 def capture_screen(self, filepath):
616 if not self.is_installed('scrot'):
617 self.logger.debug('Could not take screenshot as scrot is not installed.')
618 return
619 try:
620
621 tmpfile = self.tempfile()
622 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
623 self.pull(tmpfile, filepath)
624 self.remove(tmpfile)
625 except TargetError as e:
626 if "Can't open X dispay." not in e.message:
627 raise e
628 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
629 self.logger.debug('Could not take screenshot: {}'.format(message))
630
Sergei Trofimov961f9572015-11-18 17:32:26 +0000631 def _resolve_paths(self):
632 if self.working_directory is None:
633 if self.connected_as_root:
634 self.working_directory = '/root/devlib-target'
635 else:
636 self.working_directory = '/home/{}/devlib-target'.format(self.user)
637 if self.executables_directory is None:
638 self.executables_directory = self.path.join(self.working_directory, 'bin')
639
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100640
641class AndroidTarget(Target):
642
643 conn_cls = AdbConnection
644 path = posixpath
645 os = 'android'
646
647 @property
648 @memoized
649 def abi(self):
650 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
651
652 @property
653 @memoized
654 def os_version(self):
655 os_version = {}
656 for k, v in self.getprop().iteritems():
657 if k.startswith('ro.build.version'):
658 part = k.split('.')[-1]
659 os_version[part] = v
660 return os_version
661
662 @property
663 def adb_name(self):
664 return self.conn.device
665
666 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000667 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000668 def android_id(self):
669 """
670 Get the device's ANDROID_ID. Which is
671
672 "A 64-bit number (as a hex string) that is randomly generated when the user
673 first sets up the device and should remain constant for the lifetime of the
674 user's device."
675
676 .. note:: This will get reset on userdata erasure.
677
678 """
679 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
680 return output.split('value=')[-1]
681
682 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100683 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000684 def model(self):
685 try:
686 return self.getprop(prop='ro.product.device')
687 except KeyError:
688 return None
689
690 @property
691 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100692 def screen_resolution(self):
693 output = self.execute('dumpsys window')
694 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
695 if match:
696 return (int(match.group('width')),
697 int(match.group('height')))
698 else:
699 return (0, 0)
700
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000701 def __init__(self,
702 connection_settings=None,
703 platform=None,
704 working_directory=None,
705 executables_directory=None,
706 connect=True,
707 modules=None,
708 load_default_modules=True,
709 shell_prompt=DEFAULT_SHELL_PROMPT,
710 package_data_directory="/data/data",
711 external_storage_directory="/sdcard",
712 ):
713 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
714 platform=platform,
715 working_directory=working_directory,
716 executables_directory=executables_directory,
717 connect=connect,
718 modules=modules,
719 load_default_modules=load_default_modules,
720 shell_prompt=shell_prompt)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000721 self.package_data_directory = package_data_directory
722
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100723 def reset(self, fastboot=False): # pylint: disable=arguments-differ
724 try:
725 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
726 as_root=self.is_rooted, timeout=2)
727 except (TargetError, TimeoutError, subprocess.CalledProcessError):
728 # on some targets "reboot" doesn't return gracefully
729 pass
730
731 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
732 start = time.time()
733 device = self.connection_settings.get('device')
734 if device and ':' in device:
735 # ADB does not automatically remove a network device from it's
736 # devices list when the connection is broken by the remote, so the
737 # adb connection may have gone "stale", resulting in adb blocking
738 # indefinitely when making calls to the device. To avoid this,
739 # always disconnect first.
740 adb_disconnect(device)
741 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100742
743 if check_boot_completed:
744 boot_completed = boolean(self.getprop('sys.boot_completed'))
745 while not boot_completed and timeout >= time.time() - start:
746 time.sleep(5)
747 boot_completed = boolean(self.getprop('sys.boot_completed'))
748 if not boot_completed:
749 raise TargetError('Connected but Android did not fully boot.')
750
751 def setup(self, executables=None):
752 super(AndroidTarget, self).setup(executables)
753 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
754
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100755 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100756 """
757 Like execute but closes adb session and returns immediately, leaving the command running on the
758 device (this is different from execute(background=True) which keeps adb connection open and returns
759 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100760 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100761 if as_root is None:
762 as_root = self.is_rooted
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100763 try:
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100764 command = 'cd {} && {} nohup {}'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100765 output = self.execute(command, timeout=1, as_root=as_root)
766 except TimeoutError:
767 pass
768 else:
769 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
770
771 def list_directory(self, path, as_root=False):
772 contents = self.execute('ls {}'.format(path), as_root=as_root)
773 return [x.strip() for x in contents.split('\n') if x.strip()]
774
775 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
776 ext = os.path.splitext(filepath)[1].lower()
777 if ext == '.apk':
778 return self.install_apk(filepath, timeout)
779 else:
780 return self.install_executable(filepath, with_name)
781
782 def uninstall(self, name):
783 if self.package_is_installed(name):
784 self.uninstall_package(name)
785 else:
786 self.uninstall_executable(name)
787
788 def get_pids_of(self, process_name):
789 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
790 if result and 'not found' not in result:
791 return [int(x.split()[1]) for x in result.split('\n')[1:]]
792 else:
793 return []
794
795 def ps(self, **kwargs):
796 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
797 lines.next() # header
798 result = []
799 for line in lines:
800 parts = line.split()
801 if parts:
802 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
803 if not kwargs:
804 return result
805 else:
806 filtered_result = []
807 for entry in result:
808 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
809 filtered_result.append(entry)
810 return filtered_result
811
812 def capture_screen(self, filepath):
813 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
814 self.execute('screencap -p {}'.format(on_device_file))
815 self.pull(on_device_file, filepath)
816 self.remove(on_device_file)
817
818 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
819 if not as_root:
820 self.conn.push(source, dest, timeout=timeout)
821 else:
822 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000823 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100824 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000825 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100826
827 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
828 if not as_root:
829 self.conn.pull(source, dest, timeout=timeout)
830 else:
831 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000832 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
833 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100834 self.conn.pull(device_tempfile, dest, timeout=timeout)
835
836 # Android-specific
837
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000838 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100839 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100840 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000841 if direction == "horizontal":
842 swipe_heigh = height * 2 // 3
843 start = 100
844 stop = width - start
845 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
846 if direction == "vertical":
847 swipe_middle = height / 2
848 swipe_heigh = height * 2 // 3
849 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
850 else:
851 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100852
853 def getprop(self, prop=None):
854 props = AndroidProperties(self.execute('getprop'))
855 if prop:
856 return props[prop]
857 return props
858
859 def is_installed(self, name):
860 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
861
862 def package_is_installed(self, package_name):
863 return package_name in self.list_packages()
864
865 def list_packages(self):
866 output = self.execute('pm list packages')
867 output = output.replace('package:', '')
868 return output.split()
869
870 def get_package_version(self, package):
871 output = self.execute('dumpsys package {}'.format(package))
872 for line in convert_new_lines(output).split('\n'):
873 if 'versionName' in line:
874 return line.split('=', 1)[1]
875 return None
876
877 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
878 ext = os.path.splitext(filepath)[1].lower()
879 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000880 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100881 else:
882 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
883
884 def install_executable(self, filepath, with_name=None):
885 self._ensure_executables_directory_is_writable()
886 executable_name = with_name or os.path.basename(filepath)
887 on_device_file = self.path.join(self.working_directory, executable_name)
888 on_device_executable = self.path.join(self.executables_directory, executable_name)
889 self.push(filepath, on_device_file)
890 if on_device_file != on_device_executable:
891 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
892 self.remove(on_device_file, as_root=self.is_rooted)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000893 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.is_rooted)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100894 self._installed_binaries[executable_name] = on_device_executable
895 return on_device_executable
896
897 def uninstall_package(self, package):
898 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
899
900 def uninstall_executable(self, executable_name):
901 on_device_executable = self.path.join(self.executables_directory, executable_name)
902 self._ensure_executables_directory_is_writable()
903 self.remove(on_device_executable, as_root=self.is_rooted)
904
905 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +0000906 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100907 filtstr = ' -s {}'.format(filter) if filter else ''
908 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
909 adb_command(self.adb_name, command, timeout=timeout)
910
911 def clear_logcat(self):
912 adb_command(self.adb_name, 'logcat -c', timeout=30)
913
914 def is_screen_on(self):
915 output = self.execute('dumpsys power')
916 match = ANDROID_SCREEN_STATE_REGEX.search(output)
917 if match:
918 return boolean(match.group(1))
919 else:
920 raise TargetError('Could not establish screen state.')
921
922 def ensure_screen_is_on(self):
923 if not self.is_screen_on():
924 self.execute('input keyevent 26')
925
Sergei Trofimov961f9572015-11-18 17:32:26 +0000926 def _resolve_paths(self):
927 if self.working_directory is None:
928 self.working_directory = '/data/local/tmp/devlib-target'
929 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
930 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +0000931 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +0000932
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100933 def _ensure_executables_directory_is_writable(self):
934 matched = []
935 for entry in self.list_file_systems():
936 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
937 matched.append(entry)
938 if matched:
939 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
940 if 'rw' not in entry.options:
941 self.execute('mount -o rw,remount {} {}'.format(entry.device,
942 entry.mount_point),
943 as_root=True)
944 else:
945 message = 'Could not find mount point for executables directory {}'
946 raise TargetError(message.format(self.executables_directory))
947
948
949FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
950PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000951LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100952
953
954class Cpuinfo(object):
955
956 @property
957 @memoized
958 def architecture(self):
959 for section in self.sections:
960 if 'CPU architecture' in section:
961 return section['CPU architecture']
962 if 'architecture' in section:
963 return section['architecture']
964
965 @property
966 @memoized
967 def cpu_names(self):
968 cpu_names = []
969 global_name = None
970 for section in self.sections:
971 if 'processor' in section:
972 if 'CPU part' in section:
973 cpu_names.append(_get_part_name(section))
974 elif 'model name' in section:
975 cpu_names.append(_get_model_name(section))
976 else:
977 cpu_names.append(None)
978 elif 'CPU part' in section:
979 global_name = _get_part_name(section)
980 return [caseless_string(c or global_name) for c in cpu_names]
981
982 def __init__(self, text):
983 self.sections = None
984 self.text = None
985 self.parse(text)
986
987 @memoized
988 def get_cpu_features(self, cpuid=0):
989 global_features = []
990 for section in self.sections:
991 if 'processor' in section:
992 if int(section.get('processor')) != cpuid:
993 continue
994 if 'Features' in section:
995 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +0000996 elif 'flags' in section:
997 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100998 elif 'Features' in section:
999 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001000 elif 'flags' in section:
1001 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001002 return global_features
1003
1004 def parse(self, text):
1005 self.sections = []
1006 current_section = {}
1007 self.text = text.strip()
1008 for line in self.text.split('\n'):
1009 line = line.strip()
1010 if line:
1011 key, value = line.split(':', 1)
1012 current_section[key.strip()] = value.strip()
1013 else: # not line
1014 self.sections.append(current_section)
1015 current_section = {}
1016 self.sections.append(current_section)
1017
1018 def __str__(self):
1019 return 'CpuInfo({})'.format(self.cpu_names)
1020
1021 __repr__ = __str__
1022
1023
1024class KernelVersion(object):
1025
1026 def __init__(self, version_string):
1027 if ' #' in version_string:
1028 release, version = version_string.split(' #')
1029 self.release = release
1030 self.version = version
1031 elif version_string.startswith('#'):
1032 self.release = ''
1033 self.version = version_string
1034 else:
1035 self.release = version_string
1036 self.version = ''
1037
1038 def __str__(self):
1039 return '{} {}'.format(self.release, self.version)
1040
1041 __repr__ = __str__
1042
1043
1044class KernelConfig(object):
1045
1046 not_set_regex = re.compile(r'# (\S+) is not set')
1047
1048 @staticmethod
1049 def get_config_name(name):
1050 name = name.upper()
1051 if not name.startswith('CONFIG_'):
1052 name = 'CONFIG_' + name
1053 return name
1054
1055 def iteritems(self):
1056 return self._config.iteritems()
1057
1058 def __init__(self, text):
1059 self.text = text
1060 self._config = {}
1061 for line in text.split('\n'):
1062 line = line.strip()
1063 if line.startswith('#'):
1064 match = self.not_set_regex.search(line)
1065 if match:
1066 self._config[match.group(1)] = 'n'
1067 elif '=' in line:
1068 name, value = line.split('=', 1)
1069 self._config[name.strip()] = value.strip()
1070
1071 def get(self, name):
1072 return self._config.get(self.get_config_name(name))
1073
1074 def like(self, name):
1075 regex = re.compile(name, re.I)
1076 result = {}
1077 for k, v in self._config.iteritems():
1078 if regex.search(k):
1079 result[k] = v
1080 return result
1081
1082 def is_enabled(self, name):
1083 return self.get(name) == 'y'
1084
1085 def is_module(self, name):
1086 return self.get(name) == 'm'
1087
1088 def is_not_set(self, name):
1089 return self.get(name) == 'n'
1090
1091 def has(self, name):
1092 return self.get(name) in ['m', 'y']
1093
1094
1095class LocalLinuxTarget(LinuxTarget):
1096
1097 conn_cls = LocalConnection
1098
Sergei Trofimov961f9572015-11-18 17:32:26 +00001099 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001100 if self.working_directory is None:
1101 self.working_directory = '/tmp'
1102 if self.executables_directory is None:
1103 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001104
1105
1106def _get_model_name(section):
1107 name_string = section['model name']
1108 parts = name_string.split('@')[0].strip().split()
1109 return ' '.join([p for p in parts
1110 if '(' not in p and p != 'CPU'])
1111
1112
1113def _get_part_name(section):
1114 implementer = section.get('CPU implementer', '0x0')
1115 part = section['CPU part']
1116 variant = section.get('CPU variant', '0x0')
1117 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1118 if name is None:
1119 name = '{}/{}/{}'.format(implementer, part, variant)
1120 return name