blob: f3aaea0421d4f6a4acc7ba749aadd68b87ce84f1 [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
105 @memoized
106 def cpuinfo(self):
107 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
108
109 @property
110 @memoized
111 def number_of_cpus(self):
112 num_cpus = 0
113 corere = re.compile(r'^\s*cpu\d+\s*$')
114 output = self.execute('ls /sys/devices/system/cpu')
115 for entry in output.split():
116 if corere.match(entry):
117 num_cpus += 1
118 return num_cpus
119
120 @property
121 @memoized
122 def config(self):
123 try:
124 return KernelConfig(self.execute('zcat /proc/config.gz'))
125 except TargetError:
126 for path in ['/boot/config', '/boot/config-$(uname -r)']:
127 try:
128 return KernelConfig(self.execute('cat {}'.format(path)))
129 except TargetError:
130 pass
131 return KernelConfig('')
132
133 @property
134 @memoized
135 def user(self):
136 return self.getenv('USER')
137
138 @property
139 def conn(self):
140 if self._connections:
141 tid = id(threading.current_thread())
142 if tid not in self._connections:
143 self._connections[tid] = self.get_connection()
144 return self._connections[tid]
145 else:
146 return None
147
148 def __init__(self,
149 connection_settings=None,
150 platform=None,
151 working_directory=None,
152 executables_directory=None,
153 connect=True,
154 modules=None,
155 load_default_modules=True,
156 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000157 conn_cls=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100158 ):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +0000159 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100160 self.connection_settings = connection_settings or {}
Anouk Van Laer21f40032017-01-31 13:11:03 +0000161 # Set self.platform: either it's given directly (by platform argument)
162 # or it's given in the connection_settings argument
163 # If neither, create default Platform()
164 if platform is None:
165 self.platform = self.connection_settings.get('platform', Platform())
166 else:
167 self.platform = platform
168 # Check if the user hasn't given two different platforms
169 if 'platform' in self.connection_settings:
170 if connection_settings['platform'] is not platform:
171 raise TargetError('Platform specified in connection_settings '
172 '({}) differs from that directly passed '
173 '({})!)'
174 .format(connection_settings['platform'],
175 self.platform))
176 self.connection_settings['platform'] = self.platform
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100177 self.working_directory = working_directory
178 self.executables_directory = executables_directory
179 self.modules = modules or []
180 self.load_default_modules = load_default_modules
181 self.shell_prompt = shell_prompt
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000182 self.conn_cls = conn_cls
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100183 self.logger = logging.getLogger(self.__class__.__name__)
184 self._installed_binaries = {}
185 self._installed_modules = {}
186 self._cache = {}
187 self._connections = {}
188 self.busybox = None
189
190 if load_default_modules:
191 module_lists = [self.default_modules]
192 else:
193 module_lists = []
194 module_lists += [self.modules, self.platform.modules]
195 self.modules = merge_lists(*module_lists, duplicates='first')
196 self._update_modules('early')
197 if connect:
198 self.connect()
199
200 # connection and initialization
201
202 def connect(self, timeout=None):
203 self.platform.init_target_connection(self)
204 tid = id(threading.current_thread())
205 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000206 self._resolve_paths()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100207 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100208 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100209 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100210 if self.platform.big_core and self.load_default_modules:
211 self._install_module(get_module('bl'))
212
213 def disconnect(self):
214 for conn in self._connections.itervalues():
215 conn.close()
216 self._connections = {}
217
218 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000219 if self.conn_cls == None:
220 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100221 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
222
223 def setup(self, executables=None):
224 self.execute('mkdir -p {}'.format(self.working_directory))
225 self.execute('mkdir -p {}'.format(self.executables_directory))
226 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000227
228 # Setup shutils script for the target
229 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
230 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
231 shell_path = '/bin/sh'
232 if self.os == 'android':
233 shell_path = '/system/bin/sh'
234 with open(shutils_ifile) as fh:
235 lines = fh.readlines()
236 with open(shutils_ofile, 'w') as ofile:
237 for line in lines:
238 line = line.replace("__DEVLIB_SHELL__", shell_path)
239 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
240 ofile.write(line)
241 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
242
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100243 for host_exe in (executables or []): # pylint: disable=superfluous-parens
244 self.install(host_exe)
245
Anouk Van Laer21f40032017-01-31 13:11:03 +0000246 # Check for platform dependent setup procedures
247 self.platform.setup(self)
248
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100249 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
250 self._update_modules('setup')
251
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100252 def reboot(self, hard=False, connect=True, timeout=180):
253 if hard:
254 if not self.has('hard_reset'):
255 raise TargetError('Hard reset not supported for this target.')
256 self.hard_reset() # pylint: disable=no-member
257 else:
258 if not self.is_connected:
259 message = 'Cannot reboot target becuase it is disconnected. ' +\
260 'Either connect() first, or specify hard=True ' +\
261 '(in which case, a hard_reset module must be installed)'
262 raise TargetError(message)
263 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000264 # Wait a fixed delay before starting polling to give the target time to
265 # shut down, otherwise, might create the connection while it's still shutting
266 # down resulting in subsequenct connection failing.
267 self.logger.debug('Waiting for target to power down...')
268 reset_delay = 20
269 time.sleep(reset_delay)
270 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100271 if self.has('boot'):
272 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000273 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100274 if connect:
275 self.connect(timeout=timeout)
276
277 # file transfer
278
279 def push(self, source, dest, timeout=None):
280 return self.conn.push(source, dest, timeout=timeout)
281
282 def pull(self, source, dest, timeout=None):
283 return self.conn.pull(source, dest, timeout=timeout)
284
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +0100285 def get_directory(self, source_dir, dest):
286 """ Pull a directory from the device, after compressing dir """
287 # Create all file names
288 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
289 # Host location of dir
290 outdir = os.path.join(dest, tar_file_name)
291 # Host location of archive
292 tar_file_name = '{}.tar'.format(tar_file_name)
293 tempfile = os.path.join(dest, tar_file_name)
294
295 # Does the folder exist?
296 self.execute('ls -la {}'.format(source_dir))
297 # Try compressing the folder
298 try:
299 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
300 source_dir))
301 except TargetError:
302 self.logger.debug('Failed to run tar command on target! ' \
303 'Not pulling directory {}'.format(source_dir))
304 # Pull the file
305 os.mkdir(outdir)
306 self.pull(tar_file_name, tempfile )
307 # Decompress
308 f = tarfile.open(tempfile, 'r')
309 f.extractall(outdir)
310 os.remove(tempfile)
311
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100312 # execution
313
314 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
setrofimb7386552017-05-23 17:39:12 +0100315 return self.conn.execute(command, timeout, check_exit_code, as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100316
317 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
318 return self.conn.background(command, stdout, stderr, as_root)
319
320 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
321 as_root=False, timeout=30):
322 """
323 Executes the specified binary under the specified conditions.
324
325 :binary: binary to execute. Must be present and executable on the device.
326 :args: arguments to be passed to the binary. The can be either a list or
327 a string.
328 :in_directory: execute the binary in the specified directory. This must
329 be an absolute path.
330 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
331 case, it will be interpreted as the mask), a list of ``ints``, in which
332 case this will be interpreted as the list of cpus, or string, which
333 will be interpreted as a comma-separated list of cpu ranges, e.g.
334 ``"0,4-7"``.
335 :as_root: Specify whether the command should be run as root
336 :timeout: If the invocation does not terminate within this number of seconds,
337 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
338 invocation should not timeout.
339
Brendan Jackman27f545f2016-11-15 16:58:57 +0000340 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100341 """
342 command = binary
343 if args:
344 if isiterable(args):
345 args = ' '.join(args)
346 command = '{} {}'.format(command, args)
347 if on_cpus:
348 on_cpus = bitmask(on_cpus)
349 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
350 if in_directory:
351 command = 'cd {} && {}'.format(in_directory, command)
352 return self.execute(command, as_root=as_root, timeout=timeout)
353
354 def kick_off(self, command, as_root=False):
355 raise NotImplementedError()
356
357 # sysfs interaction
358
359 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100360 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100361 if kind:
362 return kind(output)
363 else:
364 return output
365
366 def read_int(self, path):
367 return self.read_value(path, kind=integer)
368
369 def read_bool(self, path):
370 return self.read_value(path, kind=boolean)
371
372 def write_value(self, path, value, verify=True):
373 value = str(value)
374 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
375 if verify:
376 output = self.read_value(path)
377 if not output == value:
378 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
379 raise TargetError(message)
380
381 def reset(self):
382 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100383 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100384 except (TargetError, TimeoutError, subprocess.CalledProcessError):
385 # on some targets "reboot" doesn't return gracefully
386 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000387 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100388
389 def check_responsive(self):
390 try:
391 self.conn.execute('ls /', timeout=5)
392 except (TimeoutError, subprocess.CalledProcessError):
393 raise TargetNotRespondingError(self.conn.name)
394
395 # process management
396
397 def kill(self, pid, signal=None, as_root=False):
398 signal_string = '-s {}'.format(signal) if signal else ''
399 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
400
401 def killall(self, process_name, signal=None, as_root=False):
402 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000403 try:
404 self.kill(pid, signal=signal, as_root=as_root)
405 except TargetError:
406 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100407
408 def get_pids_of(self, process_name):
409 raise NotImplementedError()
410
411 def ps(self, **kwargs):
412 raise NotImplementedError()
413
414 # files
415
416 def file_exists(self, filepath):
417 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000418 output = self.execute(command.format(filepath), as_root=self.is_rooted)
419 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100420
Sebastian Goscik33603c62016-02-15 15:07:19 +0000421 def directory_exists(self, filepath):
422 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
423 # output from ssh my contain part of the expression in the buffer,
424 # split out everything except the last word.
425 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
426
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100427 def list_file_systems(self):
428 output = self.execute('mount')
429 fstab = []
430 for line in output.split('\n'):
431 line = line.strip()
432 if not line:
433 continue
434 match = FSTAB_ENTRY_REGEX.search(line)
435 if match:
436 fstab.append(FstabEntry(match.group(1), match.group(2),
437 match.group(3), match.group(4),
438 None, None))
439 else: # assume pre-M Android
440 fstab.append(FstabEntry(*line.split()))
441 return fstab
442
443 def list_directory(self, path, as_root=False):
444 raise NotImplementedError()
445
446 def get_workpath(self, name):
447 return self.path.join(self.working_directory, name)
448
449 def tempfile(self, prefix='', suffix=''):
450 names = tempfile._get_candidate_names() # pylint: disable=W0212
451 for _ in xrange(tempfile.TMP_MAX):
452 name = names.next()
453 path = self.get_workpath(prefix + name + suffix)
454 if not self.file_exists(path):
455 return path
456 raise IOError('No usable temporary filename found')
457
458 def remove(self, path, as_root=False):
459 self.execute('rm -rf {}'.format(path), as_root=as_root)
460
461 # misc
462 def core_cpus(self, core):
463 return [i for i, c in enumerate(self.core_names) if c == core]
464
465 def list_online_cpus(self, core=None):
466 path = self.path.join('/sys/devices/system/cpu/online')
467 output = self.read_value(path)
468 all_online = ranges_to_list(output)
469 if core:
470 cpus = self.core_cpus(core)
471 if not cpus:
472 raise ValueError(core)
473 return [o for o in all_online if o in cpus]
474 else:
475 return all_online
476
477 def list_offline_cpus(self):
478 online = self.list_online_cpus()
479 return [c for c in xrange(self.number_of_cpus)
480 if c not in online]
481
482 def getenv(self, variable):
483 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
484
485 def capture_screen(self, filepath):
486 raise NotImplementedError()
487
488 def install(self, filepath, timeout=None, with_name=None):
489 raise NotImplementedError()
490
491 def uninstall(self, name):
492 raise NotImplementedError()
493
Sebastian Goscik84151f92016-02-15 15:09:27 +0000494 def get_installed(self, name, search_system_binaries=True):
495 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000496 if self.file_exists(self.executables_directory):
497 if name in self.list_directory(self.executables_directory):
498 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000499 # Fall back to binaries in PATH
500 if search_system_binaries:
501 for path in self.getenv('PATH').split(self.path.pathsep):
502 try:
503 if name in self.list_directory(path):
504 return self.path.join(path, name)
505 except TargetError:
506 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100507
508 which = get_installed
509
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000510 def install_if_needed(self, host_path, search_system_binaries=True):
511
512 binary_path = self.get_installed(os.path.split(host_path)[1],
513 search_system_binaries=search_system_binaries)
514 if not binary_path:
515 binary_path = self.install(host_path)
516 return binary_path
517
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100518 def is_installed(self, name):
519 return bool(self.get_installed(name))
520
521 def bin(self, name):
522 return self._installed_binaries.get(name, name)
523
524 def has(self, modname):
525 return hasattr(self, identifier(modname))
526
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000527 def lsmod(self):
528 lines = self.execute('lsmod').splitlines()
529 entries = []
530 for line in lines[1:]: # first line is the header
531 if not line.strip():
532 continue
533 parts = line.split()
534 name = parts[0]
535 size = int(parts[1])
536 use_count = int(parts[2])
537 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000538 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000539 else:
540 used_by = []
541 entries.append(LsmodEntry(name, size, use_count, used_by))
542 return entries
543
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000544 def insmod(self, path):
545 target_path = self.get_workpath(os.path.basename(path))
546 self.push(path, target_path)
547 self.execute('insmod {}'.format(target_path), as_root=True)
548
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100549
550 def extract(self, path, dest=None):
551 """
552 Extact the specified on-target file. The extraction method to be used
553 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
554 If ``dest`` is specified, it must be an existing directory on target;
555 the extracted contents will be placed there.
556
557 Note that, depending on the archive file format (and therfore the
558 extraction method used), the original archive file may or may not exist
559 after the extraction.
560
561 The return value is the path to the extracted contents. In case of
562 gunzip and bunzip2, this will be path to the extracted file; for tar
563 and uzip, this will be the directory with the extracted file(s)
564 (``dest`` if it was specified otherwise, the directory that cotained
565 the archive).
566
567 """
568 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
569 '.tgz', '.tbz', '.tbz2']:
570 if path.endswith(ending):
571 return self._extract_archive(path, 'tar xf {} -C {}', dest)
572
573 ext = self.path.splitext(path)[1]
574 if ext in ['.bz', '.bz2']:
575 return self._extract_file(path, 'bunzip2 -f {}', dest)
576 elif ext == '.gz':
577 return self._extract_file(path, 'gunzip -f {}', dest)
578 elif ext == '.zip':
579 return self._extract_archive(path, 'unzip {} -d {}', dest)
580 else:
581 raise ValueError('Unknown compression format: {}'.format(ext))
582
Sergei Trofimov69a83d42017-05-12 11:54:31 +0100583 def sleep(self, duration):
584 timeout = duration + 10
585 self.execute('sleep {}'.format(duration), timeout=timeout)
586
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100587 # internal methods
588
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100589 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
590 command = '{} {}'.format(self.shutils, command)
591 return self.conn.execute(command, timeout, check_exit_code, as_root)
592
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100593 def _extract_archive(self, path, cmd, dest=None):
594 cmd = '{} ' + cmd # busybox
595 if dest:
596 extracted = dest
597 else:
598 extracted = self.path.dirname(path)
599 cmdtext = cmd.format(self.busybox, path, extracted)
600 self.execute(cmdtext)
601 return extracted
602
603 def _extract_file(self, path, cmd, dest=None):
604 cmd = '{} ' + cmd # busybox
605 cmdtext = cmd.format(self.busybox, path)
606 self.execute(cmdtext)
607 extracted = self.path.splitext(path)[0]
608 if dest:
609 self.execute('mv -f {} {}'.format(extracted, dest))
610 if dest.endswith('/'):
611 extracted = self.path.join(dest, self.path.basename(extracted))
612 else:
613 extracted = dest
614 return extracted
615
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100616 def _update_modules(self, stage):
617 for mod in self.modules:
618 if isinstance(mod, dict):
619 mod, params = mod.items()[0]
620 else:
621 params = {}
622 mod = get_module(mod)
623 if not mod.stage == stage:
624 continue
625 if mod.probe(self):
626 self._install_module(mod, **params)
627 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000628 msg = 'Module {} is not supported by the target'.format(mod.name)
629 if self.load_default_modules:
630 self.logger.debug(msg)
631 else:
632 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100633
634 def _install_module(self, mod, **params):
635 if mod.name not in self._installed_modules:
636 self.logger.debug('Installing module {}'.format(mod.name))
637 mod.install(self, **params)
638 self._installed_modules[mod.name] = mod
639 else:
640 self.logger.debug('Module {} is already installed.'.format(mod.name))
641
Sergei Trofimov961f9572015-11-18 17:32:26 +0000642 def _resolve_paths(self):
643 raise NotImplementedError()
644
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100645
646class LinuxTarget(Target):
647
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100648 path = posixpath
649 os = 'linux'
650
651 @property
652 @memoized
653 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100654 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100655 for abi, architectures in ABI_MAP.iteritems():
656 if value in architectures:
657 result = abi
658 break
659 else:
660 result = value
661 return result
662
663 @property
664 @memoized
665 def os_version(self):
666 os_version = {}
667 try:
668 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
669 version_files = self.execute(command, check_exit_code=False).strip().split()
670 for vf in version_files:
671 name = self.path.basename(vf)
672 output = self.read_value(vf)
673 os_version[name] = output.strip().replace('\n', ' ')
674 except TargetError:
675 raise
676 return os_version
677
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000678 @property
679 @memoized
680 # There is currently no better way to do this cross platform.
681 # ARM does not have dmidecode
682 def model(self):
683 if self.file_exists("/proc/device-tree/model"):
684 raw_model = self.execute("cat /proc/device-tree/model")
685 return '_'.join(raw_model.split()[:2])
686 return None
687
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000688 def __init__(self,
689 connection_settings=None,
690 platform=None,
691 working_directory=None,
692 executables_directory=None,
693 connect=True,
694 modules=None,
695 load_default_modules=True,
696 shell_prompt=DEFAULT_SHELL_PROMPT,
697 conn_cls=SshConnection,
698 ):
699 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
700 platform=platform,
701 working_directory=working_directory,
702 executables_directory=executables_directory,
703 connect=connect,
704 modules=modules,
705 load_default_modules=load_default_modules,
706 shell_prompt=shell_prompt,
707 conn_cls=conn_cls)
708
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100709 def connect(self, timeout=None):
710 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100711
712 def kick_off(self, command, as_root=False):
713 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
714 return self.conn.execute(command, as_root=as_root)
715
716 def get_pids_of(self, process_name):
717 """Returns a list of PIDs of all processes with the specified name."""
718 # result should be a column of PIDs with the first row as "PID" header
719 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
720 check_exit_code=False).strip().split()
721 if len(result) >= 2: # at least one row besides the header
722 return map(int, result[1:])
723 else:
724 return []
725
726 def ps(self, **kwargs):
727 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
728 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
729 lines.next() # header
730
731 result = []
732 for line in lines:
733 parts = re.split(r'\s+', line, maxsplit=8)
734 if parts and parts != ['']:
735 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
736
737 if not kwargs:
738 return result
739 else:
740 filtered_result = []
741 for entry in result:
742 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
743 filtered_result.append(entry)
744 return filtered_result
745
746 def list_directory(self, path, as_root=False):
747 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
748 return [x.strip() for x in contents.split('\n') if x.strip()]
749
750 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
751 destpath = self.path.join(self.executables_directory,
752 with_name and with_name or self.path.basename(filepath))
753 self.push(filepath, destpath)
754 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
755 self._installed_binaries[self.path.basename(destpath)] = destpath
756 return destpath
757
758 def uninstall(self, name):
759 path = self.path.join(self.executables_directory, name)
760 self.remove(path)
761
762 def capture_screen(self, filepath):
763 if not self.is_installed('scrot'):
764 self.logger.debug('Could not take screenshot as scrot is not installed.')
765 return
766 try:
767
768 tmpfile = self.tempfile()
769 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
770 self.pull(tmpfile, filepath)
771 self.remove(tmpfile)
772 except TargetError as e:
773 if "Can't open X dispay." not in e.message:
774 raise e
775 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
776 self.logger.debug('Could not take screenshot: {}'.format(message))
777
Sergei Trofimov961f9572015-11-18 17:32:26 +0000778 def _resolve_paths(self):
779 if self.working_directory is None:
780 if self.connected_as_root:
781 self.working_directory = '/root/devlib-target'
782 else:
783 self.working_directory = '/home/{}/devlib-target'.format(self.user)
784 if self.executables_directory is None:
785 self.executables_directory = self.path.join(self.working_directory, 'bin')
786
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100787
788class AndroidTarget(Target):
789
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100790 path = posixpath
791 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100792 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100793
794 @property
795 @memoized
796 def abi(self):
797 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
798
799 @property
800 @memoized
801 def os_version(self):
802 os_version = {}
803 for k, v in self.getprop().iteritems():
804 if k.startswith('ro.build.version'):
805 part = k.split('.')[-1]
806 os_version[part] = v
807 return os_version
808
809 @property
810 def adb_name(self):
811 return self.conn.device
812
813 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000814 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000815 def android_id(self):
816 """
817 Get the device's ANDROID_ID. Which is
818
819 "A 64-bit number (as a hex string) that is randomly generated when the user
820 first sets up the device and should remain constant for the lifetime of the
821 user's device."
822
823 .. note:: This will get reset on userdata erasure.
824
825 """
826 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
827 return output.split('value=')[-1]
828
829 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100830 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000831 def model(self):
832 try:
833 return self.getprop(prop='ro.product.device')
834 except KeyError:
835 return None
836
837 @property
838 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100839 def screen_resolution(self):
840 output = self.execute('dumpsys window')
841 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
842 if match:
843 return (int(match.group('width')),
844 int(match.group('height')))
845 else:
846 return (0, 0)
847
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000848 def __init__(self,
849 connection_settings=None,
850 platform=None,
851 working_directory=None,
852 executables_directory=None,
853 connect=True,
854 modules=None,
855 load_default_modules=True,
856 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000857 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000858 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000859 ):
860 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
861 platform=platform,
862 working_directory=working_directory,
863 executables_directory=executables_directory,
864 connect=connect,
865 modules=modules,
866 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000867 shell_prompt=shell_prompt,
868 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000869 self.package_data_directory = package_data_directory
870
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100871 def reset(self, fastboot=False): # pylint: disable=arguments-differ
872 try:
873 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100874 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100875 except (TargetError, TimeoutError, subprocess.CalledProcessError):
876 # on some targets "reboot" doesn't return gracefully
877 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000878 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100879
880 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
881 start = time.time()
882 device = self.connection_settings.get('device')
883 if device and ':' in device:
884 # ADB does not automatically remove a network device from it's
885 # devices list when the connection is broken by the remote, so the
886 # adb connection may have gone "stale", resulting in adb blocking
887 # indefinitely when making calls to the device. To avoid this,
888 # always disconnect first.
889 adb_disconnect(device)
890 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100891
892 if check_boot_completed:
893 boot_completed = boolean(self.getprop('sys.boot_completed'))
894 while not boot_completed and timeout >= time.time() - start:
895 time.sleep(5)
896 boot_completed = boolean(self.getprop('sys.boot_completed'))
897 if not boot_completed:
898 raise TargetError('Connected but Android did not fully boot.')
899
900 def setup(self, executables=None):
901 super(AndroidTarget, self).setup(executables)
902 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
903
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100904 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100905 """
906 Like execute but closes adb session and returns immediately, leaving the command running on the
907 device (this is different from execute(background=True) which keeps adb connection open and returns
908 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100909 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100910 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100911 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100912 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +0100913 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100914 output = self.execute(command, timeout=1, as_root=as_root)
915 except TimeoutError:
916 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100917
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100918 def __setup_list_directory(self):
919 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
920 # AOSP 7.0 as well, the ls command was changed.
921 # Previous versions default to a single column listing, which is nice and easy to parse.
922 # Newer versions default to a multi-column listing, which is not, but it does support
923 # a '-1' option to get into single column mode. Older versions do not support this option
924 # so we try the new version, and if it fails we use the old version.
925 self.ls_command = 'ls -1'
926 try:
Marc Bonnici06552372017-03-29 16:43:22 +0100927 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100928 except TargetError:
929 self.ls_command = 'ls'
930
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100931 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100932 if self.ls_command == '':
933 self.__setup_list_directory()
934 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100935 return [x.strip() for x in contents.split('\n') if x.strip()]
936
937 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
938 ext = os.path.splitext(filepath)[1].lower()
939 if ext == '.apk':
940 return self.install_apk(filepath, timeout)
941 else:
942 return self.install_executable(filepath, with_name)
943
944 def uninstall(self, name):
945 if self.package_is_installed(name):
946 self.uninstall_package(name)
947 else:
948 self.uninstall_executable(name)
949
950 def get_pids_of(self, process_name):
951 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
952 if result and 'not found' not in result:
953 return [int(x.split()[1]) for x in result.split('\n')[1:]]
954 else:
955 return []
956
957 def ps(self, **kwargs):
958 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
959 lines.next() # header
960 result = []
961 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +0100962 parts = line.split(None, 8)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100963 if parts:
964 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
965 if not kwargs:
966 return result
967 else:
968 filtered_result = []
969 for entry in result:
970 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
971 filtered_result.append(entry)
972 return filtered_result
973
974 def capture_screen(self, filepath):
975 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
976 self.execute('screencap -p {}'.format(on_device_file))
977 self.pull(on_device_file, filepath)
978 self.remove(on_device_file)
979
980 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
981 if not as_root:
982 self.conn.push(source, dest, timeout=timeout)
983 else:
984 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000985 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100986 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000987 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100988
989 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
990 if not as_root:
991 self.conn.pull(source, dest, timeout=timeout)
992 else:
993 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000994 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
995 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +0100996 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100997 self.conn.pull(device_tempfile, dest, timeout=timeout)
998
999 # Android-specific
1000
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001001 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001002 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001003 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001004 if direction == "horizontal":
1005 swipe_heigh = height * 2 // 3
1006 start = 100
1007 stop = width - start
1008 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
1009 if direction == "vertical":
1010 swipe_middle = height / 2
1011 swipe_heigh = height * 2 // 3
1012 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
1013 else:
1014 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001015
1016 def getprop(self, prop=None):
1017 props = AndroidProperties(self.execute('getprop'))
1018 if prop:
1019 return props[prop]
1020 return props
1021
1022 def is_installed(self, name):
1023 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1024
1025 def package_is_installed(self, package_name):
1026 return package_name in self.list_packages()
1027
1028 def list_packages(self):
1029 output = self.execute('pm list packages')
1030 output = output.replace('package:', '')
1031 return output.split()
1032
1033 def get_package_version(self, package):
1034 output = self.execute('dumpsys package {}'.format(package))
1035 for line in convert_new_lines(output).split('\n'):
1036 if 'versionName' in line:
1037 return line.split('=', 1)[1]
1038 return None
1039
Marc Bonnicid3396f22017-05-31 15:56:50 +01001040 def get_sdk_version(self):
1041 try:
1042 return int(self.getprop('ro.build.version.sdk'))
1043 except (ValueError, TypeError):
1044 return None
1045
Marc Bonnicic33dd652017-05-31 15:51:31 +01001046 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001047 ext = os.path.splitext(filepath)[1].lower()
1048 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001049 flags = []
1050 if replace:
1051 flags.append('-r') # Replace existing APK
1052 if allow_downgrade:
1053 flags.append('-d') # Install the APK even if a newer version is already installed
1054 if self.get_sdk_version() >= 23:
1055 flags.append('-g') # Grant all runtime permissions
1056 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1057 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001058 else:
1059 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1060
1061 def install_executable(self, filepath, with_name=None):
1062 self._ensure_executables_directory_is_writable()
1063 executable_name = with_name or os.path.basename(filepath)
1064 on_device_file = self.path.join(self.working_directory, executable_name)
1065 on_device_executable = self.path.join(self.executables_directory, executable_name)
1066 self.push(filepath, on_device_file)
1067 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001068 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1069 self.remove(on_device_file, as_root=self.needs_su)
1070 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001071 self._installed_binaries[executable_name] = on_device_executable
1072 return on_device_executable
1073
1074 def uninstall_package(self, package):
1075 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1076
1077 def uninstall_executable(self, executable_name):
1078 on_device_executable = self.path.join(self.executables_directory, executable_name)
1079 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001080 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001081
1082 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001083 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001084 filtstr = ' -s {}'.format(filter) if filter else ''
1085 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1086 adb_command(self.adb_name, command, timeout=timeout)
1087
1088 def clear_logcat(self):
1089 adb_command(self.adb_name, 'logcat -c', timeout=30)
1090
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001091 def adb_reboot_bootloader(self, timeout=30):
1092 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1093
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001094 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001095 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001096 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001097 return
1098 adb_command(self.adb_name, 'root', timeout=30)
1099 self._connected_as_root = True
1100 return
1101 adb_command(self.adb_name, 'unroot', timeout=30)
1102 self._connected_as_root = False
1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001104 def is_screen_on(self):
1105 output = self.execute('dumpsys power')
1106 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1107 if match:
1108 return boolean(match.group(1))
1109 else:
1110 raise TargetError('Could not establish screen state.')
1111
1112 def ensure_screen_is_on(self):
1113 if not self.is_screen_on():
1114 self.execute('input keyevent 26')
1115
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001116 def ensure_screen_is_off(self):
1117 if self.is_screen_on():
1118 self.execute('input keyevent 26')
1119
1120 def homescreen(self):
1121 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1122
Sergei Trofimov961f9572015-11-18 17:32:26 +00001123 def _resolve_paths(self):
1124 if self.working_directory is None:
1125 self.working_directory = '/data/local/tmp/devlib-target'
1126 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1127 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001128 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001129
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001130 def _ensure_executables_directory_is_writable(self):
1131 matched = []
1132 for entry in self.list_file_systems():
1133 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1134 matched.append(entry)
1135 if matched:
1136 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1137 if 'rw' not in entry.options:
1138 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1139 entry.mount_point),
1140 as_root=True)
1141 else:
1142 message = 'Could not find mount point for executables directory {}'
1143 raise TargetError(message.format(self.executables_directory))
1144
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001145 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1146
1147 @property
1148 def charging_enabled(self):
1149 """
1150 Whether drawing power to charge the battery is enabled
1151
1152 Not all devices have the ability to enable/disable battery charging
1153 (e.g. because they don't have a battery). In that case,
1154 ``charging_enabled`` is None.
1155 """
1156 if not self.file_exists(self._charging_enabled_path):
1157 return None
1158 return self.read_bool(self._charging_enabled_path)
1159
1160 @charging_enabled.setter
1161 def charging_enabled(self, enabled):
1162 """
1163 Enable/disable drawing power to charge the battery
1164
1165 Not all devices have this facility. In that case, do nothing.
1166 """
1167 if not self.file_exists(self._charging_enabled_path):
1168 return
1169 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001170
1171FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1172PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001173LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001174
1175
1176class Cpuinfo(object):
1177
1178 @property
1179 @memoized
1180 def architecture(self):
1181 for section in self.sections:
1182 if 'CPU architecture' in section:
1183 return section['CPU architecture']
1184 if 'architecture' in section:
1185 return section['architecture']
1186
1187 @property
1188 @memoized
1189 def cpu_names(self):
1190 cpu_names = []
1191 global_name = None
1192 for section in self.sections:
1193 if 'processor' in section:
1194 if 'CPU part' in section:
1195 cpu_names.append(_get_part_name(section))
1196 elif 'model name' in section:
1197 cpu_names.append(_get_model_name(section))
1198 else:
1199 cpu_names.append(None)
1200 elif 'CPU part' in section:
1201 global_name = _get_part_name(section)
1202 return [caseless_string(c or global_name) for c in cpu_names]
1203
1204 def __init__(self, text):
1205 self.sections = None
1206 self.text = None
1207 self.parse(text)
1208
1209 @memoized
1210 def get_cpu_features(self, cpuid=0):
1211 global_features = []
1212 for section in self.sections:
1213 if 'processor' in section:
1214 if int(section.get('processor')) != cpuid:
1215 continue
1216 if 'Features' in section:
1217 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001218 elif 'flags' in section:
1219 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001220 elif 'Features' in section:
1221 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001222 elif 'flags' in section:
1223 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001224 return global_features
1225
1226 def parse(self, text):
1227 self.sections = []
1228 current_section = {}
1229 self.text = text.strip()
1230 for line in self.text.split('\n'):
1231 line = line.strip()
1232 if line:
1233 key, value = line.split(':', 1)
1234 current_section[key.strip()] = value.strip()
1235 else: # not line
1236 self.sections.append(current_section)
1237 current_section = {}
1238 self.sections.append(current_section)
1239
1240 def __str__(self):
1241 return 'CpuInfo({})'.format(self.cpu_names)
1242
1243 __repr__ = __str__
1244
1245
1246class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001247 """
1248 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001249
Brendan Jackman54adf802017-02-20 17:57:51 +00001250 Not expected to work for very old (pre-3.0) kernel version numbers.
1251
1252 :ivar release: Version number/revision string. Typical output of
1253 ``uname -r``
1254 :type release: str
1255 :ivar version: Extra version info (aside from ``release``) reported by
1256 ``uname``
1257 :type version: str
1258 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1259 :type version_number: int
1260 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1261 :type major: int
1262 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1263 be None
1264 :type minor: int
1265 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1266 :type rc: int
1267 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1268 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001269
1270 :ivar parts: Tuple of version number components. Can be used for
1271 lexicographically comparing kernel versions.
1272 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001273 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001274 def __init__(self, version_string):
1275 if ' #' in version_string:
1276 release, version = version_string.split(' #')
1277 self.release = release
1278 self.version = version
1279 elif version_string.startswith('#'):
1280 self.release = ''
1281 self.version = version_string
1282 else:
1283 self.release = version_string
1284 self.version = ''
1285
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001286 self.version_number = None
1287 self.major = None
1288 self.minor = None
1289 self.sha1 = None
1290 self.rc = None
1291 match = KVERSION_REGEX.match(version_string)
1292 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001293 groups = match.groupdict()
1294 self.version_number = int(groups['version'])
1295 self.major = int(groups['major'])
1296 if groups['minor'] is not None:
1297 self.minor = int(groups['minor'])
1298 if groups['rc'] is not None:
1299 self.rc = int(groups['rc'])
1300 if groups['sha1'] is not None:
1301 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001302
Brendan Jackman18b77b82017-02-20 17:52:56 +00001303 self.parts = (self.version_number, self.major, self.minor)
1304
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001305 def __str__(self):
1306 return '{} {}'.format(self.release, self.version)
1307
1308 __repr__ = __str__
1309
1310
1311class KernelConfig(object):
1312
1313 not_set_regex = re.compile(r'# (\S+) is not set')
1314
1315 @staticmethod
1316 def get_config_name(name):
1317 name = name.upper()
1318 if not name.startswith('CONFIG_'):
1319 name = 'CONFIG_' + name
1320 return name
1321
1322 def iteritems(self):
1323 return self._config.iteritems()
1324
1325 def __init__(self, text):
1326 self.text = text
1327 self._config = {}
1328 for line in text.split('\n'):
1329 line = line.strip()
1330 if line.startswith('#'):
1331 match = self.not_set_regex.search(line)
1332 if match:
1333 self._config[match.group(1)] = 'n'
1334 elif '=' in line:
1335 name, value = line.split('=', 1)
1336 self._config[name.strip()] = value.strip()
1337
1338 def get(self, name):
1339 return self._config.get(self.get_config_name(name))
1340
1341 def like(self, name):
1342 regex = re.compile(name, re.I)
1343 result = {}
1344 for k, v in self._config.iteritems():
1345 if regex.search(k):
1346 result[k] = v
1347 return result
1348
1349 def is_enabled(self, name):
1350 return self.get(name) == 'y'
1351
1352 def is_module(self, name):
1353 return self.get(name) == 'm'
1354
1355 def is_not_set(self, name):
1356 return self.get(name) == 'n'
1357
1358 def has(self, name):
1359 return self.get(name) in ['m', 'y']
1360
1361
1362class LocalLinuxTarget(LinuxTarget):
1363
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001364 def __init__(self,
1365 connection_settings=None,
1366 platform=None,
1367 working_directory=None,
1368 executables_directory=None,
1369 connect=True,
1370 modules=None,
1371 load_default_modules=True,
1372 shell_prompt=DEFAULT_SHELL_PROMPT,
1373 conn_cls=LocalConnection,
1374 ):
1375 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1376 platform=platform,
1377 working_directory=working_directory,
1378 executables_directory=executables_directory,
1379 connect=connect,
1380 modules=modules,
1381 load_default_modules=load_default_modules,
1382 shell_prompt=shell_prompt,
1383 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001384
Sergei Trofimov961f9572015-11-18 17:32:26 +00001385 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001386 if self.working_directory is None:
1387 self.working_directory = '/tmp'
1388 if self.executables_directory is None:
1389 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001390
1391
1392def _get_model_name(section):
1393 name_string = section['model name']
1394 parts = name_string.split('@')[0].strip().split()
1395 return ' '.join([p for p in parts
1396 if '(' not in p and p != 'CPU'])
1397
1398
1399def _get_part_name(section):
1400 implementer = section.get('CPU implementer', '0x0')
1401 part = section['CPU part']
1402 variant = section.get('CPU variant', '0x0')
1403 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1404 if name is None:
1405 name = '{}/{}/{}'.format(implementer, part, variant)
1406 return name