blob: 350ce554f00fceb6b9a065b14af6143b572ea006 [file] [log] [blame]
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001import os
2import re
3import time
4import logging
5import posixpath
6import subprocess
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +01007import tarfile
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01008import tempfile
9import threading
10from collections import namedtuple
11
12from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
13from devlib.module import get_module
14from devlib.platform import Platform
15from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError
16from devlib.utils.ssh import SshConnection
Valentin Schneider7c2fd872017-09-11 16:58:24 +010017from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, adb_disconnect
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010018from devlib.utils.misc import memoized, isiterable, convert_new_lines, merge_lists
19from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_double_quotes
20from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
21
22
Sebastian Goscik040daab2016-02-23 10:27:45 +000023FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
Sebastian Goscik1890db72016-02-15 14:43:30 +000024ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010025 re.IGNORECASE)
26ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
27 r'\s+(?P<width>\d+)x(?P<height>\d+)')
28DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
29 re.MULTILINE)
Patrick Bellasi9a8d5392017-02-17 15:28:07 +000030KVERSION_REGEX =re.compile(
Brendan Jackman66656932017-02-20 18:29:49 +000031 r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(.*-g(?P<sha1>[0-9a-fA-F]{7,}))?'
Patrick Bellasi9a8d5392017-02-17 15:28:07 +000032)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010033
Brendan Jackman486b3f52017-10-18 11:28:08 +010034GOOGLE_DNS_SERVER_ADDRESS = '8.8.8.8'
35
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010036
37class Target(object):
38
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010039 path = None
40 os = None
41
42 default_modules = [
43 'hotplug',
44 'cpufreq',
45 'cpuidle',
46 'cgroups',
47 'hwmon',
48 ]
49
50 @property
51 def core_names(self):
52 return self.platform.core_names
53
54 @property
55 def core_clusters(self):
56 return self.platform.core_clusters
57
58 @property
59 def big_core(self):
60 return self.platform.big_core
61
62 @property
63 def little_core(self):
64 return self.platform.little_core
65
66 @property
67 def is_connected(self):
68 return self.conn is not None
69
70 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010071 def connected_as_root(self):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +000072 if self._connected_as_root is None:
73 result = self.execute('id')
74 self._connected_as_root = 'uid=0(' in result
75 return self._connected_as_root
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010076
77 @property
78 @memoized
79 def is_rooted(self):
80 if self.connected_as_root:
81 return True
82 try:
83 self.execute('ls /', timeout=2, as_root=True)
84 return True
85 except (TargetError, TimeoutError):
86 return False
87
88 @property
89 @memoized
Javi Merino16d87c62016-06-23 14:55:19 +010090 def needs_su(self):
91 return not self.connected_as_root and self.is_rooted
92
93 @property
94 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010095 def kernel_version(self):
Sebastian Goscikbdbf4742016-02-24 14:26:18 +000096 return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +010097
98 @property
99 def os_version(self): # pylint: disable=no-self-use
100 return {}
101
102 @property
103 def abi(self): # pylint: disable=no-self-use
104 return None
105
106 @property
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100107 def supported_abi(self):
108 return [self.abi]
109
110 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100111 @memoized
112 def cpuinfo(self):
113 return Cpuinfo(self.execute('cat /proc/cpuinfo'))
114
115 @property
116 @memoized
117 def number_of_cpus(self):
118 num_cpus = 0
119 corere = re.compile(r'^\s*cpu\d+\s*$')
120 output = self.execute('ls /sys/devices/system/cpu')
121 for entry in output.split():
122 if corere.match(entry):
123 num_cpus += 1
124 return num_cpus
125
126 @property
127 @memoized
128 def config(self):
129 try:
130 return KernelConfig(self.execute('zcat /proc/config.gz'))
131 except TargetError:
132 for path in ['/boot/config', '/boot/config-$(uname -r)']:
133 try:
134 return KernelConfig(self.execute('cat {}'.format(path)))
135 except TargetError:
136 pass
137 return KernelConfig('')
138
139 @property
140 @memoized
141 def user(self):
142 return self.getenv('USER')
143
144 @property
145 def conn(self):
146 if self._connections:
147 tid = id(threading.current_thread())
148 if tid not in self._connections:
149 self._connections[tid] = self.get_connection()
150 return self._connections[tid]
151 else:
152 return None
153
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100154 @property
155 def shutils(self):
156 if self._shutils is None:
157 self._setup_shutils()
158 return self._shutils
159
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100160 def __init__(self,
161 connection_settings=None,
162 platform=None,
163 working_directory=None,
164 executables_directory=None,
165 connect=True,
166 modules=None,
167 load_default_modules=True,
168 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000169 conn_cls=None,
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100170 ):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +0000171 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100172 self.connection_settings = connection_settings or {}
Anouk Van Laer21f40032017-01-31 13:11:03 +0000173 # Set self.platform: either it's given directly (by platform argument)
174 # or it's given in the connection_settings argument
175 # If neither, create default Platform()
176 if platform is None:
177 self.platform = self.connection_settings.get('platform', Platform())
178 else:
179 self.platform = platform
180 # Check if the user hasn't given two different platforms
181 if 'platform' in self.connection_settings:
182 if connection_settings['platform'] is not platform:
183 raise TargetError('Platform specified in connection_settings '
184 '({}) differs from that directly passed '
185 '({})!)'
186 .format(connection_settings['platform'],
187 self.platform))
188 self.connection_settings['platform'] = self.platform
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100189 self.working_directory = working_directory
190 self.executables_directory = executables_directory
191 self.modules = modules or []
192 self.load_default_modules = load_default_modules
193 self.shell_prompt = shell_prompt
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000194 self.conn_cls = conn_cls
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100195 self.logger = logging.getLogger(self.__class__.__name__)
196 self._installed_binaries = {}
197 self._installed_modules = {}
198 self._cache = {}
199 self._connections = {}
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100200 self._shutils = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100201 self.busybox = None
202
203 if load_default_modules:
204 module_lists = [self.default_modules]
205 else:
206 module_lists = []
207 module_lists += [self.modules, self.platform.modules]
208 self.modules = merge_lists(*module_lists, duplicates='first')
209 self._update_modules('early')
210 if connect:
211 self.connect()
212
213 # connection and initialization
214
215 def connect(self, timeout=None):
216 self.platform.init_target_connection(self)
217 tid = id(threading.current_thread())
218 self._connections[tid] = self.get_connection(timeout=timeout)
Sergei Trofimov961f9572015-11-18 17:32:26 +0000219 self._resolve_paths()
Brendan Jackman17bcabd2017-10-09 15:15:24 +0100220 self.execute('mkdir -p {}'.format(self.working_directory))
221 self.execute('mkdir -p {}'.format(self.executables_directory))
222 self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100223 self.platform.update_from_target(self)
Patrick Bellasib83e5182015-10-12 12:37:11 +0100224 self._update_modules('connected')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100225 if self.platform.big_core and self.load_default_modules:
226 self._install_module(get_module('bl'))
227
228 def disconnect(self):
229 for conn in self._connections.itervalues():
230 conn.close()
231 self._connections = {}
232
233 def get_connection(self, timeout=None):
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000234 if self.conn_cls == None:
235 raise ValueError('Connection class not specified on Target creation.')
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100236 return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
237
238 def setup(self, executables=None):
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100239 self._setup_shutils()
Patrick Bellasif2eac512015-11-27 16:35:57 +0000240
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100241 for host_exe in (executables or []): # pylint: disable=superfluous-parens
242 self.install(host_exe)
243
Anouk Van Laer21f40032017-01-31 13:11:03 +0000244 # Check for platform dependent setup procedures
245 self.platform.setup(self)
246
Patrick Bellasic4e46b72016-05-13 18:15:51 +0100247 # Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
248 self._update_modules('setup')
249
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100250 def reboot(self, hard=False, connect=True, timeout=180):
251 if hard:
252 if not self.has('hard_reset'):
253 raise TargetError('Hard reset not supported for this target.')
254 self.hard_reset() # pylint: disable=no-member
255 else:
256 if not self.is_connected:
257 message = 'Cannot reboot target becuase it is disconnected. ' +\
258 'Either connect() first, or specify hard=True ' +\
259 '(in which case, a hard_reset module must be installed)'
260 raise TargetError(message)
261 self.reset()
Sergei Trofimovebe3a8a2016-02-25 10:28:45 +0000262 # Wait a fixed delay before starting polling to give the target time to
263 # shut down, otherwise, might create the connection while it's still shutting
264 # down resulting in subsequenct connection failing.
265 self.logger.debug('Waiting for target to power down...')
266 reset_delay = 20
267 time.sleep(reset_delay)
268 timeout = max(timeout - reset_delay, 10)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100269 if self.has('boot'):
270 self.boot() # pylint: disable=no-member
Marc Bonnici0687dac2017-02-28 13:48:10 +0000271 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100272 if connect:
273 self.connect(timeout=timeout)
274
275 # file transfer
276
277 def push(self, source, dest, timeout=None):
278 return self.conn.push(source, dest, timeout=timeout)
279
280 def pull(self, source, dest, timeout=None):
281 return self.conn.pull(source, dest, timeout=timeout)
282
Anouk Van Laer0b7ab6a2017-05-17 17:13:33 +0100283 def get_directory(self, source_dir, dest):
284 """ Pull a directory from the device, after compressing dir """
285 # Create all file names
286 tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
287 # Host location of dir
288 outdir = os.path.join(dest, tar_file_name)
289 # Host location of archive
290 tar_file_name = '{}.tar'.format(tar_file_name)
291 tempfile = os.path.join(dest, tar_file_name)
292
293 # Does the folder exist?
294 self.execute('ls -la {}'.format(source_dir))
295 # Try compressing the folder
296 try:
297 self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
298 source_dir))
299 except TargetError:
300 self.logger.debug('Failed to run tar command on target! ' \
301 'Not pulling directory {}'.format(source_dir))
302 # Pull the file
303 os.mkdir(outdir)
304 self.pull(tar_file_name, tempfile )
305 # Decompress
306 f = tarfile.open(tempfile, 'r')
307 f.extractall(outdir)
308 os.remove(tempfile)
309
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100310 # execution
311
312 def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
setrofimb7386552017-05-23 17:39:12 +0100313 return self.conn.execute(command, timeout, check_exit_code, as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100314
315 def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
316 return self.conn.background(command, stdout, stderr, as_root)
317
318 def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
319 as_root=False, timeout=30):
320 """
321 Executes the specified binary under the specified conditions.
322
323 :binary: binary to execute. Must be present and executable on the device.
324 :args: arguments to be passed to the binary. The can be either a list or
325 a string.
326 :in_directory: execute the binary in the specified directory. This must
327 be an absolute path.
328 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
329 case, it will be interpreted as the mask), a list of ``ints``, in which
330 case this will be interpreted as the list of cpus, or string, which
331 will be interpreted as a comma-separated list of cpu ranges, e.g.
332 ``"0,4-7"``.
333 :as_root: Specify whether the command should be run as root
334 :timeout: If the invocation does not terminate within this number of seconds,
335 a ``TimeoutError`` exception will be raised. Set to ``None`` if the
336 invocation should not timeout.
337
Brendan Jackman27f545f2016-11-15 16:58:57 +0000338 :returns: output of command.
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100339 """
340 command = binary
341 if args:
342 if isiterable(args):
343 args = ' '.join(args)
344 command = '{} {}'.format(command, args)
345 if on_cpus:
346 on_cpus = bitmask(on_cpus)
347 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
348 if in_directory:
349 command = 'cd {} && {}'.format(in_directory, command)
350 return self.execute(command, as_root=as_root, timeout=timeout)
351
Valentin Schneider92b0c252017-07-06 16:01:15 +0100352 def background_invoke(self, binary, args=None, in_directory=None,
353 on_cpus=None, as_root=False):
354 """
355 Executes the specified binary as a background task under the
356 specified conditions.
357
358 :binary: binary to execute. Must be present and executable on the device.
359 :args: arguments to be passed to the binary. The can be either a list or
360 a string.
361 :in_directory: execute the binary in the specified directory. This must
362 be an absolute path.
363 :on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
364 case, it will be interpreted as the mask), a list of ``ints``, in which
365 case this will be interpreted as the list of cpus, or string, which
366 will be interpreted as a comma-separated list of cpu ranges, e.g.
367 ``"0,4-7"``.
368 :as_root: Specify whether the command should be run as root
369
370 :returns: the subprocess instance handling that command
371 """
372 command = binary
373 if args:
374 if isiterable(args):
375 args = ' '.join(args)
376 command = '{} {}'.format(command, args)
377 if on_cpus:
378 on_cpus = bitmask(on_cpus)
379 command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
380 if in_directory:
381 command = 'cd {} && {}'.format(in_directory, command)
382 return self.background(command, as_root=as_root)
383
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100384 def kick_off(self, command, as_root=False):
385 raise NotImplementedError()
386
387 # sysfs interaction
388
389 def read_value(self, path, kind=None):
Javi Merino16d87c62016-06-23 14:55:19 +0100390 output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100391 if kind:
392 return kind(output)
393 else:
394 return output
395
396 def read_int(self, path):
397 return self.read_value(path, kind=integer)
398
399 def read_bool(self, path):
400 return self.read_value(path, kind=boolean)
401
402 def write_value(self, path, value, verify=True):
403 value = str(value)
404 self.execute('echo {} > \'{}\''.format(value, path), check_exit_code=False, as_root=True)
405 if verify:
406 output = self.read_value(path)
407 if not output == value:
408 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
409 raise TargetError(message)
410
411 def reset(self):
412 try:
Javi Merino16d87c62016-06-23 14:55:19 +0100413 self.execute('reboot', as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100414 except (TargetError, TimeoutError, subprocess.CalledProcessError):
415 # on some targets "reboot" doesn't return gracefully
416 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +0000417 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100418
419 def check_responsive(self):
420 try:
421 self.conn.execute('ls /', timeout=5)
422 except (TimeoutError, subprocess.CalledProcessError):
423 raise TargetNotRespondingError(self.conn.name)
424
425 # process management
426
427 def kill(self, pid, signal=None, as_root=False):
428 signal_string = '-s {}'.format(signal) if signal else ''
429 self.execute('kill {} {}'.format(signal_string, pid), as_root=as_root)
430
431 def killall(self, process_name, signal=None, as_root=False):
432 for pid in self.get_pids_of(process_name):
Sergei Trofimov6351a3b2017-01-30 11:14:36 +0000433 try:
434 self.kill(pid, signal=signal, as_root=as_root)
435 except TargetError:
436 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100437
438 def get_pids_of(self, process_name):
439 raise NotImplementedError()
440
441 def ps(self, **kwargs):
442 raise NotImplementedError()
443
444 # files
445
446 def file_exists(self, filepath):
447 command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
Brendan Jackmanc1b51522016-11-23 13:44:00 +0000448 output = self.execute(command.format(filepath), as_root=self.is_rooted)
449 return boolean(output.strip())
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100450
Sebastian Goscik33603c62016-02-15 15:07:19 +0000451 def directory_exists(self, filepath):
452 output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
453 # output from ssh my contain part of the expression in the buffer,
454 # split out everything except the last word.
455 return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
456
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100457 def list_file_systems(self):
458 output = self.execute('mount')
459 fstab = []
460 for line in output.split('\n'):
461 line = line.strip()
462 if not line:
463 continue
464 match = FSTAB_ENTRY_REGEX.search(line)
465 if match:
466 fstab.append(FstabEntry(match.group(1), match.group(2),
467 match.group(3), match.group(4),
468 None, None))
469 else: # assume pre-M Android
470 fstab.append(FstabEntry(*line.split()))
471 return fstab
472
473 def list_directory(self, path, as_root=False):
474 raise NotImplementedError()
475
476 def get_workpath(self, name):
477 return self.path.join(self.working_directory, name)
478
479 def tempfile(self, prefix='', suffix=''):
480 names = tempfile._get_candidate_names() # pylint: disable=W0212
481 for _ in xrange(tempfile.TMP_MAX):
482 name = names.next()
483 path = self.get_workpath(prefix + name + suffix)
484 if not self.file_exists(path):
485 return path
486 raise IOError('No usable temporary filename found')
487
488 def remove(self, path, as_root=False):
489 self.execute('rm -rf {}'.format(path), as_root=as_root)
490
491 # misc
492 def core_cpus(self, core):
493 return [i for i, c in enumerate(self.core_names) if c == core]
494
495 def list_online_cpus(self, core=None):
496 path = self.path.join('/sys/devices/system/cpu/online')
497 output = self.read_value(path)
498 all_online = ranges_to_list(output)
499 if core:
500 cpus = self.core_cpus(core)
501 if not cpus:
502 raise ValueError(core)
503 return [o for o in all_online if o in cpus]
504 else:
505 return all_online
506
507 def list_offline_cpus(self):
508 online = self.list_online_cpus()
509 return [c for c in xrange(self.number_of_cpus)
510 if c not in online]
511
512 def getenv(self, variable):
513 return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
514
515 def capture_screen(self, filepath):
516 raise NotImplementedError()
517
518 def install(self, filepath, timeout=None, with_name=None):
519 raise NotImplementedError()
520
521 def uninstall(self, name):
522 raise NotImplementedError()
523
Sebastian Goscik84151f92016-02-15 15:09:27 +0000524 def get_installed(self, name, search_system_binaries=True):
525 # Check user installed binaries first
Sergei Trofimovb5324532015-11-18 18:07:47 +0000526 if self.file_exists(self.executables_directory):
527 if name in self.list_directory(self.executables_directory):
528 return self.path.join(self.executables_directory, name)
Sebastian Goscik84151f92016-02-15 15:09:27 +0000529 # Fall back to binaries in PATH
530 if search_system_binaries:
531 for path in self.getenv('PATH').split(self.path.pathsep):
532 try:
533 if name in self.list_directory(path):
534 return self.path.join(path, name)
535 except TargetError:
536 pass # directory does not exist or no executable premssions
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100537
538 which = get_installed
539
Sebastian Goscikbe8f9722016-02-15 15:11:38 +0000540 def install_if_needed(self, host_path, search_system_binaries=True):
541
542 binary_path = self.get_installed(os.path.split(host_path)[1],
543 search_system_binaries=search_system_binaries)
544 if not binary_path:
545 binary_path = self.install(host_path)
546 return binary_path
547
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100548 def is_installed(self, name):
549 return bool(self.get_installed(name))
550
551 def bin(self, name):
552 return self._installed_binaries.get(name, name)
553
554 def has(self, modname):
555 return hasattr(self, identifier(modname))
556
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000557 def lsmod(self):
558 lines = self.execute('lsmod').splitlines()
559 entries = []
560 for line in lines[1:]: # first line is the header
561 if not line.strip():
562 continue
563 parts = line.split()
564 name = parts[0]
565 size = int(parts[1])
566 use_count = int(parts[2])
567 if len(parts) > 3:
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000568 used_by = ''.join(parts[3:]).split(',')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +0000569 else:
570 used_by = []
571 entries.append(LsmodEntry(name, size, use_count, used_by))
572 return entries
573
Sergei Trofimov10a80d22016-01-27 17:02:59 +0000574 def insmod(self, path):
575 target_path = self.get_workpath(os.path.basename(path))
576 self.push(path, target_path)
577 self.execute('insmod {}'.format(target_path), as_root=True)
578
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100579
580 def extract(self, path, dest=None):
581 """
582 Extact the specified on-target file. The extraction method to be used
583 (unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
584 If ``dest`` is specified, it must be an existing directory on target;
585 the extracted contents will be placed there.
586
587 Note that, depending on the archive file format (and therfore the
588 extraction method used), the original archive file may or may not exist
589 after the extraction.
590
591 The return value is the path to the extracted contents. In case of
592 gunzip and bunzip2, this will be path to the extracted file; for tar
593 and uzip, this will be the directory with the extracted file(s)
594 (``dest`` if it was specified otherwise, the directory that cotained
595 the archive).
596
597 """
598 for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
599 '.tgz', '.tbz', '.tbz2']:
600 if path.endswith(ending):
601 return self._extract_archive(path, 'tar xf {} -C {}', dest)
602
603 ext = self.path.splitext(path)[1]
604 if ext in ['.bz', '.bz2']:
605 return self._extract_file(path, 'bunzip2 -f {}', dest)
606 elif ext == '.gz':
607 return self._extract_file(path, 'gunzip -f {}', dest)
608 elif ext == '.zip':
609 return self._extract_archive(path, 'unzip {} -d {}', dest)
610 else:
611 raise ValueError('Unknown compression format: {}'.format(ext))
612
Sergei Trofimov69a83d42017-05-12 11:54:31 +0100613 def sleep(self, duration):
614 timeout = duration + 10
615 self.execute('sleep {}'.format(duration), timeout=timeout)
616
Sergei Trofimov181bc182017-10-03 16:28:09 +0100617 def read_tree_values_flat(self, path, depth=1, check_exit_code=True):
618 command = 'read_tree_values {} {}'.format(path, depth)
619 output = self._execute_util(command, as_root=self.is_rooted,
620 check_exit_code=check_exit_code)
621 result = {}
622 for entry in output.strip().split('\n'):
Sergei Trofimov7e073c12017-10-06 13:37:00 +0100623 if ':' not in entry:
Sergei Trofimovd560aea2017-10-05 09:35:11 +0100624 continue
Sergei Trofimov181bc182017-10-03 16:28:09 +0100625 path, value = entry.strip().split(':', 1)
626 result[path] = value
627 return result
628
629 def read_tree_values(self, path, depth=1, dictcls=dict, check_exit_code=True):
630 value_map = self.read_tree_values_flat(path, depth, check_exit_code)
631 return _build_path_tree(value_map, path, self.path.sep, dictcls)
632
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100633 # internal methods
634
Sergei Trofimovbfb47212017-10-03 16:47:11 +0100635 def _setup_shutils(self):
636 shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
637 shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
638 shell_path = '/bin/sh'
639 if self.os == 'android':
640 shell_path = '/system/bin/sh'
641 with open(shutils_ifile) as fh:
642 lines = fh.readlines()
643 with open(shutils_ofile, 'w') as ofile:
644 for line in lines:
645 line = line.replace("__DEVLIB_SHELL__", shell_path)
646 line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
647 ofile.write(line)
648 self._shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
649
Sergei Trofimov8b2ac8d2017-05-12 11:48:19 +0100650 def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
651 command = '{} {}'.format(self.shutils, command)
652 return self.conn.execute(command, timeout, check_exit_code, as_root)
653
Sergei Trofimovbaaa67b2016-07-14 11:00:24 +0100654 def _extract_archive(self, path, cmd, dest=None):
655 cmd = '{} ' + cmd # busybox
656 if dest:
657 extracted = dest
658 else:
659 extracted = self.path.dirname(path)
660 cmdtext = cmd.format(self.busybox, path, extracted)
661 self.execute(cmdtext)
662 return extracted
663
664 def _extract_file(self, path, cmd, dest=None):
665 cmd = '{} ' + cmd # busybox
666 cmdtext = cmd.format(self.busybox, path)
667 self.execute(cmdtext)
668 extracted = self.path.splitext(path)[0]
669 if dest:
670 self.execute('mv -f {} {}'.format(extracted, dest))
671 if dest.endswith('/'):
672 extracted = self.path.join(dest, self.path.basename(extracted))
673 else:
674 extracted = dest
675 return extracted
676
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100677 def _update_modules(self, stage):
678 for mod in self.modules:
679 if isinstance(mod, dict):
680 mod, params = mod.items()[0]
681 else:
682 params = {}
683 mod = get_module(mod)
684 if not mod.stage == stage:
685 continue
686 if mod.probe(self):
687 self._install_module(mod, **params)
688 else:
Javi Merinod0c71fb2016-01-18 12:27:48 +0000689 msg = 'Module {} is not supported by the target'.format(mod.name)
690 if self.load_default_modules:
691 self.logger.debug(msg)
692 else:
693 self.logger.warning(msg)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100694
695 def _install_module(self, mod, **params):
696 if mod.name not in self._installed_modules:
697 self.logger.debug('Installing module {}'.format(mod.name))
698 mod.install(self, **params)
699 self._installed_modules[mod.name] = mod
700 else:
701 self.logger.debug('Module {} is already installed.'.format(mod.name))
702
Sergei Trofimov961f9572015-11-18 17:32:26 +0000703 def _resolve_paths(self):
704 raise NotImplementedError()
705
Brendan Jackman486b3f52017-10-18 11:28:08 +0100706 def is_network_connected(self):
707 self.logger.debug('Checking for internet connectivity...')
708
709 timeout_s = 5
710 # It would be nice to use busybox for this, but that means we'd need
711 # root (ping is usually setuid so it can open raw sockets to send ICMP)
712 command = 'ping -q -c 1 -w {} {} 2>&1'.format(timeout_s,
713 GOOGLE_DNS_SERVER_ADDRESS)
714
715 # We'll use our own retrying mechanism (rather than just using ping's -c
716 # to send multiple packets) so that we don't slow things down in the
717 # 'good' case where the first packet gets echoed really quickly.
Brendan Jackman417ab3d2017-10-24 16:03:10 +0100718 attempts = 5
719 for _ in range(attempts):
Brendan Jackman486b3f52017-10-18 11:28:08 +0100720 try:
721 self.execute(command)
722 return True
723 except TargetError as e:
724 err = str(e).lower()
725 if '100% packet loss' in err:
726 # We sent a packet but got no response.
727 # Try again - we don't want this to fail just because of a
728 # transient drop in connection quality.
729 self.logger.debug('No ping response from {} after {}s'
730 .format(GOOGLE_DNS_SERVER_ADDRESS, timeout_s))
731 continue
732 elif 'network is unreachable' in err:
733 # No internet connection at all, we can fail straight away
734 self.logger.debug('Network unreachable')
735 return False
736 else:
737 # Something else went wrong, we don't know what, raise an
738 # error.
739 raise
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100740
Brendan Jackman417ab3d2017-10-24 16:03:10 +0100741 self.logger.debug('Failed to ping {} after {} attempts'.format(
742 GOOGLE_DNS_SERVER_ADDRESS, attempts))
743 return False
744
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100745class LinuxTarget(Target):
746
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100747 path = posixpath
748 os = 'linux'
749
750 @property
751 @memoized
752 def abi(self):
Sebastian Goscik5880f6e2016-06-16 13:31:53 +0100753 value = self.execute('uname -m').strip()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100754 for abi, architectures in ABI_MAP.iteritems():
755 if value in architectures:
756 result = abi
757 break
758 else:
759 result = value
760 return result
761
762 @property
763 @memoized
764 def os_version(self):
765 os_version = {}
766 try:
767 command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
768 version_files = self.execute(command, check_exit_code=False).strip().split()
769 for vf in version_files:
770 name = self.path.basename(vf)
771 output = self.read_value(vf)
772 os_version[name] = output.strip().replace('\n', ' ')
773 except TargetError:
774 raise
775 return os_version
776
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000777 @property
778 @memoized
779 # There is currently no better way to do this cross platform.
780 # ARM does not have dmidecode
781 def model(self):
782 if self.file_exists("/proc/device-tree/model"):
783 raw_model = self.execute("cat /proc/device-tree/model")
784 return '_'.join(raw_model.split()[:2])
785 return None
786
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000787 def __init__(self,
788 connection_settings=None,
789 platform=None,
790 working_directory=None,
791 executables_directory=None,
792 connect=True,
793 modules=None,
794 load_default_modules=True,
795 shell_prompt=DEFAULT_SHELL_PROMPT,
796 conn_cls=SshConnection,
797 ):
798 super(LinuxTarget, self).__init__(connection_settings=connection_settings,
799 platform=platform,
800 working_directory=working_directory,
801 executables_directory=executables_directory,
802 connect=connect,
803 modules=modules,
804 load_default_modules=load_default_modules,
805 shell_prompt=shell_prompt,
806 conn_cls=conn_cls)
807
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100808 def connect(self, timeout=None):
809 super(LinuxTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100810
811 def kick_off(self, command, as_root=False):
812 command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
813 return self.conn.execute(command, as_root=as_root)
814
815 def get_pids_of(self, process_name):
816 """Returns a list of PIDs of all processes with the specified name."""
817 # result should be a column of PIDs with the first row as "PID" header
818 result = self.execute('ps -C {} -o pid'.format(process_name), # NOQA
819 check_exit_code=False).strip().split()
820 if len(result) >= 2: # at least one row besides the header
821 return map(int, result[1:])
822 else:
823 return []
824
825 def ps(self, **kwargs):
826 command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
827 lines = iter(convert_new_lines(self.execute(command)).split('\n'))
828 lines.next() # header
829
830 result = []
831 for line in lines:
832 parts = re.split(r'\s+', line, maxsplit=8)
833 if parts and parts != ['']:
834 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
835
836 if not kwargs:
837 return result
838 else:
839 filtered_result = []
840 for entry in result:
841 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
842 filtered_result.append(entry)
843 return filtered_result
844
845 def list_directory(self, path, as_root=False):
846 contents = self.execute('ls -1 {}'.format(path), as_root=as_root)
847 return [x.strip() for x in contents.split('\n') if x.strip()]
848
849 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
850 destpath = self.path.join(self.executables_directory,
851 with_name and with_name or self.path.basename(filepath))
852 self.push(filepath, destpath)
853 self.execute('chmod a+x {}'.format(destpath), timeout=timeout)
854 self._installed_binaries[self.path.basename(destpath)] = destpath
855 return destpath
856
857 def uninstall(self, name):
858 path = self.path.join(self.executables_directory, name)
859 self.remove(path)
860
861 def capture_screen(self, filepath):
862 if not self.is_installed('scrot'):
863 self.logger.debug('Could not take screenshot as scrot is not installed.')
864 return
865 try:
866
867 tmpfile = self.tempfile()
868 self.execute('DISPLAY=:0.0 scrot {}'.format(tmpfile))
869 self.pull(tmpfile, filepath)
870 self.remove(tmpfile)
871 except TargetError as e:
872 if "Can't open X dispay." not in e.message:
873 raise e
874 message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
875 self.logger.debug('Could not take screenshot: {}'.format(message))
876
Sergei Trofimov961f9572015-11-18 17:32:26 +0000877 def _resolve_paths(self):
878 if self.working_directory is None:
879 if self.connected_as_root:
880 self.working_directory = '/root/devlib-target'
881 else:
882 self.working_directory = '/home/{}/devlib-target'.format(self.user)
883 if self.executables_directory is None:
884 self.executables_directory = self.path.join(self.working_directory, 'bin')
885
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100886
887class AndroidTarget(Target):
888
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100889 path = posixpath
890 os = 'android'
Chris Redpath2a4eafa2016-09-27 12:08:33 +0100891 ls_command = ''
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100892
893 @property
894 @memoized
895 def abi(self):
896 return self.getprop()['ro.product.cpu.abi'].split('-')[0]
897
898 @property
899 @memoized
Marc Bonnici98fb2e22017-07-14 17:41:16 +0100900 def supported_abi(self):
901 props = self.getprop()
902 result = [props['ro.product.cpu.abi']]
903 if 'ro.product.cpu.abi2' in props:
904 result.append(props['ro.product.cpu.abi2'])
905 if 'ro.product.cpu.abilist' in props:
906 for abi in props['ro.product.cpu.abilist'].split(','):
907 if abi not in result:
908 result.append(abi)
909
910 mapped_result = []
911 for supported_abi in result:
912 for abi, architectures in ABI_MAP.iteritems():
913 found = False
914 if supported_abi in architectures and abi not in mapped_result:
915 mapped_result.append(abi)
916 found = True
917 break
918 if not found and supported_abi not in mapped_result:
919 mapped_result.append(supported_abi)
920 return mapped_result
921
922 @property
923 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100924 def os_version(self):
925 os_version = {}
926 for k, v in self.getprop().iteritems():
927 if k.startswith('ro.build.version'):
928 part = k.split('.')[-1]
929 os_version[part] = v
930 return os_version
931
932 @property
933 def adb_name(self):
934 return self.conn.device
935
936 @property
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000937 @memoized
Sebastian Goscikcafeb812016-02-15 15:17:32 +0000938 def android_id(self):
939 """
940 Get the device's ANDROID_ID. Which is
941
942 "A 64-bit number (as a hex string) that is randomly generated when the user
943 first sets up the device and should remain constant for the lifetime of the
944 user's device."
945
946 .. note:: This will get reset on userdata erasure.
947
948 """
949 output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
950 return output.split('value=')[-1]
951
952 @property
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100953 @memoized
Sebastian Goscikf5b7c822016-02-23 17:10:01 +0000954 def model(self):
955 try:
956 return self.getprop(prop='ro.product.device')
957 except KeyError:
958 return None
959
960 @property
961 @memoized
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100962 def screen_resolution(self):
963 output = self.execute('dumpsys window')
964 match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
965 if match:
966 return (int(match.group('width')),
967 int(match.group('height')))
968 else:
969 return (0, 0)
970
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000971 def __init__(self,
972 connection_settings=None,
973 platform=None,
974 working_directory=None,
975 executables_directory=None,
976 connect=True,
977 modules=None,
978 load_default_modules=True,
979 shell_prompt=DEFAULT_SHELL_PROMPT,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000980 conn_cls=AdbConnection,
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000981 package_data_directory="/data/data",
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000982 ):
983 super(AndroidTarget, self).__init__(connection_settings=connection_settings,
984 platform=platform,
985 working_directory=working_directory,
986 executables_directory=executables_directory,
987 connect=connect,
988 modules=modules,
989 load_default_modules=load_default_modules,
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +0000990 shell_prompt=shell_prompt,
991 conn_cls=conn_cls)
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000992 self.package_data_directory = package_data_directory
Brendan Jackman8a0554f2017-10-09 17:08:38 +0100993 self.clear_logcat_lock = threading.Lock()
Sebastian Goscik0a8b0c62016-02-16 17:24:19 +0000994
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100995 def reset(self, fastboot=False): # pylint: disable=arguments-differ
996 try:
997 self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
Javi Merino16d87c62016-06-23 14:55:19 +0100998 as_root=self.needs_su, timeout=2)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +0100999 except (TargetError, TimeoutError, subprocess.CalledProcessError):
1000 # on some targets "reboot" doesn't return gracefully
1001 pass
Marc Bonnici0687dac2017-02-28 13:48:10 +00001002 self._connected_as_root = None
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001003
Patrick Bellasif26f9422017-07-12 12:27:28 +01001004 def wait_boot_complete(self, timeout=10):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001005 start = time.time()
Patrick Bellasif26f9422017-07-12 12:27:28 +01001006 boot_completed = boolean(self.getprop('sys.boot_completed'))
1007 while not boot_completed and timeout >= time.time() - start:
1008 time.sleep(5)
1009 boot_completed = boolean(self.getprop('sys.boot_completed'))
1010 if not boot_completed:
1011 raise TargetError('Connected but Android did not fully boot.')
1012
1013 def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001014 device = self.connection_settings.get('device')
1015 if device and ':' in device:
1016 # ADB does not automatically remove a network device from it's
1017 # devices list when the connection is broken by the remote, so the
1018 # adb connection may have gone "stale", resulting in adb blocking
1019 # indefinitely when making calls to the device. To avoid this,
1020 # always disconnect first.
1021 adb_disconnect(device)
1022 super(AndroidTarget, self).connect(timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001023
1024 if check_boot_completed:
Patrick Bellasif26f9422017-07-12 12:27:28 +01001025 self.wait_boot_complete(timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001026
1027 def setup(self, executables=None):
1028 super(AndroidTarget, self).setup(executables)
1029 self.execute('mkdir -p {}'.format(self._file_transfer_cache))
1030
Sebastian Goscik9af32ec2016-05-27 16:26:30 +01001031 def kick_off(self, command, as_root=None):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001032 """
1033 Like execute but closes adb session and returns immediately, leaving the command running on the
1034 device (this is different from execute(background=True) which keeps adb connection open and returns
1035 a subprocess object).
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001036 """
Sebastian Goscik9af32ec2016-05-27 16:26:30 +01001037 if as_root is None:
Javi Merino16d87c62016-06-23 14:55:19 +01001038 as_root = self.needs_su
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001039 try:
Sergei Trofimovca0b6e82016-08-30 14:25:11 +01001040 command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001041 output = self.execute(command, timeout=1, as_root=as_root)
1042 except TimeoutError:
1043 pass
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001044
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001045 def __setup_list_directory(self):
1046 # In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
1047 # AOSP 7.0 as well, the ls command was changed.
1048 # Previous versions default to a single column listing, which is nice and easy to parse.
1049 # Newer versions default to a multi-column listing, which is not, but it does support
1050 # a '-1' option to get into single column mode. Older versions do not support this option
1051 # so we try the new version, and if it fails we use the old version.
1052 self.ls_command = 'ls -1'
1053 try:
Marc Bonnici06552372017-03-29 16:43:22 +01001054 self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001055 except TargetError:
1056 self.ls_command = 'ls'
1057
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001058 def list_directory(self, path, as_root=False):
Chris Redpath2a4eafa2016-09-27 12:08:33 +01001059 if self.ls_command == '':
1060 self.__setup_list_directory()
1061 contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001062 return [x.strip() for x in contents.split('\n') if x.strip()]
1063
1064 def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
1065 ext = os.path.splitext(filepath)[1].lower()
1066 if ext == '.apk':
1067 return self.install_apk(filepath, timeout)
1068 else:
1069 return self.install_executable(filepath, with_name)
1070
1071 def uninstall(self, name):
1072 if self.package_is_installed(name):
1073 self.uninstall_package(name)
1074 else:
1075 self.uninstall_executable(name)
1076
1077 def get_pids_of(self, process_name):
Sergei Trofimov96693a32017-09-22 17:39:17 +01001078 result = []
1079 search_term = process_name[-15:]
1080 for entry in self.ps():
1081 if search_term in entry.name:
1082 result.append(entry.pid)
1083 return result
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001084
1085 def ps(self, **kwargs):
1086 lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
1087 lines.next() # header
1088 result = []
1089 for line in lines:
Brendan Jackman55c27e22017-04-12 16:30:58 +01001090 parts = line.split(None, 8)
Sergei Trofimov109fcc62017-09-26 13:30:15 +01001091 if not parts:
1092 continue
1093 if len(parts) == 8:
1094 # wchan was blank; insert an empty field where it should be.
1095 parts.insert(5, '')
1096 result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001097 if not kwargs:
1098 return result
1099 else:
1100 filtered_result = []
1101 for entry in result:
1102 if all(getattr(entry, k) == v for k, v in kwargs.iteritems()):
1103 filtered_result.append(entry)
1104 return filtered_result
1105
1106 def capture_screen(self, filepath):
1107 on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
1108 self.execute('screencap -p {}'.format(on_device_file))
1109 self.pull(on_device_file, filepath)
1110 self.remove(on_device_file)
1111
1112 def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1113 if not as_root:
1114 self.conn.push(source, dest, timeout=timeout)
1115 else:
1116 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001117 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001118 self.conn.push(source, device_tempfile, timeout=timeout)
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001119 self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001120
1121 def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
1122 if not as_root:
1123 self.conn.pull(source, dest, timeout=timeout)
1124 else:
1125 device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
Sebastian Goscik1424ceb2016-02-15 15:27:19 +00001126 self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
1127 self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
Marc Bonnicif6d02c62017-04-21 15:21:40 +01001128 self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001129 self.conn.pull(device_tempfile, dest, timeout=timeout)
1130
1131 # Android-specific
1132
Marc Bonnici8839ed02017-08-10 17:19:13 +01001133 def swipe_to_unlock(self, direction="diagonal"):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001134 width, height = self.screen_resolution
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001135 command = 'input swipe {} {} {} {}'
Marc Bonnici8839ed02017-08-10 17:19:13 +01001136 if direction == "diagonal":
1137 start = 100
1138 stop = width - start
1139 swipe_height = height * 2 // 3
1140 self.execute(command.format(start, swipe_height, stop, 0))
1141 elif direction == "horizontal":
Marc Bonnici1229af02017-07-26 11:29:10 +01001142 swipe_height = height * 2 // 3
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001143 start = 100
1144 stop = width - start
Marc Bonnici1229af02017-07-26 11:29:10 +01001145 self.execute(command.format(start, swipe_height, stop, swipe_height))
1146 elif direction == "vertical":
1147 swipe_middle = width / 2
1148 swipe_height = height * 2 // 3
1149 self.execute(command.format(swipe_middle, swipe_height, swipe_middle, 0))
Sebastian Goscik880a0bc2016-02-15 15:19:47 +00001150 else:
Marc Bonnici1229af02017-07-26 11:29:10 +01001151 raise TargetError("Invalid swipe direction: {}".format(direction))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001152
1153 def getprop(self, prop=None):
1154 props = AndroidProperties(self.execute('getprop'))
1155 if prop:
1156 return props[prop]
1157 return props
1158
1159 def is_installed(self, name):
1160 return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
1161
1162 def package_is_installed(self, package_name):
1163 return package_name in self.list_packages()
1164
1165 def list_packages(self):
1166 output = self.execute('pm list packages')
1167 output = output.replace('package:', '')
1168 return output.split()
1169
1170 def get_package_version(self, package):
1171 output = self.execute('dumpsys package {}'.format(package))
1172 for line in convert_new_lines(output).split('\n'):
1173 if 'versionName' in line:
1174 return line.split('=', 1)[1]
1175 return None
1176
Marc Bonnicid3396f22017-05-31 15:56:50 +01001177 def get_sdk_version(self):
1178 try:
1179 return int(self.getprop('ro.build.version.sdk'))
1180 except (ValueError, TypeError):
1181 return None
1182
Marc Bonnicic33dd652017-05-31 15:51:31 +01001183 def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001184 ext = os.path.splitext(filepath)[1].lower()
1185 if ext == '.apk':
Marc Bonnicic33dd652017-05-31 15:51:31 +01001186 flags = []
1187 if replace:
1188 flags.append('-r') # Replace existing APK
1189 if allow_downgrade:
1190 flags.append('-d') # Install the APK even if a newer version is already installed
1191 if self.get_sdk_version() >= 23:
1192 flags.append('-g') # Grant all runtime permissions
1193 self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
1194 return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001195 else:
1196 raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
1197
1198 def install_executable(self, filepath, with_name=None):
1199 self._ensure_executables_directory_is_writable()
1200 executable_name = with_name or os.path.basename(filepath)
1201 on_device_file = self.path.join(self.working_directory, executable_name)
1202 on_device_executable = self.path.join(self.executables_directory, executable_name)
1203 self.push(filepath, on_device_file)
1204 if on_device_file != on_device_executable:
Javi Merino16d87c62016-06-23 14:55:19 +01001205 self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
1206 self.remove(on_device_file, as_root=self.needs_su)
1207 self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001208 self._installed_binaries[executable_name] = on_device_executable
1209 return on_device_executable
1210
1211 def uninstall_package(self, package):
1212 adb_command(self.adb_name, "uninstall {}".format(package), timeout=30)
1213
1214 def uninstall_executable(self, executable_name):
1215 on_device_executable = self.path.join(self.executables_directory, executable_name)
1216 self._ensure_executables_directory_is_writable()
Javi Merino16d87c62016-06-23 14:55:19 +01001217 self.remove(on_device_executable, as_root=self.needs_su)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001218
1219 def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
Sebastian Goscikaab487c2016-02-15 15:21:40 +00001220 op = '>>' if append else '>'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001221 filtstr = ' -s {}'.format(filter) if filter else ''
1222 command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
1223 adb_command(self.adb_name, command, timeout=timeout)
1224
1225 def clear_logcat(self):
Brendan Jackman8a0554f2017-10-09 17:08:38 +01001226 with self.clear_logcat_lock:
1227 adb_command(self.adb_name, 'logcat -c', timeout=30)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001228
Valentin Schneider7c2fd872017-09-11 16:58:24 +01001229 def get_logcat_monitor(self, regexps=None):
1230 return LogcatMonitor(self, regexps)
1231
Patrick Bellasi0c7eb9e2017-07-12 12:30:30 +01001232 def adb_kill_server(self, timeout=30):
1233 adb_command(self.adb_name, 'kill-server', timeout)
1234
1235 def adb_wait_for_device(self, timeout=30):
1236 adb_command(self.adb_name, 'wait-for-device', timeout)
1237
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001238 def adb_reboot_bootloader(self, timeout=30):
1239 adb_command(self.adb_name, 'reboot-bootloader', timeout)
1240
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001241 def adb_root(self, enable=True, force=False):
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001242 if enable:
Ionela Voinescu66eaf152017-02-24 14:19:25 +00001243 if self._connected_as_root and not force:
Patrick Bellasi9ce57c02017-02-16 13:11:02 +00001244 return
1245 adb_command(self.adb_name, 'root', timeout=30)
1246 self._connected_as_root = True
1247 return
1248 adb_command(self.adb_name, 'unroot', timeout=30)
1249 self._connected_as_root = False
1250
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001251 def is_screen_on(self):
1252 output = self.execute('dumpsys power')
1253 match = ANDROID_SCREEN_STATE_REGEX.search(output)
1254 if match:
1255 return boolean(match.group(1))
1256 else:
1257 raise TargetError('Could not establish screen state.')
1258
1259 def ensure_screen_is_on(self):
1260 if not self.is_screen_on():
1261 self.execute('input keyevent 26')
1262
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001263 def ensure_screen_is_off(self):
1264 if self.is_screen_on():
1265 self.execute('input keyevent 26')
1266
Marc Bonniciddd2e292017-07-06 17:30:12 +01001267 def set_auto_brightness(self, auto_brightness):
1268 cmd = 'settings put system screen_brightness_mode {}'
1269 self.execute(cmd.format(int(boolean(auto_brightness))))
1270
1271 def get_auto_brightness(self):
1272 cmd = 'settings get system screen_brightness_mode'
1273 return boolean(self.execute(cmd).strip())
1274
1275 def set_brightness(self, value):
1276 if not 0 <= value <= 255:
1277 msg = 'Invalid brightness "{}"; Must be between 0 and 255'
1278 raise ValueError(msg.format(value))
1279 self.set_auto_brightness(False)
1280 cmd = 'settings put system screen_brightness {}'
1281 self.execute(cmd.format(int(value)))
1282
1283 def get_brightness(self):
1284 cmd = 'settings get system screen_brightness'
1285 return integer(self.execute(cmd).strip())
1286
Marc Bonnici3e751742017-07-06 17:31:02 +01001287 def get_airplane_mode(self):
1288 cmd = 'settings get global airplane_mode_on'
1289 return boolean(self.execute(cmd).strip())
1290
1291 def set_airplane_mode(self, mode):
1292 root_required = self.get_sdk_version() > 23
1293 if root_required and not self.is_rooted:
1294 raise TargetError('Root is required to toggle airplane mode on Android 7+')
Brendan Jackman0bfb6e42017-10-11 13:09:15 +01001295 mode = int(boolean(mode))
Marc Bonnici3e751742017-07-06 17:31:02 +01001296 cmd = 'settings put global airplane_mode_on {}'
Brendan Jackman0bfb6e42017-10-11 13:09:15 +01001297 self.execute(cmd.format(mode))
1298 self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE '
1299 '--ez state {}'.format(mode), as_root=root_required)
Marc Bonnici3e751742017-07-06 17:31:02 +01001300
Marc Bonnici003785d2017-07-24 17:44:33 +01001301 def get_auto_rotation(self):
1302 cmd = 'settings get system accelerometer_rotation'
1303 return boolean(self.execute(cmd).strip())
1304
1305 def set_auto_rotation(self, autorotate):
1306 cmd = 'settings put system accelerometer_rotation {}'
1307 self.execute(cmd.format(int(boolean(autorotate))))
1308
1309 def set_natural_rotation(self):
1310 self.set_rotation(0)
1311
1312 def set_left_rotation(self):
1313 self.set_rotation(1)
1314
1315 def set_inverted_rotation(self):
1316 self.set_rotation(2)
1317
1318 def set_right_rotation(self):
1319 self.set_rotation(3)
1320
1321 def get_rotation(self):
1322 cmd = 'settings get system user_rotation'
Brendan Jackmanfe403b62017-10-18 14:26:09 +01001323 return int(self.execute(cmd).strip())
Marc Bonnici003785d2017-07-24 17:44:33 +01001324
1325 def set_rotation(self, rotation):
1326 if not 0 <= rotation <= 3:
1327 raise ValueError('Rotation value must be between 0 and 3')
1328 self.set_auto_rotation(False)
1329 cmd = 'settings put system user_rotation {}'
1330 self.execute(cmd.format(rotation))
1331
Sergei Trofimov69a83d42017-05-12 11:54:31 +01001332 def homescreen(self):
1333 self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
1334
Sergei Trofimov961f9572015-11-18 17:32:26 +00001335 def _resolve_paths(self):
1336 if self.working_directory is None:
1337 self.working_directory = '/data/local/tmp/devlib-target'
1338 self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
1339 if self.executables_directory is None:
Sebastian Goscikff8261e2016-02-15 15:28:20 +00001340 self.executables_directory = '/data/local/tmp/bin'
Sergei Trofimov961f9572015-11-18 17:32:26 +00001341
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001342 def _ensure_executables_directory_is_writable(self):
1343 matched = []
1344 for entry in self.list_file_systems():
1345 if self.executables_directory.rstrip('/').startswith(entry.mount_point):
1346 matched.append(entry)
1347 if matched:
1348 entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
1349 if 'rw' not in entry.options:
1350 self.execute('mount -o rw,remount {} {}'.format(entry.device,
1351 entry.mount_point),
1352 as_root=True)
1353 else:
1354 message = 'Could not find mount point for executables directory {}'
1355 raise TargetError(message.format(self.executables_directory))
1356
Brendan Jackmanbaa32ec2017-02-09 11:53:11 +00001357 _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
1358
1359 @property
1360 def charging_enabled(self):
1361 """
1362 Whether drawing power to charge the battery is enabled
1363
1364 Not all devices have the ability to enable/disable battery charging
1365 (e.g. because they don't have a battery). In that case,
1366 ``charging_enabled`` is None.
1367 """
1368 if not self.file_exists(self._charging_enabled_path):
1369 return None
1370 return self.read_bool(self._charging_enabled_path)
1371
1372 @charging_enabled.setter
1373 def charging_enabled(self, enabled):
1374 """
1375 Enable/disable drawing power to charge the battery
1376
1377 Not all devices have this facility. In that case, do nothing.
1378 """
1379 if not self.file_exists(self._charging_enabled_path):
1380 return
1381 self.write_value(self._charging_enabled_path, int(bool(enabled)))
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001382
1383FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
1384PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
Sergei Trofimov5a81fe92016-01-27 16:34:26 +00001385LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001386
1387
1388class Cpuinfo(object):
1389
1390 @property
1391 @memoized
1392 def architecture(self):
1393 for section in self.sections:
1394 if 'CPU architecture' in section:
1395 return section['CPU architecture']
1396 if 'architecture' in section:
1397 return section['architecture']
1398
1399 @property
1400 @memoized
1401 def cpu_names(self):
1402 cpu_names = []
1403 global_name = None
1404 for section in self.sections:
1405 if 'processor' in section:
1406 if 'CPU part' in section:
1407 cpu_names.append(_get_part_name(section))
1408 elif 'model name' in section:
1409 cpu_names.append(_get_model_name(section))
1410 else:
1411 cpu_names.append(None)
1412 elif 'CPU part' in section:
1413 global_name = _get_part_name(section)
1414 return [caseless_string(c or global_name) for c in cpu_names]
1415
1416 def __init__(self, text):
1417 self.sections = None
1418 self.text = None
1419 self.parse(text)
1420
1421 @memoized
1422 def get_cpu_features(self, cpuid=0):
1423 global_features = []
1424 for section in self.sections:
1425 if 'processor' in section:
1426 if int(section.get('processor')) != cpuid:
1427 continue
1428 if 'Features' in section:
1429 return section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001430 elif 'flags' in section:
1431 return section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001432 elif 'Features' in section:
1433 global_features = section.get('Features').split()
Sergei Trofimov59f4f812015-12-15 18:07:34 +00001434 elif 'flags' in section:
1435 global_features = section.get('flags').split()
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001436 return global_features
1437
1438 def parse(self, text):
1439 self.sections = []
1440 current_section = {}
1441 self.text = text.strip()
1442 for line in self.text.split('\n'):
1443 line = line.strip()
1444 if line:
1445 key, value = line.split(':', 1)
1446 current_section[key.strip()] = value.strip()
1447 else: # not line
1448 self.sections.append(current_section)
1449 current_section = {}
1450 self.sections.append(current_section)
1451
1452 def __str__(self):
1453 return 'CpuInfo({})'.format(self.cpu_names)
1454
1455 __repr__ = __str__
1456
1457
1458class KernelVersion(object):
Brendan Jackman54adf802017-02-20 17:57:51 +00001459 """
1460 Class representing the version of a target kernel
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001461
Brendan Jackman54adf802017-02-20 17:57:51 +00001462 Not expected to work for very old (pre-3.0) kernel version numbers.
1463
1464 :ivar release: Version number/revision string. Typical output of
1465 ``uname -r``
1466 :type release: str
1467 :ivar version: Extra version info (aside from ``release``) reported by
1468 ``uname``
1469 :type version: str
1470 :ivar version_number: Main version number (e.g. 3 for Linux 3.18)
1471 :type version_number: int
1472 :ivar major: Major version number (e.g. 18 for Linux 3.18)
1473 :type major: int
1474 :ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
1475 be None
1476 :type minor: int
1477 :ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
1478 :type rc: int
1479 :ivar sha1: Kernel git revision hash, if available (otherwise None)
1480 :type sha1: str
Brendan Jackman18b77b82017-02-20 17:52:56 +00001481
1482 :ivar parts: Tuple of version number components. Can be used for
1483 lexicographically comparing kernel versions.
1484 :type parts: tuple(int)
Brendan Jackman54adf802017-02-20 17:57:51 +00001485 """
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001486 def __init__(self, version_string):
1487 if ' #' in version_string:
1488 release, version = version_string.split(' #')
1489 self.release = release
1490 self.version = version
1491 elif version_string.startswith('#'):
1492 self.release = ''
1493 self.version = version_string
1494 else:
1495 self.release = version_string
1496 self.version = ''
1497
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001498 self.version_number = None
1499 self.major = None
1500 self.minor = None
1501 self.sha1 = None
1502 self.rc = None
1503 match = KVERSION_REGEX.match(version_string)
1504 if match:
Brendan Jackman03561ee2017-02-20 17:51:17 +00001505 groups = match.groupdict()
1506 self.version_number = int(groups['version'])
1507 self.major = int(groups['major'])
1508 if groups['minor'] is not None:
1509 self.minor = int(groups['minor'])
1510 if groups['rc'] is not None:
1511 self.rc = int(groups['rc'])
1512 if groups['sha1'] is not None:
1513 self.sha1 = match.group('sha1')
Patrick Bellasi9a8d5392017-02-17 15:28:07 +00001514
Brendan Jackman18b77b82017-02-20 17:52:56 +00001515 self.parts = (self.version_number, self.major, self.minor)
1516
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001517 def __str__(self):
1518 return '{} {}'.format(self.release, self.version)
1519
1520 __repr__ = __str__
1521
1522
1523class KernelConfig(object):
1524
1525 not_set_regex = re.compile(r'# (\S+) is not set')
1526
1527 @staticmethod
1528 def get_config_name(name):
1529 name = name.upper()
1530 if not name.startswith('CONFIG_'):
1531 name = 'CONFIG_' + name
1532 return name
1533
1534 def iteritems(self):
1535 return self._config.iteritems()
1536
1537 def __init__(self, text):
1538 self.text = text
1539 self._config = {}
1540 for line in text.split('\n'):
1541 line = line.strip()
1542 if line.startswith('#'):
1543 match = self.not_set_regex.search(line)
1544 if match:
1545 self._config[match.group(1)] = 'n'
1546 elif '=' in line:
1547 name, value = line.split('=', 1)
1548 self._config[name.strip()] = value.strip()
1549
1550 def get(self, name):
1551 return self._config.get(self.get_config_name(name))
1552
1553 def like(self, name):
1554 regex = re.compile(name, re.I)
1555 result = {}
1556 for k, v in self._config.iteritems():
1557 if regex.search(k):
1558 result[k] = v
1559 return result
1560
1561 def is_enabled(self, name):
1562 return self.get(name) == 'y'
1563
1564 def is_module(self, name):
1565 return self.get(name) == 'm'
1566
1567 def is_not_set(self, name):
1568 return self.get(name) == 'n'
1569
1570 def has(self, name):
1571 return self.get(name) in ['m', 'y']
1572
1573
1574class LocalLinuxTarget(LinuxTarget):
1575
Sergei Trofimovbeaf8d42016-12-07 15:11:32 +00001576 def __init__(self,
1577 connection_settings=None,
1578 platform=None,
1579 working_directory=None,
1580 executables_directory=None,
1581 connect=True,
1582 modules=None,
1583 load_default_modules=True,
1584 shell_prompt=DEFAULT_SHELL_PROMPT,
1585 conn_cls=LocalConnection,
1586 ):
1587 super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
1588 platform=platform,
1589 working_directory=working_directory,
1590 executables_directory=executables_directory,
1591 connect=connect,
1592 modules=modules,
1593 load_default_modules=load_default_modules,
1594 shell_prompt=shell_prompt,
1595 conn_cls=conn_cls)
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001596
Sergei Trofimov961f9572015-11-18 17:32:26 +00001597 def _resolve_paths(self):
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001598 if self.working_directory is None:
1599 self.working_directory = '/tmp'
1600 if self.executables_directory is None:
1601 self.executables_directory = '/tmp'
Sergei Trofimov4e6afe92015-10-09 09:30:04 +01001602
1603
1604def _get_model_name(section):
1605 name_string = section['model name']
1606 parts = name_string.split('@')[0].strip().split()
1607 return ' '.join([p for p in parts
1608 if '(' not in p and p != 'CPU'])
1609
1610
1611def _get_part_name(section):
1612 implementer = section.get('CPU implementer', '0x0')
1613 part = section['CPU part']
1614 variant = section.get('CPU variant', '0x0')
1615 name = get_cpu_name(*map(integer, [implementer, part, variant]))
1616 if name is None:
1617 name = '{}/{}/{}'.format(implementer, part, variant)
1618 return name
Sergei Trofimov181bc182017-10-03 16:28:09 +01001619
1620
1621def _build_path_tree(path_map, basepath, sep=os.path.sep, dictcls=dict):
1622 """
1623 Convert a flat mapping of paths to values into a nested structure of
1624 dict-line object (``dict``'s by default), mirroring the directory hierarchy
1625 represented by the paths relative to ``basepath``.
1626
1627 """
1628 def process_node(node, path, value):
1629 parts = path.split(sep, 1)
1630 if len(parts) == 1: # leaf
1631 node[parts[0]] = value
1632 else: # branch
1633 if parts[0] not in node:
1634 node[parts[0]] = dictcls()
1635 process_node(node[parts[0]], parts[1], value)
1636
1637 relpath_map = {os.path.relpath(p, basepath): v
1638 for p, v in path_map.iteritems()}
1639
1640 if len(relpath_map) == 1 and relpath_map.keys()[0] == '.':
1641 result = relpath_map.values()[0]
1642 else:
1643 result = dictcls()
1644 for path, value in relpath_map.iteritems():
1645 process_node(result, path, value)
1646
1647 return result