blob: 8798ad495f2d489b93347cb803f43f7fc53a3730 [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 Bonnicic33dd652017-05-31 15:51:31 +01001040 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001041 ext = os.path.splitext(filepath)[1].lower()
1042 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001043 flags = []
1044 if replace:
1045 flags.append('-r') # Replace existing APK
1046 if allow_downgrade:
1047 flags.append('-d') # Install the APK even if a newer version is already installed
1048 if self.get_sdk_version() >= 23:
1049 flags.append('-g') # Grant all runtime permissions
1050 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1051 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001052 else:
1053 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1054
1055 def install_executable(self, filepath, with_name=None):
1056 self._ensure_executables_directory_is_writable()
1057 executable_name = with_name or os.path.basename(filepath)
1058 on_device_file = self.path.join(self.working_directory, executable_name)
1059 on_device_executable = self.path.join(self.executables_directory, executable_name)
1060 self.push(filepath, on_device_file)
1061 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001062 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1063 self.remove(on_device_file, as_root=self.needs_su)
1064 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001065 self._installed_binaries[executable_name] = on_device_executable
1066 return on_device_executable
1067
1068 def uninstall_package(self, package):
1069 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1070
1071 def uninstall_executable(self, executable_name):
1072 on_device_executable = self.path.join(self.executables_directory, executable_name)
1073 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001074 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001075
1076 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001077 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001078 filtstr = ' -s {}'.format(filter) if filter else ''
1079 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1080 adb_command(self.adb_name, command, timeout=timeout)
1081
1082 def clear_logcat(self):
1083 adb_command(self.adb_name, 'logcat -c', timeout=30)
1084
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001085 def adb_reboot_bootloader(self, timeout=30):
1086 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1087
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001088 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001089 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001090 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001091 return
1092 adb_command(self.adb_name, 'root', timeout=30)
1093 self._connected_as_root = True
1094 return
1095 adb_command(self.adb_name, 'unroot', timeout=30)
1096 self._connected_as_root = False
1097
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001098 def is_screen_on(self):
1099 output = self.execute('dumpsys power')
1100 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1101 if match:
1102 return boolean(match.group(1))
1103 else:
1104 raise TargetError('Could not establish screen state.')
1105
1106 def ensure_screen_is_on(self):
1107 if not self.is_screen_on():
1108 self.execute('input keyevent 26')
1109
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001110 def ensure_screen_is_off(self):
1111 if self.is_screen_on():
1112 self.execute('input keyevent 26')
1113
1114 def homescreen(self):
1115 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1116
Sergei Trofimov961f9572015-11-18 17:32:26 +00001117 def _resolve_paths(self):
1118 if self.working_directory is None:
1119 self.working_directory = '/data/local/tmp/devlib-target'
1120 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1121 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001122 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001123
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001124 def _ensure_executables_directory_is_writable(self):
1125 matched = []
1126 for entry in self.list_file_systems():
1127 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1128 matched.append(entry)
1129 if matched:
1130 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1131 if 'rw' not in entry.options:
1132 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1133 entry.mount_point),
1134 as_root=True)
1135 else:
1136 message = 'Could not find mount point for executables directory {}'
1137 raise TargetError(message.format(self.executables_directory))
1138
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001139 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1140
1141 @property
1142 def charging_enabled(self):
1143 """
1144 Whether drawing power to charge the battery is enabled
1145
1146 Not all devices have the ability to enable/disable battery charging
1147 (e.g. because they don't have a battery). In that case,
1148 ``charging_enabled`` is None.
1149 """
1150 if not self.file_exists(self._charging_enabled_path):
1151 return None
1152 return self.read_bool(self._charging_enabled_path)
1153
1154 @charging_enabled.setter
1155 def charging_enabled(self, enabled):
1156 """
1157 Enable/disable drawing power to charge the battery
1158
1159 Not all devices have this facility. In that case, do nothing.
1160 """
1161 if not self.file_exists(self._charging_enabled_path):
1162 return
1163 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001164
1165FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1166PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001167LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001168
1169
1170class Cpuinfo(object):
1171
1172 @property
1173 @memoized
1174 def architecture(self):
1175 for section in self.sections:
1176 if 'CPU architecture' in section:
1177 return section['CPU architecture']
1178 if 'architecture' in section:
1179 return section['architecture']
1180
1181 @property
1182 @memoized
1183 def cpu_names(self):
1184 cpu_names = []
1185 global_name = None
1186 for section in self.sections:
1187 if 'processor' in section:
1188 if 'CPU part' in section:
1189 cpu_names.append(_get_part_name(section))
1190 elif 'model name' in section:
1191 cpu_names.append(_get_model_name(section))
1192 else:
1193 cpu_names.append(None)
1194 elif 'CPU part' in section:
1195 global_name = _get_part_name(section)
1196 return [caseless_string(c or global_name) for c in cpu_names]
1197
1198 def __init__(self, text):
1199 self.sections = None
1200 self.text = None
1201 self.parse(text)
1202
1203 @memoized
1204 def get_cpu_features(self, cpuid=0):
1205 global_features = []
1206 for section in self.sections:
1207 if 'processor' in section:
1208 if int(section.get('processor')) != cpuid:
1209 continue
1210 if 'Features' in section:
1211 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001212 elif 'flags' in section:
1213 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001214 elif 'Features' in section:
1215 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001216 elif 'flags' in section:
1217 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001218 return global_features
1219
1220 def parse(self, text):
1221 self.sections = []
1222 current_section = {}
1223 self.text = text.strip()
1224 for line in self.text.split('\n'):
1225 line = line.strip()
1226 if line:
1227 key, value = line.split(':', 1)
1228 current_section[key.strip()] = value.strip()
1229 else: # not line
1230 self.sections.append(current_section)
1231 current_section = {}
1232 self.sections.append(current_section)
1233
1234 def __str__(self):
1235 return 'CpuInfo({})'.format(self.cpu_names)
1236
1237 __repr__ = __str__
1238
1239
1240class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001241 """
1242 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001243
Brendan Jackman54adf802017-02-20 17:57:51 +00001244 Not expected to work for very old (pre-3.0) kernel version numbers.
1245
1246 :ivar release: Version number/revision string. Typical output of
1247 ``uname -r``
1248 :type release: str
1249 :ivar version: Extra version info (aside from ``release``) reported by
1250 ``uname``
1251 :type version: str
1252 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1253 :type version_number: int
1254 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1255 :type major: int
1256 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1257 be None
1258 :type minor: int
1259 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1260 :type rc: int
1261 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1262 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001263
1264 :ivar parts: Tuple of version number components. Can be used for
1265 lexicographically comparing kernel versions.
1266 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001267 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001268 def __init__(self, version_string):
1269 if ' #' in version_string:
1270 release, version = version_string.split(' #')
1271 self.release = release
1272 self.version = version
1273 elif version_string.startswith('#'):
1274 self.release = ''
1275 self.version = version_string
1276 else:
1277 self.release = version_string
1278 self.version = ''
1279
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001280 self.version_number = None
1281 self.major = None
1282 self.minor = None
1283 self.sha1 = None
1284 self.rc = None
1285 match = KVERSION_REGEX.match(version_string)
1286 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001287 groups = match.groupdict()
1288 self.version_number = int(groups['version'])
1289 self.major = int(groups['major'])
1290 if groups['minor'] is not None:
1291 self.minor = int(groups['minor'])
1292 if groups['rc'] is not None:
1293 self.rc = int(groups['rc'])
1294 if groups['sha1'] is not None:
1295 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001296
Brendan Jackman18b77b82017-02-20 17:52:56 +00001297 self.parts = (self.version_number, self.major, self.minor)
1298
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001299 def __str__(self):
1300 return '{} {}'.format(self.release, self.version)
1301
1302 __repr__ = __str__
1303
1304
1305class KernelConfig(object):
1306
1307 not_set_regex = re.compile(r'# (\S+) is not set')
1308
1309 @staticmethod
1310 def get_config_name(name):
1311 name = name.upper()
1312 if not name.startswith('CONFIG_'):
1313 name = 'CONFIG_' + name
1314 return name
1315
1316 def iteritems(self):
1317 return self._config.iteritems()
1318
1319 def __init__(self, text):
1320 self.text = text
1321 self._config = {}
1322 for line in text.split('\n'):
1323 line = line.strip()
1324 if line.startswith('#'):
1325 match = self.not_set_regex.search(line)
1326 if match:
1327 self._config[match.group(1)] = 'n'
1328 elif '=' in line:
1329 name, value = line.split('=', 1)
1330 self._config[name.strip()] = value.strip()
1331
1332 def get(self, name):
1333 return self._config.get(self.get_config_name(name))
1334
1335 def like(self, name):
1336 regex = re.compile(name, re.I)
1337 result = {}
1338 for k, v in self._config.iteritems():
1339 if regex.search(k):
1340 result[k] = v
1341 return result
1342
1343 def is_enabled(self, name):
1344 return self.get(name) == 'y'
1345
1346 def is_module(self, name):
1347 return self.get(name) == 'm'
1348
1349 def is_not_set(self, name):
1350 return self.get(name) == 'n'
1351
1352 def has(self, name):
1353 return self.get(name) in ['m', 'y']
1354
1355
1356class LocalLinuxTarget(LinuxTarget):
1357
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001358 def __init__(self,
1359 connection_settings=None,
1360 platform=None,
1361 working_directory=None,
1362 executables_directory=None,
1363 connect=True,
1364 modules=None,
1365 load_default_modules=True,
1366 shell_prompt=DEFAULT_SHELL_PROMPT,
1367 conn_cls=LocalConnection,
1368 ):
1369 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1370 platform=platform,
1371 working_directory=working_directory,
1372 executables_directory=executables_directory,
1373 connect=connect,
1374 modules=modules,
1375 load_default_modules=load_default_modules,
1376 shell_prompt=shell_prompt,
1377 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001378
Sergei Trofimov961f9572015-11-18 17:32:26 +00001379 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001380 if self.working_directory is None:
1381 self.working_directory = '/tmp'
1382 if self.executables_directory is None:
1383 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001384
1385
1386def _get_model_name(section):
1387 name_string = section['model name']
1388 parts = name_string.split('@')[0].strip().split()
1389 return ' '.join([p for p in parts
1390 if '(' not in p and p != 'CPU'])
1391
1392
1393def _get_part_name(section):
1394 implementer = section.get('CPU implementer', '0x0')
1395 part = section['CPU part']
1396 variant = section.get('CPU variant', '0x0')
1397 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1398 if name is None:
1399 name = '{}/{}/{}'.format(implementer, part, variant)
1400 return name