blob: 98bb268b5b49ac89c0da5d7c092f3cea72b77a82 [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
672
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100673 def reset(self, fastboot=False): # pylint: disable=arguments-differ
674 try:
675 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
676 as_root=self.is_rooted, timeout=2)
677 except (TargetError, TimeoutError, subprocess.CalledProcessError):
678 # on some targets "reboot" doesn't return gracefully
679 pass
680
681 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
682 start = time.time()
683 device = self.connection_settings.get('device')
684 if device and ':' in device:
685 # ADB does not automatically remove a network device from it's
686 # devices list when the connection is broken by the remote, so the
687 # adb connection may have gone "stale", resulting in adb blocking
688 # indefinitely when making calls to the device. To avoid this,
689 # always disconnect first.
690 adb_disconnect(device)
691 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100692
693 if check_boot_completed:
694 boot_completed = boolean(self.getprop('sys.boot_completed'))
695 while not boot_completed and timeout >= time.time() - start:
696 time.sleep(5)
697 boot_completed = boolean(self.getprop('sys.boot_completed'))
698 if not boot_completed:
699 raise TargetError('Connected but Android did not fully boot.')
700
701 def setup(self, executables=None):
702 super(AndroidTarget, self).setup(executables)
703 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
704
705 def kick_off(self, command, as_root=False):
706 """
707 Like execute but closes adb session and returns immediately, leaving the command running on the
708 device (this is different from execute(background=True) which keeps adb connection open and returns
709 a subprocess object).
710
711 .. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.
712
713 """
714 if not self.is_rooted:
715 raise TargetError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.')
716 try:
717 command = 'cd {} && {} nohup {}'.format(self.working_directory, self.bin('busybox'), command)
718 output = self.execute(command, timeout=1, as_root=as_root)
719 except TimeoutError:
720 pass
721 else:
722 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
723
724 def list_directory(self, path, as_root=False):
725 contents = self.execute('ls {}'.format(path), as_root=as_root)
726 return [x.strip() for x in contents.split('\n') if x.strip()]
727
728 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
729 ext = os.path.splitext(filepath)[1].lower()
730 if ext == '.apk':
731 return self.install_apk(filepath, timeout)
732 else:
733 return self.install_executable(filepath, with_name)
734
735 def uninstall(self, name):
736 if self.package_is_installed(name):
737 self.uninstall_package(name)
738 else:
739 self.uninstall_executable(name)
740
741 def get_pids_of(self, process_name):
742 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
743 if result and 'not found' not in result:
744 return [int(x.split()[1]) for x in result.split('\n')[1:]]
745 else:
746 return []
747
748 def ps(self, **kwargs):
749 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
750 lines.next() # header
751 result = []
752 for line in lines:
753 parts = line.split()
754 if parts:
755 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
756 if not kwargs:
757 return result
758 else:
759 filtered_result = []
760 for entry in result:
761 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
762 filtered_result.append(entry)
763 return filtered_result
764
765 def capture_screen(self, filepath):
766 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
767 self.execute('screencap -p {}'.format(on_device_file))
768 self.pull(on_device_file, filepath)
769 self.remove(on_device_file)
770
771 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
772 if not as_root:
773 self.conn.push(source, dest, timeout=timeout)
774 else:
775 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
776 self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
777 self.conn.push(source, device_tempfile, timeout=timeout)
778 self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True)
779
780 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
781 if not as_root:
782 self.conn.pull(source, dest, timeout=timeout)
783 else:
784 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
785 self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
786 self.execute('cp {} {}'.format(source, device_tempfile), as_root=True)
787 self.conn.pull(device_tempfile, dest, timeout=timeout)
788
789 # Android-specific
790
791 def swipe_to_unlock(self):
792 width, height = self.screen_resolution
793 swipe_heigh = height * 2 // 3
794 start = 100
795 stop = width - start
796 command = 'input swipe {} {} {} {}'
797 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
798
799 def getprop(self, prop=None):
800 props = AndroidProperties(self.execute('getprop'))
801 if prop:
802 return props[prop]
803 return props
804
805 def is_installed(self, name):
806 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
807
808 def package_is_installed(self, package_name):
809 return package_name in self.list_packages()
810
811 def list_packages(self):
812 output = self.execute('pm list packages')
813 output = output.replace('package:', '')
814 return output.split()
815
816 def get_package_version(self, package):
817 output = self.execute('dumpsys package {}'.format(package))
818 for line in convert_new_lines(output).split('\n'):
819 if 'versionName' in line:
820 return line.split('=', 1)[1]
821 return None
822
823 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
824 ext = os.path.splitext(filepath)[1].lower()
825 if ext == '.apk':
826 return adb_command(self.adb_name, "install {}".format(filepath), timeout=timeout)
827 else:
828 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
829
830 def install_executable(self, filepath, with_name=None):
831 self._ensure_executables_directory_is_writable()
832 executable_name = with_name or os.path.basename(filepath)
833 on_device_file = self.path.join(self.working_directory, executable_name)
834 on_device_executable = self.path.join(self.executables_directory, executable_name)
835 self.push(filepath, on_device_file)
836 if on_device_file != on_device_executable:
837 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
838 self.remove(on_device_file, as_root=self.is_rooted)
839 self.execute('chmod 0777 {}'.format(on_device_executable), as_root=self.is_rooted)
840 self._installed_binaries[executable_name] = on_device_executable
841 return on_device_executable
842
843 def uninstall_package(self, package):
844 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
845
846 def uninstall_executable(self, executable_name):
847 on_device_executable = self.path.join(self.executables_directory, executable_name)
848 self._ensure_executables_directory_is_writable()
849 self.remove(on_device_executable, as_root=self.is_rooted)
850
851 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
852 op = '>>' if append == True else '>'
853 filtstr = ' -s {}'.format(filter) if filter else ''
854 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
855 adb_command(self.adb_name, command, timeout=timeout)
856
857 def clear_logcat(self):
858 adb_command(self.adb_name, 'logcat -c', timeout=30)
859
860 def is_screen_on(self):
861 output = self.execute('dumpsys power')
862 match = ANDROID_SCREEN_STATE_REGEX.search(output)
863 if match:
864 return boolean(match.group(1))
865 else:
866 raise TargetError('Could not establish screen state.')
867
868 def ensure_screen_is_on(self):
869 if not self.is_screen_on():
870 self.execute('input keyevent 26')
871
Sergei Trofimov961f9572015-11-18 17:32:26 +0000872 def _resolve_paths(self):
873 if self.working_directory is None:
874 self.working_directory = '/data/local/tmp/devlib-target'
875 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
876 if self.executables_directory is None:
877 self.executables_directory = self.path.join(self.working_directory, 'bin')
878
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100879 def _ensure_executables_directory_is_writable(self):
880 matched = []
881 for entry in self.list_file_systems():
882 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
883 matched.append(entry)
884 if matched:
885 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
886 if 'rw' not in entry.options:
887 self.execute('mount -o rw,remount {} {}'.format(entry.device,
888 entry.mount_point),
889 as_root=True)
890 else:
891 message = 'Could not find mount point for executables directory {}'
892 raise TargetError(message.format(self.executables_directory))
893
894
895FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
896PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000897LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100898
899
900class Cpuinfo(object):
901
902 @property
903 @memoized
904 def architecture(self):
905 for section in self.sections:
906 if 'CPU architecture' in section:
907 return section['CPU architecture']
908 if 'architecture' in section:
909 return section['architecture']
910
911 @property
912 @memoized
913 def cpu_names(self):
914 cpu_names = []
915 global_name = None
916 for section in self.sections:
917 if 'processor' in section:
918 if 'CPU part' in section:
919 cpu_names.append(_get_part_name(section))
920 elif 'model name' in section:
921 cpu_names.append(_get_model_name(section))
922 else:
923 cpu_names.append(None)
924 elif 'CPU part' in section:
925 global_name = _get_part_name(section)
926 return [caseless_string(c or global_name) for c in cpu_names]
927
928 def __init__(self, text):
929 self.sections = None
930 self.text = None
931 self.parse(text)
932
933 @memoized
934 def get_cpu_features(self, cpuid=0):
935 global_features = []
936 for section in self.sections:
937 if 'processor' in section:
938 if int(section.get('processor')) != cpuid:
939 continue
940 if 'Features' in section:
941 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +0000942 elif 'flags' in section:
943 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100944 elif 'Features' in section:
945 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +0000946 elif 'flags' in section:
947 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100948 return global_features
949
950 def parse(self, text):
951 self.sections = []
952 current_section = {}
953 self.text = text.strip()
954 for line in self.text.split('\n'):
955 line = line.strip()
956 if line:
957 key, value = line.split(':', 1)
958 current_section[key.strip()] = value.strip()
959 else: # not line
960 self.sections.append(current_section)
961 current_section = {}
962 self.sections.append(current_section)
963
964 def __str__(self):
965 return 'CpuInfo({})'.format(self.cpu_names)
966
967 __repr__ = __str__
968
969
970class KernelVersion(object):
971
972 def __init__(self, version_string):
973 if ' #' in version_string:
974 release, version = version_string.split(' #')
975 self.release = release
976 self.version = version
977 elif version_string.startswith('#'):
978 self.release = ''
979 self.version = version_string
980 else:
981 self.release = version_string
982 self.version = ''
983
984 def __str__(self):
985 return '{} {}'.format(self.release, self.version)
986
987 __repr__ = __str__
988
989
990class KernelConfig(object):
991
992 not_set_regex = re.compile(r'# (\S+) is not set')
993
994 @staticmethod
995 def get_config_name(name):
996 name = name.upper()
997 if not name.startswith('CONFIG_'):
998 name = 'CONFIG_' + name
999 return name
1000
1001 def iteritems(self):
1002 return self._config.iteritems()
1003
1004 def __init__(self, text):
1005 self.text = text
1006 self._config = {}
1007 for line in text.split('\n'):
1008 line = line.strip()
1009 if line.startswith('#'):
1010 match = self.not_set_regex.search(line)
1011 if match:
1012 self._config[match.group(1)] = 'n'
1013 elif '=' in line:
1014 name, value = line.split('=', 1)
1015 self._config[name.strip()] = value.strip()
1016
1017 def get(self, name):
1018 return self._config.get(self.get_config_name(name))
1019
1020 def like(self, name):
1021 regex = re.compile(name, re.I)
1022 result = {}
1023 for k, v in self._config.iteritems():
1024 if regex.search(k):
1025 result[k] = v
1026 return result
1027
1028 def is_enabled(self, name):
1029 return self.get(name) == 'y'
1030
1031 def is_module(self, name):
1032 return self.get(name) == 'm'
1033
1034 def is_not_set(self, name):
1035 return self.get(name) == 'n'
1036
1037 def has(self, name):
1038 return self.get(name) in ['m', 'y']
1039
1040
1041class LocalLinuxTarget(LinuxTarget):
1042
1043 conn_cls = LocalConnection
1044
Sergei Trofimov961f9572015-11-18 17:32:26 +00001045 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001046 if self.working_directory is None:
1047 self.working_directory = '/tmp'
1048 if self.executables_directory is None:
1049 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001050
1051
1052def _get_model_name(section):
1053 name_string = section['model name']
1054 parts = name_string.split('@')[0].strip().split()
1055 return ' '.join([p for p in parts
1056 if '(' not in p and p != 'CPU'])
1057
1058
1059def _get_part_name(section):
1060 implementer = section.get('CPU implementer', '0x0')
1061 part = section['CPU part']
1062 variant = section.get('CPU variant', '0x0')
1063 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1064 if name is None:
1065 name = '{}/{}/{}'.format(implementer, part, variant)
1066 return name
1067