blob: 8609fec21f1d635676ed21cde6fe4d605d78839e [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'):
623 path, value = entry.strip().split(':', 1)
624 result[path] = value
625 return result
626
627 def read_tree_values(self, path, depth=1, dictcls=dict, check_exit_code=True):
628 value_map = self.read_tree_values_flat(path, depth, check_exit_code)
629 return _build_path_tree(value_map, path, self.path.sep, dictcls)
630
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100631 # internal methods
632
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100633 def _setup_shutils(self):
634 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
635 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
636 shell_path = '/bin/sh'
637 if self.os == 'android':
638 shell_path = '/system/bin/sh'
639 with open(shutils_ifile) as fh:
640 lines = fh.readlines()
641 with open(shutils_ofile, 'w') as ofile:
642 for line in lines:
643 line = line.replace("__DEVLIB_SHELL__", shell_path)
644 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
645 ofile.write(line)
646 self._shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
647
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100648 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
649 command = '{} {}'.format(self.shutils, command)
650 return self.conn.execute(command, timeout, check_exit_code, as_root)
651
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100652 def _extract_archive(self, path, cmd, dest=None):
653 cmd = '{} ' + cmd # busybox
654 if dest:
655 extracted = dest
656 else:
657 extracted = self.path.dirname(path)
658 cmdtext = cmd.format(self.busybox, path, extracted)
659 self.execute(cmdtext)
660 return extracted
661
662 def _extract_file(self, path, cmd, dest=None):
663 cmd = '{} ' + cmd # busybox
664 cmdtext = cmd.format(self.busybox, path)
665 self.execute(cmdtext)
666 extracted = self.path.splitext(path)[0]
667 if dest:
668 self.execute('mv -f {} {}'.format(extracted, dest))
669 if dest.endswith('/'):
670 extracted = self.path.join(dest, self.path.basename(extracted))
671 else:
672 extracted = dest
673 return extracted
674
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100675 def _update_modules(self, stage):
676 for mod in self.modules:
677 if isinstance(mod, dict):
678 mod, params = mod.items()[0]
679 else:
680 params = {}
681 mod = get_module(mod)
682 if not mod.stage == stage:
683 continue
684 if mod.probe(self):
685 self._install_module(mod, **params)
686 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000687 msg = 'Module {} is not supported by the target'.format(mod.name)
688 if self.load_default_modules:
689 self.logger.debug(msg)
690 else:
691 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100692
693 def _install_module(self, mod, **params):
694 if mod.name not in self._installed_modules:
695 self.logger.debug('Installing module {}'.format(mod.name))
696 mod.install(self, **params)
697 self._installed_modules[mod.name] = mod
698 else:
699 self.logger.debug('Module {} is already installed.'.format(mod.name))
700
Sergei Trofimov961f9572015-11-18 17:32:26 +0000701 def _resolve_paths(self):
702 raise NotImplementedError()
703
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100704
705class LinuxTarget(Target):
706
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100707 path = posixpath
708 os = 'linux'
709
710 @property
711 @memoized
712 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100713 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100714 for abi, architectures in ABI_MAP.iteritems():
715 if value in architectures:
716 result = abi
717 break
718 else:
719 result = value
720 return result
721
722 @property
723 @memoized
724 def os_version(self):
725 os_version = {}
726 try:
727 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
728 version_files = self.execute(command, check_exit_code=False).strip().split()
729 for vf in version_files:
730 name = self.path.basename(vf)
731 output = self.read_value(vf)
732 os_version[name] = output.strip().replace('\n', ' ')
733 except TargetError:
734 raise
735 return os_version
736
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000737 @property
738 @memoized
739 # There is currently no better way to do this cross platform.
740 # ARM does not have dmidecode
741 def model(self):
742 if self.file_exists("/proc/device-tree/model"):
743 raw_model = self.execute("cat /proc/device-tree/model")
744 return '_'.join(raw_model.split()[:2])
745 return None
746
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000747 def __init__(self,
748 connection_settings=None,
749 platform=None,
750 working_directory=None,
751 executables_directory=None,
752 connect=True,
753 modules=None,
754 load_default_modules=True,
755 shell_prompt=DEFAULT_SHELL_PROMPT,
756 conn_cls=SshConnection,
757 ):
758 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
759 platform=platform,
760 working_directory=working_directory,
761 executables_directory=executables_directory,
762 connect=connect,
763 modules=modules,
764 load_default_modules=load_default_modules,
765 shell_prompt=shell_prompt,
766 conn_cls=conn_cls)
767
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100768 def connect(self, timeout=None):
769 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100770
771 def kick_off(self, command, as_root=False):
772 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
773 return self.conn.execute(command, as_root=as_root)
774
775 def get_pids_of(self, process_name):
776 """Returns a list of PIDs of all processes with the specified name."""
777 # result should be a column of PIDs with the first row as "PID" header
778 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
779 check_exit_code=False).strip().split()
780 if len(result) >= 2: # at least one row besides the header
781 return map(int, result[1:])
782 else:
783 return []
784
785 def ps(self, **kwargs):
786 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
787 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
788 lines.next() # header
789
790 result = []
791 for line in lines:
792 parts = re.split(r'\s+', line, maxsplit=8)
793 if parts and parts != ['']:
794 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
795
796 if not kwargs:
797 return result
798 else:
799 filtered_result = []
800 for entry in result:
801 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
802 filtered_result.append(entry)
803 return filtered_result
804
805 def list_directory(self, path, as_root=False):
806 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
807 return [x.strip() for x in contents.split('\n') if x.strip()]
808
809 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
810 destpath = self.path.join(self.executables_directory,
811 with_name and with_name or self.path.basename(filepath))
812 self.push(filepath, destpath)
813 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
814 self._installed_binaries[self.path.basename(destpath)] = destpath
815 return destpath
816
817 def uninstall(self, name):
818 path = self.path.join(self.executables_directory, name)
819 self.remove(path)
820
821 def capture_screen(self, filepath):
822 if not self.is_installed('scrot'):
823 self.logger.debug('Could not take screenshot as scrot is not installed.')
824 return
825 try:
826
827 tmpfile = self.tempfile()
828 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
829 self.pull(tmpfile, filepath)
830 self.remove(tmpfile)
831 except TargetError as e:
832 if "Can't open X dispay." not in e.message:
833 raise e
834 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
835 self.logger.debug('Could not take screenshot: {}'.format(message))
836
Sergei Trofimov961f9572015-11-18 17:32:26 +0000837 def _resolve_paths(self):
838 if self.working_directory is None:
839 if self.connected_as_root:
840 self.working_directory = '/root/devlib-target'
841 else:
842 self.working_directory = '/home/{}/devlib-target'.format(self.user)
843 if self.executables_directory is None:
844 self.executables_directory = self.path.join(self.working_directory, 'bin')
845
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100846
847class AndroidTarget(Target):
848
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100849 path = posixpath
850 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100851 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100852
853 @property
854 @memoized
855 def abi(self):
856 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
857
858 @property
859 @memoized
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100860 def supported_abi(self):
861 props = self.getprop()
862 result = [props['ro.product.cpu.abi']]
863 if 'ro.product.cpu.abi2' in props:
864 result.append(props['ro.product.cpu.abi2'])
865 if 'ro.product.cpu.abilist' in props:
866 for abi in props['ro.product.cpu.abilist'].split(','):
867 if abi not in result:
868 result.append(abi)
869
870 mapped_result = []
871 for supported_abi in result:
872 for abi, architectures in ABI_MAP.iteritems():
873 found = False
874 if supported_abi in architectures and abi not in mapped_result:
875 mapped_result.append(abi)
876 found = True
877 break
878 if not found and supported_abi not in mapped_result:
879 mapped_result.append(supported_abi)
880 return mapped_result
881
882 @property
883 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100884 def os_version(self):
885 os_version = {}
886 for k, v in self.getprop().iteritems():
887 if k.startswith('ro.build.version'):
888 part = k.split('.')[-1]
889 os_version[part] = v
890 return os_version
891
892 @property
893 def adb_name(self):
894 return self.conn.device
895
896 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000897 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000898 def android_id(self):
899 """
900 Get the device's ANDROID_ID. Which is
901
902 "A 64-bit number (as a hex string) that is randomly generated when the user
903 first sets up the device and should remain constant for the lifetime of the
904 user's device."
905
906 .. note:: This will get reset on userdata erasure.
907
908 """
909 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
910 return output.split('value=')[-1]
911
912 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100913 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000914 def model(self):
915 try:
916 return self.getprop(prop='ro.product.device')
917 except KeyError:
918 return None
919
920 @property
921 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100922 def screen_resolution(self):
923 output = self.execute('dumpsys window')
924 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
925 if match:
926 return (int(match.group('width')),
927 int(match.group('height')))
928 else:
929 return (0, 0)
930
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000931 def __init__(self,
932 connection_settings=None,
933 platform=None,
934 working_directory=None,
935 executables_directory=None,
936 connect=True,
937 modules=None,
938 load_default_modules=True,
939 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000940 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000941 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000942 ):
943 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
944 platform=platform,
945 working_directory=working_directory,
946 executables_directory=executables_directory,
947 connect=connect,
948 modules=modules,
949 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000950 shell_prompt=shell_prompt,
951 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000952 self.package_data_directory = package_data_directory
953
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100954 def reset(self, fastboot=False): # pylint: disable=arguments-differ
955 try:
956 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100957 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100958 except (TargetError, TimeoutError, subprocess.CalledProcessError):
959 # on some targets "reboot" doesn't return gracefully
960 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000961 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100962
Patrick Bellasif26f9422017-07-12 12:27:28 +0100963 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100964 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +0100965 boot_completed = boolean(self.getprop('sys.boot_completed'))
966 while not boot_completed and timeout >= time.time() - start:
967 time.sleep(5)
968 boot_completed = boolean(self.getprop('sys.boot_completed'))
969 if not boot_completed:
970 raise TargetError('Connected but Android did not fully boot.')
971
972 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100973 device = self.connection_settings.get('device')
974 if device and ':' in device:
975 # ADB does not automatically remove a network device from it's
976 # devices list when the connection is broken by the remote, so the
977 # adb connection may have gone "stale", resulting in adb blocking
978 # indefinitely when making calls to the device. To avoid this,
979 # always disconnect first.
980 adb_disconnect(device)
981 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100982
983 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +0100984 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100985
986 def setup(self, executables=None):
987 super(AndroidTarget, self).setup(executables)
988 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
989
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100990 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100991 """
992 Like execute but closes adb session and returns immediately, leaving the command running on the
993 device (this is different from execute(background=True) which keeps adb connection open and returns
994 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100995 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100996 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100997 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100998 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +0100999 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001000 output = self.execute(command, timeout=1, as_root=as_root)
1001 except TimeoutError:
1002 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001003
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001004 def __setup_list_directory(self):
1005 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
1006 # AOSP 7.0 as well, the ls command was changed.
1007 # Previous versions default to a single column listing, which is nice and easy to parse.
1008 # Newer versions default to a multi-column listing, which is not, but it does support
1009 # a '-1' option to get into single column mode. Older versions do not support this option
1010 # so we try the new version, and if it fails we use the old version.
1011 self.ls_command = 'ls -1'
1012 try:
Marc Bonnici06552372017-03-29 16:43:22 +01001013 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001014 except TargetError:
1015 self.ls_command = 'ls'
1016
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001017 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001018 if self.ls_command == '':
1019 self.__setup_list_directory()
1020 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001021 return [x.strip() for x in contents.split('\n') if x.strip()]
1022
1023 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1024 ext = os.path.splitext(filepath)[1].lower()
1025 if ext == '.apk':
1026 return self.install_apk(filepath, timeout)
1027 else:
1028 return self.install_executable(filepath, with_name)
1029
1030 def uninstall(self, name):
1031 if self.package_is_installed(name):
1032 self.uninstall_package(name)
1033 else:
1034 self.uninstall_executable(name)
1035
1036 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001037 result = []
1038 search_term = process_name[-15:]
1039 for entry in self.ps():
1040 if search_term in entry.name:
1041 result.append(entry.pid)
1042 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001043
1044 def ps(self, **kwargs):
1045 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1046 lines.next() # header
1047 result = []
1048 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001049 parts = line.split(None, 8)
Sergei Trofimov109fcc62017-09-26 13:30:15 +01001050 if not parts:
1051 continue
1052 if len(parts) == 8:
1053 # wchan was blank; insert an empty field where it should be.
1054 parts.insert(5, '')
1055 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001056 if not kwargs:
1057 return result
1058 else:
1059 filtered_result = []
1060 for entry in result:
1061 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1062 filtered_result.append(entry)
1063 return filtered_result
1064
1065 def capture_screen(self, filepath):
1066 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1067 self.execute('screencap -p {}'.format(on_device_file))
1068 self.pull(on_device_file, filepath)
1069 self.remove(on_device_file)
1070
1071 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1072 if not as_root:
1073 self.conn.push(source, dest, timeout=timeout)
1074 else:
1075 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001076 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001077 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001078 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001079
1080 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1081 if not as_root:
1082 self.conn.pull(source, dest, timeout=timeout)
1083 else:
1084 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001085 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1086 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001087 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001088 self.conn.pull(device_tempfile, dest, timeout=timeout)
1089
1090 # Android-specific
1091
Marc Bonnici8839ed02017-08-10 17:19:13 +01001092 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001093 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001094 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001095 if direction == "diagonal":
1096 start = 100
1097 stop = width - start
1098 swipe_height = height * 2 // 3
1099 self.execute(command.format(start, swipe_height, stop, 0))
1100 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001101 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001102 start = 100
1103 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001104 self.execute(command.format(start, swipe_height, stop, swipe_height))
1105 elif direction == "vertical":
1106 swipe_middle = width / 2
1107 swipe_height = height * 2 // 3
1108 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001109 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001110 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001111
1112 def getprop(self, prop=None):
1113 props = AndroidProperties(self.execute('getprop'))
1114 if prop:
1115 return props[prop]
1116 return props
1117
1118 def is_installed(self, name):
1119 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1120
1121 def package_is_installed(self, package_name):
1122 return package_name in self.list_packages()
1123
1124 def list_packages(self):
1125 output = self.execute('pm list packages')
1126 output = output.replace('package:', '')
1127 return output.split()
1128
1129 def get_package_version(self, package):
1130 output = self.execute('dumpsys package {}'.format(package))
1131 for line in convert_new_lines(output).split('\n'):
1132 if 'versionName' in line:
1133 return line.split('=', 1)[1]
1134 return None
1135
Marc Bonnicid3396f22017-05-31 15:56:50 +01001136 def get_sdk_version(self):
1137 try:
1138 return int(self.getprop('ro.build.version.sdk'))
1139 except (ValueError, TypeError):
1140 return None
1141
Marc Bonnicic33dd652017-05-31 15:51:31 +01001142 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001143 ext = os.path.splitext(filepath)[1].lower()
1144 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001145 flags = []
1146 if replace:
1147 flags.append('-r') # Replace existing APK
1148 if allow_downgrade:
1149 flags.append('-d') # Install the APK even if a newer version is already installed
1150 if self.get_sdk_version() >= 23:
1151 flags.append('-g') # Grant all runtime permissions
1152 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1153 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001154 else:
1155 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1156
1157 def install_executable(self, filepath, with_name=None):
1158 self._ensure_executables_directory_is_writable()
1159 executable_name = with_name or os.path.basename(filepath)
1160 on_device_file = self.path.join(self.working_directory, executable_name)
1161 on_device_executable = self.path.join(self.executables_directory, executable_name)
1162 self.push(filepath, on_device_file)
1163 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001164 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1165 self.remove(on_device_file, as_root=self.needs_su)
1166 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001167 self._installed_binaries[executable_name] = on_device_executable
1168 return on_device_executable
1169
1170 def uninstall_package(self, package):
1171 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1172
1173 def uninstall_executable(self, executable_name):
1174 on_device_executable = self.path.join(self.executables_directory, executable_name)
1175 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001176 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001177
1178 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001179 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001180 filtstr = ' -s {}'.format(filter) if filter else ''
1181 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1182 adb_command(self.adb_name, command, timeout=timeout)
1183
1184 def clear_logcat(self):
1185 adb_command(self.adb_name, 'logcat -c', timeout=30)
1186
Valentin Schneider7c2fd872017-09-11 16:58:24 +01001187 def get_logcat_monitor(self, regexps=None):
1188 return LogcatMonitor(self, regexps)
1189
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001190 def adb_kill_server(self, timeout=30):
1191 adb_command(self.adb_name, 'kill-server', timeout)
1192
1193 def adb_wait_for_device(self, timeout=30):
1194 adb_command(self.adb_name, 'wait-for-device', timeout)
1195
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001196 def adb_reboot_bootloader(self, timeout=30):
1197 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1198
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001199 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001200 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001201 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001202 return
1203 adb_command(self.adb_name, 'root', timeout=30)
1204 self._connected_as_root = True
1205 return
1206 adb_command(self.adb_name, 'unroot', timeout=30)
1207 self._connected_as_root = False
1208
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001209 def is_screen_on(self):
1210 output = self.execute('dumpsys power')
1211 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1212 if match:
1213 return boolean(match.group(1))
1214 else:
1215 raise TargetError('Could not establish screen state.')
1216
1217 def ensure_screen_is_on(self):
1218 if not self.is_screen_on():
1219 self.execute('input keyevent 26')
1220
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001221 def ensure_screen_is_off(self):
1222 if self.is_screen_on():
1223 self.execute('input keyevent 26')
1224
Marc Bonniciddd2e292017-07-06 17:30:12 +01001225 def set_auto_brightness(self, auto_brightness):
1226 cmd = 'settings put system screen_brightness_mode {}'
1227 self.execute(cmd.format(int(boolean(auto_brightness))))
1228
1229 def get_auto_brightness(self):
1230 cmd = 'settings get system screen_brightness_mode'
1231 return boolean(self.execute(cmd).strip())
1232
1233 def set_brightness(self, value):
1234 if not 0 <= value <= 255:
1235 msg = 'Invalid brightness "{}"; Must be between 0 and 255'
1236 raise ValueError(msg.format(value))
1237 self.set_auto_brightness(False)
1238 cmd = 'settings put system screen_brightness {}'
1239 self.execute(cmd.format(int(value)))
1240
1241 def get_brightness(self):
1242 cmd = 'settings get system screen_brightness'
1243 return integer(self.execute(cmd).strip())
1244
Marc Bonnici3e751742017-07-06 17:31:02 +01001245 def get_airplane_mode(self):
1246 cmd = 'settings get global airplane_mode_on'
1247 return boolean(self.execute(cmd).strip())
1248
1249 def set_airplane_mode(self, mode):
1250 root_required = self.get_sdk_version() > 23
1251 if root_required and not self.is_rooted:
1252 raise TargetError('Root is required to toggle airplane mode on Android 7+')
1253 cmd = 'settings put global airplane_mode_on {}'
1254 self.execute(cmd.format(int(boolean(mode))))
1255 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE', as_root=root_required)
1256
Marc Bonnici003785d2017-07-24 17:44:33 +01001257 def get_auto_rotation(self):
1258 cmd = 'settings get system accelerometer_rotation'
1259 return boolean(self.execute(cmd).strip())
1260
1261 def set_auto_rotation(self, autorotate):
1262 cmd = 'settings put system accelerometer_rotation {}'
1263 self.execute(cmd.format(int(boolean(autorotate))))
1264
1265 def set_natural_rotation(self):
1266 self.set_rotation(0)
1267
1268 def set_left_rotation(self):
1269 self.set_rotation(1)
1270
1271 def set_inverted_rotation(self):
1272 self.set_rotation(2)
1273
1274 def set_right_rotation(self):
1275 self.set_rotation(3)
1276
1277 def get_rotation(self):
1278 cmd = 'settings get system user_rotation'
1279 return self.execute(cmd).strip()
1280
1281 def set_rotation(self, rotation):
1282 if not 0 <= rotation <= 3:
1283 raise ValueError('Rotation value must be between 0 and 3')
1284 self.set_auto_rotation(False)
1285 cmd = 'settings put system user_rotation {}'
1286 self.execute(cmd.format(rotation))
1287
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001288 def homescreen(self):
1289 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1290
Sergei Trofimov961f9572015-11-18 17:32:26 +00001291 def _resolve_paths(self):
1292 if self.working_directory is None:
1293 self.working_directory = '/data/local/tmp/devlib-target'
1294 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1295 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001296 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001297
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001298 def _ensure_executables_directory_is_writable(self):
1299 matched = []
1300 for entry in self.list_file_systems():
1301 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1302 matched.append(entry)
1303 if matched:
1304 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1305 if 'rw' not in entry.options:
1306 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1307 entry.mount_point),
1308 as_root=True)
1309 else:
1310 message = 'Could not find mount point for executables directory {}'
1311 raise TargetError(message.format(self.executables_directory))
1312
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001313 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1314
1315 @property
1316 def charging_enabled(self):
1317 """
1318 Whether drawing power to charge the battery is enabled
1319
1320 Not all devices have the ability to enable/disable battery charging
1321 (e.g. because they don't have a battery). In that case,
1322 ``charging_enabled`` is None.
1323 """
1324 if not self.file_exists(self._charging_enabled_path):
1325 return None
1326 return self.read_bool(self._charging_enabled_path)
1327
1328 @charging_enabled.setter
1329 def charging_enabled(self, enabled):
1330 """
1331 Enable/disable drawing power to charge the battery
1332
1333 Not all devices have this facility. In that case, do nothing.
1334 """
1335 if not self.file_exists(self._charging_enabled_path):
1336 return
1337 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001338
1339FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1340PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001341LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001342
1343
1344class Cpuinfo(object):
1345
1346 @property
1347 @memoized
1348 def architecture(self):
1349 for section in self.sections:
1350 if 'CPU architecture' in section:
1351 return section['CPU architecture']
1352 if 'architecture' in section:
1353 return section['architecture']
1354
1355 @property
1356 @memoized
1357 def cpu_names(self):
1358 cpu_names = []
1359 global_name = None
1360 for section in self.sections:
1361 if 'processor' in section:
1362 if 'CPU part' in section:
1363 cpu_names.append(_get_part_name(section))
1364 elif 'model name' in section:
1365 cpu_names.append(_get_model_name(section))
1366 else:
1367 cpu_names.append(None)
1368 elif 'CPU part' in section:
1369 global_name = _get_part_name(section)
1370 return [caseless_string(c or global_name) for c in cpu_names]
1371
1372 def __init__(self, text):
1373 self.sections = None
1374 self.text = None
1375 self.parse(text)
1376
1377 @memoized
1378 def get_cpu_features(self, cpuid=0):
1379 global_features = []
1380 for section in self.sections:
1381 if 'processor' in section:
1382 if int(section.get('processor')) != cpuid:
1383 continue
1384 if 'Features' in section:
1385 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001386 elif 'flags' in section:
1387 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001388 elif 'Features' in section:
1389 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001390 elif 'flags' in section:
1391 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001392 return global_features
1393
1394 def parse(self, text):
1395 self.sections = []
1396 current_section = {}
1397 self.text = text.strip()
1398 for line in self.text.split('\n'):
1399 line = line.strip()
1400 if line:
1401 key, value = line.split(':', 1)
1402 current_section[key.strip()] = value.strip()
1403 else: # not line
1404 self.sections.append(current_section)
1405 current_section = {}
1406 self.sections.append(current_section)
1407
1408 def __str__(self):
1409 return 'CpuInfo({})'.format(self.cpu_names)
1410
1411 __repr__ = __str__
1412
1413
1414class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001415 """
1416 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001417
Brendan Jackman54adf802017-02-20 17:57:51 +00001418 Not expected to work for very old (pre-3.0) kernel version numbers.
1419
1420 :ivar release: Version number/revision string. Typical output of
1421 ``uname -r``
1422 :type release: str
1423 :ivar version: Extra version info (aside from ``release``) reported by
1424 ``uname``
1425 :type version: str
1426 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1427 :type version_number: int
1428 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1429 :type major: int
1430 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1431 be None
1432 :type minor: int
1433 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1434 :type rc: int
1435 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1436 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001437
1438 :ivar parts: Tuple of version number components. Can be used for
1439 lexicographically comparing kernel versions.
1440 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001441 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001442 def __init__(self, version_string):
1443 if ' #' in version_string:
1444 release, version = version_string.split(' #')
1445 self.release = release
1446 self.version = version
1447 elif version_string.startswith('#'):
1448 self.release = ''
1449 self.version = version_string
1450 else:
1451 self.release = version_string
1452 self.version = ''
1453
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001454 self.version_number = None
1455 self.major = None
1456 self.minor = None
1457 self.sha1 = None
1458 self.rc = None
1459 match = KVERSION_REGEX.match(version_string)
1460 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001461 groups = match.groupdict()
1462 self.version_number = int(groups['version'])
1463 self.major = int(groups['major'])
1464 if groups['minor'] is not None:
1465 self.minor = int(groups['minor'])
1466 if groups['rc'] is not None:
1467 self.rc = int(groups['rc'])
1468 if groups['sha1'] is not None:
1469 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001470
Brendan Jackman18b77b82017-02-20 17:52:56 +00001471 self.parts = (self.version_number, self.major, self.minor)
1472
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001473 def __str__(self):
1474 return '{} {}'.format(self.release, self.version)
1475
1476 __repr__ = __str__
1477
1478
1479class KernelConfig(object):
1480
1481 not_set_regex = re.compile(r'# (\S+) is not set')
1482
1483 @staticmethod
1484 def get_config_name(name):
1485 name = name.upper()
1486 if not name.startswith('CONFIG_'):
1487 name = 'CONFIG_' + name
1488 return name
1489
1490 def iteritems(self):
1491 return self._config.iteritems()
1492
1493 def __init__(self, text):
1494 self.text = text
1495 self._config = {}
1496 for line in text.split('\n'):
1497 line = line.strip()
1498 if line.startswith('#'):
1499 match = self.not_set_regex.search(line)
1500 if match:
1501 self._config[match.group(1)] = 'n'
1502 elif '=' in line:
1503 name, value = line.split('=', 1)
1504 self._config[name.strip()] = value.strip()
1505
1506 def get(self, name):
1507 return self._config.get(self.get_config_name(name))
1508
1509 def like(self, name):
1510 regex = re.compile(name, re.I)
1511 result = {}
1512 for k, v in self._config.iteritems():
1513 if regex.search(k):
1514 result[k] = v
1515 return result
1516
1517 def is_enabled(self, name):
1518 return self.get(name) == 'y'
1519
1520 def is_module(self, name):
1521 return self.get(name) == 'm'
1522
1523 def is_not_set(self, name):
1524 return self.get(name) == 'n'
1525
1526 def has(self, name):
1527 return self.get(name) in ['m', 'y']
1528
1529
1530class LocalLinuxTarget(LinuxTarget):
1531
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001532 def __init__(self,
1533 connection_settings=None,
1534 platform=None,
1535 working_directory=None,
1536 executables_directory=None,
1537 connect=True,
1538 modules=None,
1539 load_default_modules=True,
1540 shell_prompt=DEFAULT_SHELL_PROMPT,
1541 conn_cls=LocalConnection,
1542 ):
1543 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1544 platform=platform,
1545 working_directory=working_directory,
1546 executables_directory=executables_directory,
1547 connect=connect,
1548 modules=modules,
1549 load_default_modules=load_default_modules,
1550 shell_prompt=shell_prompt,
1551 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001552
Sergei Trofimov961f9572015-11-18 17:32:26 +00001553 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001554 if self.working_directory is None:
1555 self.working_directory = '/tmp'
1556 if self.executables_directory is None:
1557 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001558
1559
1560def _get_model_name(section):
1561 name_string = section['model name']
1562 parts = name_string.split('@')[0].strip().split()
1563 return ' '.join([p for p in parts
1564 if '(' not in p and p != 'CPU'])
1565
1566
1567def _get_part_name(section):
1568 implementer = section.get('CPU implementer', '0x0')
1569 part = section['CPU part']
1570 variant = section.get('CPU variant', '0x0')
1571 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1572 if name is None:
1573 name = '{}/{}/{}'.format(implementer, part, variant)
1574 return name
Sergei Trofimov181bc182017-10-03 16:28:09 +01001575
1576
1577def _build_path_tree(path_map, basepath, sep=os.path.sep, dictcls=dict):
1578 """
1579 Convert a flat mapping of paths to values into a nested structure of
1580 dict-line object (``dict``'s by default), mirroring the directory hierarchy
1581 represented by the paths relative to ``basepath``.
1582
1583 """
1584 def process_node(node, path, value):
1585 parts = path.split(sep, 1)
1586 if len(parts) == 1: # leaf
1587 node[parts[0]] = value
1588 else: # branch
1589 if parts[0] not in node:
1590 node[parts[0]] = dictcls()
1591 process_node(node[parts[0]], parts[1], value)
1592
1593 relpath_map = {os.path.relpath(p, basepath): v
1594 for p, v in path_map.iteritems()}
1595
1596 if len(relpath_map) == 1 and relpath_map.keys()[0] == '.':
1597 result = relpath_map.values()[0]
1598 else:
1599 result = dictcls()
1600 for path, value in relpath_map.iteritems():
1601 process_node(result, path, value)
1602
1603 return result