blob: 6d2a12d8f3f717d9fa871468d7d629455bf9ab29 [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
955
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100956 def reset(self, fastboot=False): # pylint: disable=arguments-differ
957 try:
958 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100959 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100960 except (TargetError, TimeoutError, subprocess.CalledProcessError):
961 # on some targets "reboot" doesn't return gracefully
962 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000963 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100964
Patrick Bellasif26f9422017-07-12 12:27:28 +0100965 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100966 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +0100967 boot_completed = boolean(self.getprop('sys.boot_completed'))
968 while not boot_completed and timeout >= time.time() - start:
969 time.sleep(5)
970 boot_completed = boolean(self.getprop('sys.boot_completed'))
971 if not boot_completed:
972 raise TargetError('Connected but Android did not fully boot.')
973
974 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100975 device = self.connection_settings.get('device')
976 if device and ':' in device:
977 # ADB does not automatically remove a network device from it's
978 # devices list when the connection is broken by the remote, so the
979 # adb connection may have gone "stale", resulting in adb blocking
980 # indefinitely when making calls to the device. To avoid this,
981 # always disconnect first.
982 adb_disconnect(device)
983 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100984
985 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +0100986 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100987
988 def setup(self, executables=None):
989 super(AndroidTarget, self).setup(executables)
990 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
991
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100992 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100993 """
994 Like execute but closes adb session and returns immediately, leaving the command running on the
995 device (this is different from execute(background=True) which keeps adb connection open and returns
996 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100997 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100998 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100999 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001000 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +01001001 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001002 output = self.execute(command, timeout=1, as_root=as_root)
1003 except TimeoutError:
1004 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001005
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001006 def __setup_list_directory(self):
1007 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
1008 # AOSP 7.0 as well, the ls command was changed.
1009 # Previous versions default to a single column listing, which is nice and easy to parse.
1010 # Newer versions default to a multi-column listing, which is not, but it does support
1011 # a '-1' option to get into single column mode. Older versions do not support this option
1012 # so we try the new version, and if it fails we use the old version.
1013 self.ls_command = 'ls -1'
1014 try:
Marc Bonnici06552372017-03-29 16:43:22 +01001015 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001016 except TargetError:
1017 self.ls_command = 'ls'
1018
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001019 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001020 if self.ls_command == '':
1021 self.__setup_list_directory()
1022 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001023 return [x.strip() for x in contents.split('\n') if x.strip()]
1024
1025 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1026 ext = os.path.splitext(filepath)[1].lower()
1027 if ext == '.apk':
1028 return self.install_apk(filepath, timeout)
1029 else:
1030 return self.install_executable(filepath, with_name)
1031
1032 def uninstall(self, name):
1033 if self.package_is_installed(name):
1034 self.uninstall_package(name)
1035 else:
1036 self.uninstall_executable(name)
1037
1038 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001039 result = []
1040 search_term = process_name[-15:]
1041 for entry in self.ps():
1042 if search_term in entry.name:
1043 result.append(entry.pid)
1044 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001045
1046 def ps(self, **kwargs):
1047 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1048 lines.next() # header
1049 result = []
1050 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001051 parts = line.split(None, 8)
Sergei Trofimov109fcc62017-09-26 13:30:15 +01001052 if not parts:
1053 continue
1054 if len(parts) == 8:
1055 # wchan was blank; insert an empty field where it should be.
1056 parts.insert(5, '')
1057 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001058 if not kwargs:
1059 return result
1060 else:
1061 filtered_result = []
1062 for entry in result:
1063 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1064 filtered_result.append(entry)
1065 return filtered_result
1066
1067 def capture_screen(self, filepath):
1068 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1069 self.execute('screencap -p {}'.format(on_device_file))
1070 self.pull(on_device_file, filepath)
1071 self.remove(on_device_file)
1072
1073 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1074 if not as_root:
1075 self.conn.push(source, dest, timeout=timeout)
1076 else:
1077 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001078 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001079 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001080 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001081
1082 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1083 if not as_root:
1084 self.conn.pull(source, dest, timeout=timeout)
1085 else:
1086 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001087 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1088 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001089 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001090 self.conn.pull(device_tempfile, dest, timeout=timeout)
1091
1092 # Android-specific
1093
Marc Bonnici8839ed02017-08-10 17:19:13 +01001094 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001095 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001096 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001097 if direction == "diagonal":
1098 start = 100
1099 stop = width - start
1100 swipe_height = height * 2 // 3
1101 self.execute(command.format(start, swipe_height, stop, 0))
1102 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001103 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001104 start = 100
1105 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001106 self.execute(command.format(start, swipe_height, stop, swipe_height))
1107 elif direction == "vertical":
1108 swipe_middle = width / 2
1109 swipe_height = height * 2 // 3
1110 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001111 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001112 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001113
1114 def getprop(self, prop=None):
1115 props = AndroidProperties(self.execute('getprop'))
1116 if prop:
1117 return props[prop]
1118 return props
1119
1120 def is_installed(self, name):
1121 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1122
1123 def package_is_installed(self, package_name):
1124 return package_name in self.list_packages()
1125
1126 def list_packages(self):
1127 output = self.execute('pm list packages')
1128 output = output.replace('package:', '')
1129 return output.split()
1130
1131 def get_package_version(self, package):
1132 output = self.execute('dumpsys package {}'.format(package))
1133 for line in convert_new_lines(output).split('\n'):
1134 if 'versionName' in line:
1135 return line.split('=', 1)[1]
1136 return None
1137
Marc Bonnicid3396f22017-05-31 15:56:50 +01001138 def get_sdk_version(self):
1139 try:
1140 return int(self.getprop('ro.build.version.sdk'))
1141 except (ValueError, TypeError):
1142 return None
1143
Marc Bonnicic33dd652017-05-31 15:51:31 +01001144 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001145 ext = os.path.splitext(filepath)[1].lower()
1146 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001147 flags = []
1148 if replace:
1149 flags.append('-r') # Replace existing APK
1150 if allow_downgrade:
1151 flags.append('-d') # Install the APK even if a newer version is already installed
1152 if self.get_sdk_version() >= 23:
1153 flags.append('-g') # Grant all runtime permissions
1154 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1155 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001156 else:
1157 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1158
1159 def install_executable(self, filepath, with_name=None):
1160 self._ensure_executables_directory_is_writable()
1161 executable_name = with_name or os.path.basename(filepath)
1162 on_device_file = self.path.join(self.working_directory, executable_name)
1163 on_device_executable = self.path.join(self.executables_directory, executable_name)
1164 self.push(filepath, on_device_file)
1165 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001166 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1167 self.remove(on_device_file, as_root=self.needs_su)
1168 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001169 self._installed_binaries[executable_name] = on_device_executable
1170 return on_device_executable
1171
1172 def uninstall_package(self, package):
1173 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1174
1175 def uninstall_executable(self, executable_name):
1176 on_device_executable = self.path.join(self.executables_directory, executable_name)
1177 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001178 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001179
1180 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001181 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001182 filtstr = ' -s {}'.format(filter) if filter else ''
1183 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1184 adb_command(self.adb_name, command, timeout=timeout)
1185
1186 def clear_logcat(self):
1187 adb_command(self.adb_name, 'logcat -c', timeout=30)
1188
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+')
1255 cmd = 'settings put global airplane_mode_on {}'
1256 self.execute(cmd.format(int(boolean(mode))))
1257 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE', as_root=root_required)
1258
Marc Bonnici003785d2017-07-24 17:44:33 +01001259 def get_auto_rotation(self):
1260 cmd = 'settings get system accelerometer_rotation'
1261 return boolean(self.execute(cmd).strip())
1262
1263 def set_auto_rotation(self, autorotate):
1264 cmd = 'settings put system accelerometer_rotation {}'
1265 self.execute(cmd.format(int(boolean(autorotate))))
1266
1267 def set_natural_rotation(self):
1268 self.set_rotation(0)
1269
1270 def set_left_rotation(self):
1271 self.set_rotation(1)
1272
1273 def set_inverted_rotation(self):
1274 self.set_rotation(2)
1275
1276 def set_right_rotation(self):
1277 self.set_rotation(3)
1278
1279 def get_rotation(self):
1280 cmd = 'settings get system user_rotation'
1281 return self.execute(cmd).strip()
1282
1283 def set_rotation(self, rotation):
1284 if not 0 <= rotation <= 3:
1285 raise ValueError('Rotation value must be between 0 and 3')
1286 self.set_auto_rotation(False)
1287 cmd = 'settings put system user_rotation {}'
1288 self.execute(cmd.format(rotation))
1289
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001290 def homescreen(self):
1291 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1292
Sergei Trofimov961f9572015-11-18 17:32:26 +00001293 def _resolve_paths(self):
1294 if self.working_directory is None:
1295 self.working_directory = '/data/local/tmp/devlib-target'
1296 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1297 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001298 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001299
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001300 def _ensure_executables_directory_is_writable(self):
1301 matched = []
1302 for entry in self.list_file_systems():
1303 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1304 matched.append(entry)
1305 if matched:
1306 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1307 if 'rw' not in entry.options:
1308 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1309 entry.mount_point),
1310 as_root=True)
1311 else:
1312 message = 'Could not find mount point for executables directory {}'
1313 raise TargetError(message.format(self.executables_directory))
1314
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001315 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1316
1317 @property
1318 def charging_enabled(self):
1319 """
1320 Whether drawing power to charge the battery is enabled
1321
1322 Not all devices have the ability to enable/disable battery charging
1323 (e.g. because they don't have a battery). In that case,
1324 ``charging_enabled`` is None.
1325 """
1326 if not self.file_exists(self._charging_enabled_path):
1327 return None
1328 return self.read_bool(self._charging_enabled_path)
1329
1330 @charging_enabled.setter
1331 def charging_enabled(self, enabled):
1332 """
1333 Enable/disable drawing power to charge the battery
1334
1335 Not all devices have this facility. In that case, do nothing.
1336 """
1337 if not self.file_exists(self._charging_enabled_path):
1338 return
1339 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001340
1341FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1342PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001343LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001344
1345
1346class Cpuinfo(object):
1347
1348 @property
1349 @memoized
1350 def architecture(self):
1351 for section in self.sections:
1352 if 'CPU architecture' in section:
1353 return section['CPU architecture']
1354 if 'architecture' in section:
1355 return section['architecture']
1356
1357 @property
1358 @memoized
1359 def cpu_names(self):
1360 cpu_names = []
1361 global_name = None
1362 for section in self.sections:
1363 if 'processor' in section:
1364 if 'CPU part' in section:
1365 cpu_names.append(_get_part_name(section))
1366 elif 'model name' in section:
1367 cpu_names.append(_get_model_name(section))
1368 else:
1369 cpu_names.append(None)
1370 elif 'CPU part' in section:
1371 global_name = _get_part_name(section)
1372 return [caseless_string(c or global_name) for c in cpu_names]
1373
1374 def __init__(self, text):
1375 self.sections = None
1376 self.text = None
1377 self.parse(text)
1378
1379 @memoized
1380 def get_cpu_features(self, cpuid=0):
1381 global_features = []
1382 for section in self.sections:
1383 if 'processor' in section:
1384 if int(section.get('processor')) != cpuid:
1385 continue
1386 if 'Features' in section:
1387 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001388 elif 'flags' in section:
1389 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001390 elif 'Features' in section:
1391 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001392 elif 'flags' in section:
1393 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001394 return global_features
1395
1396 def parse(self, text):
1397 self.sections = []
1398 current_section = {}
1399 self.text = text.strip()
1400 for line in self.text.split('\n'):
1401 line = line.strip()
1402 if line:
1403 key, value = line.split(':', 1)
1404 current_section[key.strip()] = value.strip()
1405 else: # not line
1406 self.sections.append(current_section)
1407 current_section = {}
1408 self.sections.append(current_section)
1409
1410 def __str__(self):
1411 return 'CpuInfo({})'.format(self.cpu_names)
1412
1413 __repr__ = __str__
1414
1415
1416class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001417 """
1418 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001419
Brendan Jackman54adf802017-02-20 17:57:51 +00001420 Not expected to work for very old (pre-3.0) kernel version numbers.
1421
1422 :ivar release: Version number/revision string. Typical output of
1423 ``uname -r``
1424 :type release: str
1425 :ivar version: Extra version info (aside from ``release``) reported by
1426 ``uname``
1427 :type version: str
1428 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1429 :type version_number: int
1430 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1431 :type major: int
1432 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1433 be None
1434 :type minor: int
1435 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1436 :type rc: int
1437 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1438 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001439
1440 :ivar parts: Tuple of version number components. Can be used for
1441 lexicographically comparing kernel versions.
1442 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001443 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001444 def __init__(self, version_string):
1445 if ' #' in version_string:
1446 release, version = version_string.split(' #')
1447 self.release = release
1448 self.version = version
1449 elif version_string.startswith('#'):
1450 self.release = ''
1451 self.version = version_string
1452 else:
1453 self.release = version_string
1454 self.version = ''
1455
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001456 self.version_number = None
1457 self.major = None
1458 self.minor = None
1459 self.sha1 = None
1460 self.rc = None
1461 match = KVERSION_REGEX.match(version_string)
1462 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001463 groups = match.groupdict()
1464 self.version_number = int(groups['version'])
1465 self.major = int(groups['major'])
1466 if groups['minor'] is not None:
1467 self.minor = int(groups['minor'])
1468 if groups['rc'] is not None:
1469 self.rc = int(groups['rc'])
1470 if groups['sha1'] is not None:
1471 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001472
Brendan Jackman18b77b82017-02-20 17:52:56 +00001473 self.parts = (self.version_number, self.major, self.minor)
1474
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001475 def __str__(self):
1476 return '{} {}'.format(self.release, self.version)
1477
1478 __repr__ = __str__
1479
1480
1481class KernelConfig(object):
1482
1483 not_set_regex = re.compile(r'# (\S+) is not set')
1484
1485 @staticmethod
1486 def get_config_name(name):
1487 name = name.upper()
1488 if not name.startswith('CONFIG_'):
1489 name = 'CONFIG_' + name
1490 return name
1491
1492 def iteritems(self):
1493 return self._config.iteritems()
1494
1495 def __init__(self, text):
1496 self.text = text
1497 self._config = {}
1498 for line in text.split('\n'):
1499 line = line.strip()
1500 if line.startswith('#'):
1501 match = self.not_set_regex.search(line)
1502 if match:
1503 self._config[match.group(1)] = 'n'
1504 elif '=' in line:
1505 name, value = line.split('=', 1)
1506 self._config[name.strip()] = value.strip()
1507
1508 def get(self, name):
1509 return self._config.get(self.get_config_name(name))
1510
1511 def like(self, name):
1512 regex = re.compile(name, re.I)
1513 result = {}
1514 for k, v in self._config.iteritems():
1515 if regex.search(k):
1516 result[k] = v
1517 return result
1518
1519 def is_enabled(self, name):
1520 return self.get(name) == 'y'
1521
1522 def is_module(self, name):
1523 return self.get(name) == 'm'
1524
1525 def is_not_set(self, name):
1526 return self.get(name) == 'n'
1527
1528 def has(self, name):
1529 return self.get(name) in ['m', 'y']
1530
1531
1532class LocalLinuxTarget(LinuxTarget):
1533
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001534 def __init__(self,
1535 connection_settings=None,
1536 platform=None,
1537 working_directory=None,
1538 executables_directory=None,
1539 connect=True,
1540 modules=None,
1541 load_default_modules=True,
1542 shell_prompt=DEFAULT_SHELL_PROMPT,
1543 conn_cls=LocalConnection,
1544 ):
1545 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1546 platform=platform,
1547 working_directory=working_directory,
1548 executables_directory=executables_directory,
1549 connect=connect,
1550 modules=modules,
1551 load_default_modules=load_default_modules,
1552 shell_prompt=shell_prompt,
1553 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001554
Sergei Trofimov961f9572015-11-18 17:32:26 +00001555 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001556 if self.working_directory is None:
1557 self.working_directory = '/tmp'
1558 if self.executables_directory is None:
1559 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001560
1561
1562def _get_model_name(section):
1563 name_string = section['model name']
1564 parts = name_string.split('@')[0].strip().split()
1565 return ' '.join([p for p in parts
1566 if '(' not in p and p != 'CPU'])
1567
1568
1569def _get_part_name(section):
1570 implementer = section.get('CPU implementer', '0x0')
1571 part = section['CPU part']
1572 variant = section.get('CPU variant', '0x0')
1573 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1574 if name is None:
1575 name = '{}/{}/{}'.format(implementer, part, variant)
1576 return name
Sergei Trofimov181bc182017-10-03 16:28:09 +01001577
1578
1579def _build_path_tree(path_map, basepath, sep=os.path.sep, dictcls=dict):
1580 """
1581 Convert a flat mapping of paths to values into a nested structure of
1582 dict-line object (``dict``'s by default), mirroring the directory hierarchy
1583 represented by the paths relative to ``basepath``.
1584
1585 """
1586 def process_node(node, path, value):
1587 parts = path.split(sep, 1)
1588 if len(parts) == 1: # leaf
1589 node[parts[0]] = value
1590 else: # branch
1591 if parts[0] not in node:
1592 node[parts[0]] = dictcls()
1593 process_node(node[parts[0]], parts[1], value)
1594
1595 relpath_map = {os.path.relpath(p, basepath): v
1596 for p, v in path_map.iteritems()}
1597
1598 if len(relpath_map) == 1 and relpath_map.keys()[0] == '.':
1599 result = relpath_map.values()[0]
1600 else:
1601 result = dictcls()
1602 for path, value in relpath_map.iteritems():
1603 process_node(result, path, value)
1604
1605 return result