blob: c9b0121d9cd235b48ccfe0a38cc96d78b20771b7 [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
885 else:
886 raise ValueError('Background command exited before timeout; got "{}"'.format(output))
887
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100888 def __setup_list_directory(self):
889 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
890 # AOSP 7.0 as well, the ls command was changed.
891 # Previous versions default to a single column listing, which is nice and easy to parse.
892 # Newer versions default to a multi-column listing, which is not, but it does support
893 # a '-1' option to get into single column mode. Older versions do not support this option
894 # so we try the new version, and if it fails we use the old version.
895 self.ls_command = 'ls -1'
896 try:
897 self.execute('ls -1 /', as_root=False)
898 except TargetError:
899 self.ls_command = 'ls'
900
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100901 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100902 if self.ls_command == '':
903 self.__setup_list_directory()
904 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100905 return [x.strip() for x in contents.split('\n') if x.strip()]
906
907 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
908 ext = os.path.splitext(filepath)[1].lower()
909 if ext == '.apk':
910 return self.install_apk(filepath, timeout)
911 else:
912 return self.install_executable(filepath, with_name)
913
914 def uninstall(self, name):
915 if self.package_is_installed(name):
916 self.uninstall_package(name)
917 else:
918 self.uninstall_executable(name)
919
920 def get_pids_of(self, process_name):
921 result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
922 if result and 'not found' not in result:
923 return [int(x.split()[1]) for x in result.split('\n')[1:]]
924 else:
925 return []
926
927 def ps(self, **kwargs):
928 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
929 lines.next() # header
930 result = []
931 for line in lines:
932 parts = line.split()
933 if parts:
934 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
935 if not kwargs:
936 return result
937 else:
938 filtered_result = []
939 for entry in result:
940 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
941 filtered_result.append(entry)
942 return filtered_result
943
944 def capture_screen(self, filepath):
945 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
946 self.execute('screencap -p {}'.format(on_device_file))
947 self.pull(on_device_file, filepath)
948 self.remove(on_device_file)
949
950 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
951 if not as_root:
952 self.conn.push(source, dest, timeout=timeout)
953 else:
954 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000955 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100956 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000957 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100958
959 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
960 if not as_root:
961 self.conn.pull(source, dest, timeout=timeout)
962 else:
963 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +0000964 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
965 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100966 self.conn.pull(device_tempfile, dest, timeout=timeout)
967
968 # Android-specific
969
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000970 def swipe_to_unlock(self, direction="horizontal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100971 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100972 command = 'input swipe {} {} {} {}'
Sebastian Goscik880a0bc2016-02-15 15:19:47 +0000973 if direction == "horizontal":
974 swipe_heigh = height * 2 // 3
975 start = 100
976 stop = width - start
977 self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
978 if direction == "vertical":
979 swipe_middle = height / 2
980 swipe_heigh = height * 2 // 3
981 self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
982 else:
983 raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100984
985 def getprop(self, prop=None):
986 props = AndroidProperties(self.execute('getprop'))
987 if prop:
988 return props[prop]
989 return props
990
991 def is_installed(self, name):
992 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
993
994 def package_is_installed(self, package_name):
995 return package_name in self.list_packages()
996
997 def list_packages(self):
998 output = self.execute('pm list packages')
999 output = output.replace('package:', '')
1000 return output.split()
1001
1002 def get_package_version(self, package):
1003 output = self.execute('dumpsys package {}'.format(package))
1004 for line in convert_new_lines(output).split('\n'):
1005 if 'versionName' in line:
1006 return line.split('=', 1)[1]
1007 return None
1008
1009 def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
1010 ext = os.path.splitext(filepath)[1].lower()
1011 if ext == '.apk':
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001012 return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001013 else:
1014 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1015
1016 def install_executable(self, filepath, with_name=None):
1017 self._ensure_executables_directory_is_writable()
1018 executable_name = with_name or os.path.basename(filepath)
1019 on_device_file = self.path.join(self.working_directory, executable_name)
1020 on_device_executable = self.path.join(self.executables_directory, executable_name)
1021 self.push(filepath, on_device_file)
1022 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001023 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1024 self.remove(on_device_file, as_root=self.needs_su)
1025 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001026 self._installed_binaries[executable_name] = on_device_executable
1027 return on_device_executable
1028
1029 def uninstall_package(self, package):
1030 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1031
1032 def uninstall_executable(self, executable_name):
1033 on_device_executable = self.path.join(self.executables_directory, executable_name)
1034 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001035 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001036
1037 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001038 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001039 filtstr = ' -s {}'.format(filter) if filter else ''
1040 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1041 adb_command(self.adb_name, command, timeout=timeout)
1042
1043 def clear_logcat(self):
1044 adb_command(self.adb_name, 'logcat -c', timeout=30)
1045
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001046 def adb_reboot_bootloader(self, timeout=30):
1047 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1048
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001049 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001050 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001051 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001052 return
1053 adb_command(self.adb_name, 'root', timeout=30)
1054 self._connected_as_root = True
1055 return
1056 adb_command(self.adb_name, 'unroot', timeout=30)
1057 self._connected_as_root = False
1058
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001059 def is_screen_on(self):
1060 output = self.execute('dumpsys power')
1061 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1062 if match:
1063 return boolean(match.group(1))
1064 else:
1065 raise TargetError('Could not establish screen state.')
1066
1067 def ensure_screen_is_on(self):
1068 if not self.is_screen_on():
1069 self.execute('input keyevent 26')
1070
Sergei Trofimov961f9572015-11-18 17:32:26 +00001071 def _resolve_paths(self):
1072 if self.working_directory is None:
1073 self.working_directory = '/data/local/tmp/devlib-target'
1074 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1075 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001076 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001077
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001078 def _ensure_executables_directory_is_writable(self):
1079 matched = []
1080 for entry in self.list_file_systems():
1081 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1082 matched.append(entry)
1083 if matched:
1084 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1085 if 'rw' not in entry.options:
1086 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1087 entry.mount_point),
1088 as_root=True)
1089 else:
1090 message = 'Could not find mount point for executables directory {}'
1091 raise TargetError(message.format(self.executables_directory))
1092
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001093 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1094
1095 @property
1096 def charging_enabled(self):
1097 """
1098 Whether drawing power to charge the battery is enabled
1099
1100 Not all devices have the ability to enable/disable battery charging
1101 (e.g. because they don't have a battery). In that case,
1102 ``charging_enabled`` is None.
1103 """
1104 if not self.file_exists(self._charging_enabled_path):
1105 return None
1106 return self.read_bool(self._charging_enabled_path)
1107
1108 @charging_enabled.setter
1109 def charging_enabled(self, enabled):
1110 """
1111 Enable/disable drawing power to charge the battery
1112
1113 Not all devices have this facility. In that case, do nothing.
1114 """
1115 if not self.file_exists(self._charging_enabled_path):
1116 return
1117 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001118
1119FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1120PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001121LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001122
1123
1124class Cpuinfo(object):
1125
1126 @property
1127 @memoized
1128 def architecture(self):
1129 for section in self.sections:
1130 if 'CPU architecture' in section:
1131 return section['CPU architecture']
1132 if 'architecture' in section:
1133 return section['architecture']
1134
1135 @property
1136 @memoized
1137 def cpu_names(self):
1138 cpu_names = []
1139 global_name = None
1140 for section in self.sections:
1141 if 'processor' in section:
1142 if 'CPU part' in section:
1143 cpu_names.append(_get_part_name(section))
1144 elif 'model name' in section:
1145 cpu_names.append(_get_model_name(section))
1146 else:
1147 cpu_names.append(None)
1148 elif 'CPU part' in section:
1149 global_name = _get_part_name(section)
1150 return [caseless_string(c or global_name) for c in cpu_names]
1151
1152 def __init__(self, text):
1153 self.sections = None
1154 self.text = None
1155 self.parse(text)
1156
1157 @memoized
1158 def get_cpu_features(self, cpuid=0):
1159 global_features = []
1160 for section in self.sections:
1161 if 'processor' in section:
1162 if int(section.get('processor')) != cpuid:
1163 continue
1164 if 'Features' in section:
1165 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001166 elif 'flags' in section:
1167 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001168 elif 'Features' in section:
1169 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001170 elif 'flags' in section:
1171 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001172 return global_features
1173
1174 def parse(self, text):
1175 self.sections = []
1176 current_section = {}
1177 self.text = text.strip()
1178 for line in self.text.split('\n'):
1179 line = line.strip()
1180 if line:
1181 key, value = line.split(':', 1)
1182 current_section[key.strip()] = value.strip()
1183 else: # not line
1184 self.sections.append(current_section)
1185 current_section = {}
1186 self.sections.append(current_section)
1187
1188 def __str__(self):
1189 return 'CpuInfo({})'.format(self.cpu_names)
1190
1191 __repr__ = __str__
1192
1193
1194class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001195 """
1196 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001197
Brendan Jackman54adf802017-02-20 17:57:51 +00001198 Not expected to work for very old (pre-3.0) kernel version numbers.
1199
1200 :ivar release: Version number/revision string. Typical output of
1201 ``uname -r``
1202 :type release: str
1203 :ivar version: Extra version info (aside from ``release``) reported by
1204 ``uname``
1205 :type version: str
1206 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1207 :type version_number: int
1208 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1209 :type major: int
1210 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1211 be None
1212 :type minor: int
1213 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1214 :type rc: int
1215 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1216 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001217
1218 :ivar parts: Tuple of version number components. Can be used for
1219 lexicographically comparing kernel versions.
1220 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001221 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001222 def __init__(self, version_string):
1223 if ' #' in version_string:
1224 release, version = version_string.split(' #')
1225 self.release = release
1226 self.version = version
1227 elif version_string.startswith('#'):
1228 self.release = ''
1229 self.version = version_string
1230 else:
1231 self.release = version_string
1232 self.version = ''
1233
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001234 self.version_number = None
1235 self.major = None
1236 self.minor = None
1237 self.sha1 = None
1238 self.rc = None
1239 match = KVERSION_REGEX.match(version_string)
1240 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001241 groups = match.groupdict()
1242 self.version_number = int(groups['version'])
1243 self.major = int(groups['major'])
1244 if groups['minor'] is not None:
1245 self.minor = int(groups['minor'])
1246 if groups['rc'] is not None:
1247 self.rc = int(groups['rc'])
1248 if groups['sha1'] is not None:
1249 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001250
Brendan Jackman18b77b82017-02-20 17:52:56 +00001251 self.parts = (self.version_number, self.major, self.minor)
1252
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001253 def __str__(self):
1254 return '{} {}'.format(self.release, self.version)
1255
1256 __repr__ = __str__
1257
1258
1259class KernelConfig(object):
1260
1261 not_set_regex = re.compile(r'# (\S+) is not set')
1262
1263 @staticmethod
1264 def get_config_name(name):
1265 name = name.upper()
1266 if not name.startswith('CONFIG_'):
1267 name = 'CONFIG_' + name
1268 return name
1269
1270 def iteritems(self):
1271 return self._config.iteritems()
1272
1273 def __init__(self, text):
1274 self.text = text
1275 self._config = {}
1276 for line in text.split('\n'):
1277 line = line.strip()
1278 if line.startswith('#'):
1279 match = self.not_set_regex.search(line)
1280 if match:
1281 self._config[match.group(1)] = 'n'
1282 elif '=' in line:
1283 name, value = line.split('=', 1)
1284 self._config[name.strip()] = value.strip()
1285
1286 def get(self, name):
1287 return self._config.get(self.get_config_name(name))
1288
1289 def like(self, name):
1290 regex = re.compile(name, re.I)
1291 result = {}
1292 for k, v in self._config.iteritems():
1293 if regex.search(k):
1294 result[k] = v
1295 return result
1296
1297 def is_enabled(self, name):
1298 return self.get(name) == 'y'
1299
1300 def is_module(self, name):
1301 return self.get(name) == 'm'
1302
1303 def is_not_set(self, name):
1304 return self.get(name) == 'n'
1305
1306 def has(self, name):
1307 return self.get(name) in ['m', 'y']
1308
1309
1310class LocalLinuxTarget(LinuxTarget):
1311
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001312 def __init__(self,
1313 connection_settings=None,
1314 platform=None,
1315 working_directory=None,
1316 executables_directory=None,
1317 connect=True,
1318 modules=None,
1319 load_default_modules=True,
1320 shell_prompt=DEFAULT_SHELL_PROMPT,
1321 conn_cls=LocalConnection,
1322 ):
1323 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1324 platform=platform,
1325 working_directory=working_directory,
1326 executables_directory=executables_directory,
1327 connect=connect,
1328 modules=modules,
1329 load_default_modules=load_default_modules,
1330 shell_prompt=shell_prompt,
1331 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001332
Sergei Trofimov961f9572015-11-18 17:32:26 +00001333 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001334 if self.working_directory is None:
1335 self.working_directory = '/tmp'
1336 if self.executables_directory is None:
1337 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001338
1339
1340def _get_model_name(section):
1341 name_string = section['model name']
1342 parts = name_string.split('@')[0].strip().split()
1343 return ' '.join([p for p in parts
1344 if '(' not in p and p != 'CPU'])
1345
1346
1347def _get_part_name(section):
1348 implementer = section.get('CPU implementer', '0x0')
1349 part = section['CPU part']
1350 variant = section.get('CPU variant', '0x0')
1351 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1352 if name is None:
1353 name = '{}/{}/{}'.format(implementer, part, variant)
1354 return name