blob: 6132f2bd5782b304558d9de38cd21af958ae1ea4 [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001import os
2import re
3import time
4import logging
5import posixpath
6import subprocess
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +01007import tarfile
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01008import tempfile
9import threading
10from collections import namedtuple
11
12from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
13from devlib.module import get_module
14from devlib.platform import Platform
15from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError
16from devlib.utils.ssh import SshConnection
Valentin Schneider7c2fd872017-09-11 16:58:24 +010017from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, adb_disconnect
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010018from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists
19from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes
20from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
21
22
Sebastian Goscik040daab2016-02-23 10:27:45 +000023FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
Sebastian Goscik1890db72016-02-15 14:43:30 +000024ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010025 re.IGNORECASE)
26ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
27 r'\s+(?P<width>\d+)x(?P<height>\d+)')
28DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
29 re.MULTILINE)
Patrick Bellasi9a8d5392017-02-17 15:28:07 +000030KVERSION_REGEX =re.compile(
Brendan Jackman66656932017-02-20 18:29:49 +000031 r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(.*-g(?P<sha1>[0-9a-fA-F]{7,}))?'
Patrick Bellasi9a8d5392017-02-17 15:28:07 +000032)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010033
34
35class Target(object):
36
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010037 path = None
38 os = None
39
40 default_modules = [
41 'hotplug',
42 'cpufreq',
43 'cpuidle',
44 'cgroups',
45 'hwmon',
46 ]
47
48 @property
49 def core_names(self):
50 return self.platform.core_names
51
52 @property
53 def core_clusters(self):
54 return self.platform.core_clusters
55
56 @property
57 def big_core(self):
58 return self.platform.big_core
59
60 @property
61 def little_core(self):
62 return self.platform.little_core
63
64 @property
65 def is_connected(self):
66 return self.conn is not None
67
68 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010069 def connected_as_root(self):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +000070 if self._connected_as_root is None:
71 result = self.execute('id')
72 self._connected_as_root = 'uid=0(' in result
73 return self._connected_as_root
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010074
75 @property
76 @memoized
77 def is_rooted(self):
78 if self.connected_as_root:
79 return True
80 try:
81 self.execute('ls /', timeout=2, as_root=True)
82 return True
83 except (TargetError, TimeoutError):
84 return False
85
86 @property
87 @memoized
Javi Merino16d87c62016-06-23 14:55:19 +010088 def needs_su(self):
89 return not self.connected_as_root and self.is_rooted
90
91 @property
92 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010093 def kernel_version(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +000094 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010095
96 @property
97 def os_version(self): # pylint: disable=no-self-use
98 return {}
99
100 @property
101 def abi(self): # pylint: disable=no-self-use
102 return None
103
104 @property
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100105 def supported_abi(self):
106 return [self.abi]
107
108 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100109 @memoized
110 def cpuinfo(self):
111 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
112
113 @property
114 @memoized
115 def number_of_cpus(self):
116 num_cpus = 0
117 corere = re.compile(r'^\s*cpu\d+\s*$')
118 output = self.execute('ls /sys/devices/system/cpu')
119 for entry in output.split():
120 if corere.match(entry):
121 num_cpus += 1
122 return num_cpus
123
124 @property
125 @memoized
126 def config(self):
127 try:
128 return KernelConfig(self.execute('zcat /proc/config.gz'))
129 except TargetError:
130 for path in ['/boot/config', '/boot/config-$(uname -r)']:
131 try:
132 return KernelConfig(self.execute('cat {}'.format(path)))
133 except TargetError:
134 pass
135 return KernelConfig('')
136
137 @property
138 @memoized
139 def user(self):
140 return self.getenv('USER')
141
142 @property
143 def conn(self):
144 if self._connections:
145 tid = id(threading.current_thread())
146 if tid not in self._connections:
147 self._connections[tid] = self.get_connection()
148 return self._connections[tid]
149 else:
150 return None
151
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100152 @property
153 def shutils(self):
154 if self._shutils is None:
155 self._setup_shutils()
156 return self._shutils
157
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100158 def __init__(self,
159 connection_settings=None,
160 platform=None,
161 working_directory=None,
162 executables_directory=None,
163 connect=True,
164 modules=None,
165 load_default_modules=True,
166 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000167 conn_cls=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100168 ):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +0000169 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100170 self.connection_settings = connection_settings or {}
Anouk Van Laer21f40032017-01-31 13:11:03 +0000171 # Set self.platform: either it's given directly (by platform argument)
172 # or it's given in the connection_settings argument
173 # If neither, create default Platform()
174 if platform is None:
175 self.platform = self.connection_settings.get('platform', Platform())
176 else:
177 self.platform = platform
178 # Check if the user hasn't given two different platforms
179 if 'platform' in self.connection_settings:
180 if connection_settings['platform'] is not platform:
181 raise TargetError('Platform specified in connection_settings '
182 '({}) differs from that directly passed '
183 '({})!)'
184 .format(connection_settings['platform'],
185 self.platform))
186 self.connection_settings['platform'] = self.platform
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100187 self.working_directory = working_directory
188 self.executables_directory = executables_directory
189 self.modules = modules or []
190 self.load_default_modules = load_default_modules
191 self.shell_prompt = shell_prompt
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000192 self.conn_cls = conn_cls
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100193 self.logger = logging.getLogger(self.__class__.__name__)
194 self._installed_binaries = {}
195 self._installed_modules = {}
196 self._cache = {}
197 self._connections = {}
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100198 self._shutils = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100199 self.busybox = None
200
201 if load_default_modules:
202 module_lists = [self.default_modules]
203 else:
204 module_lists = []
205 module_lists += [self.modules, self.platform.modules]
206 self.modules = merge_lists(*module_lists, duplicates='first')
207 self._update_modules('early')
208 if connect:
209 self.connect()
210
211 # connection and initialization
212
213 def connect(self, timeout=None):
214 self.platform.init_target_connection(self)
215 tid = id(threading.current_thread())
216 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000217 self._resolve_paths()
Brendan Jackman17bcabd2017-10-09 15:15:24 +0100218 self.execute('mkdir -p {}'.format(self.working_directory))
219 self.execute('mkdir -p {}'.format(self.executables_directory))
220 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100221 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100222 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100223 if self.platform.big_core and self.load_default_modules:
224 self._install_module(get_module('bl'))
225
226 def disconnect(self):
227 for conn in self._connections.itervalues():
228 conn.close()
229 self._connections = {}
230
231 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000232 if self.conn_cls == None:
233 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100234 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
235
236 def setup(self, executables=None):
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100237 self._setup_shutils()
Patrick Bellasif2eac512015-11-27 16:35:57 +0000238
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100239 for host_exe in (executables or []): # pylint: disable=superfluous-parens
240 self.install(host_exe)
241
Anouk Van Laer21f40032017-01-31 13:11:03 +0000242 # Check for platform dependent setup procedures
243 self.platform.setup(self)
244
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100245 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
246 self._update_modules('setup')
247
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100248 def reboot(self, hard=False, connect=True, timeout=180):
249 if hard:
250 if not self.has('hard_reset'):
251 raise TargetError('Hard reset not supported for this target.')
252 self.hard_reset() # pylint: disable=no-member
253 else:
254 if not self.is_connected:
255 message = 'Cannot reboot target becuase it is disconnected. ' +\
256 'Either connect() first, or specify hard=True ' +\
257 '(in which case, a hard_reset module must be installed)'
258 raise TargetError(message)
259 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000260 # Wait a fixed delay before starting polling to give the target time to
261 # shut down, otherwise, might create the connection while it's still shutting
262 # down resulting in subsequenct connection failing.
263 self.logger.debug('Waiting for target to power down...')
264 reset_delay = 20
265 time.sleep(reset_delay)
266 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100267 if self.has('boot'):
268 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000269 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100270 if connect:
271 self.connect(timeout=timeout)
272
273 # file transfer
274
275 def push(self, source, dest, timeout=None):
276 return self.conn.push(source, dest, timeout=timeout)
277
278 def pull(self, source, dest, timeout=None):
279 return self.conn.pull(source, dest, timeout=timeout)
280
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +0100281 def get_directory(self, source_dir, dest):
282 """ Pull a directory from the device, after compressing dir """
283 # Create all file names
284 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
285 # Host location of dir
286 outdir = os.path.join(dest, tar_file_name)
287 # Host location of archive
288 tar_file_name = '{}.tar'.format(tar_file_name)
289 tempfile = os.path.join(dest, tar_file_name)
290
291 # Does the folder exist?
292 self.execute('ls -la {}'.format(source_dir))
293 # Try compressing the folder
294 try:
295 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
296 source_dir))
297 except TargetError:
298 self.logger.debug('Failed to run tar command on target! ' \
299 'Not pulling directory {}'.format(source_dir))
300 # Pull the file
301 os.mkdir(outdir)
302 self.pull(tar_file_name, tempfile )
303 # Decompress
304 f = tarfile.open(tempfile, 'r')
305 f.extractall(outdir)
306 os.remove(tempfile)
307
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100308 # execution
309
310 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
setrofimb7386552017-05-23 17:39:12 +0100311 return self.conn.execute(command, timeout, check_exit_code, as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100312
313 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
314 return self.conn.background(command, stdout, stderr, as_root)
315
316 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
317 as_root=False, timeout=30):
318 """
319 Executes the specified binary under the specified conditions.
320
321 :binary: binary to execute. Must be present and executable on the device.
322 :args: arguments to be passed to the binary. The can be either a list or
323 a string.
324 :in_directory: execute the binary in the specified directory. This must
325 be an absolute path.
326 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
327 case, it will be interpreted as the mask), a list of ``ints``, in which
328 case this will be interpreted as the list of cpus, or string, which
329 will be interpreted as a comma-separated list of cpu ranges, e.g.
330 ``"0,4-7"``.
331 :as_root: Specify whether the command should be run as root
332 :timeout: If the invocation does not terminate within this number of seconds,
333 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
334 invocation should not timeout.
335
Brendan Jackman27f545f2016-11-15 16:58:57 +0000336 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100337 """
338 command = binary
339 if args:
340 if isiterable(args):
341 args = ' '.join(args)
342 command = '{} {}'.format(command, args)
343 if on_cpus:
344 on_cpus = bitmask(on_cpus)
345 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
346 if in_directory:
347 command = 'cd {} && {}'.format(in_directory, command)
348 return self.execute(command, as_root=as_root, timeout=timeout)
349
Valentin Schneider92b0c252017-07-06 16:01:15 +0100350 def background_invoke(self, binary, args=None, in_directory=None,
351 on_cpus=None, as_root=False):
352 """
353 Executes the specified binary as a background task under the
354 specified conditions.
355
356 :binary: binary to execute. Must be present and executable on the device.
357 :args: arguments to be passed to the binary. The can be either a list or
358 a string.
359 :in_directory: execute the binary in the specified directory. This must
360 be an absolute path.
361 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
362 case, it will be interpreted as the mask), a list of ``ints``, in which
363 case this will be interpreted as the list of cpus, or string, which
364 will be interpreted as a comma-separated list of cpu ranges, e.g.
365 ``"0,4-7"``.
366 :as_root: Specify whether the command should be run as root
367
368 :returns: the subprocess instance handling that command
369 """
370 command = binary
371 if args:
372 if isiterable(args):
373 args = ' '.join(args)
374 command = '{} {}'.format(command, args)
375 if on_cpus:
376 on_cpus = bitmask(on_cpus)
377 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
378 if in_directory:
379 command = 'cd {} && {}'.format(in_directory, command)
380 return self.background(command, as_root=as_root)
381
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100382 def kick_off(self, command, as_root=False):
383 raise NotImplementedError()
384
385 # sysfs interaction
386
387 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100388 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100389 if kind:
390 return kind(output)
391 else:
392 return output
393
394 def read_int(self, path):
395 return self.read_value(path, kind=integer)
396
397 def read_bool(self, path):
398 return self.read_value(path, kind=boolean)
399
400 def write_value(self, path, value, verify=True):
401 value = str(value)
402 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
403 if verify:
404 output = self.read_value(path)
405 if not output == value:
406 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
407 raise TargetError(message)
408
409 def reset(self):
410 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100411 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100412 except (TargetError, TimeoutError, subprocess.CalledProcessError):
413 # on some targets "reboot" doesn't return gracefully
414 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000415 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100416
417 def check_responsive(self):
418 try:
419 self.conn.execute('ls /', timeout=5)
420 except (TimeoutError, subprocess.CalledProcessError):
421 raise TargetNotRespondingError(self.conn.name)
422
423 # process management
424
425 def kill(self, pid, signal=None, as_root=False):
426 signal_string = '-s {}'.format(signal) if signal else ''
427 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
428
429 def killall(self, process_name, signal=None, as_root=False):
430 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000431 try:
432 self.kill(pid, signal=signal, as_root=as_root)
433 except TargetError:
434 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100435
436 def get_pids_of(self, process_name):
437 raise NotImplementedError()
438
439 def ps(self, **kwargs):
440 raise NotImplementedError()
441
442 # files
443
444 def file_exists(self, filepath):
445 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000446 output = self.execute(command.format(filepath), as_root=self.is_rooted)
447 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100448
Sebastian Goscik33603c62016-02-15 15:07:19 +0000449 def directory_exists(self, filepath):
450 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
451 # output from ssh my contain part of the expression in the buffer,
452 # split out everything except the last word.
453 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
454
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100455 def list_file_systems(self):
456 output = self.execute('mount')
457 fstab = []
458 for line in output.split('\n'):
459 line = line.strip()
460 if not line:
461 continue
462 match = FSTAB_ENTRY_REGEX.search(line)
463 if match:
464 fstab.append(FstabEntry(match.group(1), match.group(2),
465 match.group(3), match.group(4),
466 None, None))
467 else: # assume pre-M Android
468 fstab.append(FstabEntry(*line.split()))
469 return fstab
470
471 def list_directory(self, path, as_root=False):
472 raise NotImplementedError()
473
474 def get_workpath(self, name):
475 return self.path.join(self.working_directory, name)
476
477 def tempfile(self, prefix='', suffix=''):
478 names = tempfile._get_candidate_names() # pylint: disable=W0212
479 for _ in xrange(tempfile.TMP_MAX):
480 name = names.next()
481 path = self.get_workpath(prefix + name + suffix)
482 if not self.file_exists(path):
483 return path
484 raise IOError('No usable temporary filename found')
485
486 def remove(self, path, as_root=False):
487 self.execute('rm -rf {}'.format(path), as_root=as_root)
488
489 # misc
490 def core_cpus(self, core):
491 return [i for i, c in enumerate(self.core_names) if c == core]
492
493 def list_online_cpus(self, core=None):
494 path = self.path.join('/sys/devices/system/cpu/online')
495 output = self.read_value(path)
496 all_online = ranges_to_list(output)
497 if core:
498 cpus = self.core_cpus(core)
499 if not cpus:
500 raise ValueError(core)
501 return [o for o in all_online if o in cpus]
502 else:
503 return all_online
504
505 def list_offline_cpus(self):
506 online = self.list_online_cpus()
507 return [c for c in xrange(self.number_of_cpus)
508 if c not in online]
509
510 def getenv(self, variable):
511 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
512
513 def capture_screen(self, filepath):
514 raise NotImplementedError()
515
516 def install(self, filepath, timeout=None, with_name=None):
517 raise NotImplementedError()
518
519 def uninstall(self, name):
520 raise NotImplementedError()
521
Sebastian Goscik84151f92016-02-15 15:09:27 +0000522 def get_installed(self, name, search_system_binaries=True):
523 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000524 if self.file_exists(self.executables_directory):
525 if name in self.list_directory(self.executables_directory):
526 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000527 # Fall back to binaries in PATH
528 if search_system_binaries:
529 for path in self.getenv('PATH').split(self.path.pathsep):
530 try:
531 if name in self.list_directory(path):
532 return self.path.join(path, name)
533 except TargetError:
534 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100535
536 which = get_installed
537
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000538 def install_if_needed(self, host_path, search_system_binaries=True):
539
540 binary_path = self.get_installed(os.path.split(host_path)[1],
541 search_system_binaries=search_system_binaries)
542 if not binary_path:
543 binary_path = self.install(host_path)
544 return binary_path
545
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100546 def is_installed(self, name):
547 return bool(self.get_installed(name))
548
549 def bin(self, name):
550 return self._installed_binaries.get(name, name)
551
552 def has(self, modname):
553 return hasattr(self, identifier(modname))
554
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000555 def lsmod(self):
556 lines = self.execute('lsmod').splitlines()
557 entries = []
558 for line in lines[1:]: # first line is the header
559 if not line.strip():
560 continue
561 parts = line.split()
562 name = parts[0]
563 size = int(parts[1])
564 use_count = int(parts[2])
565 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000566 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000567 else:
568 used_by = []
569 entries.append(LsmodEntry(name, size, use_count, used_by))
570 return entries
571
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000572 def insmod(self, path):
573 target_path = self.get_workpath(os.path.basename(path))
574 self.push(path, target_path)
575 self.execute('insmod {}'.format(target_path), as_root=True)
576
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100577
578 def extract(self, path, dest=None):
579 """
580 Extact the specified on-target file. The extraction method to be used
581 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
582 If ``dest`` is specified, it must be an existing directory on target;
583 the extracted contents will be placed there.
584
585 Note that, depending on the archive file format (and therfore the
586 extraction method used), the original archive file may or may not exist
587 after the extraction.
588
589 The return value is the path to the extracted contents. In case of
590 gunzip and bunzip2, this will be path to the extracted file; for tar
591 and uzip, this will be the directory with the extracted file(s)
592 (``dest`` if it was specified otherwise, the directory that cotained
593 the archive).
594
595 """
596 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
597 '.tgz', '.tbz', '.tbz2']:
598 if path.endswith(ending):
599 return self._extract_archive(path, 'tar xf {} -C {}', dest)
600
601 ext = self.path.splitext(path)[1]
602 if ext in ['.bz', '.bz2']:
603 return self._extract_file(path, 'bunzip2 -f {}', dest)
604 elif ext == '.gz':
605 return self._extract_file(path, 'gunzip -f {}', dest)
606 elif ext == '.zip':
607 return self._extract_archive(path, 'unzip {} -d {}', dest)
608 else:
609 raise ValueError('Unknown compression format: {}'.format(ext))
610
Sergei Trofimov69a83d42017-05-12 11:54:31 +0100611 def sleep(self, duration):
612 timeout = duration + 10
613 self.execute('sleep {}'.format(duration), timeout=timeout)
614
Sergei Trofimov181bc182017-10-03 16:28:09 +0100615 def read_tree_values_flat(self, path, depth=1, check_exit_code=True):
616 command = 'read_tree_values {} {}'.format(path, depth)
617 output = self._execute_util(command, as_root=self.is_rooted,
618 check_exit_code=check_exit_code)
619 result = {}
620 for entry in output.strip().split('\n'):
Sergei Trofimov7e073c12017-10-06 13:37:00 +0100621 if ':' not in entry:
Sergei Trofimovd560aea2017-10-05 09:35:11 +0100622 continue
Sergei Trofimov181bc182017-10-03 16:28:09 +0100623 path, value = entry.strip().split(':', 1)
624 result[path] = value
625 return result
626
627 def read_tree_values(self, path, depth=1, dictcls=dict, check_exit_code=True):
628 value_map = self.read_tree_values_flat(path, depth, check_exit_code)
629 return _build_path_tree(value_map, path, self.path.sep, dictcls)
630
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100631 # internal methods
632
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100633 def _setup_shutils(self):
634 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
635 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
636 shell_path = '/bin/sh'
637 if self.os == 'android':
638 shell_path = '/system/bin/sh'
639 with open(shutils_ifile) as fh:
640 lines = fh.readlines()
641 with open(shutils_ofile, 'w') as ofile:
642 for line in lines:
643 line = line.replace("__DEVLIB_SHELL__", shell_path)
644 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
645 ofile.write(line)
646 self._shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
647
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100648 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
649 command = '{} {}'.format(self.shutils, command)
650 return self.conn.execute(command, timeout, check_exit_code, as_root)
651
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100652 def _extract_archive(self, path, cmd, dest=None):
653 cmd = '{} ' + cmd # busybox
654 if dest:
655 extracted = dest
656 else:
657 extracted = self.path.dirname(path)
658 cmdtext = cmd.format(self.busybox, path, extracted)
659 self.execute(cmdtext)
660 return extracted
661
662 def _extract_file(self, path, cmd, dest=None):
663 cmd = '{} ' + cmd # busybox
664 cmdtext = cmd.format(self.busybox, path)
665 self.execute(cmdtext)
666 extracted = self.path.splitext(path)[0]
667 if dest:
668 self.execute('mv -f {} {}'.format(extracted, dest))
669 if dest.endswith('/'):
670 extracted = self.path.join(dest, self.path.basename(extracted))
671 else:
672 extracted = dest
673 return extracted
674
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100675 def _update_modules(self, stage):
676 for mod in self.modules:
677 if isinstance(mod, dict):
678 mod, params = mod.items()[0]
679 else:
680 params = {}
681 mod = get_module(mod)
682 if not mod.stage == stage:
683 continue
684 if mod.probe(self):
685 self._install_module(mod, **params)
686 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000687 msg = 'Module {} is not supported by the target'.format(mod.name)
688 if self.load_default_modules:
689 self.logger.debug(msg)
690 else:
691 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100692
693 def _install_module(self, mod, **params):
694 if mod.name not in self._installed_modules:
695 self.logger.debug('Installing module {}'.format(mod.name))
696 mod.install(self, **params)
697 self._installed_modules[mod.name] = mod
698 else:
699 self.logger.debug('Module {} is already installed.'.format(mod.name))
700
Sergei Trofimov961f9572015-11-18 17:32:26 +0000701 def _resolve_paths(self):
702 raise NotImplementedError()
703
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100704
705class LinuxTarget(Target):
706
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100707 path = posixpath
708 os = 'linux'
709
710 @property
711 @memoized
712 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100713 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100714 for abi, architectures in ABI_MAP.iteritems():
715 if value in architectures:
716 result = abi
717 break
718 else:
719 result = value
720 return result
721
722 @property
723 @memoized
724 def os_version(self):
725 os_version = {}
726 try:
727 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
728 version_files = self.execute(command, check_exit_code=False).strip().split()
729 for vf in version_files:
730 name = self.path.basename(vf)
731 output = self.read_value(vf)
732 os_version[name] = output.strip().replace('\n', ' ')
733 except TargetError:
734 raise
735 return os_version
736
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000737 @property
738 @memoized
739 # There is currently no better way to do this cross platform.
740 # ARM does not have dmidecode
741 def model(self):
742 if self.file_exists("/proc/device-tree/model"):
743 raw_model = self.execute("cat /proc/device-tree/model")
744 return '_'.join(raw_model.split()[:2])
745 return None
746
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000747 def __init__(self,
748 connection_settings=None,
749 platform=None,
750 working_directory=None,
751 executables_directory=None,
752 connect=True,
753 modules=None,
754 load_default_modules=True,
755 shell_prompt=DEFAULT_SHELL_PROMPT,
756 conn_cls=SshConnection,
757 ):
758 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
759 platform=platform,
760 working_directory=working_directory,
761 executables_directory=executables_directory,
762 connect=connect,
763 modules=modules,
764 load_default_modules=load_default_modules,
765 shell_prompt=shell_prompt,
766 conn_cls=conn_cls)
767
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100768 def connect(self, timeout=None):
769 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100770
771 def kick_off(self, command, as_root=False):
772 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
773 return self.conn.execute(command, as_root=as_root)
774
775 def get_pids_of(self, process_name):
776 """Returns a list of PIDs of all processes with the specified name."""
777 # result should be a column of PIDs with the first row as "PID" header
778 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
779 check_exit_code=False).strip().split()
780 if len(result) >= 2: # at least one row besides the header
781 return map(int, result[1:])
782 else:
783 return []
784
785 def ps(self, **kwargs):
786 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
787 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
788 lines.next() # header
789
790 result = []
791 for line in lines:
792 parts = re.split(r'\s+', line, maxsplit=8)
793 if parts and parts != ['']:
794 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
795
796 if not kwargs:
797 return result
798 else:
799 filtered_result = []
800 for entry in result:
801 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
802 filtered_result.append(entry)
803 return filtered_result
804
805 def list_directory(self, path, as_root=False):
806 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
807 return [x.strip() for x in contents.split('\n') if x.strip()]
808
809 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
810 destpath = self.path.join(self.executables_directory,
811 with_name and with_name or self.path.basename(filepath))
812 self.push(filepath, destpath)
813 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
814 self._installed_binaries[self.path.basename(destpath)] = destpath
815 return destpath
816
817 def uninstall(self, name):
818 path = self.path.join(self.executables_directory, name)
819 self.remove(path)
820
821 def capture_screen(self, filepath):
822 if not self.is_installed('scrot'):
823 self.logger.debug('Could not take screenshot as scrot is not installed.')
824 return
825 try:
826
827 tmpfile = self.tempfile()
828 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
829 self.pull(tmpfile, filepath)
830 self.remove(tmpfile)
831 except TargetError as e:
832 if "Can't open X dispay." not in e.message:
833 raise e
834 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
835 self.logger.debug('Could not take screenshot: {}'.format(message))
836
Sergei Trofimov961f9572015-11-18 17:32:26 +0000837 def _resolve_paths(self):
838 if self.working_directory is None:
839 if self.connected_as_root:
840 self.working_directory = '/root/devlib-target'
841 else:
842 self.working_directory = '/home/{}/devlib-target'.format(self.user)
843 if self.executables_directory is None:
844 self.executables_directory = self.path.join(self.working_directory, 'bin')
845
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100846
847class AndroidTarget(Target):
848
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100849 path = posixpath
850 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100851 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100852
853 @property
854 @memoized
855 def abi(self):
856 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
857
858 @property
859 @memoized
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100860 def supported_abi(self):
861 props = self.getprop()
862 result = [props['ro.product.cpu.abi']]
863 if 'ro.product.cpu.abi2' in props:
864 result.append(props['ro.product.cpu.abi2'])
865 if 'ro.product.cpu.abilist' in props:
866 for abi in props['ro.product.cpu.abilist'].split(','):
867 if abi not in result:
868 result.append(abi)
869
870 mapped_result = []
871 for supported_abi in result:
872 for abi, architectures in ABI_MAP.iteritems():
873 found = False
874 if supported_abi in architectures and abi not in mapped_result:
875 mapped_result.append(abi)
876 found = True
877 break
878 if not found and supported_abi not in mapped_result:
879 mapped_result.append(supported_abi)
880 return mapped_result
881
882 @property
883 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100884 def os_version(self):
885 os_version = {}
886 for k, v in self.getprop().iteritems():
887 if k.startswith('ro.build.version'):
888 part = k.split('.')[-1]
889 os_version[part] = v
890 return os_version
891
892 @property
893 def adb_name(self):
894 return self.conn.device
895
896 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000897 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000898 def android_id(self):
899 """
900 Get the device's ANDROID_ID. Which is
901
902 "A 64-bit number (as a hex string) that is randomly generated when the user
903 first sets up the device and should remain constant for the lifetime of the
904 user's device."
905
906 .. note:: This will get reset on userdata erasure.
907
908 """
909 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
910 return output.split('value=')[-1]
911
912 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100913 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000914 def model(self):
915 try:
916 return self.getprop(prop='ro.product.device')
917 except KeyError:
918 return None
919
920 @property
921 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100922 def screen_resolution(self):
923 output = self.execute('dumpsys window')
924 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
925 if match:
926 return (int(match.group('width')),
927 int(match.group('height')))
928 else:
929 return (0, 0)
930
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000931 def __init__(self,
932 connection_settings=None,
933 platform=None,
934 working_directory=None,
935 executables_directory=None,
936 connect=True,
937 modules=None,
938 load_default_modules=True,
939 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000940 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000941 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000942 ):
943 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
944 platform=platform,
945 working_directory=working_directory,
946 executables_directory=executables_directory,
947 connect=connect,
948 modules=modules,
949 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000950 shell_prompt=shell_prompt,
951 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000952 self.package_data_directory = package_data_directory
Brendan Jackman8a0554f2017-10-09 17:08:38 +0100953 self.clear_logcat_lock = threading.Lock()
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000954
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100955 def reset(self, fastboot=False): # pylint: disable=arguments-differ
956 try:
957 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100958 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100959 except (TargetError, TimeoutError, subprocess.CalledProcessError):
960 # on some targets "reboot" doesn't return gracefully
961 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000962 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100963
Patrick Bellasif26f9422017-07-12 12:27:28 +0100964 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100965 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +0100966 boot_completed = boolean(self.getprop('sys.boot_completed'))
967 while not boot_completed and timeout >= time.time() - start:
968 time.sleep(5)
969 boot_completed = boolean(self.getprop('sys.boot_completed'))
970 if not boot_completed:
971 raise TargetError('Connected but Android did not fully boot.')
972
973 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100974 device = self.connection_settings.get('device')
975 if device and ':' in device:
976 # ADB does not automatically remove a network device from it's
977 # devices list when the connection is broken by the remote, so the
978 # adb connection may have gone "stale", resulting in adb blocking
979 # indefinitely when making calls to the device. To avoid this,
980 # always disconnect first.
981 adb_disconnect(device)
982 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100983
984 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +0100985 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100986
987 def setup(self, executables=None):
988 super(AndroidTarget, self).setup(executables)
989 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
990
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100991 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100992 """
993 Like execute but closes adb session and returns immediately, leaving the command running on the
994 device (this is different from execute(background=True) which keeps adb connection open and returns
995 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100996 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100997 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100998 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100999 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +01001000 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001001 output = self.execute(command, timeout=1, as_root=as_root)
1002 except TimeoutError:
1003 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001004
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001005 def __setup_list_directory(self):
1006 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
1007 # AOSP 7.0 as well, the ls command was changed.
1008 # Previous versions default to a single column listing, which is nice and easy to parse.
1009 # Newer versions default to a multi-column listing, which is not, but it does support
1010 # a '-1' option to get into single column mode. Older versions do not support this option
1011 # so we try the new version, and if it fails we use the old version.
1012 self.ls_command = 'ls -1'
1013 try:
Marc Bonnici06552372017-03-29 16:43:22 +01001014 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001015 except TargetError:
1016 self.ls_command = 'ls'
1017
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001018 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001019 if self.ls_command == '':
1020 self.__setup_list_directory()
1021 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001022 return [x.strip() for x in contents.split('\n') if x.strip()]
1023
1024 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1025 ext = os.path.splitext(filepath)[1].lower()
1026 if ext == '.apk':
1027 return self.install_apk(filepath, timeout)
1028 else:
1029 return self.install_executable(filepath, with_name)
1030
1031 def uninstall(self, name):
1032 if self.package_is_installed(name):
1033 self.uninstall_package(name)
1034 else:
1035 self.uninstall_executable(name)
1036
1037 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001038 result = []
1039 search_term = process_name[-15:]
1040 for entry in self.ps():
1041 if search_term in entry.name:
1042 result.append(entry.pid)
1043 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001044
1045 def ps(self, **kwargs):
1046 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1047 lines.next() # header
1048 result = []
1049 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001050 parts = line.split(None, 8)
Sergei Trofimov109fcc62017-09-26 13:30:15 +01001051 if not parts:
1052 continue
1053 if len(parts) == 8:
1054 # wchan was blank; insert an empty field where it should be.
1055 parts.insert(5, '')
1056 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001057 if not kwargs:
1058 return result
1059 else:
1060 filtered_result = []
1061 for entry in result:
1062 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1063 filtered_result.append(entry)
1064 return filtered_result
1065
1066 def capture_screen(self, filepath):
1067 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1068 self.execute('screencap -p {}'.format(on_device_file))
1069 self.pull(on_device_file, filepath)
1070 self.remove(on_device_file)
1071
1072 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1073 if not as_root:
1074 self.conn.push(source, dest, timeout=timeout)
1075 else:
1076 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001077 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001078 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001079 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001080
1081 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1082 if not as_root:
1083 self.conn.pull(source, dest, timeout=timeout)
1084 else:
1085 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001086 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1087 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001088 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001089 self.conn.pull(device_tempfile, dest, timeout=timeout)
1090
1091 # Android-specific
1092
Marc Bonnici8839ed02017-08-10 17:19:13 +01001093 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001094 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001095 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001096 if direction == "diagonal":
1097 start = 100
1098 stop = width - start
1099 swipe_height = height * 2 // 3
1100 self.execute(command.format(start, swipe_height, stop, 0))
1101 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001102 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001103 start = 100
1104 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001105 self.execute(command.format(start, swipe_height, stop, swipe_height))
1106 elif direction == "vertical":
1107 swipe_middle = width / 2
1108 swipe_height = height * 2 // 3
1109 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001110 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001111 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001112
1113 def getprop(self, prop=None):
1114 props = AndroidProperties(self.execute('getprop'))
1115 if prop:
1116 return props[prop]
1117 return props
1118
1119 def is_installed(self, name):
1120 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1121
1122 def package_is_installed(self, package_name):
1123 return package_name in self.list_packages()
1124
1125 def list_packages(self):
1126 output = self.execute('pm list packages')
1127 output = output.replace('package:', '')
1128 return output.split()
1129
1130 def get_package_version(self, package):
1131 output = self.execute('dumpsys package {}'.format(package))
1132 for line in convert_new_lines(output).split('\n'):
1133 if 'versionName' in line:
1134 return line.split('=', 1)[1]
1135 return None
1136
Marc Bonnicid3396f22017-05-31 15:56:50 +01001137 def get_sdk_version(self):
1138 try:
1139 return int(self.getprop('ro.build.version.sdk'))
1140 except (ValueError, TypeError):
1141 return None
1142
Marc Bonnicic33dd652017-05-31 15:51:31 +01001143 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001144 ext = os.path.splitext(filepath)[1].lower()
1145 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001146 flags = []
1147 if replace:
1148 flags.append('-r') # Replace existing APK
1149 if allow_downgrade:
1150 flags.append('-d') # Install the APK even if a newer version is already installed
1151 if self.get_sdk_version() >= 23:
1152 flags.append('-g') # Grant all runtime permissions
1153 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1154 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001155 else:
1156 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1157
1158 def install_executable(self, filepath, with_name=None):
1159 self._ensure_executables_directory_is_writable()
1160 executable_name = with_name or os.path.basename(filepath)
1161 on_device_file = self.path.join(self.working_directory, executable_name)
1162 on_device_executable = self.path.join(self.executables_directory, executable_name)
1163 self.push(filepath, on_device_file)
1164 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001165 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1166 self.remove(on_device_file, as_root=self.needs_su)
1167 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001168 self._installed_binaries[executable_name] = on_device_executable
1169 return on_device_executable
1170
1171 def uninstall_package(self, package):
1172 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1173
1174 def uninstall_executable(self, executable_name):
1175 on_device_executable = self.path.join(self.executables_directory, executable_name)
1176 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001177 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001178
1179 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001180 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001181 filtstr = ' -s {}'.format(filter) if filter else ''
1182 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1183 adb_command(self.adb_name, command, timeout=timeout)
1184
1185 def clear_logcat(self):
Brendan Jackman8a0554f2017-10-09 17:08:38 +01001186 with self.clear_logcat_lock:
1187 adb_command(self.adb_name, 'logcat -c', timeout=30)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001188
Valentin Schneider7c2fd872017-09-11 16:58:24 +01001189 def get_logcat_monitor(self, regexps=None):
1190 return LogcatMonitor(self, regexps)
1191
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001192 def adb_kill_server(self, timeout=30):
1193 adb_command(self.adb_name, 'kill-server', timeout)
1194
1195 def adb_wait_for_device(self, timeout=30):
1196 adb_command(self.adb_name, 'wait-for-device', timeout)
1197
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001198 def adb_reboot_bootloader(self, timeout=30):
1199 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1200
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001201 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001202 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001203 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001204 return
1205 adb_command(self.adb_name, 'root', timeout=30)
1206 self._connected_as_root = True
1207 return
1208 adb_command(self.adb_name, 'unroot', timeout=30)
1209 self._connected_as_root = False
1210
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001211 def is_screen_on(self):
1212 output = self.execute('dumpsys power')
1213 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1214 if match:
1215 return boolean(match.group(1))
1216 else:
1217 raise TargetError('Could not establish screen state.')
1218
1219 def ensure_screen_is_on(self):
1220 if not self.is_screen_on():
1221 self.execute('input keyevent 26')
1222
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001223 def ensure_screen_is_off(self):
1224 if self.is_screen_on():
1225 self.execute('input keyevent 26')
1226
Marc Bonniciddd2e292017-07-06 17:30:12 +01001227 def set_auto_brightness(self, auto_brightness):
1228 cmd = 'settings put system screen_brightness_mode {}'
1229 self.execute(cmd.format(int(boolean(auto_brightness))))
1230
1231 def get_auto_brightness(self):
1232 cmd = 'settings get system screen_brightness_mode'
1233 return boolean(self.execute(cmd).strip())
1234
1235 def set_brightness(self, value):
1236 if not 0 <= value <= 255:
1237 msg = 'Invalid brightness "{}"; Must be between 0 and 255'
1238 raise ValueError(msg.format(value))
1239 self.set_auto_brightness(False)
1240 cmd = 'settings put system screen_brightness {}'
1241 self.execute(cmd.format(int(value)))
1242
1243 def get_brightness(self):
1244 cmd = 'settings get system screen_brightness'
1245 return integer(self.execute(cmd).strip())
1246
Marc Bonnici3e751742017-07-06 17:31:02 +01001247 def get_airplane_mode(self):
1248 cmd = 'settings get global airplane_mode_on'
1249 return boolean(self.execute(cmd).strip())
1250
1251 def set_airplane_mode(self, mode):
1252 root_required = self.get_sdk_version() > 23
1253 if root_required and not self.is_rooted:
1254 raise TargetError('Root is required to toggle airplane mode on Android 7+')
Brendan Jackman0bfb6e42017-10-11 13:09:15 +01001255 mode = int(boolean(mode))
Marc Bonnici3e751742017-07-06 17:31:02 +01001256 cmd = 'settings put global airplane_mode_on {}'
Brendan Jackman0bfb6e42017-10-11 13:09:15 +01001257 self.execute(cmd.format(mode))
1258 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE '
1259 '--ez state {}'.format(mode), as_root=root_required)
Marc Bonnici3e751742017-07-06 17:31:02 +01001260
Marc Bonnici003785d2017-07-24 17:44:33 +01001261 def get_auto_rotation(self):
1262 cmd = 'settings get system accelerometer_rotation'
1263 return boolean(self.execute(cmd).strip())
1264
1265 def set_auto_rotation(self, autorotate):
1266 cmd = 'settings put system accelerometer_rotation {}'
1267 self.execute(cmd.format(int(boolean(autorotate))))
1268
1269 def set_natural_rotation(self):
1270 self.set_rotation(0)
1271
1272 def set_left_rotation(self):
1273 self.set_rotation(1)
1274
1275 def set_inverted_rotation(self):
1276 self.set_rotation(2)
1277
1278 def set_right_rotation(self):
1279 self.set_rotation(3)
1280
1281 def get_rotation(self):
1282 cmd = 'settings get system user_rotation'
1283 return self.execute(cmd).strip()
1284
1285 def set_rotation(self, rotation):
1286 if not 0 <= rotation <= 3:
1287 raise ValueError('Rotation value must be between 0 and 3')
1288 self.set_auto_rotation(False)
1289 cmd = 'settings put system user_rotation {}'
1290 self.execute(cmd.format(rotation))
1291
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001292 def homescreen(self):
1293 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1294
Sergei Trofimov961f9572015-11-18 17:32:26 +00001295 def _resolve_paths(self):
1296 if self.working_directory is None:
1297 self.working_directory = '/data/local/tmp/devlib-target'
1298 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1299 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001300 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001301
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001302 def _ensure_executables_directory_is_writable(self):
1303 matched = []
1304 for entry in self.list_file_systems():
1305 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1306 matched.append(entry)
1307 if matched:
1308 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1309 if 'rw' not in entry.options:
1310 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1311 entry.mount_point),
1312 as_root=True)
1313 else:
1314 message = 'Could not find mount point for executables directory {}'
1315 raise TargetError(message.format(self.executables_directory))
1316
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001317 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1318
1319 @property
1320 def charging_enabled(self):
1321 """
1322 Whether drawing power to charge the battery is enabled
1323
1324 Not all devices have the ability to enable/disable battery charging
1325 (e.g. because they don't have a battery). In that case,
1326 ``charging_enabled`` is None.
1327 """
1328 if not self.file_exists(self._charging_enabled_path):
1329 return None
1330 return self.read_bool(self._charging_enabled_path)
1331
1332 @charging_enabled.setter
1333 def charging_enabled(self, enabled):
1334 """
1335 Enable/disable drawing power to charge the battery
1336
1337 Not all devices have this facility. In that case, do nothing.
1338 """
1339 if not self.file_exists(self._charging_enabled_path):
1340 return
1341 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001342
1343FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1344PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001345LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001346
1347
1348class Cpuinfo(object):
1349
1350 @property
1351 @memoized
1352 def architecture(self):
1353 for section in self.sections:
1354 if 'CPU architecture' in section:
1355 return section['CPU architecture']
1356 if 'architecture' in section:
1357 return section['architecture']
1358
1359 @property
1360 @memoized
1361 def cpu_names(self):
1362 cpu_names = []
1363 global_name = None
1364 for section in self.sections:
1365 if 'processor' in section:
1366 if 'CPU part' in section:
1367 cpu_names.append(_get_part_name(section))
1368 elif 'model name' in section:
1369 cpu_names.append(_get_model_name(section))
1370 else:
1371 cpu_names.append(None)
1372 elif 'CPU part' in section:
1373 global_name = _get_part_name(section)
1374 return [caseless_string(c or global_name) for c in cpu_names]
1375
1376 def __init__(self, text):
1377 self.sections = None
1378 self.text = None
1379 self.parse(text)
1380
1381 @memoized
1382 def get_cpu_features(self, cpuid=0):
1383 global_features = []
1384 for section in self.sections:
1385 if 'processor' in section:
1386 if int(section.get('processor')) != cpuid:
1387 continue
1388 if 'Features' in section:
1389 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001390 elif 'flags' in section:
1391 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001392 elif 'Features' in section:
1393 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001394 elif 'flags' in section:
1395 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001396 return global_features
1397
1398 def parse(self, text):
1399 self.sections = []
1400 current_section = {}
1401 self.text = text.strip()
1402 for line in self.text.split('\n'):
1403 line = line.strip()
1404 if line:
1405 key, value = line.split(':', 1)
1406 current_section[key.strip()] = value.strip()
1407 else: # not line
1408 self.sections.append(current_section)
1409 current_section = {}
1410 self.sections.append(current_section)
1411
1412 def __str__(self):
1413 return 'CpuInfo({})'.format(self.cpu_names)
1414
1415 __repr__ = __str__
1416
1417
1418class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001419 """
1420 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001421
Brendan Jackman54adf802017-02-20 17:57:51 +00001422 Not expected to work for very old (pre-3.0) kernel version numbers.
1423
1424 :ivar release: Version number/revision string. Typical output of
1425 ``uname -r``
1426 :type release: str
1427 :ivar version: Extra version info (aside from ``release``) reported by
1428 ``uname``
1429 :type version: str
1430 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1431 :type version_number: int
1432 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1433 :type major: int
1434 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1435 be None
1436 :type minor: int
1437 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1438 :type rc: int
1439 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1440 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001441
1442 :ivar parts: Tuple of version number components. Can be used for
1443 lexicographically comparing kernel versions.
1444 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001445 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001446 def __init__(self, version_string):
1447 if ' #' in version_string:
1448 release, version = version_string.split(' #')
1449 self.release = release
1450 self.version = version
1451 elif version_string.startswith('#'):
1452 self.release = ''
1453 self.version = version_string
1454 else:
1455 self.release = version_string
1456 self.version = ''
1457
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001458 self.version_number = None
1459 self.major = None
1460 self.minor = None
1461 self.sha1 = None
1462 self.rc = None
1463 match = KVERSION_REGEX.match(version_string)
1464 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001465 groups = match.groupdict()
1466 self.version_number = int(groups['version'])
1467 self.major = int(groups['major'])
1468 if groups['minor'] is not None:
1469 self.minor = int(groups['minor'])
1470 if groups['rc'] is not None:
1471 self.rc = int(groups['rc'])
1472 if groups['sha1'] is not None:
1473 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001474
Brendan Jackman18b77b82017-02-20 17:52:56 +00001475 self.parts = (self.version_number, self.major, self.minor)
1476
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001477 def __str__(self):
1478 return '{} {}'.format(self.release, self.version)
1479
1480 __repr__ = __str__
1481
1482
1483class KernelConfig(object):
1484
1485 not_set_regex = re.compile(r'# (\S+) is not set')
1486
1487 @staticmethod
1488 def get_config_name(name):
1489 name = name.upper()
1490 if not name.startswith('CONFIG_'):
1491 name = 'CONFIG_' + name
1492 return name
1493
1494 def iteritems(self):
1495 return self._config.iteritems()
1496
1497 def __init__(self, text):
1498 self.text = text
1499 self._config = {}
1500 for line in text.split('\n'):
1501 line = line.strip()
1502 if line.startswith('#'):
1503 match = self.not_set_regex.search(line)
1504 if match:
1505 self._config[match.group(1)] = 'n'
1506 elif '=' in line:
1507 name, value = line.split('=', 1)
1508 self._config[name.strip()] = value.strip()
1509
1510 def get(self, name):
1511 return self._config.get(self.get_config_name(name))
1512
1513 def like(self, name):
1514 regex = re.compile(name, re.I)
1515 result = {}
1516 for k, v in self._config.iteritems():
1517 if regex.search(k):
1518 result[k] = v
1519 return result
1520
1521 def is_enabled(self, name):
1522 return self.get(name) == 'y'
1523
1524 def is_module(self, name):
1525 return self.get(name) == 'm'
1526
1527 def is_not_set(self, name):
1528 return self.get(name) == 'n'
1529
1530 def has(self, name):
1531 return self.get(name) in ['m', 'y']
1532
1533
1534class LocalLinuxTarget(LinuxTarget):
1535
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001536 def __init__(self,
1537 connection_settings=None,
1538 platform=None,
1539 working_directory=None,
1540 executables_directory=None,
1541 connect=True,
1542 modules=None,
1543 load_default_modules=True,
1544 shell_prompt=DEFAULT_SHELL_PROMPT,
1545 conn_cls=LocalConnection,
1546 ):
1547 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1548 platform=platform,
1549 working_directory=working_directory,
1550 executables_directory=executables_directory,
1551 connect=connect,
1552 modules=modules,
1553 load_default_modules=load_default_modules,
1554 shell_prompt=shell_prompt,
1555 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001556
Sergei Trofimov961f9572015-11-18 17:32:26 +00001557 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001558 if self.working_directory is None:
1559 self.working_directory = '/tmp'
1560 if self.executables_directory is None:
1561 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001562
1563
1564def _get_model_name(section):
1565 name_string = section['model name']
1566 parts = name_string.split('@')[0].strip().split()
1567 return ' '.join([p for p in parts
1568 if '(' not in p and p != 'CPU'])
1569
1570
1571def _get_part_name(section):
1572 implementer = section.get('CPU implementer', '0x0')
1573 part = section['CPU part']
1574 variant = section.get('CPU variant', '0x0')
1575 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1576 if name is None:
1577 name = '{}/{}/{}'.format(implementer, part, variant)
1578 return name
Sergei Trofimov181bc182017-10-03 16:28:09 +01001579
1580
1581def _build_path_tree(path_map, basepath, sep=os.path.sep, dictcls=dict):
1582 """
1583 Convert a flat mapping of paths to values into a nested structure of
1584 dict-line object (``dict``'s by default), mirroring the directory hierarchy
1585 represented by the paths relative to ``basepath``.
1586
1587 """
1588 def process_node(node, path, value):
1589 parts = path.split(sep, 1)
1590 if len(parts) == 1: # leaf
1591 node[parts[0]] = value
1592 else: # branch
1593 if parts[0] not in node:
1594 node[parts[0]] = dictcls()
1595 process_node(node[parts[0]], parts[1], value)
1596
1597 relpath_map = {os.path.relpath(p, basepath): v
1598 for p, v in path_map.iteritems()}
1599
1600 if len(relpath_map) == 1 and relpath_map.keys()[0] == '.':
1601 result = relpath_map.values()[0]
1602 else:
1603 result = dictcls()
1604 for path, value in relpath_map.iteritems():
1605 process_node(result, path, value)
1606
1607 return result