blob: be2d021606f3d3cf91de1f06a79eb6f2914d030f [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001import os
2import re
3import time
4import logging
5import posixpath
6import subprocess
7import tempfile
8import threading
9from collections import namedtuple
10
11from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
12from devlib.module import get_module
13from devlib.platform import Platform
14from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError
15from devlib.utils.ssh import SshConnection
16from devlib.utils.android import AdbConnection, AndroidProperties, adb_command, adb_disconnect
17from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists
18from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes
19from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
20
21
Sebastian Goscik040daab2016-02-23 10:27:45 +000022FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
Sebastian Goscik1890db72016-02-15 14:43:30 +000023ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010024 re.IGNORECASE)
25ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
26 r'\s+(?P<width>\d+)x(?P<height>\d+)')
27DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
28 re.MULTILINE)
Patrick Bellasi9a8d5392017-02-17 15:28:07 +000029KVERSION_REGEX =re.compile(
Brendan Jackman66656932017-02-20 18:29:49 +000030 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 +000031)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010032
33
34class Target(object):
35
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010036 path = None
37 os = None
38
39 default_modules = [
40 'hotplug',
41 'cpufreq',
42 'cpuidle',
43 'cgroups',
44 'hwmon',
45 ]
46
47 @property
48 def core_names(self):
49 return self.platform.core_names
50
51 @property
52 def core_clusters(self):
53 return self.platform.core_clusters
54
55 @property
56 def big_core(self):
57 return self.platform.big_core
58
59 @property
60 def little_core(self):
61 return self.platform.little_core
62
63 @property
64 def is_connected(self):
65 return self.conn is not None
66
67 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010068 def connected_as_root(self):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +000069 if self._connected_as_root is None:
70 result = self.execute('id')
71 self._connected_as_root = 'uid=0(' in result
72 return self._connected_as_root
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010073
74 @property
75 @memoized
76 def is_rooted(self):
77 if self.connected_as_root:
78 return True
79 try:
80 self.execute('ls /', timeout=2, as_root=True)
81 return True
82 except (TargetError, TimeoutError):
83 return False
84
85 @property
86 @memoized
Javi Merino16d87c62016-06-23 14:55:19 +010087 def needs_su(self):
88 return not self.connected_as_root and self.is_rooted
89
90 @property
91 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010092 def kernel_version(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +000093 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010094
95 @property
96 def os_version(self): # pylint: disable=no-self-use
97 return {}
98
99 @property
100 def abi(self): # pylint: disable=no-self-use
101 return None
102
103 @property
104 @memoized
105 def cpuinfo(self):
106 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
107
108 @property
109 @memoized
110 def number_of_cpus(self):
111 num_cpus = 0
112 corere = re.compile(r'^\s*cpu\d+\s*$')
113 output = self.execute('ls /sys/devices/system/cpu')
114 for entry in output.split():
115 if corere.match(entry):
116 num_cpus += 1
117 return num_cpus
118
119 @property
120 @memoized
121 def config(self):
122 try:
123 return KernelConfig(self.execute('zcat /proc/config.gz'))
124 except TargetError:
125 for path in ['/boot/config', '/boot/config-$(uname -r)']:
126 try:
127 return KernelConfig(self.execute('cat {}'.format(path)))
128 except TargetError:
129 pass
130 return KernelConfig('')
131
132 @property
133 @memoized
134 def user(self):
135 return self.getenv('USER')
136
137 @property
138 def conn(self):
139 if self._connections:
140 tid = id(threading.current_thread())
141 if tid not in self._connections:
142 self._connections[tid] = self.get_connection()
143 return self._connections[tid]
144 else:
145 return None
146
147 def __init__(self,
148 connection_settings=None,
149 platform=None,
150 working_directory=None,
151 executables_directory=None,
152 connect=True,
153 modules=None,
154 load_default_modules=True,
155 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000156 conn_cls=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100157 ):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +0000158 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100159 self.connection_settings = connection_settings or {}
Anouk Van Laer21f40032017-01-31 13:11:03 +0000160 # Set self.platform: either it's given directly (by platform argument)
161 # or it's given in the connection_settings argument
162 # If neither, create default Platform()
163 if platform is None:
164 self.platform = self.connection_settings.get('platform', Platform())
165 else:
166 self.platform = platform
167 # Check if the user hasn't given two different platforms
168 if 'platform' in self.connection_settings:
169 if connection_settings['platform'] is not platform:
170 raise TargetError('Platform specified in connection_settings '
171 '({}) differs from that directly passed '
172 '({})!)'
173 .format(connection_settings['platform'],
174 self.platform))
175 self.connection_settings['platform'] = self.platform
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100176 self.working_directory = working_directory
177 self.executables_directory = executables_directory
178 self.modules = modules or []
179 self.load_default_modules = load_default_modules
180 self.shell_prompt = shell_prompt
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000181 self.conn_cls = conn_cls
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100182 self.logger = logging.getLogger(self.__class__.__name__)
183 self._installed_binaries = {}
184 self._installed_modules = {}
185 self._cache = {}
186 self._connections = {}
187 self.busybox = None
188
189 if load_default_modules:
190 module_lists = [self.default_modules]
191 else:
192 module_lists = []
193 module_lists += [self.modules, self.platform.modules]
194 self.modules = merge_lists(*module_lists, duplicates='first')
195 self._update_modules('early')
196 if connect:
197 self.connect()
198
199 # connection and initialization
200
201 def connect(self, timeout=None):
202 self.platform.init_target_connection(self)
203 tid = id(threading.current_thread())
204 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000205 self._resolve_paths()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100206 self.busybox = self.get_installed('busybox')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100207 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100208 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100209 if self.platform.big_core and self.load_default_modules:
210 self._install_module(get_module('bl'))
211
212 def disconnect(self):
213 for conn in self._connections.itervalues():
214 conn.close()
215 self._connections = {}
216
217 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000218 if self.conn_cls == None:
219 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100220 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
221
222 def setup(self, executables=None):
223 self.execute('mkdir -p {}'.format(self.working_directory))
224 self.execute('mkdir -p {}'.format(self.executables_directory))
225 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Patrick Bellasif2eac512015-11-27 16:35:57 +0000226
227 # Setup shutils script for the target
228 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
229 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
230 shell_path = '/bin/sh'
231 if self.os == 'android':
232 shell_path = '/system/bin/sh'
233 with open(shutils_ifile) as fh:
234 lines = fh.readlines()
235 with open(shutils_ofile, 'w') as ofile:
236 for line in lines:
237 line = line.replace("__DEVLIB_SHELL__", shell_path)
238 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
239 ofile.write(line)
240 self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
241
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100242 for host_exe in (executables or []): # pylint: disable=superfluous-parens
243 self.install(host_exe)
244
Anouk Van Laer21f40032017-01-31 13:11:03 +0000245 # Check for platform dependent setup procedures
246 self.platform.setup(self)
247
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100248 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
249 self._update_modules('setup')
250
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100251 def reboot(self, hard=False, connect=True, timeout=180):
252 if hard:
253 if not self.has('hard_reset'):
254 raise TargetError('Hard reset not supported for this target.')
255 self.hard_reset() # pylint: disable=no-member
256 else:
257 if not self.is_connected:
258 message = 'Cannot reboot target becuase it is disconnected. ' +\
259 'Either connect() first, or specify hard=True ' +\
260 '(in which case, a hard_reset module must be installed)'
261 raise TargetError(message)
262 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000263 # Wait a fixed delay before starting polling to give the target time to
264 # shut down, otherwise, might create the connection while it's still shutting
265 # down resulting in subsequenct connection failing.
266 self.logger.debug('Waiting for target to power down...')
267 reset_delay = 20
268 time.sleep(reset_delay)
269 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100270 if self.has('boot'):
271 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000272 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100273 if connect:
274 self.connect(timeout=timeout)
275
276 # file transfer
277
278 def push(self, source, dest, timeout=None):
279 return self.conn.push(source, dest, timeout=timeout)
280
281 def pull(self, source, dest, timeout=None):
282 return self.conn.pull(source, dest, timeout=timeout)
283
284 # execution
285
Patrick Bellasif2eac512015-11-27 16:35:57 +0000286 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
287 command = '{} {}'.format(self.shutils, command)
288 return self.conn.execute(command, timeout, check_exit_code, as_root)
289
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100290 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
291 return self.conn.execute(command, timeout, check_exit_code, as_root)
292
293 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
294 return self.conn.background(command, stdout, stderr, as_root)
295
296 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
297 as_root=False, timeout=30):
298 """
299 Executes the specified binary under the specified conditions.
300
301 :binary: binary to execute. Must be present and executable on the device.
302 :args: arguments to be passed to the binary. The can be either a list or
303 a string.
304 :in_directory: execute the binary in the specified directory. This must
305 be an absolute path.
306 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
307 case, it will be interpreted as the mask), a list of ``ints``, in which
308 case this will be interpreted as the list of cpus, or string, which
309 will be interpreted as a comma-separated list of cpu ranges, e.g.
310 ``"0,4-7"``.
311 :as_root: Specify whether the command should be run as root
312 :timeout: If the invocation does not terminate within this number of seconds,
313 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
314 invocation should not timeout.
315
Brendan Jackman27f545f2016-11-15 16:58:57 +0000316 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100317 """
318 command = binary
319 if args:
320 if isiterable(args):
321 args = ' '.join(args)
322 command = '{} {}'.format(command, args)
323 if on_cpus:
324 on_cpus = bitmask(on_cpus)
325 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
326 if in_directory:
327 command = 'cd {} && {}'.format(in_directory, command)
328 return self.execute(command, as_root=as_root, timeout=timeout)
329
330 def kick_off(self, command, as_root=False):
331 raise NotImplementedError()
332
333 # sysfs interaction
334
335 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100336 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100337 if kind:
338 return kind(output)
339 else:
340 return output
341
342 def read_int(self, path):
343 return self.read_value(path, kind=integer)
344
345 def read_bool(self, path):
346 return self.read_value(path, kind=boolean)
347
348 def write_value(self, path, value, verify=True):
349 value = str(value)
350 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
351 if verify:
352 output = self.read_value(path)
353 if not output == value:
354 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
355 raise TargetError(message)
356
357 def reset(self):
358 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100359 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100360 except (TargetError, TimeoutError, subprocess.CalledProcessError):
361 # on some targets "reboot" doesn't return gracefully
362 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000363 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100364
365 def check_responsive(self):
366 try:
367 self.conn.execute('ls /', timeout=5)
368 except (TimeoutError, subprocess.CalledProcessError):
369 raise TargetNotRespondingError(self.conn.name)
370
371 # process management
372
373 def kill(self, pid, signal=None, as_root=False):
374 signal_string = '-s {}'.format(signal) if signal else ''
375 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
376
377 def killall(self, process_name, signal=None, as_root=False):
378 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000379 try:
380 self.kill(pid, signal=signal, as_root=as_root)
381 except TargetError:
382 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100383
384 def get_pids_of(self, process_name):
385 raise NotImplementedError()
386
387 def ps(self, **kwargs):
388 raise NotImplementedError()
389
390 # files
391
392 def file_exists(self, filepath):
393 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000394 output = self.execute(command.format(filepath), as_root=self.is_rooted)
395 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100396
Sebastian Goscik33603c62016-02-15 15:07:19 +0000397 def directory_exists(self, filepath):
398 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
399 # output from ssh my contain part of the expression in the buffer,
400 # split out everything except the last word.
401 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
402
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100403 def list_file_systems(self):
404 output = self.execute('mount')
405 fstab = []
406 for line in output.split('\n'):
407 line = line.strip()
408 if not line:
409 continue
410 match = FSTAB_ENTRY_REGEX.search(line)
411 if match:
412 fstab.append(FstabEntry(match.group(1), match.group(2),
413 match.group(3), match.group(4),
414 None, None))
415 else: # assume pre-M Android
416 fstab.append(FstabEntry(*line.split()))
417 return fstab
418
419 def list_directory(self, path, as_root=False):
420 raise NotImplementedError()
421
422 def get_workpath(self, name):
423 return self.path.join(self.working_directory, name)
424
425 def tempfile(self, prefix='', suffix=''):
426 names = tempfile._get_candidate_names() # pylint: disable=W0212
427 for _ in xrange(tempfile.TMP_MAX):
428 name = names.next()
429 path = self.get_workpath(prefix + name + suffix)
430 if not self.file_exists(path):
431 return path
432 raise IOError('No usable temporary filename found')
433
434 def remove(self, path, as_root=False):
435 self.execute('rm -rf {}'.format(path), as_root=as_root)
436
437 # misc
438 def core_cpus(self, core):
439 return [i for i, c in enumerate(self.core_names) if c == core]
440
441 def list_online_cpus(self, core=None):
442 path = self.path.join('/sys/devices/system/cpu/online')
443 output = self.read_value(path)
444 all_online = ranges_to_list(output)
445 if core:
446 cpus = self.core_cpus(core)
447 if not cpus:
448 raise ValueError(core)
449 return [o for o in all_online if o in cpus]
450 else:
451 return all_online
452
453 def list_offline_cpus(self):
454 online = self.list_online_cpus()
455 return [c for c in xrange(self.number_of_cpus)
456 if c not in online]
457
458 def getenv(self, variable):
459 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
460
461 def capture_screen(self, filepath):
462 raise NotImplementedError()
463
464 def install(self, filepath, timeout=None, with_name=None):
465 raise NotImplementedError()
466
467 def uninstall(self, name):
468 raise NotImplementedError()
469
Sebastian Goscik84151f92016-02-15 15:09:27 +0000470 def get_installed(self, name, search_system_binaries=True):
471 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000472 if self.file_exists(self.executables_directory):
473 if name in self.list_directory(self.executables_directory):
474 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000475 # Fall back to binaries in PATH
476 if search_system_binaries:
477 for path in self.getenv('PATH').split(self.path.pathsep):
478 try:
479 if name in self.list_directory(path):
480 return self.path.join(path, name)
481 except TargetError:
482 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100483
484 which = get_installed
485
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000486 def install_if_needed(self, host_path, search_system_binaries=True):
487
488 binary_path = self.get_installed(os.path.split(host_path)[1],
489 search_system_binaries=search_system_binaries)
490 if not binary_path:
491 binary_path = self.install(host_path)
492 return binary_path
493
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100494 def is_installed(self, name):
495 return bool(self.get_installed(name))
496
497 def bin(self, name):
498 return self._installed_binaries.get(name, name)
499
500 def has(self, modname):
501 return hasattr(self, identifier(modname))
502
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000503 def lsmod(self):
504 lines = self.execute('lsmod').splitlines()
505 entries = []
506 for line in lines[1:]: # first line is the header
507 if not line.strip():
508 continue
509 parts = line.split()
510 name = parts[0]
511 size = int(parts[1])
512 use_count = int(parts[2])
513 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000514 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000515 else:
516 used_by = []
517 entries.append(LsmodEntry(name, size, use_count, used_by))
518 return entries
519
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000520 def insmod(self, path):
521 target_path = self.get_workpath(os.path.basename(path))
522 self.push(path, target_path)
523 self.execute('insmod {}'.format(target_path), as_root=True)
524
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100525
526 def extract(self, path, dest=None):
527 """
528 Extact the specified on-target file. The extraction method to be used
529 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
530 If ``dest`` is specified, it must be an existing directory on target;
531 the extracted contents will be placed there.
532
533 Note that, depending on the archive file format (and therfore the
534 extraction method used), the original archive file may or may not exist
535 after the extraction.
536
537 The return value is the path to the extracted contents. In case of
538 gunzip and bunzip2, this will be path to the extracted file; for tar
539 and uzip, this will be the directory with the extracted file(s)
540 (``dest`` if it was specified otherwise, the directory that cotained
541 the archive).
542
543 """
544 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
545 '.tgz', '.tbz', '.tbz2']:
546 if path.endswith(ending):
547 return self._extract_archive(path, 'tar xf {} -C {}', dest)
548
549 ext = self.path.splitext(path)[1]
550 if ext in ['.bz', '.bz2']:
551 return self._extract_file(path, 'bunzip2 -f {}', dest)
552 elif ext == '.gz':
553 return self._extract_file(path, 'gunzip -f {}', dest)
554 elif ext == '.zip':
555 return self._extract_archive(path, 'unzip {} -d {}', dest)
556 else:
557 raise ValueError('Unknown compression format: {}'.format(ext))
558
559 # internal methods
560
561 def _extract_archive(self, path, cmd, dest=None):
562 cmd = '{} ' + cmd # busybox
563 if dest:
564 extracted = dest
565 else:
566 extracted = self.path.dirname(path)
567 cmdtext = cmd.format(self.busybox, path, extracted)
568 self.execute(cmdtext)
569 return extracted
570
571 def _extract_file(self, path, cmd, dest=None):
572 cmd = '{} ' + cmd # busybox
573 cmdtext = cmd.format(self.busybox, path)
574 self.execute(cmdtext)
575 extracted = self.path.splitext(path)[0]
576 if dest:
577 self.execute('mv -f {} {}'.format(extracted, dest))
578 if dest.endswith('/'):
579 extracted = self.path.join(dest, self.path.basename(extracted))
580 else:
581 extracted = dest
582 return extracted
583
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100584 def _update_modules(self, stage):
585 for mod in self.modules:
586 if isinstance(mod, dict):
587 mod, params = mod.items()[0]
588 else:
589 params = {}
590 mod = get_module(mod)
591 if not mod.stage == stage:
592 continue
593 if mod.probe(self):
594 self._install_module(mod, **params)
595 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000596 msg = 'Module {} is not supported by the target'.format(mod.name)
597 if self.load_default_modules:
598 self.logger.debug(msg)
599 else:
600 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100601
602 def _install_module(self, mod, **params):
603 if mod.name not in self._installed_modules:
604 self.logger.debug('Installing module {}'.format(mod.name))
605 mod.install(self, **params)
606 self._installed_modules[mod.name] = mod
607 else:
608 self.logger.debug('Module {} is already installed.'.format(mod.name))
609
Sergei Trofimov961f9572015-11-18 17:32:26 +0000610 def _resolve_paths(self):
611 raise NotImplementedError()
612
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100613
614class LinuxTarget(Target):
615
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100616 path = posixpath
617 os = 'linux'
618
619 @property
620 @memoized
621 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100622 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100623 for abi, architectures in ABI_MAP.iteritems():
624 if value in architectures:
625 result = abi
626 break
627 else:
628 result = value
629 return result
630
631 @property
632 @memoized
633 def os_version(self):
634 os_version = {}
635 try:
636 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
637 version_files = self.execute(command, check_exit_code=False).strip().split()
638 for vf in version_files:
639 name = self.path.basename(vf)
640 output = self.read_value(vf)
641 os_version[name] = output.strip().replace('\n', ' ')
642 except TargetError:
643 raise
644 return os_version
645
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000646 @property
647 @memoized
648 # There is currently no better way to do this cross platform.
649 # ARM does not have dmidecode
650 def model(self):
651 if self.file_exists("/proc/device-tree/model"):
652 raw_model = self.execute("cat /proc/device-tree/model")
653 return '_'.join(raw_model.split()[:2])
654 return None
655
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000656 def __init__(self,
657 connection_settings=None,
658 platform=None,
659 working_directory=None,
660 executables_directory=None,
661 connect=True,
662 modules=None,
663 load_default_modules=True,
664 shell_prompt=DEFAULT_SHELL_PROMPT,
665 conn_cls=SshConnection,
666 ):
667 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
668 platform=platform,
669 working_directory=working_directory,
670 executables_directory=executables_directory,
671 connect=connect,
672 modules=modules,
673 load_default_modules=load_default_modules,
674 shell_prompt=shell_prompt,
675 conn_cls=conn_cls)
676
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100677 def connect(self, timeout=None):
678 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100679
680 def kick_off(self, command, as_root=False):
681 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
682 return self.conn.execute(command, as_root=as_root)
683
684 def get_pids_of(self, process_name):
685 """Returns a list of PIDs of all processes with the specified name."""
686 # result should be a column of PIDs with the first row as "PID" header
687 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
688 check_exit_code=False).strip().split()
689 if len(result) >= 2: # at least one row besides the header
690 return map(int, result[1:])
691 else:
692 return []
693
694 def ps(self, **kwargs):
695 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
696 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
697 lines.next() # header
698
699 result = []
700 for line in lines:
701 parts = re.split(r'\s+', line, maxsplit=8)
702 if parts and parts != ['']:
703 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
704
705 if not kwargs:
706 return result
707 else:
708 filtered_result = []
709 for entry in result:
710 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
711 filtered_result.append(entry)
712 return filtered_result
713
714 def list_directory(self, path, as_root=False):
715 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
716 return [x.strip() for x in contents.split('\n') if x.strip()]
717
718 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
719 destpath = self.path.join(self.executables_directory,
720 with_name and with_name or self.path.basename(filepath))
721 self.push(filepath, destpath)
722 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
723 self._installed_binaries[self.path.basename(destpath)] = destpath
724 return destpath
725
726 def uninstall(self, name):
727 path = self.path.join(self.executables_directory, name)
728 self.remove(path)
729
730 def capture_screen(self, filepath):
731 if not self.is_installed('scrot'):
732 self.logger.debug('Could not take screenshot as scrot is not installed.')
733 return
734 try:
735
736 tmpfile = self.tempfile()
737 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
738 self.pull(tmpfile, filepath)
739 self.remove(tmpfile)
740 except TargetError as e:
741 if "Can't open X dispay." not in e.message:
742 raise e
743 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
744 self.logger.debug('Could not take screenshot: {}'.format(message))
745
Sergei Trofimov961f9572015-11-18 17:32:26 +0000746 def _resolve_paths(self):
747 if self.working_directory is None:
748 if self.connected_as_root:
749 self.working_directory = '/root/devlib-target'
750 else:
751 self.working_directory = '/home/{}/devlib-target'.format(self.user)
752 if self.executables_directory is None:
753 self.executables_directory = self.path.join(self.working_directory, 'bin')
754
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100755
756class AndroidTarget(Target):
757
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100758 path = posixpath
759 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100760 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100761
762 @property
763 @memoized
764 def abi(self):
765 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
766
767 @property
768 @memoized
769 def os_version(self):
770 os_version = {}
771 for k, v in self.getprop().iteritems():
772 if k.startswith('ro.build.version'):
773 part = k.split('.')[-1]
774 os_version[part] = v
775 return os_version
776
777 @property
778 def adb_name(self):
779 return self.conn.device
780
781 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000782 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000783 def android_id(self):
784 """
785 Get the device's ANDROID_ID. Which is
786
787 "A 64-bit number (as a hex string) that is randomly generated when the user
788 first sets up the device and should remain constant for the lifetime of the
789 user's device."
790
791 .. note:: This will get reset on userdata erasure.
792
793 """
794 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
795 return output.split('value=')[-1]
796
797 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100798 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000799 def model(self):
800 try:
801 return self.getprop(prop='ro.product.device')
802 except KeyError:
803 return None
804
805 @property
806 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100807 def screen_resolution(self):
808 output = self.execute('dumpsys window')
809 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
810 if match:
811 return (int(match.group('width')),
812 int(match.group('height')))
813 else:
814 return (0, 0)
815
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000816 def __init__(self,
817 connection_settings=None,
818 platform=None,
819 working_directory=None,
820 executables_directory=None,
821 connect=True,
822 modules=None,
823 load_default_modules=True,
824 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000825 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000826 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000827 ):
828 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
829 platform=platform,
830 working_directory=working_directory,
831 executables_directory=executables_directory,
832 connect=connect,
833 modules=modules,
834 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000835 shell_prompt=shell_prompt,
836 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000837 self.package_data_directory = package_data_directory
838
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100839 def reset(self, fastboot=False): # pylint: disable=arguments-differ
840 try:
841 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100842 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100843 except (TargetError, TimeoutError, subprocess.CalledProcessError):
844 # on some targets "reboot" doesn't return gracefully
845 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000846 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100847
848 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
849 start = time.time()
850 device = self.connection_settings.get('device')
851 if device and ':' in device:
852 # ADB does not automatically remove a network device from it's
853 # devices list when the connection is broken by the remote, so the
854 # adb connection may have gone "stale", resulting in adb blocking
855 # indefinitely when making calls to the device. To avoid this,
856 # always disconnect first.
857 adb_disconnect(device)
858 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100859
860 if check_boot_completed:
861 boot_completed = boolean(self.getprop('sys.boot_completed'))
862 while not boot_completed and timeout >= time.time() - start:
863 time.sleep(5)
864 boot_completed = boolean(self.getprop('sys.boot_completed'))
865 if not boot_completed:
866 raise TargetError('Connected but Android did not fully boot.')
867
868 def setup(self, executables=None):
869 super(AndroidTarget, self).setup(executables)
870 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
871
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100872 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100873 """
874 Like execute but closes adb session and returns immediately, leaving the command running on the
875 device (this is different from execute(background=True) which keeps adb connection open and returns
876 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100877 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +0100878 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +0100879 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100880 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +0100881 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100882 output = self.execute(command, timeout=1, as_root=as_root)
883 except TimeoutError:
884 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100885
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100886 def __setup_list_directory(self):
887 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
888 # AOSP 7.0 as well, the ls command was changed.
889 # Previous versions default to a single column listing, which is nice and easy to parse.
890 # Newer versions default to a multi-column listing, which is not, but it does support
891 # a '-1' option to get into single column mode. Older versions do not support this option
892 # so we try the new version, and if it fails we use the old version.
893 self.ls_command = 'ls -1'
894 try:
Marc Bonnici06552372017-03-29 16:43:22 +0100895 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100896 except TargetError:
897 self.ls_command = 'ls'
898
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100899 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100900 if self.ls_command == '':
901 self.__setup_list_directory()
902 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100903 return [x.strip() for x in contents.split('\n') if x.strip()]
904
905 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
906 ext = os.path.splitext(filepath)[1].lower()
907 if ext == '.apk':
908 return self.install_apk(filepath, timeout)
909 else:
910 return self.install_executable(filepath, with_name)
911
912 def uninstall(self, name):
913 if self.package_is_installed(name):
914 self.uninstall_package(name)
915 else:
916 self.uninstall_executable(name)
917
918 def get_pids_of(self, process_name):
919 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
920 if result and 'not found' not in result:
921 return [int(x.split()[1]) for x in result.split('\n')[1:]]
922 else:
923 return []
924
925 def ps(self, **kwargs):
926 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
927 lines.next() # header
928 result = []
929 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +0100930 parts = line.split(None, 8)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100931 if parts:
932 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
933 if not kwargs:
934 return result
935 else:
936 filtered_result = []
937 for entry in result:
938 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
939 filtered_result.append(entry)
940 return filtered_result
941
942 def capture_screen(self, filepath):
943 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
944 self.execute('screencap -p {}'.format(on_device_file))
945 self.pull(on_device_file, filepath)
946 self.remove(on_device_file)
947
948 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
949 if not as_root:
950 self.conn.push(source, dest, timeout=timeout)
951 else:
952 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000953 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100954 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000955 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100956
957 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
958 if not as_root:
959 self.conn.pull(source, dest, timeout=timeout)
960 else:
961 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000962 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
963 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100964 self.conn.pull(device_tempfile, dest, timeout=timeout)
965
966 # Android-specific
967
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000968 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100969 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100970 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000971 if direction == "horizontal":
972 swipe_heigh = height * 2 // 3
973 start = 100
974 stop = width - start
975 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
976 if direction == "vertical":
977 swipe_middle = height / 2
978 swipe_heigh = height * 2 // 3
979 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
980 else:
981 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100982
983 def getprop(self, prop=None):
984 props = AndroidProperties(self.execute('getprop'))
985 if prop:
986 return props[prop]
987 return props
988
989 def is_installed(self, name):
990 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
991
992 def package_is_installed(self, package_name):
993 return package_name in self.list_packages()
994
995 def list_packages(self):
996 output = self.execute('pm list packages')
997 output = output.replace('package:', '')
998 return output.split()
999
1000 def get_package_version(self, package):
1001 output = self.execute('dumpsys package {}'.format(package))
1002 for line in convert_new_lines(output).split('\n'):
1003 if 'versionName' in line:
1004 return line.split('=', 1)[1]
1005 return None
1006
1007 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
1008 ext = os.path.splitext(filepath)[1].lower()
1009 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001010 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001011 else:
1012 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1013
1014 def install_executable(self, filepath, with_name=None):
1015 self._ensure_executables_directory_is_writable()
1016 executable_name = with_name or os.path.basename(filepath)
1017 on_device_file = self.path.join(self.working_directory, executable_name)
1018 on_device_executable = self.path.join(self.executables_directory, executable_name)
1019 self.push(filepath, on_device_file)
1020 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001021 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1022 self.remove(on_device_file, as_root=self.needs_su)
1023 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001024 self._installed_binaries[executable_name] = on_device_executable
1025 return on_device_executable
1026
1027 def uninstall_package(self, package):
1028 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1029
1030 def uninstall_executable(self, executable_name):
1031 on_device_executable = self.path.join(self.executables_directory, executable_name)
1032 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001033 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001034
1035 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001036 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001037 filtstr = ' -s {}'.format(filter) if filter else ''
1038 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1039 adb_command(self.adb_name, command, timeout=timeout)
1040
1041 def clear_logcat(self):
1042 adb_command(self.adb_name, 'logcat -c', timeout=30)
1043
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001044 def adb_reboot_bootloader(self, timeout=30):
1045 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1046
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001047 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001048 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001049 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001050 return
1051 adb_command(self.adb_name, 'root', timeout=30)
1052 self._connected_as_root = True
1053 return
1054 adb_command(self.adb_name, 'unroot', timeout=30)
1055 self._connected_as_root = False
1056
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001057 def is_screen_on(self):
1058 output = self.execute('dumpsys power')
1059 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1060 if match:
1061 return boolean(match.group(1))
1062 else:
1063 raise TargetError('Could not establish screen state.')
1064
1065 def ensure_screen_is_on(self):
1066 if not self.is_screen_on():
1067 self.execute('input keyevent 26')
1068
Sergei Trofimov961f9572015-11-18 17:32:26 +00001069 def _resolve_paths(self):
1070 if self.working_directory is None:
1071 self.working_directory = '/data/local/tmp/devlib-target'
1072 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1073 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001074 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001075
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001076 def _ensure_executables_directory_is_writable(self):
1077 matched = []
1078 for entry in self.list_file_systems():
1079 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1080 matched.append(entry)
1081 if matched:
1082 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1083 if 'rw' not in entry.options:
1084 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1085 entry.mount_point),
1086 as_root=True)
1087 else:
1088 message = 'Could not find mount point for executables directory {}'
1089 raise TargetError(message.format(self.executables_directory))
1090
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001091 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1092
1093 @property
1094 def charging_enabled(self):
1095 """
1096 Whether drawing power to charge the battery is enabled
1097
1098 Not all devices have the ability to enable/disable battery charging
1099 (e.g. because they don't have a battery). In that case,
1100 ``charging_enabled`` is None.
1101 """
1102 if not self.file_exists(self._charging_enabled_path):
1103 return None
1104 return self.read_bool(self._charging_enabled_path)
1105
1106 @charging_enabled.setter
1107 def charging_enabled(self, enabled):
1108 """
1109 Enable/disable drawing power to charge the battery
1110
1111 Not all devices have this facility. In that case, do nothing.
1112 """
1113 if not self.file_exists(self._charging_enabled_path):
1114 return
1115 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001116
1117FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1118PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001119LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001120
1121
1122class Cpuinfo(object):
1123
1124 @property
1125 @memoized
1126 def architecture(self):
1127 for section in self.sections:
1128 if 'CPU architecture' in section:
1129 return section['CPU architecture']
1130 if 'architecture' in section:
1131 return section['architecture']
1132
1133 @property
1134 @memoized
1135 def cpu_names(self):
1136 cpu_names = []
1137 global_name = None
1138 for section in self.sections:
1139 if 'processor' in section:
1140 if 'CPU part' in section:
1141 cpu_names.append(_get_part_name(section))
1142 elif 'model name' in section:
1143 cpu_names.append(_get_model_name(section))
1144 else:
1145 cpu_names.append(None)
1146 elif 'CPU part' in section:
1147 global_name = _get_part_name(section)
1148 return [caseless_string(c or global_name) for c in cpu_names]
1149
1150 def __init__(self, text):
1151 self.sections = None
1152 self.text = None
1153 self.parse(text)
1154
1155 @memoized
1156 def get_cpu_features(self, cpuid=0):
1157 global_features = []
1158 for section in self.sections:
1159 if 'processor' in section:
1160 if int(section.get('processor')) != cpuid:
1161 continue
1162 if 'Features' in section:
1163 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001164 elif 'flags' in section:
1165 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001166 elif 'Features' in section:
1167 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001168 elif 'flags' in section:
1169 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001170 return global_features
1171
1172 def parse(self, text):
1173 self.sections = []
1174 current_section = {}
1175 self.text = text.strip()
1176 for line in self.text.split('\n'):
1177 line = line.strip()
1178 if line:
1179 key, value = line.split(':', 1)
1180 current_section[key.strip()] = value.strip()
1181 else: # not line
1182 self.sections.append(current_section)
1183 current_section = {}
1184 self.sections.append(current_section)
1185
1186 def __str__(self):
1187 return 'CpuInfo({})'.format(self.cpu_names)
1188
1189 __repr__ = __str__
1190
1191
1192class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001193 """
1194 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001195
Brendan Jackman54adf802017-02-20 17:57:51 +00001196 Not expected to work for very old (pre-3.0) kernel version numbers.
1197
1198 :ivar release: Version number/revision string. Typical output of
1199 ``uname -r``
1200 :type release: str
1201 :ivar version: Extra version info (aside from ``release``) reported by
1202 ``uname``
1203 :type version: str
1204 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1205 :type version_number: int
1206 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1207 :type major: int
1208 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1209 be None
1210 :type minor: int
1211 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1212 :type rc: int
1213 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1214 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001215
1216 :ivar parts: Tuple of version number components. Can be used for
1217 lexicographically comparing kernel versions.
1218 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001219 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001220 def __init__(self, version_string):
1221 if ' #' in version_string:
1222 release, version = version_string.split(' #')
1223 self.release = release
1224 self.version = version
1225 elif version_string.startswith('#'):
1226 self.release = ''
1227 self.version = version_string
1228 else:
1229 self.release = version_string
1230 self.version = ''
1231
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001232 self.version_number = None
1233 self.major = None
1234 self.minor = None
1235 self.sha1 = None
1236 self.rc = None
1237 match = KVERSION_REGEX.match(version_string)
1238 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001239 groups = match.groupdict()
1240 self.version_number = int(groups['version'])
1241 self.major = int(groups['major'])
1242 if groups['minor'] is not None:
1243 self.minor = int(groups['minor'])
1244 if groups['rc'] is not None:
1245 self.rc = int(groups['rc'])
1246 if groups['sha1'] is not None:
1247 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001248
Brendan Jackman18b77b82017-02-20 17:52:56 +00001249 self.parts = (self.version_number, self.major, self.minor)
1250
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001251 def __str__(self):
1252 return '{} {}'.format(self.release, self.version)
1253
1254 __repr__ = __str__
1255
1256
1257class KernelConfig(object):
1258
1259 not_set_regex = re.compile(r'# (\S+) is not set')
1260
1261 @staticmethod
1262 def get_config_name(name):
1263 name = name.upper()
1264 if not name.startswith('CONFIG_'):
1265 name = 'CONFIG_' + name
1266 return name
1267
1268 def iteritems(self):
1269 return self._config.iteritems()
1270
1271 def __init__(self, text):
1272 self.text = text
1273 self._config = {}
1274 for line in text.split('\n'):
1275 line = line.strip()
1276 if line.startswith('#'):
1277 match = self.not_set_regex.search(line)
1278 if match:
1279 self._config[match.group(1)] = 'n'
1280 elif '=' in line:
1281 name, value = line.split('=', 1)
1282 self._config[name.strip()] = value.strip()
1283
1284 def get(self, name):
1285 return self._config.get(self.get_config_name(name))
1286
1287 def like(self, name):
1288 regex = re.compile(name, re.I)
1289 result = {}
1290 for k, v in self._config.iteritems():
1291 if regex.search(k):
1292 result[k] = v
1293 return result
1294
1295 def is_enabled(self, name):
1296 return self.get(name) == 'y'
1297
1298 def is_module(self, name):
1299 return self.get(name) == 'm'
1300
1301 def is_not_set(self, name):
1302 return self.get(name) == 'n'
1303
1304 def has(self, name):
1305 return self.get(name) in ['m', 'y']
1306
1307
1308class LocalLinuxTarget(LinuxTarget):
1309
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001310 def __init__(self,
1311 connection_settings=None,
1312 platform=None,
1313 working_directory=None,
1314 executables_directory=None,
1315 connect=True,
1316 modules=None,
1317 load_default_modules=True,
1318 shell_prompt=DEFAULT_SHELL_PROMPT,
1319 conn_cls=LocalConnection,
1320 ):
1321 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1322 platform=platform,
1323 working_directory=working_directory,
1324 executables_directory=executables_directory,
1325 connect=connect,
1326 modules=modules,
1327 load_default_modules=load_default_modules,
1328 shell_prompt=shell_prompt,
1329 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001330
Sergei Trofimov961f9572015-11-18 17:32:26 +00001331 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001332 if self.working_directory is None:
1333 self.working_directory = '/tmp'
1334 if self.executables_directory is None:
1335 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001336
1337
1338def _get_model_name(section):
1339 name_string = section['model name']
1340 parts = name_string.split('@')[0].strip().split()
1341 return ' '.join([p for p in parts
1342 if '(' not in p and p != 'CPU'])
1343
1344
1345def _get_part_name(section):
1346 implementer = section.get('CPU implementer', '0x0')
1347 part = section['CPU part']
1348 variant = section.get('CPU variant', '0x0')
1349 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1350 if name is None:
1351 name = '{}/{}/{}'.format(implementer, part, variant)
1352 return name