blob: 9d7f28207bec8c33c67772eac0f277738d41d4e1 [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)
Marc Bonnicif6d02c62017-04-21 15:21:40 +0100964 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100965 self.conn.pull(device_tempfile, dest, timeout=timeout)
966
967 # Android-specific
968
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000969 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100970 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100971 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000972 if direction == "horizontal":
973 swipe_heigh = height * 2 // 3
974 start = 100
975 stop = width - start
976 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
977 if direction == "vertical":
978 swipe_middle = height / 2
979 swipe_heigh = height * 2 // 3
980 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
981 else:
982 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100983
984 def getprop(self, prop=None):
985 props = AndroidProperties(self.execute('getprop'))
986 if prop:
987 return props[prop]
988 return props
989
990 def is_installed(self, name):
991 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
992
993 def package_is_installed(self, package_name):
994 return package_name in self.list_packages()
995
996 def list_packages(self):
997 output = self.execute('pm list packages')
998 output = output.replace('package:', '')
999 return output.split()
1000
1001 def get_package_version(self, package):
1002 output = self.execute('dumpsys package {}'.format(package))
1003 for line in convert_new_lines(output).split('\n'):
1004 if 'versionName' in line:
1005 return line.split('=', 1)[1]
1006 return None
1007
1008 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
1009 ext = os.path.splitext(filepath)[1].lower()
1010 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001011 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001012 else:
1013 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1014
1015 def install_executable(self, filepath, with_name=None):
1016 self._ensure_executables_directory_is_writable()
1017 executable_name = with_name or os.path.basename(filepath)
1018 on_device_file = self.path.join(self.working_directory, executable_name)
1019 on_device_executable = self.path.join(self.executables_directory, executable_name)
1020 self.push(filepath, on_device_file)
1021 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001022 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1023 self.remove(on_device_file, as_root=self.needs_su)
1024 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001025 self._installed_binaries[executable_name] = on_device_executable
1026 return on_device_executable
1027
1028 def uninstall_package(self, package):
1029 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1030
1031 def uninstall_executable(self, executable_name):
1032 on_device_executable = self.path.join(self.executables_directory, executable_name)
1033 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001034 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001035
1036 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001037 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001038 filtstr = ' -s {}'.format(filter) if filter else ''
1039 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1040 adb_command(self.adb_name, command, timeout=timeout)
1041
1042 def clear_logcat(self):
1043 adb_command(self.adb_name, 'logcat -c', timeout=30)
1044
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001045 def adb_reboot_bootloader(self, timeout=30):
1046 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1047
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001048 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001049 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001050 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001051 return
1052 adb_command(self.adb_name, 'root', timeout=30)
1053 self._connected_as_root = True
1054 return
1055 adb_command(self.adb_name, 'unroot', timeout=30)
1056 self._connected_as_root = False
1057
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001058 def is_screen_on(self):
1059 output = self.execute('dumpsys power')
1060 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1061 if match:
1062 return boolean(match.group(1))
1063 else:
1064 raise TargetError('Could not establish screen state.')
1065
1066 def ensure_screen_is_on(self):
1067 if not self.is_screen_on():
1068 self.execute('input keyevent 26')
1069
Sergei Trofimov961f9572015-11-18 17:32:26 +00001070 def _resolve_paths(self):
1071 if self.working_directory is None:
1072 self.working_directory = '/data/local/tmp/devlib-target'
1073 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1074 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001075 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001076
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001077 def _ensure_executables_directory_is_writable(self):
1078 matched = []
1079 for entry in self.list_file_systems():
1080 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1081 matched.append(entry)
1082 if matched:
1083 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1084 if 'rw' not in entry.options:
1085 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1086 entry.mount_point),
1087 as_root=True)
1088 else:
1089 message = 'Could not find mount point for executables directory {}'
1090 raise TargetError(message.format(self.executables_directory))
1091
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001092 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1093
1094 @property
1095 def charging_enabled(self):
1096 """
1097 Whether drawing power to charge the battery is enabled
1098
1099 Not all devices have the ability to enable/disable battery charging
1100 (e.g. because they don't have a battery). In that case,
1101 ``charging_enabled`` is None.
1102 """
1103 if not self.file_exists(self._charging_enabled_path):
1104 return None
1105 return self.read_bool(self._charging_enabled_path)
1106
1107 @charging_enabled.setter
1108 def charging_enabled(self, enabled):
1109 """
1110 Enable/disable drawing power to charge the battery
1111
1112 Not all devices have this facility. In that case, do nothing.
1113 """
1114 if not self.file_exists(self._charging_enabled_path):
1115 return
1116 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001117
1118FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1119PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001120LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001121
1122
1123class Cpuinfo(object):
1124
1125 @property
1126 @memoized
1127 def architecture(self):
1128 for section in self.sections:
1129 if 'CPU architecture' in section:
1130 return section['CPU architecture']
1131 if 'architecture' in section:
1132 return section['architecture']
1133
1134 @property
1135 @memoized
1136 def cpu_names(self):
1137 cpu_names = []
1138 global_name = None
1139 for section in self.sections:
1140 if 'processor' in section:
1141 if 'CPU part' in section:
1142 cpu_names.append(_get_part_name(section))
1143 elif 'model name' in section:
1144 cpu_names.append(_get_model_name(section))
1145 else:
1146 cpu_names.append(None)
1147 elif 'CPU part' in section:
1148 global_name = _get_part_name(section)
1149 return [caseless_string(c or global_name) for c in cpu_names]
1150
1151 def __init__(self, text):
1152 self.sections = None
1153 self.text = None
1154 self.parse(text)
1155
1156 @memoized
1157 def get_cpu_features(self, cpuid=0):
1158 global_features = []
1159 for section in self.sections:
1160 if 'processor' in section:
1161 if int(section.get('processor')) != cpuid:
1162 continue
1163 if 'Features' in section:
1164 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001165 elif 'flags' in section:
1166 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001167 elif 'Features' in section:
1168 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001169 elif 'flags' in section:
1170 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001171 return global_features
1172
1173 def parse(self, text):
1174 self.sections = []
1175 current_section = {}
1176 self.text = text.strip()
1177 for line in self.text.split('\n'):
1178 line = line.strip()
1179 if line:
1180 key, value = line.split(':', 1)
1181 current_section[key.strip()] = value.strip()
1182 else: # not line
1183 self.sections.append(current_section)
1184 current_section = {}
1185 self.sections.append(current_section)
1186
1187 def __str__(self):
1188 return 'CpuInfo({})'.format(self.cpu_names)
1189
1190 __repr__ = __str__
1191
1192
1193class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001194 """
1195 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001196
Brendan Jackman54adf802017-02-20 17:57:51 +00001197 Not expected to work for very old (pre-3.0) kernel version numbers.
1198
1199 :ivar release: Version number/revision string. Typical output of
1200 ``uname -r``
1201 :type release: str
1202 :ivar version: Extra version info (aside from ``release``) reported by
1203 ``uname``
1204 :type version: str
1205 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1206 :type version_number: int
1207 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1208 :type major: int
1209 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1210 be None
1211 :type minor: int
1212 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1213 :type rc: int
1214 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1215 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001216
1217 :ivar parts: Tuple of version number components. Can be used for
1218 lexicographically comparing kernel versions.
1219 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001220 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001221 def __init__(self, version_string):
1222 if ' #' in version_string:
1223 release, version = version_string.split(' #')
1224 self.release = release
1225 self.version = version
1226 elif version_string.startswith('#'):
1227 self.release = ''
1228 self.version = version_string
1229 else:
1230 self.release = version_string
1231 self.version = ''
1232
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001233 self.version_number = None
1234 self.major = None
1235 self.minor = None
1236 self.sha1 = None
1237 self.rc = None
1238 match = KVERSION_REGEX.match(version_string)
1239 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001240 groups = match.groupdict()
1241 self.version_number = int(groups['version'])
1242 self.major = int(groups['major'])
1243 if groups['minor'] is not None:
1244 self.minor = int(groups['minor'])
1245 if groups['rc'] is not None:
1246 self.rc = int(groups['rc'])
1247 if groups['sha1'] is not None:
1248 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001249
Brendan Jackman18b77b82017-02-20 17:52:56 +00001250 self.parts = (self.version_number, self.major, self.minor)
1251
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001252 def __str__(self):
1253 return '{} {}'.format(self.release, self.version)
1254
1255 __repr__ = __str__
1256
1257
1258class KernelConfig(object):
1259
1260 not_set_regex = re.compile(r'# (\S+) is not set')
1261
1262 @staticmethod
1263 def get_config_name(name):
1264 name = name.upper()
1265 if not name.startswith('CONFIG_'):
1266 name = 'CONFIG_' + name
1267 return name
1268
1269 def iteritems(self):
1270 return self._config.iteritems()
1271
1272 def __init__(self, text):
1273 self.text = text
1274 self._config = {}
1275 for line in text.split('\n'):
1276 line = line.strip()
1277 if line.startswith('#'):
1278 match = self.not_set_regex.search(line)
1279 if match:
1280 self._config[match.group(1)] = 'n'
1281 elif '=' in line:
1282 name, value = line.split('=', 1)
1283 self._config[name.strip()] = value.strip()
1284
1285 def get(self, name):
1286 return self._config.get(self.get_config_name(name))
1287
1288 def like(self, name):
1289 regex = re.compile(name, re.I)
1290 result = {}
1291 for k, v in self._config.iteritems():
1292 if regex.search(k):
1293 result[k] = v
1294 return result
1295
1296 def is_enabled(self, name):
1297 return self.get(name) == 'y'
1298
1299 def is_module(self, name):
1300 return self.get(name) == 'm'
1301
1302 def is_not_set(self, name):
1303 return self.get(name) == 'n'
1304
1305 def has(self, name):
1306 return self.get(name) in ['m', 'y']
1307
1308
1309class LocalLinuxTarget(LinuxTarget):
1310
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001311 def __init__(self,
1312 connection_settings=None,
1313 platform=None,
1314 working_directory=None,
1315 executables_directory=None,
1316 connect=True,
1317 modules=None,
1318 load_default_modules=True,
1319 shell_prompt=DEFAULT_SHELL_PROMPT,
1320 conn_cls=LocalConnection,
1321 ):
1322 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1323 platform=platform,
1324 working_directory=working_directory,
1325 executables_directory=executables_directory,
1326 connect=connect,
1327 modules=modules,
1328 load_default_modules=load_default_modules,
1329 shell_prompt=shell_prompt,
1330 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001331
Sergei Trofimov961f9572015-11-18 17:32:26 +00001332 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001333 if self.working_directory is None:
1334 self.working_directory = '/tmp'
1335 if self.executables_directory is None:
1336 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001337
1338
1339def _get_model_name(section):
1340 name_string = section['model name']
1341 parts = name_string.split('@')[0].strip().split()
1342 return ' '.join([p for p in parts
1343 if '(' not in p and p != 'CPU'])
1344
1345
1346def _get_part_name(section):
1347 implementer = section.get('CPU implementer', '0x0')
1348 part = section['CPU part']
1349 variant = section.get('CPU variant', '0x0')
1350 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1351 if name is None:
1352 name = '{}/{}/{}'.format(implementer, part, variant)
1353 return name