blob: fca798cff7a19d5276566db6173f3640bcc37e2a [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
152 def __init__(self,
153 connection_settings=None,
154 platform=None,
155 working_directory=None,
156 executables_directory=None,
157 connect=True,
158 modules=None,
159 load_default_modules=True,
160 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000161 conn_cls=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100162 ):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +0000163 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100164 self.connection_settings = connection_settings or {}
Anouk Van Laer21f40032017-01-31 13:11:03 +0000165 # Set self.platform: either it's given directly (by platform argument)
166 # or it's given in the connection_settings argument
167 # If neither, create default Platform()
168 if platform is None:
169 self.platform = self.connection_settings.get('platform', Platform())
170 else:
171 self.platform = platform
172 # Check if the user hasn't given two different platforms
173 if 'platform' in self.connection_settings:
174 if connection_settings['platform'] is not platform:
175 raise TargetError('Platform specified in connection_settings '
176 '({}) differs from that directly passed '
177 '({})!)'
178 .format(connection_settings['platform'],
179 self.platform))
180 self.connection_settings['platform'] = self.platform
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100181 self.working_directory = working_directory
182 self.executables_directory = executables_directory
183 self.modules = modules or []
184 self.load_default_modules = load_default_modules
185 self.shell_prompt = shell_prompt
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000186 self.conn_cls = conn_cls
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100187 self.logger = logging.getLogger(self.__class__.__name__)
188 self._installed_binaries = {}
189 self._installed_modules = {}
190 self._cache = {}
191 self._connections = {}
192 self.busybox = None
193
194 if load_default_modules:
195 module_lists = [self.default_modules]
196 else:
197 module_lists = []
198 module_lists += [self.modules, self.platform.modules]
199 self.modules = merge_lists(*module_lists, duplicates='first')
200 self._update_modules('early')
201 if connect:
202 self.connect()
203
204 # connection and initialization
205
206 def connect(self, timeout=None):
207 self.platform.init_target_connection(self)
208 tid = id(threading.current_thread())
209 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000210 self._resolve_paths()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100211 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100212 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100213 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100214 if self.platform.big_core and self.load_default_modules:
215 self._install_module(get_module('bl'))
216
217 def disconnect(self):
218 for conn in self._connections.itervalues():
219 conn.close()
220 self._connections = {}
221
222 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000223 if self.conn_cls == None:
224 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100225 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
226
227 def setup(self, executables=None):
228 self.execute('mkdir -p {}'.format(self.working_directory))
229 self.execute('mkdir -p {}'.format(self.executables_directory))
230 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000231
232 # Setup shutils script for the target
233 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
234 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
235 shell_path = '/bin/sh'
236 if self.os == 'android':
237 shell_path = '/system/bin/sh'
238 with open(shutils_ifile) as fh:
239 lines = fh.readlines()
240 with open(shutils_ofile, 'w') as ofile:
241 for line in lines:
242 line = line.replace("__DEVLIB_SHELL__", shell_path)
243 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
244 ofile.write(line)
245 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
246
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100247 for host_exe in (executables or []): # pylint: disable=superfluous-parens
248 self.install(host_exe)
249
Anouk Van Laer21f40032017-01-31 13:11:03 +0000250 # Check for platform dependent setup procedures
251 self.platform.setup(self)
252
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100253 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
254 self._update_modules('setup')
255
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100256 def reboot(self, hard=False, connect=True, timeout=180):
257 if hard:
258 if not self.has('hard_reset'):
259 raise TargetError('Hard reset not supported for this target.')
260 self.hard_reset() # pylint: disable=no-member
261 else:
262 if not self.is_connected:
263 message = 'Cannot reboot target becuase it is disconnected. ' +\
264 'Either connect() first, or specify hard=True ' +\
265 '(in which case, a hard_reset module must be installed)'
266 raise TargetError(message)
267 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000268 # Wait a fixed delay before starting polling to give the target time to
269 # shut down, otherwise, might create the connection while it's still shutting
270 # down resulting in subsequenct connection failing.
271 self.logger.debug('Waiting for target to power down...')
272 reset_delay = 20
273 time.sleep(reset_delay)
274 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100275 if self.has('boot'):
276 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000277 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100278 if connect:
279 self.connect(timeout=timeout)
280
281 # file transfer
282
283 def push(self, source, dest, timeout=None):
284 return self.conn.push(source, dest, timeout=timeout)
285
286 def pull(self, source, dest, timeout=None):
287 return self.conn.pull(source, dest, timeout=timeout)
288
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +0100289 def get_directory(self, source_dir, dest):
290 """ Pull a directory from the device, after compressing dir """
291 # Create all file names
292 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
293 # Host location of dir
294 outdir = os.path.join(dest, tar_file_name)
295 # Host location of archive
296 tar_file_name = '{}.tar'.format(tar_file_name)
297 tempfile = os.path.join(dest, tar_file_name)
298
299 # Does the folder exist?
300 self.execute('ls -la {}'.format(source_dir))
301 # Try compressing the folder
302 try:
303 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
304 source_dir))
305 except TargetError:
306 self.logger.debug('Failed to run tar command on target! ' \
307 'Not pulling directory {}'.format(source_dir))
308 # Pull the file
309 os.mkdir(outdir)
310 self.pull(tar_file_name, tempfile )
311 # Decompress
312 f = tarfile.open(tempfile, 'r')
313 f.extractall(outdir)
314 os.remove(tempfile)
315
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100316 # execution
317
318 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
setrofimb7386552017-05-23 17:39:12 +0100319 return self.conn.execute(command, timeout, check_exit_code, as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100320
321 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
322 return self.conn.background(command, stdout, stderr, as_root)
323
324 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
325 as_root=False, timeout=30):
326 """
327 Executes the specified binary under the specified conditions.
328
329 :binary: binary to execute. Must be present and executable on the device.
330 :args: arguments to be passed to the binary. The can be either a list or
331 a string.
332 :in_directory: execute the binary in the specified directory. This must
333 be an absolute path.
334 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
335 case, it will be interpreted as the mask), a list of ``ints``, in which
336 case this will be interpreted as the list of cpus, or string, which
337 will be interpreted as a comma-separated list of cpu ranges, e.g.
338 ``"0,4-7"``.
339 :as_root: Specify whether the command should be run as root
340 :timeout: If the invocation does not terminate within this number of seconds,
341 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
342 invocation should not timeout.
343
Brendan Jackman27f545f2016-11-15 16:58:57 +0000344 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100345 """
346 command = binary
347 if args:
348 if isiterable(args):
349 args = ' '.join(args)
350 command = '{} {}'.format(command, args)
351 if on_cpus:
352 on_cpus = bitmask(on_cpus)
353 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
354 if in_directory:
355 command = 'cd {} && {}'.format(in_directory, command)
356 return self.execute(command, as_root=as_root, timeout=timeout)
357
Valentin Schneider92b0c252017-07-06 16:01:15 +0100358 def background_invoke(self, binary, args=None, in_directory=None,
359 on_cpus=None, as_root=False):
360 """
361 Executes the specified binary as a background task under the
362 specified conditions.
363
364 :binary: binary to execute. Must be present and executable on the device.
365 :args: arguments to be passed to the binary. The can be either a list or
366 a string.
367 :in_directory: execute the binary in the specified directory. This must
368 be an absolute path.
369 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
370 case, it will be interpreted as the mask), a list of ``ints``, in which
371 case this will be interpreted as the list of cpus, or string, which
372 will be interpreted as a comma-separated list of cpu ranges, e.g.
373 ``"0,4-7"``.
374 :as_root: Specify whether the command should be run as root
375
376 :returns: the subprocess instance handling that command
377 """
378 command = binary
379 if args:
380 if isiterable(args):
381 args = ' '.join(args)
382 command = '{} {}'.format(command, args)
383 if on_cpus:
384 on_cpus = bitmask(on_cpus)
385 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
386 if in_directory:
387 command = 'cd {} && {}'.format(in_directory, command)
388 return self.background(command, as_root=as_root)
389
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100390 def kick_off(self, command, as_root=False):
391 raise NotImplementedError()
392
393 # sysfs interaction
394
395 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100396 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100397 if kind:
398 return kind(output)
399 else:
400 return output
401
402 def read_int(self, path):
403 return self.read_value(path, kind=integer)
404
405 def read_bool(self, path):
406 return self.read_value(path, kind=boolean)
407
408 def write_value(self, path, value, verify=True):
409 value = str(value)
410 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
411 if verify:
412 output = self.read_value(path)
413 if not output == value:
414 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
415 raise TargetError(message)
416
417 def reset(self):
418 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100419 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100420 except (TargetError, TimeoutError, subprocess.CalledProcessError):
421 # on some targets "reboot" doesn't return gracefully
422 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000423 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100424
425 def check_responsive(self):
426 try:
427 self.conn.execute('ls /', timeout=5)
428 except (TimeoutError, subprocess.CalledProcessError):
429 raise TargetNotRespondingError(self.conn.name)
430
431 # process management
432
433 def kill(self, pid, signal=None, as_root=False):
434 signal_string = '-s {}'.format(signal) if signal else ''
435 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
436
437 def killall(self, process_name, signal=None, as_root=False):
438 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000439 try:
440 self.kill(pid, signal=signal, as_root=as_root)
441 except TargetError:
442 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100443
444 def get_pids_of(self, process_name):
445 raise NotImplementedError()
446
447 def ps(self, **kwargs):
448 raise NotImplementedError()
449
450 # files
451
452 def file_exists(self, filepath):
453 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000454 output = self.execute(command.format(filepath), as_root=self.is_rooted)
455 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100456
Sebastian Goscik33603c62016-02-15 15:07:19 +0000457 def directory_exists(self, filepath):
458 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
459 # output from ssh my contain part of the expression in the buffer,
460 # split out everything except the last word.
461 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
462
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100463 def list_file_systems(self):
464 output = self.execute('mount')
465 fstab = []
466 for line in output.split('\n'):
467 line = line.strip()
468 if not line:
469 continue
470 match = FSTAB_ENTRY_REGEX.search(line)
471 if match:
472 fstab.append(FstabEntry(match.group(1), match.group(2),
473 match.group(3), match.group(4),
474 None, None))
475 else: # assume pre-M Android
476 fstab.append(FstabEntry(*line.split()))
477 return fstab
478
479 def list_directory(self, path, as_root=False):
480 raise NotImplementedError()
481
482 def get_workpath(self, name):
483 return self.path.join(self.working_directory, name)
484
485 def tempfile(self, prefix='', suffix=''):
486 names = tempfile._get_candidate_names() # pylint: disable=W0212
487 for _ in xrange(tempfile.TMP_MAX):
488 name = names.next()
489 path = self.get_workpath(prefix + name + suffix)
490 if not self.file_exists(path):
491 return path
492 raise IOError('No usable temporary filename found')
493
494 def remove(self, path, as_root=False):
495 self.execute('rm -rf {}'.format(path), as_root=as_root)
496
497 # misc
498 def core_cpus(self, core):
499 return [i for i, c in enumerate(self.core_names) if c == core]
500
501 def list_online_cpus(self, core=None):
502 path = self.path.join('/sys/devices/system/cpu/online')
503 output = self.read_value(path)
504 all_online = ranges_to_list(output)
505 if core:
506 cpus = self.core_cpus(core)
507 if not cpus:
508 raise ValueError(core)
509 return [o for o in all_online if o in cpus]
510 else:
511 return all_online
512
513 def list_offline_cpus(self):
514 online = self.list_online_cpus()
515 return [c for c in xrange(self.number_of_cpus)
516 if c not in online]
517
518 def getenv(self, variable):
519 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
520
521 def capture_screen(self, filepath):
522 raise NotImplementedError()
523
524 def install(self, filepath, timeout=None, with_name=None):
525 raise NotImplementedError()
526
527 def uninstall(self, name):
528 raise NotImplementedError()
529
Sebastian Goscik84151f92016-02-15 15:09:27 +0000530 def get_installed(self, name, search_system_binaries=True):
531 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000532 if self.file_exists(self.executables_directory):
533 if name in self.list_directory(self.executables_directory):
534 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000535 # Fall back to binaries in PATH
536 if search_system_binaries:
537 for path in self.getenv('PATH').split(self.path.pathsep):
538 try:
539 if name in self.list_directory(path):
540 return self.path.join(path, name)
541 except TargetError:
542 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100543
544 which = get_installed
545
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000546 def install_if_needed(self, host_path, search_system_binaries=True):
547
548 binary_path = self.get_installed(os.path.split(host_path)[1],
549 search_system_binaries=search_system_binaries)
550 if not binary_path:
551 binary_path = self.install(host_path)
552 return binary_path
553
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100554 def is_installed(self, name):
555 return bool(self.get_installed(name))
556
557 def bin(self, name):
558 return self._installed_binaries.get(name, name)
559
560 def has(self, modname):
561 return hasattr(self, identifier(modname))
562
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000563 def lsmod(self):
564 lines = self.execute('lsmod').splitlines()
565 entries = []
566 for line in lines[1:]: # first line is the header
567 if not line.strip():
568 continue
569 parts = line.split()
570 name = parts[0]
571 size = int(parts[1])
572 use_count = int(parts[2])
573 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000574 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000575 else:
576 used_by = []
577 entries.append(LsmodEntry(name, size, use_count, used_by))
578 return entries
579
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000580 def insmod(self, path):
581 target_path = self.get_workpath(os.path.basename(path))
582 self.push(path, target_path)
583 self.execute('insmod {}'.format(target_path), as_root=True)
584
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100585
586 def extract(self, path, dest=None):
587 """
588 Extact the specified on-target file. The extraction method to be used
589 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
590 If ``dest`` is specified, it must be an existing directory on target;
591 the extracted contents will be placed there.
592
593 Note that, depending on the archive file format (and therfore the
594 extraction method used), the original archive file may or may not exist
595 after the extraction.
596
597 The return value is the path to the extracted contents. In case of
598 gunzip and bunzip2, this will be path to the extracted file; for tar
599 and uzip, this will be the directory with the extracted file(s)
600 (``dest`` if it was specified otherwise, the directory that cotained
601 the archive).
602
603 """
604 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
605 '.tgz', '.tbz', '.tbz2']:
606 if path.endswith(ending):
607 return self._extract_archive(path, 'tar xf {} -C {}', dest)
608
609 ext = self.path.splitext(path)[1]
610 if ext in ['.bz', '.bz2']:
611 return self._extract_file(path, 'bunzip2 -f {}', dest)
612 elif ext == '.gz':
613 return self._extract_file(path, 'gunzip -f {}', dest)
614 elif ext == '.zip':
615 return self._extract_archive(path, 'unzip {} -d {}', dest)
616 else:
617 raise ValueError('Unknown compression format: {}'.format(ext))
618
Sergei Trofimov69a83d42017-05-12 11:54:31 +0100619 def sleep(self, duration):
620 timeout = duration + 10
621 self.execute('sleep {}'.format(duration), timeout=timeout)
622
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100623 # internal methods
624
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100625 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
626 command = '{} {}'.format(self.shutils, command)
627 return self.conn.execute(command, timeout, check_exit_code, as_root)
628
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100629 def _extract_archive(self, path, cmd, dest=None):
630 cmd = '{} ' + cmd # busybox
631 if dest:
632 extracted = dest
633 else:
634 extracted = self.path.dirname(path)
635 cmdtext = cmd.format(self.busybox, path, extracted)
636 self.execute(cmdtext)
637 return extracted
638
639 def _extract_file(self, path, cmd, dest=None):
640 cmd = '{} ' + cmd # busybox
641 cmdtext = cmd.format(self.busybox, path)
642 self.execute(cmdtext)
643 extracted = self.path.splitext(path)[0]
644 if dest:
645 self.execute('mv -f {} {}'.format(extracted, dest))
646 if dest.endswith('/'):
647 extracted = self.path.join(dest, self.path.basename(extracted))
648 else:
649 extracted = dest
650 return extracted
651
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100652 def _update_modules(self, stage):
653 for mod in self.modules:
654 if isinstance(mod, dict):
655 mod, params = mod.items()[0]
656 else:
657 params = {}
658 mod = get_module(mod)
659 if not mod.stage == stage:
660 continue
661 if mod.probe(self):
662 self._install_module(mod, **params)
663 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000664 msg = 'Module {} is not supported by the target'.format(mod.name)
665 if self.load_default_modules:
666 self.logger.debug(msg)
667 else:
668 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100669
670 def _install_module(self, mod, **params):
671 if mod.name not in self._installed_modules:
672 self.logger.debug('Installing module {}'.format(mod.name))
673 mod.install(self, **params)
674 self._installed_modules[mod.name] = mod
675 else:
676 self.logger.debug('Module {} is already installed.'.format(mod.name))
677
Sergei Trofimov961f9572015-11-18 17:32:26 +0000678 def _resolve_paths(self):
679 raise NotImplementedError()
680
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100681
682class LinuxTarget(Target):
683
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100684 path = posixpath
685 os = 'linux'
686
687 @property
688 @memoized
689 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100690 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100691 for abi, architectures in ABI_MAP.iteritems():
692 if value in architectures:
693 result = abi
694 break
695 else:
696 result = value
697 return result
698
699 @property
700 @memoized
701 def os_version(self):
702 os_version = {}
703 try:
704 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
705 version_files = self.execute(command, check_exit_code=False).strip().split()
706 for vf in version_files:
707 name = self.path.basename(vf)
708 output = self.read_value(vf)
709 os_version[name] = output.strip().replace('\n', ' ')
710 except TargetError:
711 raise
712 return os_version
713
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000714 @property
715 @memoized
716 # There is currently no better way to do this cross platform.
717 # ARM does not have dmidecode
718 def model(self):
719 if self.file_exists("/proc/device-tree/model"):
720 raw_model = self.execute("cat /proc/device-tree/model")
721 return '_'.join(raw_model.split()[:2])
722 return None
723
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000724 def __init__(self,
725 connection_settings=None,
726 platform=None,
727 working_directory=None,
728 executables_directory=None,
729 connect=True,
730 modules=None,
731 load_default_modules=True,
732 shell_prompt=DEFAULT_SHELL_PROMPT,
733 conn_cls=SshConnection,
734 ):
735 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
736 platform=platform,
737 working_directory=working_directory,
738 executables_directory=executables_directory,
739 connect=connect,
740 modules=modules,
741 load_default_modules=load_default_modules,
742 shell_prompt=shell_prompt,
743 conn_cls=conn_cls)
744
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100745 def connect(self, timeout=None):
746 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100747
748 def kick_off(self, command, as_root=False):
749 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
750 return self.conn.execute(command, as_root=as_root)
751
752 def get_pids_of(self, process_name):
753 """Returns a list of PIDs of all processes with the specified name."""
754 # result should be a column of PIDs with the first row as "PID" header
755 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
756 check_exit_code=False).strip().split()
757 if len(result) >= 2: # at least one row besides the header
758 return map(int, result[1:])
759 else:
760 return []
761
762 def ps(self, **kwargs):
763 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
764 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
765 lines.next() # header
766
767 result = []
768 for line in lines:
769 parts = re.split(r'\s+', line, maxsplit=8)
770 if parts and parts != ['']:
771 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
772
773 if not kwargs:
774 return result
775 else:
776 filtered_result = []
777 for entry in result:
778 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
779 filtered_result.append(entry)
780 return filtered_result
781
782 def list_directory(self, path, as_root=False):
783 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
784 return [x.strip() for x in contents.split('\n') if x.strip()]
785
786 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
787 destpath = self.path.join(self.executables_directory,
788 with_name and with_name or self.path.basename(filepath))
789 self.push(filepath, destpath)
790 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
791 self._installed_binaries[self.path.basename(destpath)] = destpath
792 return destpath
793
794 def uninstall(self, name):
795 path = self.path.join(self.executables_directory, name)
796 self.remove(path)
797
798 def capture_screen(self, filepath):
799 if not self.is_installed('scrot'):
800 self.logger.debug('Could not take screenshot as scrot is not installed.')
801 return
802 try:
803
804 tmpfile = self.tempfile()
805 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
806 self.pull(tmpfile, filepath)
807 self.remove(tmpfile)
808 except TargetError as e:
809 if "Can't open X dispay." not in e.message:
810 raise e
811 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
812 self.logger.debug('Could not take screenshot: {}'.format(message))
813
Sergei Trofimov961f9572015-11-18 17:32:26 +0000814 def _resolve_paths(self):
815 if self.working_directory is None:
816 if self.connected_as_root:
817 self.working_directory = '/root/devlib-target'
818 else:
819 self.working_directory = '/home/{}/devlib-target'.format(self.user)
820 if self.executables_directory is None:
821 self.executables_directory = self.path.join(self.working_directory, 'bin')
822
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100823
824class AndroidTarget(Target):
825
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100826 path = posixpath
827 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100828 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100829
830 @property
831 @memoized
832 def abi(self):
833 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
834
835 @property
836 @memoized
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100837 def supported_abi(self):
838 props = self.getprop()
839 result = [props['ro.product.cpu.abi']]
840 if 'ro.product.cpu.abi2' in props:
841 result.append(props['ro.product.cpu.abi2'])
842 if 'ro.product.cpu.abilist' in props:
843 for abi in props['ro.product.cpu.abilist'].split(','):
844 if abi not in result:
845 result.append(abi)
846
847 mapped_result = []
848 for supported_abi in result:
849 for abi, architectures in ABI_MAP.iteritems():
850 found = False
851 if supported_abi in architectures and abi not in mapped_result:
852 mapped_result.append(abi)
853 found = True
854 break
855 if not found and supported_abi not in mapped_result:
856 mapped_result.append(supported_abi)
857 return mapped_result
858
859 @property
860 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100861 def os_version(self):
862 os_version = {}
863 for k, v in self.getprop().iteritems():
864 if k.startswith('ro.build.version'):
865 part = k.split('.')[-1]
866 os_version[part] = v
867 return os_version
868
869 @property
870 def adb_name(self):
871 return self.conn.device
872
873 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000874 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000875 def android_id(self):
876 """
877 Get the device's ANDROID_ID. Which is
878
879 "A 64-bit number (as a hex string) that is randomly generated when the user
880 first sets up the device and should remain constant for the lifetime of the
881 user's device."
882
883 .. note:: This will get reset on userdata erasure.
884
885 """
886 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
887 return output.split('value=')[-1]
888
889 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100890 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000891 def model(self):
892 try:
893 return self.getprop(prop='ro.product.device')
894 except KeyError:
895 return None
896
897 @property
898 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100899 def screen_resolution(self):
900 output = self.execute('dumpsys window')
901 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
902 if match:
903 return (int(match.group('width')),
904 int(match.group('height')))
905 else:
906 return (0, 0)
907
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000908 def __init__(self,
909 connection_settings=None,
910 platform=None,
911 working_directory=None,
912 executables_directory=None,
913 connect=True,
914 modules=None,
915 load_default_modules=True,
916 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000917 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000918 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000919 ):
920 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
921 platform=platform,
922 working_directory=working_directory,
923 executables_directory=executables_directory,
924 connect=connect,
925 modules=modules,
926 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000927 shell_prompt=shell_prompt,
928 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000929 self.package_data_directory = package_data_directory
930
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100931 def reset(self, fastboot=False): # pylint: disable=arguments-differ
932 try:
933 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100934 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100935 except (TargetError, TimeoutError, subprocess.CalledProcessError):
936 # on some targets "reboot" doesn't return gracefully
937 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000938 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100939
Patrick Bellasif26f9422017-07-12 12:27:28 +0100940 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100941 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +0100942 boot_completed = boolean(self.getprop('sys.boot_completed'))
943 while not boot_completed and timeout >= time.time() - start:
944 time.sleep(5)
945 boot_completed = boolean(self.getprop('sys.boot_completed'))
946 if not boot_completed:
947 raise TargetError('Connected but Android did not fully boot.')
948
949 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100950 device = self.connection_settings.get('device')
951 if device and ':' in device:
952 # ADB does not automatically remove a network device from it's
953 # devices list when the connection is broken by the remote, so the
954 # adb connection may have gone "stale", resulting in adb blocking
955 # indefinitely when making calls to the device. To avoid this,
956 # always disconnect first.
957 adb_disconnect(device)
958 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100959
960 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +0100961 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100962
963 def setup(self, executables=None):
964 super(AndroidTarget, self).setup(executables)
965 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
966
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100967 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100968 """
969 Like execute but closes adb session and returns immediately, leaving the command running on the
970 device (this is different from execute(background=True) which keeps adb connection open and returns
971 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100972 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100973 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100974 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100975 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +0100976 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100977 output = self.execute(command, timeout=1, as_root=as_root)
978 except TimeoutError:
979 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100980
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100981 def __setup_list_directory(self):
982 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
983 # AOSP 7.0 as well, the ls command was changed.
984 # Previous versions default to a single column listing, which is nice and easy to parse.
985 # Newer versions default to a multi-column listing, which is not, but it does support
986 # a '-1' option to get into single column mode. Older versions do not support this option
987 # so we try the new version, and if it fails we use the old version.
988 self.ls_command = 'ls -1'
989 try:
Marc Bonnici06552372017-03-29 16:43:22 +0100990 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100991 except TargetError:
992 self.ls_command = 'ls'
993
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100994 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100995 if self.ls_command == '':
996 self.__setup_list_directory()
997 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100998 return [x.strip() for x in contents.split('\n') if x.strip()]
999
1000 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1001 ext = os.path.splitext(filepath)[1].lower()
1002 if ext == '.apk':
1003 return self.install_apk(filepath, timeout)
1004 else:
1005 return self.install_executable(filepath, with_name)
1006
1007 def uninstall(self, name):
1008 if self.package_is_installed(name):
1009 self.uninstall_package(name)
1010 else:
1011 self.uninstall_executable(name)
1012
1013 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001014 result = []
1015 search_term = process_name[-15:]
1016 for entry in self.ps():
1017 if search_term in entry.name:
1018 result.append(entry.pid)
1019 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001020
1021 def ps(self, **kwargs):
1022 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1023 lines.next() # header
1024 result = []
1025 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001026 parts = line.split(None, 8)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001027 if parts:
1028 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
1029 if not kwargs:
1030 return result
1031 else:
1032 filtered_result = []
1033 for entry in result:
1034 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1035 filtered_result.append(entry)
1036 return filtered_result
1037
1038 def capture_screen(self, filepath):
1039 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1040 self.execute('screencap -p {}'.format(on_device_file))
1041 self.pull(on_device_file, filepath)
1042 self.remove(on_device_file)
1043
1044 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1045 if not as_root:
1046 self.conn.push(source, dest, timeout=timeout)
1047 else:
1048 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001049 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001050 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001051 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001052
1053 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1054 if not as_root:
1055 self.conn.pull(source, dest, timeout=timeout)
1056 else:
1057 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001058 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1059 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001060 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001061 self.conn.pull(device_tempfile, dest, timeout=timeout)
1062
1063 # Android-specific
1064
Marc Bonnici8839ed02017-08-10 17:19:13 +01001065 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001066 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001067 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001068 if direction == "diagonal":
1069 start = 100
1070 stop = width - start
1071 swipe_height = height * 2 // 3
1072 self.execute(command.format(start, swipe_height, stop, 0))
1073 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001074 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001075 start = 100
1076 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001077 self.execute(command.format(start, swipe_height, stop, swipe_height))
1078 elif direction == "vertical":
1079 swipe_middle = width / 2
1080 swipe_height = height * 2 // 3
1081 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001082 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001083 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001084
1085 def getprop(self, prop=None):
1086 props = AndroidProperties(self.execute('getprop'))
1087 if prop:
1088 return props[prop]
1089 return props
1090
1091 def is_installed(self, name):
1092 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1093
1094 def package_is_installed(self, package_name):
1095 return package_name in self.list_packages()
1096
1097 def list_packages(self):
1098 output = self.execute('pm list packages')
1099 output = output.replace('package:', '')
1100 return output.split()
1101
1102 def get_package_version(self, package):
1103 output = self.execute('dumpsys package {}'.format(package))
1104 for line in convert_new_lines(output).split('\n'):
1105 if 'versionName' in line:
1106 return line.split('=', 1)[1]
1107 return None
1108
Marc Bonnicid3396f22017-05-31 15:56:50 +01001109 def get_sdk_version(self):
1110 try:
1111 return int(self.getprop('ro.build.version.sdk'))
1112 except (ValueError, TypeError):
1113 return None
1114
Marc Bonnicic33dd652017-05-31 15:51:31 +01001115 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001116 ext = os.path.splitext(filepath)[1].lower()
1117 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001118 flags = []
1119 if replace:
1120 flags.append('-r') # Replace existing APK
1121 if allow_downgrade:
1122 flags.append('-d') # Install the APK even if a newer version is already installed
1123 if self.get_sdk_version() >= 23:
1124 flags.append('-g') # Grant all runtime permissions
1125 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1126 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001127 else:
1128 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1129
1130 def install_executable(self, filepath, with_name=None):
1131 self._ensure_executables_directory_is_writable()
1132 executable_name = with_name or os.path.basename(filepath)
1133 on_device_file = self.path.join(self.working_directory, executable_name)
1134 on_device_executable = self.path.join(self.executables_directory, executable_name)
1135 self.push(filepath, on_device_file)
1136 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001137 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1138 self.remove(on_device_file, as_root=self.needs_su)
1139 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001140 self._installed_binaries[executable_name] = on_device_executable
1141 return on_device_executable
1142
1143 def uninstall_package(self, package):
1144 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1145
1146 def uninstall_executable(self, executable_name):
1147 on_device_executable = self.path.join(self.executables_directory, executable_name)
1148 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001149 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001150
1151 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001152 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001153 filtstr = ' -s {}'.format(filter) if filter else ''
1154 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1155 adb_command(self.adb_name, command, timeout=timeout)
1156
1157 def clear_logcat(self):
1158 adb_command(self.adb_name, 'logcat -c', timeout=30)
1159
Valentin Schneider7c2fd872017-09-11 16:58:24 +01001160 def get_logcat_monitor(self, regexps=None):
1161 return LogcatMonitor(self, regexps)
1162
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001163 def adb_kill_server(self, timeout=30):
1164 adb_command(self.adb_name, 'kill-server', timeout)
1165
1166 def adb_wait_for_device(self, timeout=30):
1167 adb_command(self.adb_name, 'wait-for-device', timeout)
1168
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001169 def adb_reboot_bootloader(self, timeout=30):
1170 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1171
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001172 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001173 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001174 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001175 return
1176 adb_command(self.adb_name, 'root', timeout=30)
1177 self._connected_as_root = True
1178 return
1179 adb_command(self.adb_name, 'unroot', timeout=30)
1180 self._connected_as_root = False
1181
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001182 def is_screen_on(self):
1183 output = self.execute('dumpsys power')
1184 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1185 if match:
1186 return boolean(match.group(1))
1187 else:
1188 raise TargetError('Could not establish screen state.')
1189
1190 def ensure_screen_is_on(self):
1191 if not self.is_screen_on():
1192 self.execute('input keyevent 26')
1193
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001194 def ensure_screen_is_off(self):
1195 if self.is_screen_on():
1196 self.execute('input keyevent 26')
1197
Marc Bonniciddd2e292017-07-06 17:30:12 +01001198 def set_auto_brightness(self, auto_brightness):
1199 cmd = 'settings put system screen_brightness_mode {}'
1200 self.execute(cmd.format(int(boolean(auto_brightness))))
1201
1202 def get_auto_brightness(self):
1203 cmd = 'settings get system screen_brightness_mode'
1204 return boolean(self.execute(cmd).strip())
1205
1206 def set_brightness(self, value):
1207 if not 0 <= value <= 255:
1208 msg = 'Invalid brightness "{}"; Must be between 0 and 255'
1209 raise ValueError(msg.format(value))
1210 self.set_auto_brightness(False)
1211 cmd = 'settings put system screen_brightness {}'
1212 self.execute(cmd.format(int(value)))
1213
1214 def get_brightness(self):
1215 cmd = 'settings get system screen_brightness'
1216 return integer(self.execute(cmd).strip())
1217
Marc Bonnici3e751742017-07-06 17:31:02 +01001218 def get_airplane_mode(self):
1219 cmd = 'settings get global airplane_mode_on'
1220 return boolean(self.execute(cmd).strip())
1221
1222 def set_airplane_mode(self, mode):
1223 root_required = self.get_sdk_version() > 23
1224 if root_required and not self.is_rooted:
1225 raise TargetError('Root is required to toggle airplane mode on Android 7+')
1226 cmd = 'settings put global airplane_mode_on {}'
1227 self.execute(cmd.format(int(boolean(mode))))
1228 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE', as_root=root_required)
1229
Marc Bonnici003785d2017-07-24 17:44:33 +01001230 def get_auto_rotation(self):
1231 cmd = 'settings get system accelerometer_rotation'
1232 return boolean(self.execute(cmd).strip())
1233
1234 def set_auto_rotation(self, autorotate):
1235 cmd = 'settings put system accelerometer_rotation {}'
1236 self.execute(cmd.format(int(boolean(autorotate))))
1237
1238 def set_natural_rotation(self):
1239 self.set_rotation(0)
1240
1241 def set_left_rotation(self):
1242 self.set_rotation(1)
1243
1244 def set_inverted_rotation(self):
1245 self.set_rotation(2)
1246
1247 def set_right_rotation(self):
1248 self.set_rotation(3)
1249
1250 def get_rotation(self):
1251 cmd = 'settings get system user_rotation'
1252 return self.execute(cmd).strip()
1253
1254 def set_rotation(self, rotation):
1255 if not 0 <= rotation <= 3:
1256 raise ValueError('Rotation value must be between 0 and 3')
1257 self.set_auto_rotation(False)
1258 cmd = 'settings put system user_rotation {}'
1259 self.execute(cmd.format(rotation))
1260
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001261 def homescreen(self):
1262 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1263
Sergei Trofimov961f9572015-11-18 17:32:26 +00001264 def _resolve_paths(self):
1265 if self.working_directory is None:
1266 self.working_directory = '/data/local/tmp/devlib-target'
1267 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1268 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001269 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001270
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001271 def _ensure_executables_directory_is_writable(self):
1272 matched = []
1273 for entry in self.list_file_systems():
1274 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1275 matched.append(entry)
1276 if matched:
1277 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1278 if 'rw' not in entry.options:
1279 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1280 entry.mount_point),
1281 as_root=True)
1282 else:
1283 message = 'Could not find mount point for executables directory {}'
1284 raise TargetError(message.format(self.executables_directory))
1285
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001286 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1287
1288 @property
1289 def charging_enabled(self):
1290 """
1291 Whether drawing power to charge the battery is enabled
1292
1293 Not all devices have the ability to enable/disable battery charging
1294 (e.g. because they don't have a battery). In that case,
1295 ``charging_enabled`` is None.
1296 """
1297 if not self.file_exists(self._charging_enabled_path):
1298 return None
1299 return self.read_bool(self._charging_enabled_path)
1300
1301 @charging_enabled.setter
1302 def charging_enabled(self, enabled):
1303 """
1304 Enable/disable drawing power to charge the battery
1305
1306 Not all devices have this facility. In that case, do nothing.
1307 """
1308 if not self.file_exists(self._charging_enabled_path):
1309 return
1310 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001311
1312FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1313PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001314LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001315
1316
1317class Cpuinfo(object):
1318
1319 @property
1320 @memoized
1321 def architecture(self):
1322 for section in self.sections:
1323 if 'CPU architecture' in section:
1324 return section['CPU architecture']
1325 if 'architecture' in section:
1326 return section['architecture']
1327
1328 @property
1329 @memoized
1330 def cpu_names(self):
1331 cpu_names = []
1332 global_name = None
1333 for section in self.sections:
1334 if 'processor' in section:
1335 if 'CPU part' in section:
1336 cpu_names.append(_get_part_name(section))
1337 elif 'model name' in section:
1338 cpu_names.append(_get_model_name(section))
1339 else:
1340 cpu_names.append(None)
1341 elif 'CPU part' in section:
1342 global_name = _get_part_name(section)
1343 return [caseless_string(c or global_name) for c in cpu_names]
1344
1345 def __init__(self, text):
1346 self.sections = None
1347 self.text = None
1348 self.parse(text)
1349
1350 @memoized
1351 def get_cpu_features(self, cpuid=0):
1352 global_features = []
1353 for section in self.sections:
1354 if 'processor' in section:
1355 if int(section.get('processor')) != cpuid:
1356 continue
1357 if 'Features' in section:
1358 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001359 elif 'flags' in section:
1360 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001361 elif 'Features' in section:
1362 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001363 elif 'flags' in section:
1364 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001365 return global_features
1366
1367 def parse(self, text):
1368 self.sections = []
1369 current_section = {}
1370 self.text = text.strip()
1371 for line in self.text.split('\n'):
1372 line = line.strip()
1373 if line:
1374 key, value = line.split(':', 1)
1375 current_section[key.strip()] = value.strip()
1376 else: # not line
1377 self.sections.append(current_section)
1378 current_section = {}
1379 self.sections.append(current_section)
1380
1381 def __str__(self):
1382 return 'CpuInfo({})'.format(self.cpu_names)
1383
1384 __repr__ = __str__
1385
1386
1387class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001388 """
1389 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001390
Brendan Jackman54adf802017-02-20 17:57:51 +00001391 Not expected to work for very old (pre-3.0) kernel version numbers.
1392
1393 :ivar release: Version number/revision string. Typical output of
1394 ``uname -r``
1395 :type release: str
1396 :ivar version: Extra version info (aside from ``release``) reported by
1397 ``uname``
1398 :type version: str
1399 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1400 :type version_number: int
1401 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1402 :type major: int
1403 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1404 be None
1405 :type minor: int
1406 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1407 :type rc: int
1408 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1409 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001410
1411 :ivar parts: Tuple of version number components. Can be used for
1412 lexicographically comparing kernel versions.
1413 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001414 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001415 def __init__(self, version_string):
1416 if ' #' in version_string:
1417 release, version = version_string.split(' #')
1418 self.release = release
1419 self.version = version
1420 elif version_string.startswith('#'):
1421 self.release = ''
1422 self.version = version_string
1423 else:
1424 self.release = version_string
1425 self.version = ''
1426
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001427 self.version_number = None
1428 self.major = None
1429 self.minor = None
1430 self.sha1 = None
1431 self.rc = None
1432 match = KVERSION_REGEX.match(version_string)
1433 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001434 groups = match.groupdict()
1435 self.version_number = int(groups['version'])
1436 self.major = int(groups['major'])
1437 if groups['minor'] is not None:
1438 self.minor = int(groups['minor'])
1439 if groups['rc'] is not None:
1440 self.rc = int(groups['rc'])
1441 if groups['sha1'] is not None:
1442 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001443
Brendan Jackman18b77b82017-02-20 17:52:56 +00001444 self.parts = (self.version_number, self.major, self.minor)
1445
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001446 def __str__(self):
1447 return '{} {}'.format(self.release, self.version)
1448
1449 __repr__ = __str__
1450
1451
1452class KernelConfig(object):
1453
1454 not_set_regex = re.compile(r'# (\S+) is not set')
1455
1456 @staticmethod
1457 def get_config_name(name):
1458 name = name.upper()
1459 if not name.startswith('CONFIG_'):
1460 name = 'CONFIG_' + name
1461 return name
1462
1463 def iteritems(self):
1464 return self._config.iteritems()
1465
1466 def __init__(self, text):
1467 self.text = text
1468 self._config = {}
1469 for line in text.split('\n'):
1470 line = line.strip()
1471 if line.startswith('#'):
1472 match = self.not_set_regex.search(line)
1473 if match:
1474 self._config[match.group(1)] = 'n'
1475 elif '=' in line:
1476 name, value = line.split('=', 1)
1477 self._config[name.strip()] = value.strip()
1478
1479 def get(self, name):
1480 return self._config.get(self.get_config_name(name))
1481
1482 def like(self, name):
1483 regex = re.compile(name, re.I)
1484 result = {}
1485 for k, v in self._config.iteritems():
1486 if regex.search(k):
1487 result[k] = v
1488 return result
1489
1490 def is_enabled(self, name):
1491 return self.get(name) == 'y'
1492
1493 def is_module(self, name):
1494 return self.get(name) == 'm'
1495
1496 def is_not_set(self, name):
1497 return self.get(name) == 'n'
1498
1499 def has(self, name):
1500 return self.get(name) in ['m', 'y']
1501
1502
1503class LocalLinuxTarget(LinuxTarget):
1504
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001505 def __init__(self,
1506 connection_settings=None,
1507 platform=None,
1508 working_directory=None,
1509 executables_directory=None,
1510 connect=True,
1511 modules=None,
1512 load_default_modules=True,
1513 shell_prompt=DEFAULT_SHELL_PROMPT,
1514 conn_cls=LocalConnection,
1515 ):
1516 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1517 platform=platform,
1518 working_directory=working_directory,
1519 executables_directory=executables_directory,
1520 connect=connect,
1521 modules=modules,
1522 load_default_modules=load_default_modules,
1523 shell_prompt=shell_prompt,
1524 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001525
Sergei Trofimov961f9572015-11-18 17:32:26 +00001526 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001527 if self.working_directory is None:
1528 self.working_directory = '/tmp'
1529 if self.executables_directory is None:
1530 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001531
1532
1533def _get_model_name(section):
1534 name_string = section['model name']
1535 parts = name_string.split('@')[0].strip().split()
1536 return ' '.join([p for p in parts
1537 if '(' not in p and p != 'CPU'])
1538
1539
1540def _get_part_name(section):
1541 implementer = section.get('CPU implementer', '0x0')
1542 part = section['CPU part']
1543 variant = section.get('CPU variant', '0x0')
1544 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1545 if name is None:
1546 name = '{}/{}/{}'.format(implementer, part, variant)
1547 return name