blob: 27b1c4be41a8987201ed9b8196dbbed5b462fc93 [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
17from devlib.utils.android import AdbConnection, AndroidProperties, adb_command, adb_disconnect
18from 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):
1014 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
1015 if result and 'not found' not in result:
1016 return [int(x.split()[1]) for x in result.split('\n')[1:]]
1017 else:
1018 return []
1019
1020 def ps(self, **kwargs):
1021 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1022 lines.next() # header
1023 result = []
1024 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001025 parts = line.split(None, 8)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001026 if parts:
1027 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
1028 if not kwargs:
1029 return result
1030 else:
1031 filtered_result = []
1032 for entry in result:
1033 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1034 filtered_result.append(entry)
1035 return filtered_result
1036
1037 def capture_screen(self, filepath):
1038 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1039 self.execute('screencap -p {}'.format(on_device_file))
1040 self.pull(on_device_file, filepath)
1041 self.remove(on_device_file)
1042
1043 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1044 if not as_root:
1045 self.conn.push(source, dest, timeout=timeout)
1046 else:
1047 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001048 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001049 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001050 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001051
1052 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1053 if not as_root:
1054 self.conn.pull(source, dest, timeout=timeout)
1055 else:
1056 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001057 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1058 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001059 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001060 self.conn.pull(device_tempfile, dest, timeout=timeout)
1061
1062 # Android-specific
1063
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001064 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001065 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001066 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001067 if direction == "horizontal":
1068 swipe_heigh = height * 2 // 3
1069 start = 100
1070 stop = width - start
1071 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
1072 if direction == "vertical":
1073 swipe_middle = height / 2
1074 swipe_heigh = height * 2 // 3
1075 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
1076 else:
1077 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001078
1079 def getprop(self, prop=None):
1080 props = AndroidProperties(self.execute('getprop'))
1081 if prop:
1082 return props[prop]
1083 return props
1084
1085 def is_installed(self, name):
1086 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1087
1088 def package_is_installed(self, package_name):
1089 return package_name in self.list_packages()
1090
1091 def list_packages(self):
1092 output = self.execute('pm list packages')
1093 output = output.replace('package:', '')
1094 return output.split()
1095
1096 def get_package_version(self, package):
1097 output = self.execute('dumpsys package {}'.format(package))
1098 for line in convert_new_lines(output).split('\n'):
1099 if 'versionName' in line:
1100 return line.split('=', 1)[1]
1101 return None
1102
Marc Bonnicid3396f22017-05-31 15:56:50 +01001103 def get_sdk_version(self):
1104 try:
1105 return int(self.getprop('ro.build.version.sdk'))
1106 except (ValueError, TypeError):
1107 return None
1108
Marc Bonnicic33dd652017-05-31 15:51:31 +01001109 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001110 ext = os.path.splitext(filepath)[1].lower()
1111 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001112 flags = []
1113 if replace:
1114 flags.append('-r') # Replace existing APK
1115 if allow_downgrade:
1116 flags.append('-d') # Install the APK even if a newer version is already installed
1117 if self.get_sdk_version() >= 23:
1118 flags.append('-g') # Grant all runtime permissions
1119 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1120 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001121 else:
1122 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1123
1124 def install_executable(self, filepath, with_name=None):
1125 self._ensure_executables_directory_is_writable()
1126 executable_name = with_name or os.path.basename(filepath)
1127 on_device_file = self.path.join(self.working_directory, executable_name)
1128 on_device_executable = self.path.join(self.executables_directory, executable_name)
1129 self.push(filepath, on_device_file)
1130 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001131 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1132 self.remove(on_device_file, as_root=self.needs_su)
1133 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001134 self._installed_binaries[executable_name] = on_device_executable
1135 return on_device_executable
1136
1137 def uninstall_package(self, package):
1138 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1139
1140 def uninstall_executable(self, executable_name):
1141 on_device_executable = self.path.join(self.executables_directory, executable_name)
1142 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001143 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001144
1145 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001146 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001147 filtstr = ' -s {}'.format(filter) if filter else ''
1148 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1149 adb_command(self.adb_name, command, timeout=timeout)
1150
1151 def clear_logcat(self):
1152 adb_command(self.adb_name, 'logcat -c', timeout=30)
1153
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001154 def adb_kill_server(self, timeout=30):
1155 adb_command(self.adb_name, 'kill-server', timeout)
1156
1157 def adb_wait_for_device(self, timeout=30):
1158 adb_command(self.adb_name, 'wait-for-device', timeout)
1159
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001160 def adb_reboot_bootloader(self, timeout=30):
1161 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1162
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001163 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001164 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001165 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001166 return
1167 adb_command(self.adb_name, 'root', timeout=30)
1168 self._connected_as_root = True
1169 return
1170 adb_command(self.adb_name, 'unroot', timeout=30)
1171 self._connected_as_root = False
1172
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001173 def is_screen_on(self):
1174 output = self.execute('dumpsys power')
1175 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1176 if match:
1177 return boolean(match.group(1))
1178 else:
1179 raise TargetError('Could not establish screen state.')
1180
1181 def ensure_screen_is_on(self):
1182 if not self.is_screen_on():
1183 self.execute('input keyevent 26')
1184
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001185 def ensure_screen_is_off(self):
1186 if self.is_screen_on():
1187 self.execute('input keyevent 26')
1188
1189 def homescreen(self):
1190 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1191
Sergei Trofimov961f9572015-11-18 17:32:26 +00001192 def _resolve_paths(self):
1193 if self.working_directory is None:
1194 self.working_directory = '/data/local/tmp/devlib-target'
1195 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1196 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001197 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001198
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001199 def _ensure_executables_directory_is_writable(self):
1200 matched = []
1201 for entry in self.list_file_systems():
1202 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1203 matched.append(entry)
1204 if matched:
1205 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1206 if 'rw' not in entry.options:
1207 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1208 entry.mount_point),
1209 as_root=True)
1210 else:
1211 message = 'Could not find mount point for executables directory {}'
1212 raise TargetError(message.format(self.executables_directory))
1213
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001214 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1215
1216 @property
1217 def charging_enabled(self):
1218 """
1219 Whether drawing power to charge the battery is enabled
1220
1221 Not all devices have the ability to enable/disable battery charging
1222 (e.g. because they don't have a battery). In that case,
1223 ``charging_enabled`` is None.
1224 """
1225 if not self.file_exists(self._charging_enabled_path):
1226 return None
1227 return self.read_bool(self._charging_enabled_path)
1228
1229 @charging_enabled.setter
1230 def charging_enabled(self, enabled):
1231 """
1232 Enable/disable drawing power to charge the battery
1233
1234 Not all devices have this facility. In that case, do nothing.
1235 """
1236 if not self.file_exists(self._charging_enabled_path):
1237 return
1238 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001239
1240FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1241PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001242LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001243
1244
1245class Cpuinfo(object):
1246
1247 @property
1248 @memoized
1249 def architecture(self):
1250 for section in self.sections:
1251 if 'CPU architecture' in section:
1252 return section['CPU architecture']
1253 if 'architecture' in section:
1254 return section['architecture']
1255
1256 @property
1257 @memoized
1258 def cpu_names(self):
1259 cpu_names = []
1260 global_name = None
1261 for section in self.sections:
1262 if 'processor' in section:
1263 if 'CPU part' in section:
1264 cpu_names.append(_get_part_name(section))
1265 elif 'model name' in section:
1266 cpu_names.append(_get_model_name(section))
1267 else:
1268 cpu_names.append(None)
1269 elif 'CPU part' in section:
1270 global_name = _get_part_name(section)
1271 return [caseless_string(c or global_name) for c in cpu_names]
1272
1273 def __init__(self, text):
1274 self.sections = None
1275 self.text = None
1276 self.parse(text)
1277
1278 @memoized
1279 def get_cpu_features(self, cpuid=0):
1280 global_features = []
1281 for section in self.sections:
1282 if 'processor' in section:
1283 if int(section.get('processor')) != cpuid:
1284 continue
1285 if 'Features' in section:
1286 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001287 elif 'flags' in section:
1288 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001289 elif 'Features' in section:
1290 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001291 elif 'flags' in section:
1292 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001293 return global_features
1294
1295 def parse(self, text):
1296 self.sections = []
1297 current_section = {}
1298 self.text = text.strip()
1299 for line in self.text.split('\n'):
1300 line = line.strip()
1301 if line:
1302 key, value = line.split(':', 1)
1303 current_section[key.strip()] = value.strip()
1304 else: # not line
1305 self.sections.append(current_section)
1306 current_section = {}
1307 self.sections.append(current_section)
1308
1309 def __str__(self):
1310 return 'CpuInfo({})'.format(self.cpu_names)
1311
1312 __repr__ = __str__
1313
1314
1315class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001316 """
1317 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001318
Brendan Jackman54adf802017-02-20 17:57:51 +00001319 Not expected to work for very old (pre-3.0) kernel version numbers.
1320
1321 :ivar release: Version number/revision string. Typical output of
1322 ``uname -r``
1323 :type release: str
1324 :ivar version: Extra version info (aside from ``release``) reported by
1325 ``uname``
1326 :type version: str
1327 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1328 :type version_number: int
1329 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1330 :type major: int
1331 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1332 be None
1333 :type minor: int
1334 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1335 :type rc: int
1336 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1337 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001338
1339 :ivar parts: Tuple of version number components. Can be used for
1340 lexicographically comparing kernel versions.
1341 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001342 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001343 def __init__(self, version_string):
1344 if ' #' in version_string:
1345 release, version = version_string.split(' #')
1346 self.release = release
1347 self.version = version
1348 elif version_string.startswith('#'):
1349 self.release = ''
1350 self.version = version_string
1351 else:
1352 self.release = version_string
1353 self.version = ''
1354
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001355 self.version_number = None
1356 self.major = None
1357 self.minor = None
1358 self.sha1 = None
1359 self.rc = None
1360 match = KVERSION_REGEX.match(version_string)
1361 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001362 groups = match.groupdict()
1363 self.version_number = int(groups['version'])
1364 self.major = int(groups['major'])
1365 if groups['minor'] is not None:
1366 self.minor = int(groups['minor'])
1367 if groups['rc'] is not None:
1368 self.rc = int(groups['rc'])
1369 if groups['sha1'] is not None:
1370 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001371
Brendan Jackman18b77b82017-02-20 17:52:56 +00001372 self.parts = (self.version_number, self.major, self.minor)
1373
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001374 def __str__(self):
1375 return '{} {}'.format(self.release, self.version)
1376
1377 __repr__ = __str__
1378
1379
1380class KernelConfig(object):
1381
1382 not_set_regex = re.compile(r'# (\S+) is not set')
1383
1384 @staticmethod
1385 def get_config_name(name):
1386 name = name.upper()
1387 if not name.startswith('CONFIG_'):
1388 name = 'CONFIG_' + name
1389 return name
1390
1391 def iteritems(self):
1392 return self._config.iteritems()
1393
1394 def __init__(self, text):
1395 self.text = text
1396 self._config = {}
1397 for line in text.split('\n'):
1398 line = line.strip()
1399 if line.startswith('#'):
1400 match = self.not_set_regex.search(line)
1401 if match:
1402 self._config[match.group(1)] = 'n'
1403 elif '=' in line:
1404 name, value = line.split('=', 1)
1405 self._config[name.strip()] = value.strip()
1406
1407 def get(self, name):
1408 return self._config.get(self.get_config_name(name))
1409
1410 def like(self, name):
1411 regex = re.compile(name, re.I)
1412 result = {}
1413 for k, v in self._config.iteritems():
1414 if regex.search(k):
1415 result[k] = v
1416 return result
1417
1418 def is_enabled(self, name):
1419 return self.get(name) == 'y'
1420
1421 def is_module(self, name):
1422 return self.get(name) == 'm'
1423
1424 def is_not_set(self, name):
1425 return self.get(name) == 'n'
1426
1427 def has(self, name):
1428 return self.get(name) in ['m', 'y']
1429
1430
1431class LocalLinuxTarget(LinuxTarget):
1432
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001433 def __init__(self,
1434 connection_settings=None,
1435 platform=None,
1436 working_directory=None,
1437 executables_directory=None,
1438 connect=True,
1439 modules=None,
1440 load_default_modules=True,
1441 shell_prompt=DEFAULT_SHELL_PROMPT,
1442 conn_cls=LocalConnection,
1443 ):
1444 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1445 platform=platform,
1446 working_directory=working_directory,
1447 executables_directory=executables_directory,
1448 connect=connect,
1449 modules=modules,
1450 load_default_modules=load_default_modules,
1451 shell_prompt=shell_prompt,
1452 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001453
Sergei Trofimov961f9572015-11-18 17:32:26 +00001454 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001455 if self.working_directory is None:
1456 self.working_directory = '/tmp'
1457 if self.executables_directory is None:
1458 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001459
1460
1461def _get_model_name(section):
1462 name_string = section['model name']
1463 parts = name_string.split('@')[0].strip().split()
1464 return ' '.join([p for p in parts
1465 if '(' not in p and p != 'CPU'])
1466
1467
1468def _get_part_name(section):
1469 implementer = section.get('CPU implementer', '0x0')
1470 part = section['CPU part']
1471 variant = section.get('CPU variant', '0x0')
1472 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1473 if name is None:
1474 name = '{}/{}/{}'.format(implementer, part, variant)
1475 return name