blob: 2fc64dc059596efe4114c9f36301a8849ec75a96 [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()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100218 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100219 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100220 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100221 if self.platform.big_core and self.load_default_modules:
222 self._install_module(get_module('bl'))
223
224 def disconnect(self):
225 for conn in self._connections.itervalues():
226 conn.close()
227 self._connections = {}
228
229 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000230 if self.conn_cls == None:
231 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100232 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
233
234 def setup(self, executables=None):
235 self.execute('mkdir -p {}'.format(self.working_directory))
236 self.execute('mkdir -p {}'.format(self.executables_directory))
237 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000238
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100239 self._setup_shutils()
Patrick Bellasif2eac512015-11-27 16:35:57 +0000240
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100241 for host_exe in (executables or []): # pylint: disable=superfluous-parens
242 self.install(host_exe)
243
Anouk Van Laer21f40032017-01-31 13:11:03 +0000244 # Check for platform dependent setup procedures
245 self.platform.setup(self)
246
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100247 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
248 self._update_modules('setup')
249
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100250 def reboot(self, hard=False, connect=True, timeout=180):
251 if hard:
252 if not self.has('hard_reset'):
253 raise TargetError('Hard reset not supported for this target.')
254 self.hard_reset() # pylint: disable=no-member
255 else:
256 if not self.is_connected:
257 message = 'Cannot reboot target becuase it is disconnected. ' +\
258 'Either connect() first, or specify hard=True ' +\
259 '(in which case, a hard_reset module must be installed)'
260 raise TargetError(message)
261 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000262 # Wait a fixed delay before starting polling to give the target time to
263 # shut down, otherwise, might create the connection while it's still shutting
264 # down resulting in subsequenct connection failing.
265 self.logger.debug('Waiting for target to power down...')
266 reset_delay = 20
267 time.sleep(reset_delay)
268 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100269 if self.has('boot'):
270 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000271 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100272 if connect:
273 self.connect(timeout=timeout)
274
275 # file transfer
276
277 def push(self, source, dest, timeout=None):
278 return self.conn.push(source, dest, timeout=timeout)
279
280 def pull(self, source, dest, timeout=None):
281 return self.conn.pull(source, dest, timeout=timeout)
282
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +0100283 def get_directory(self, source_dir, dest):
284 """ Pull a directory from the device, after compressing dir """
285 # Create all file names
286 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
287 # Host location of dir
288 outdir = os.path.join(dest, tar_file_name)
289 # Host location of archive
290 tar_file_name = '{}.tar'.format(tar_file_name)
291 tempfile = os.path.join(dest, tar_file_name)
292
293 # Does the folder exist?
294 self.execute('ls -la {}'.format(source_dir))
295 # Try compressing the folder
296 try:
297 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
298 source_dir))
299 except TargetError:
300 self.logger.debug('Failed to run tar command on target! ' \
301 'Not pulling directory {}'.format(source_dir))
302 # Pull the file
303 os.mkdir(outdir)
304 self.pull(tar_file_name, tempfile )
305 # Decompress
306 f = tarfile.open(tempfile, 'r')
307 f.extractall(outdir)
308 os.remove(tempfile)
309
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100310 # execution
311
312 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
setrofimb7386552017-05-23 17:39:12 +0100313 return self.conn.execute(command, timeout, check_exit_code, as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100314
315 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
316 return self.conn.background(command, stdout, stderr, as_root)
317
318 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
319 as_root=False, timeout=30):
320 """
321 Executes the specified binary under the specified conditions.
322
323 :binary: binary to execute. Must be present and executable on the device.
324 :args: arguments to be passed to the binary. The can be either a list or
325 a string.
326 :in_directory: execute the binary in the specified directory. This must
327 be an absolute path.
328 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
329 case, it will be interpreted as the mask), a list of ``ints``, in which
330 case this will be interpreted as the list of cpus, or string, which
331 will be interpreted as a comma-separated list of cpu ranges, e.g.
332 ``"0,4-7"``.
333 :as_root: Specify whether the command should be run as root
334 :timeout: If the invocation does not terminate within this number of seconds,
335 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
336 invocation should not timeout.
337
Brendan Jackman27f545f2016-11-15 16:58:57 +0000338 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100339 """
340 command = binary
341 if args:
342 if isiterable(args):
343 args = ' '.join(args)
344 command = '{} {}'.format(command, args)
345 if on_cpus:
346 on_cpus = bitmask(on_cpus)
347 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
348 if in_directory:
349 command = 'cd {} && {}'.format(in_directory, command)
350 return self.execute(command, as_root=as_root, timeout=timeout)
351
Valentin Schneider92b0c252017-07-06 16:01:15 +0100352 def background_invoke(self, binary, args=None, in_directory=None,
353 on_cpus=None, as_root=False):
354 """
355 Executes the specified binary as a background task under the
356 specified conditions.
357
358 :binary: binary to execute. Must be present and executable on the device.
359 :args: arguments to be passed to the binary. The can be either a list or
360 a string.
361 :in_directory: execute the binary in the specified directory. This must
362 be an absolute path.
363 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
364 case, it will be interpreted as the mask), a list of ``ints``, in which
365 case this will be interpreted as the list of cpus, or string, which
366 will be interpreted as a comma-separated list of cpu ranges, e.g.
367 ``"0,4-7"``.
368 :as_root: Specify whether the command should be run as root
369
370 :returns: the subprocess instance handling that command
371 """
372 command = binary
373 if args:
374 if isiterable(args):
375 args = ' '.join(args)
376 command = '{} {}'.format(command, args)
377 if on_cpus:
378 on_cpus = bitmask(on_cpus)
379 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
380 if in_directory:
381 command = 'cd {} && {}'.format(in_directory, command)
382 return self.background(command, as_root=as_root)
383
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100384 def kick_off(self, command, as_root=False):
385 raise NotImplementedError()
386
387 # sysfs interaction
388
389 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100390 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100391 if kind:
392 return kind(output)
393 else:
394 return output
395
396 def read_int(self, path):
397 return self.read_value(path, kind=integer)
398
399 def read_bool(self, path):
400 return self.read_value(path, kind=boolean)
401
402 def write_value(self, path, value, verify=True):
403 value = str(value)
404 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
405 if verify:
406 output = self.read_value(path)
407 if not output == value:
408 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
409 raise TargetError(message)
410
411 def reset(self):
412 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100413 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100414 except (TargetError, TimeoutError, subprocess.CalledProcessError):
415 # on some targets "reboot" doesn't return gracefully
416 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000417 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100418
419 def check_responsive(self):
420 try:
421 self.conn.execute('ls /', timeout=5)
422 except (TimeoutError, subprocess.CalledProcessError):
423 raise TargetNotRespondingError(self.conn.name)
424
425 # process management
426
427 def kill(self, pid, signal=None, as_root=False):
428 signal_string = '-s {}'.format(signal) if signal else ''
429 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
430
431 def killall(self, process_name, signal=None, as_root=False):
432 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000433 try:
434 self.kill(pid, signal=signal, as_root=as_root)
435 except TargetError:
436 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100437
438 def get_pids_of(self, process_name):
439 raise NotImplementedError()
440
441 def ps(self, **kwargs):
442 raise NotImplementedError()
443
444 # files
445
446 def file_exists(self, filepath):
447 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000448 output = self.execute(command.format(filepath), as_root=self.is_rooted)
449 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100450
Sebastian Goscik33603c62016-02-15 15:07:19 +0000451 def directory_exists(self, filepath):
452 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
453 # output from ssh my contain part of the expression in the buffer,
454 # split out everything except the last word.
455 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
456
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100457 def list_file_systems(self):
458 output = self.execute('mount')
459 fstab = []
460 for line in output.split('\n'):
461 line = line.strip()
462 if not line:
463 continue
464 match = FSTAB_ENTRY_REGEX.search(line)
465 if match:
466 fstab.append(FstabEntry(match.group(1), match.group(2),
467 match.group(3), match.group(4),
468 None, None))
469 else: # assume pre-M Android
470 fstab.append(FstabEntry(*line.split()))
471 return fstab
472
473 def list_directory(self, path, as_root=False):
474 raise NotImplementedError()
475
476 def get_workpath(self, name):
477 return self.path.join(self.working_directory, name)
478
479 def tempfile(self, prefix='', suffix=''):
480 names = tempfile._get_candidate_names() # pylint: disable=W0212
481 for _ in xrange(tempfile.TMP_MAX):
482 name = names.next()
483 path = self.get_workpath(prefix + name + suffix)
484 if not self.file_exists(path):
485 return path
486 raise IOError('No usable temporary filename found')
487
488 def remove(self, path, as_root=False):
489 self.execute('rm -rf {}'.format(path), as_root=as_root)
490
491 # misc
492 def core_cpus(self, core):
493 return [i for i, c in enumerate(self.core_names) if c == core]
494
495 def list_online_cpus(self, core=None):
496 path = self.path.join('/sys/devices/system/cpu/online')
497 output = self.read_value(path)
498 all_online = ranges_to_list(output)
499 if core:
500 cpus = self.core_cpus(core)
501 if not cpus:
502 raise ValueError(core)
503 return [o for o in all_online if o in cpus]
504 else:
505 return all_online
506
507 def list_offline_cpus(self):
508 online = self.list_online_cpus()
509 return [c for c in xrange(self.number_of_cpus)
510 if c not in online]
511
512 def getenv(self, variable):
513 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
514
515 def capture_screen(self, filepath):
516 raise NotImplementedError()
517
518 def install(self, filepath, timeout=None, with_name=None):
519 raise NotImplementedError()
520
521 def uninstall(self, name):
522 raise NotImplementedError()
523
Sebastian Goscik84151f92016-02-15 15:09:27 +0000524 def get_installed(self, name, search_system_binaries=True):
525 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000526 if self.file_exists(self.executables_directory):
527 if name in self.list_directory(self.executables_directory):
528 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000529 # Fall back to binaries in PATH
530 if search_system_binaries:
531 for path in self.getenv('PATH').split(self.path.pathsep):
532 try:
533 if name in self.list_directory(path):
534 return self.path.join(path, name)
535 except TargetError:
536 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100537
538 which = get_installed
539
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000540 def install_if_needed(self, host_path, search_system_binaries=True):
541
542 binary_path = self.get_installed(os.path.split(host_path)[1],
543 search_system_binaries=search_system_binaries)
544 if not binary_path:
545 binary_path = self.install(host_path)
546 return binary_path
547
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100548 def is_installed(self, name):
549 return bool(self.get_installed(name))
550
551 def bin(self, name):
552 return self._installed_binaries.get(name, name)
553
554 def has(self, modname):
555 return hasattr(self, identifier(modname))
556
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000557 def lsmod(self):
558 lines = self.execute('lsmod').splitlines()
559 entries = []
560 for line in lines[1:]: # first line is the header
561 if not line.strip():
562 continue
563 parts = line.split()
564 name = parts[0]
565 size = int(parts[1])
566 use_count = int(parts[2])
567 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000568 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000569 else:
570 used_by = []
571 entries.append(LsmodEntry(name, size, use_count, used_by))
572 return entries
573
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000574 def insmod(self, path):
575 target_path = self.get_workpath(os.path.basename(path))
576 self.push(path, target_path)
577 self.execute('insmod {}'.format(target_path), as_root=True)
578
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100579
580 def extract(self, path, dest=None):
581 """
582 Extact the specified on-target file. The extraction method to be used
583 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
584 If ``dest`` is specified, it must be an existing directory on target;
585 the extracted contents will be placed there.
586
587 Note that, depending on the archive file format (and therfore the
588 extraction method used), the original archive file may or may not exist
589 after the extraction.
590
591 The return value is the path to the extracted contents. In case of
592 gunzip and bunzip2, this will be path to the extracted file; for tar
593 and uzip, this will be the directory with the extracted file(s)
594 (``dest`` if it was specified otherwise, the directory that cotained
595 the archive).
596
597 """
598 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
599 '.tgz', '.tbz', '.tbz2']:
600 if path.endswith(ending):
601 return self._extract_archive(path, 'tar xf {} -C {}', dest)
602
603 ext = self.path.splitext(path)[1]
604 if ext in ['.bz', '.bz2']:
605 return self._extract_file(path, 'bunzip2 -f {}', dest)
606 elif ext == '.gz':
607 return self._extract_file(path, 'gunzip -f {}', dest)
608 elif ext == '.zip':
609 return self._extract_archive(path, 'unzip {} -d {}', dest)
610 else:
611 raise ValueError('Unknown compression format: {}'.format(ext))
612
Sergei Trofimov69a83d42017-05-12 11:54:31 +0100613 def sleep(self, duration):
614 timeout = duration + 10
615 self.execute('sleep {}'.format(duration), timeout=timeout)
616
Sergei Trofimov181bc182017-10-03 16:28:09 +0100617 def read_tree_values_flat(self, path, depth=1, check_exit_code=True):
618 command = 'read_tree_values {} {}'.format(path, depth)
619 output = self._execute_util(command, as_root=self.is_rooted,
620 check_exit_code=check_exit_code)
621 result = {}
622 for entry in output.strip().split('\n'):
Sergei Trofimov7e073c12017-10-06 13:37:00 +0100623 if ':' not in entry:
Sergei Trofimovd560aea2017-10-05 09:35:11 +0100624 continue
Sergei Trofimov181bc182017-10-03 16:28:09 +0100625 path, value = entry.strip().split(':', 1)
626 result[path] = value
627 return result
628
629 def read_tree_values(self, path, depth=1, dictcls=dict, check_exit_code=True):
630 value_map = self.read_tree_values_flat(path, depth, check_exit_code)
631 return _build_path_tree(value_map, path, self.path.sep, dictcls)
632
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100633 # internal methods
634
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100635 def _setup_shutils(self):
636 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
637 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
638 shell_path = '/bin/sh'
639 if self.os == 'android':
640 shell_path = '/system/bin/sh'
641 with open(shutils_ifile) as fh:
642 lines = fh.readlines()
643 with open(shutils_ofile, 'w') as ofile:
644 for line in lines:
645 line = line.replace("__DEVLIB_SHELL__", shell_path)
646 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
647 ofile.write(line)
648 self._shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
649
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100650 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
651 command = '{} {}'.format(self.shutils, command)
652 return self.conn.execute(command, timeout, check_exit_code, as_root)
653
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100654 def _extract_archive(self, path, cmd, dest=None):
655 cmd = '{} ' + cmd # busybox
656 if dest:
657 extracted = dest
658 else:
659 extracted = self.path.dirname(path)
660 cmdtext = cmd.format(self.busybox, path, extracted)
661 self.execute(cmdtext)
662 return extracted
663
664 def _extract_file(self, path, cmd, dest=None):
665 cmd = '{} ' + cmd # busybox
666 cmdtext = cmd.format(self.busybox, path)
667 self.execute(cmdtext)
668 extracted = self.path.splitext(path)[0]
669 if dest:
670 self.execute('mv -f {} {}'.format(extracted, dest))
671 if dest.endswith('/'):
672 extracted = self.path.join(dest, self.path.basename(extracted))
673 else:
674 extracted = dest
675 return extracted
676
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100677 def _update_modules(self, stage):
678 for mod in self.modules:
679 if isinstance(mod, dict):
680 mod, params = mod.items()[0]
681 else:
682 params = {}
683 mod = get_module(mod)
684 if not mod.stage == stage:
685 continue
686 if mod.probe(self):
687 self._install_module(mod, **params)
688 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000689 msg = 'Module {} is not supported by the target'.format(mod.name)
690 if self.load_default_modules:
691 self.logger.debug(msg)
692 else:
693 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100694
695 def _install_module(self, mod, **params):
696 if mod.name not in self._installed_modules:
697 self.logger.debug('Installing module {}'.format(mod.name))
698 mod.install(self, **params)
699 self._installed_modules[mod.name] = mod
700 else:
701 self.logger.debug('Module {} is already installed.'.format(mod.name))
702
Sergei Trofimov961f9572015-11-18 17:32:26 +0000703 def _resolve_paths(self):
704 raise NotImplementedError()
705
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100706
707class LinuxTarget(Target):
708
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100709 path = posixpath
710 os = 'linux'
711
712 @property
713 @memoized
714 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100715 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100716 for abi, architectures in ABI_MAP.iteritems():
717 if value in architectures:
718 result = abi
719 break
720 else:
721 result = value
722 return result
723
724 @property
725 @memoized
726 def os_version(self):
727 os_version = {}
728 try:
729 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
730 version_files = self.execute(command, check_exit_code=False).strip().split()
731 for vf in version_files:
732 name = self.path.basename(vf)
733 output = self.read_value(vf)
734 os_version[name] = output.strip().replace('\n', ' ')
735 except TargetError:
736 raise
737 return os_version
738
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000739 @property
740 @memoized
741 # There is currently no better way to do this cross platform.
742 # ARM does not have dmidecode
743 def model(self):
744 if self.file_exists("/proc/device-tree/model"):
745 raw_model = self.execute("cat /proc/device-tree/model")
746 return '_'.join(raw_model.split()[:2])
747 return None
748
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000749 def __init__(self,
750 connection_settings=None,
751 platform=None,
752 working_directory=None,
753 executables_directory=None,
754 connect=True,
755 modules=None,
756 load_default_modules=True,
757 shell_prompt=DEFAULT_SHELL_PROMPT,
758 conn_cls=SshConnection,
759 ):
760 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
761 platform=platform,
762 working_directory=working_directory,
763 executables_directory=executables_directory,
764 connect=connect,
765 modules=modules,
766 load_default_modules=load_default_modules,
767 shell_prompt=shell_prompt,
768 conn_cls=conn_cls)
769
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100770 def connect(self, timeout=None):
771 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100772
773 def kick_off(self, command, as_root=False):
774 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
775 return self.conn.execute(command, as_root=as_root)
776
777 def get_pids_of(self, process_name):
778 """Returns a list of PIDs of all processes with the specified name."""
779 # result should be a column of PIDs with the first row as "PID" header
780 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
781 check_exit_code=False).strip().split()
782 if len(result) >= 2: # at least one row besides the header
783 return map(int, result[1:])
784 else:
785 return []
786
787 def ps(self, **kwargs):
788 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
789 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
790 lines.next() # header
791
792 result = []
793 for line in lines:
794 parts = re.split(r'\s+', line, maxsplit=8)
795 if parts and parts != ['']:
796 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
797
798 if not kwargs:
799 return result
800 else:
801 filtered_result = []
802 for entry in result:
803 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
804 filtered_result.append(entry)
805 return filtered_result
806
807 def list_directory(self, path, as_root=False):
808 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
809 return [x.strip() for x in contents.split('\n') if x.strip()]
810
811 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
812 destpath = self.path.join(self.executables_directory,
813 with_name and with_name or self.path.basename(filepath))
814 self.push(filepath, destpath)
815 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
816 self._installed_binaries[self.path.basename(destpath)] = destpath
817 return destpath
818
819 def uninstall(self, name):
820 path = self.path.join(self.executables_directory, name)
821 self.remove(path)
822
823 def capture_screen(self, filepath):
824 if not self.is_installed('scrot'):
825 self.logger.debug('Could not take screenshot as scrot is not installed.')
826 return
827 try:
828
829 tmpfile = self.tempfile()
830 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
831 self.pull(tmpfile, filepath)
832 self.remove(tmpfile)
833 except TargetError as e:
834 if "Can't open X dispay." not in e.message:
835 raise e
836 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
837 self.logger.debug('Could not take screenshot: {}'.format(message))
838
Sergei Trofimov961f9572015-11-18 17:32:26 +0000839 def _resolve_paths(self):
840 if self.working_directory is None:
841 if self.connected_as_root:
842 self.working_directory = '/root/devlib-target'
843 else:
844 self.working_directory = '/home/{}/devlib-target'.format(self.user)
845 if self.executables_directory is None:
846 self.executables_directory = self.path.join(self.working_directory, 'bin')
847
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100848
849class AndroidTarget(Target):
850
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100851 path = posixpath
852 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100853 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100854
855 @property
856 @memoized
857 def abi(self):
858 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
859
860 @property
861 @memoized
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100862 def supported_abi(self):
863 props = self.getprop()
864 result = [props['ro.product.cpu.abi']]
865 if 'ro.product.cpu.abi2' in props:
866 result.append(props['ro.product.cpu.abi2'])
867 if 'ro.product.cpu.abilist' in props:
868 for abi in props['ro.product.cpu.abilist'].split(','):
869 if abi not in result:
870 result.append(abi)
871
872 mapped_result = []
873 for supported_abi in result:
874 for abi, architectures in ABI_MAP.iteritems():
875 found = False
876 if supported_abi in architectures and abi not in mapped_result:
877 mapped_result.append(abi)
878 found = True
879 break
880 if not found and supported_abi not in mapped_result:
881 mapped_result.append(supported_abi)
882 return mapped_result
883
884 @property
885 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100886 def os_version(self):
887 os_version = {}
888 for k, v in self.getprop().iteritems():
889 if k.startswith('ro.build.version'):
890 part = k.split('.')[-1]
891 os_version[part] = v
892 return os_version
893
894 @property
895 def adb_name(self):
896 return self.conn.device
897
898 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000899 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000900 def android_id(self):
901 """
902 Get the device's ANDROID_ID. Which is
903
904 "A 64-bit number (as a hex string) that is randomly generated when the user
905 first sets up the device and should remain constant for the lifetime of the
906 user's device."
907
908 .. note:: This will get reset on userdata erasure.
909
910 """
911 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
912 return output.split('value=')[-1]
913
914 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100915 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000916 def model(self):
917 try:
918 return self.getprop(prop='ro.product.device')
919 except KeyError:
920 return None
921
922 @property
923 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100924 def screen_resolution(self):
925 output = self.execute('dumpsys window')
926 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
927 if match:
928 return (int(match.group('width')),
929 int(match.group('height')))
930 else:
931 return (0, 0)
932
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000933 def __init__(self,
934 connection_settings=None,
935 platform=None,
936 working_directory=None,
937 executables_directory=None,
938 connect=True,
939 modules=None,
940 load_default_modules=True,
941 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000942 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000943 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000944 ):
945 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
946 platform=platform,
947 working_directory=working_directory,
948 executables_directory=executables_directory,
949 connect=connect,
950 modules=modules,
951 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000952 shell_prompt=shell_prompt,
953 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000954 self.package_data_directory = package_data_directory
Brendan Jackman8a0554f2017-10-09 17:08:38 +0100955 self.clear_logcat_lock = threading.Lock()
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000956
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100957 def reset(self, fastboot=False): # pylint: disable=arguments-differ
958 try:
959 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100960 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100961 except (TargetError, TimeoutError, subprocess.CalledProcessError):
962 # on some targets "reboot" doesn't return gracefully
963 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000964 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100965
Patrick Bellasif26f9422017-07-12 12:27:28 +0100966 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100967 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +0100968 boot_completed = boolean(self.getprop('sys.boot_completed'))
969 while not boot_completed and timeout >= time.time() - start:
970 time.sleep(5)
971 boot_completed = boolean(self.getprop('sys.boot_completed'))
972 if not boot_completed:
973 raise TargetError('Connected but Android did not fully boot.')
974
975 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100976 device = self.connection_settings.get('device')
977 if device and ':' in device:
978 # ADB does not automatically remove a network device from it's
979 # devices list when the connection is broken by the remote, so the
980 # adb connection may have gone "stale", resulting in adb blocking
981 # indefinitely when making calls to the device. To avoid this,
982 # always disconnect first.
983 adb_disconnect(device)
984 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100985
986 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +0100987 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100988
989 def setup(self, executables=None):
990 super(AndroidTarget, self).setup(executables)
991 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
992
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100993 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100994 """
995 Like execute but closes adb session and returns immediately, leaving the command running on the
996 device (this is different from execute(background=True) which keeps adb connection open and returns
997 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100998 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100999 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +01001000 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001001 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +01001002 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001003 output = self.execute(command, timeout=1, as_root=as_root)
1004 except TimeoutError:
1005 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001006
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001007 def __setup_list_directory(self):
1008 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
1009 # AOSP 7.0 as well, the ls command was changed.
1010 # Previous versions default to a single column listing, which is nice and easy to parse.
1011 # Newer versions default to a multi-column listing, which is not, but it does support
1012 # a '-1' option to get into single column mode. Older versions do not support this option
1013 # so we try the new version, and if it fails we use the old version.
1014 self.ls_command = 'ls -1'
1015 try:
Marc Bonnici06552372017-03-29 16:43:22 +01001016 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001017 except TargetError:
1018 self.ls_command = 'ls'
1019
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001020 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001021 if self.ls_command == '':
1022 self.__setup_list_directory()
1023 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001024 return [x.strip() for x in contents.split('\n') if x.strip()]
1025
1026 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1027 ext = os.path.splitext(filepath)[1].lower()
1028 if ext == '.apk':
1029 return self.install_apk(filepath, timeout)
1030 else:
1031 return self.install_executable(filepath, with_name)
1032
1033 def uninstall(self, name):
1034 if self.package_is_installed(name):
1035 self.uninstall_package(name)
1036 else:
1037 self.uninstall_executable(name)
1038
1039 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001040 result = []
1041 search_term = process_name[-15:]
1042 for entry in self.ps():
1043 if search_term in entry.name:
1044 result.append(entry.pid)
1045 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001046
1047 def ps(self, **kwargs):
1048 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1049 lines.next() # header
1050 result = []
1051 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001052 parts = line.split(None, 8)
Sergei Trofimov109fcc62017-09-26 13:30:15 +01001053 if not parts:
1054 continue
1055 if len(parts) == 8:
1056 # wchan was blank; insert an empty field where it should be.
1057 parts.insert(5, '')
1058 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001059 if not kwargs:
1060 return result
1061 else:
1062 filtered_result = []
1063 for entry in result:
1064 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1065 filtered_result.append(entry)
1066 return filtered_result
1067
1068 def capture_screen(self, filepath):
1069 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1070 self.execute('screencap -p {}'.format(on_device_file))
1071 self.pull(on_device_file, filepath)
1072 self.remove(on_device_file)
1073
1074 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1075 if not as_root:
1076 self.conn.push(source, dest, timeout=timeout)
1077 else:
1078 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001079 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001080 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001081 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001082
1083 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1084 if not as_root:
1085 self.conn.pull(source, dest, timeout=timeout)
1086 else:
1087 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001088 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1089 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001090 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001091 self.conn.pull(device_tempfile, dest, timeout=timeout)
1092
1093 # Android-specific
1094
Marc Bonnici8839ed02017-08-10 17:19:13 +01001095 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001096 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001097 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001098 if direction == "diagonal":
1099 start = 100
1100 stop = width - start
1101 swipe_height = height * 2 // 3
1102 self.execute(command.format(start, swipe_height, stop, 0))
1103 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001104 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001105 start = 100
1106 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001107 self.execute(command.format(start, swipe_height, stop, swipe_height))
1108 elif direction == "vertical":
1109 swipe_middle = width / 2
1110 swipe_height = height * 2 // 3
1111 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001112 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001113 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001114
1115 def getprop(self, prop=None):
1116 props = AndroidProperties(self.execute('getprop'))
1117 if prop:
1118 return props[prop]
1119 return props
1120
1121 def is_installed(self, name):
1122 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1123
1124 def package_is_installed(self, package_name):
1125 return package_name in self.list_packages()
1126
1127 def list_packages(self):
1128 output = self.execute('pm list packages')
1129 output = output.replace('package:', '')
1130 return output.split()
1131
1132 def get_package_version(self, package):
1133 output = self.execute('dumpsys package {}'.format(package))
1134 for line in convert_new_lines(output).split('\n'):
1135 if 'versionName' in line:
1136 return line.split('=', 1)[1]
1137 return None
1138
Marc Bonnicid3396f22017-05-31 15:56:50 +01001139 def get_sdk_version(self):
1140 try:
1141 return int(self.getprop('ro.build.version.sdk'))
1142 except (ValueError, TypeError):
1143 return None
1144
Marc Bonnicic33dd652017-05-31 15:51:31 +01001145 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001146 ext = os.path.splitext(filepath)[1].lower()
1147 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001148 flags = []
1149 if replace:
1150 flags.append('-r') # Replace existing APK
1151 if allow_downgrade:
1152 flags.append('-d') # Install the APK even if a newer version is already installed
1153 if self.get_sdk_version() >= 23:
1154 flags.append('-g') # Grant all runtime permissions
1155 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1156 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001157 else:
1158 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1159
1160 def install_executable(self, filepath, with_name=None):
1161 self._ensure_executables_directory_is_writable()
1162 executable_name = with_name or os.path.basename(filepath)
1163 on_device_file = self.path.join(self.working_directory, executable_name)
1164 on_device_executable = self.path.join(self.executables_directory, executable_name)
1165 self.push(filepath, on_device_file)
1166 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001167 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1168 self.remove(on_device_file, as_root=self.needs_su)
1169 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001170 self._installed_binaries[executable_name] = on_device_executable
1171 return on_device_executable
1172
1173 def uninstall_package(self, package):
1174 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1175
1176 def uninstall_executable(self, executable_name):
1177 on_device_executable = self.path.join(self.executables_directory, executable_name)
1178 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001179 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001180
1181 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001182 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001183 filtstr = ' -s {}'.format(filter) if filter else ''
1184 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1185 adb_command(self.adb_name, command, timeout=timeout)
1186
1187 def clear_logcat(self):
Brendan Jackman8a0554f2017-10-09 17:08:38 +01001188 with self.clear_logcat_lock:
1189 adb_command(self.adb_name, 'logcat -c', timeout=30)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001190
Valentin Schneider7c2fd872017-09-11 16:58:24 +01001191 def get_logcat_monitor(self, regexps=None):
1192 return LogcatMonitor(self, regexps)
1193
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001194 def adb_kill_server(self, timeout=30):
1195 adb_command(self.adb_name, 'kill-server', timeout)
1196
1197 def adb_wait_for_device(self, timeout=30):
1198 adb_command(self.adb_name, 'wait-for-device', timeout)
1199
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001200 def adb_reboot_bootloader(self, timeout=30):
1201 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1202
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001203 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001204 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001205 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001206 return
1207 adb_command(self.adb_name, 'root', timeout=30)
1208 self._connected_as_root = True
1209 return
1210 adb_command(self.adb_name, 'unroot', timeout=30)
1211 self._connected_as_root = False
1212
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001213 def is_screen_on(self):
1214 output = self.execute('dumpsys power')
1215 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1216 if match:
1217 return boolean(match.group(1))
1218 else:
1219 raise TargetError('Could not establish screen state.')
1220
1221 def ensure_screen_is_on(self):
1222 if not self.is_screen_on():
1223 self.execute('input keyevent 26')
1224
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001225 def ensure_screen_is_off(self):
1226 if self.is_screen_on():
1227 self.execute('input keyevent 26')
1228
Marc Bonniciddd2e292017-07-06 17:30:12 +01001229 def set_auto_brightness(self, auto_brightness):
1230 cmd = 'settings put system screen_brightness_mode {}'
1231 self.execute(cmd.format(int(boolean(auto_brightness))))
1232
1233 def get_auto_brightness(self):
1234 cmd = 'settings get system screen_brightness_mode'
1235 return boolean(self.execute(cmd).strip())
1236
1237 def set_brightness(self, value):
1238 if not 0 <= value <= 255:
1239 msg = 'Invalid brightness "{}"; Must be between 0 and 255'
1240 raise ValueError(msg.format(value))
1241 self.set_auto_brightness(False)
1242 cmd = 'settings put system screen_brightness {}'
1243 self.execute(cmd.format(int(value)))
1244
1245 def get_brightness(self):
1246 cmd = 'settings get system screen_brightness'
1247 return integer(self.execute(cmd).strip())
1248
Marc Bonnici3e751742017-07-06 17:31:02 +01001249 def get_airplane_mode(self):
1250 cmd = 'settings get global airplane_mode_on'
1251 return boolean(self.execute(cmd).strip())
1252
1253 def set_airplane_mode(self, mode):
1254 root_required = self.get_sdk_version() > 23
1255 if root_required and not self.is_rooted:
1256 raise TargetError('Root is required to toggle airplane mode on Android 7+')
1257 cmd = 'settings put global airplane_mode_on {}'
1258 self.execute(cmd.format(int(boolean(mode))))
1259 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE', as_root=root_required)
1260
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