2018-07-04 15:52:22 +01:00
|
|
|
# Copyright 2018 ARM Limited
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
#
|
|
|
|
|
2019-01-25 13:25:05 +00:00
|
|
|
import io
|
|
|
|
import base64
|
2020-06-16 11:56:25 +01:00
|
|
|
import functools
|
2019-01-25 13:25:05 +00:00
|
|
|
import gzip
|
2020-06-16 11:56:25 +01:00
|
|
|
import glob
|
2015-10-09 09:30:04 +01:00
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import time
|
|
|
|
import logging
|
|
|
|
import posixpath
|
|
|
|
import subprocess
|
2018-05-30 15:58:32 +01:00
|
|
|
import sys
|
2017-05-17 17:13:33 +01:00
|
|
|
import tarfile
|
2015-10-09 09:30:04 +01:00
|
|
|
import tempfile
|
|
|
|
import threading
|
2020-06-16 11:56:25 +01:00
|
|
|
import uuid
|
2018-03-21 14:57:05 +00:00
|
|
|
import xml.dom.minidom
|
2018-10-31 16:33:34 +00:00
|
|
|
import copy
|
2018-08-23 09:57:56 +01:00
|
|
|
from collections import namedtuple, defaultdict
|
2019-05-09 16:24:29 +01:00
|
|
|
from contextlib import contextmanager
|
2018-10-30 15:52:45 +00:00
|
|
|
from pipes import quote
|
2019-02-14 09:34:18 +00:00
|
|
|
from past.builtins import long
|
2018-12-17 11:53:52 +00:00
|
|
|
from past.types import basestring
|
|
|
|
from numbers import Number
|
|
|
|
try:
|
|
|
|
from collections.abc import Mapping
|
|
|
|
except ImportError:
|
|
|
|
from collections import Mapping
|
|
|
|
|
|
|
|
from enum import Enum
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
|
|
|
|
from devlib.module import get_module
|
|
|
|
from devlib.platform import Platform
|
2018-09-20 10:51:29 +01:00
|
|
|
from devlib.exception import (DevlibTransientError, TargetStableError,
|
|
|
|
TargetNotRespondingError, TimeoutError,
|
2019-09-20 15:56:02 +01:00
|
|
|
TargetTransientError, KernelConfigKeyError,
|
2020-06-16 11:56:25 +01:00
|
|
|
TargetError, HostError) # pylint: disable=redefined-builtin
|
2015-10-09 09:30:04 +01:00
|
|
|
from devlib.utils.ssh import SshConnection
|
2018-06-20 17:49:20 +01:00
|
|
|
from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, adb_disconnect, INTENT_FLAGS
|
2021-08-12 15:25:58 +01:00
|
|
|
from devlib.utils.misc import memoized, isiterable, convert_new_lines, groupby_value
|
2018-10-30 15:52:45 +00:00
|
|
|
from devlib.utils.misc import commonprefix, merge_lists
|
2017-11-09 17:02:11 +00:00
|
|
|
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list
|
2020-06-17 12:59:10 +01:00
|
|
|
from devlib.utils.misc import batch_contextmanager, tls_property, nullcontext
|
2018-06-13 17:13:08 +01:00
|
|
|
from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string, bytes_regex
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
|
2016-02-23 10:27:45 +00:00
|
|
|
FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
|
2021-09-27 14:45:55 -07:00
|
|
|
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|mWakefulness|Display Power: state)=([0-9]+|true|false|ON|OFF|DOZE|Asleep|Awake)',
|
2015-10-09 09:30:04 +01:00
|
|
|
re.IGNORECASE)
|
2018-10-19 18:29:08 +01:00
|
|
|
ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'cur=(?P<width>\d+)x(?P<height>\d+)')
|
2018-11-15 16:03:00 +00:00
|
|
|
ANDROID_SCREEN_ROTATION_REGEX = re.compile(r'orientation=(?P<rotation>[0-3])')
|
2018-04-25 17:20:28 +01:00
|
|
|
DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root|juno)@?.*:[/~]\S* *[#$] ',
|
2015-10-09 09:30:04 +01:00
|
|
|
re.MULTILINE)
|
2018-07-11 17:30:45 +01:00
|
|
|
KVERSION_REGEX = re.compile(
|
2020-02-28 10:10:23 +00:00
|
|
|
r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(-(?P<commits>\d+)-g(?P<sha1>[0-9a-fA-F]{7,}))?'
|
2017-02-17 15:28:07 +00:00
|
|
|
)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-10-18 11:28:08 +01:00
|
|
|
GOOGLE_DNS_SERVER_ADDRESS = '8.8.8.8'
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2018-03-15 16:34:45 +00:00
|
|
|
installed_package_info = namedtuple('installed_package_info', 'apk_path package')
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
|
|
|
|
def call_conn(f):
|
|
|
|
"""
|
|
|
|
Decorator to be used on all :class:`devlib.target.Target` methods that
|
|
|
|
directly use a method of ``self.conn``.
|
|
|
|
|
|
|
|
This ensures that if a call to any of the decorated method occurs while
|
|
|
|
executing, a new connection will be created in order to avoid possible
|
|
|
|
deadlocks. This can happen if e.g. a target's method is called from
|
|
|
|
``__del__``, which could be executed by the garbage collector, interrupting
|
|
|
|
another call to a method of the connection instance.
|
|
|
|
|
|
|
|
.. note:: This decorator could be applied directly to all methods with a
|
|
|
|
metaclass or ``__init_subclass__`` but it could create issues when
|
|
|
|
passing target methods as callbacks to connections' methods.
|
|
|
|
"""
|
|
|
|
|
|
|
|
@functools.wraps(f)
|
|
|
|
def wrapper(self, *args, **kwargs):
|
|
|
|
reentered = self.conn.is_in_use
|
|
|
|
disconnect = False
|
|
|
|
try:
|
|
|
|
# If the connection was already in use we need to use a different
|
|
|
|
# instance to avoid reentrancy deadlocks. This can happen even in
|
|
|
|
# single threaded code via __del__ implementations that can be
|
|
|
|
# called at any point.
|
|
|
|
if reentered:
|
|
|
|
# Shallow copy so we can use another connection instance
|
|
|
|
_self = copy.copy(self)
|
|
|
|
_self.conn = _self.get_connection()
|
|
|
|
assert self.conn is not _self.conn
|
|
|
|
disconnect = True
|
|
|
|
else:
|
|
|
|
_self = self
|
|
|
|
return f(_self, *args, **kwargs)
|
|
|
|
finally:
|
|
|
|
if disconnect:
|
|
|
|
_self.disconnect()
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
class Target(object):
|
|
|
|
|
|
|
|
path = None
|
|
|
|
os = None
|
2018-07-13 12:18:17 +01:00
|
|
|
system_id = None
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
default_modules = [
|
|
|
|
'hotplug',
|
|
|
|
'cpufreq',
|
|
|
|
'cpuidle',
|
|
|
|
'cgroups',
|
|
|
|
'hwmon',
|
|
|
|
]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def core_names(self):
|
|
|
|
return self.platform.core_names
|
|
|
|
|
|
|
|
@property
|
|
|
|
def core_clusters(self):
|
|
|
|
return self.platform.core_clusters
|
|
|
|
|
|
|
|
@property
|
|
|
|
def big_core(self):
|
|
|
|
return self.platform.big_core
|
|
|
|
|
|
|
|
@property
|
|
|
|
def little_core(self):
|
|
|
|
return self.platform.little_core
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_connected(self):
|
|
|
|
return self.conn is not None
|
|
|
|
|
|
|
|
@property
|
|
|
|
def connected_as_root(self):
|
2019-08-27 14:24:50 +01:00
|
|
|
return self.conn and self.conn.connected_as_root
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_rooted(self):
|
2019-08-27 14:28:23 +01:00
|
|
|
if self._is_rooted is None:
|
|
|
|
try:
|
|
|
|
self.execute('ls /', timeout=5, as_root=True)
|
|
|
|
self._is_rooted = True
|
2019-09-20 15:56:02 +01:00
|
|
|
except(TargetError, TimeoutError):
|
2019-08-27 14:28:23 +01:00
|
|
|
self._is_rooted = False
|
|
|
|
|
|
|
|
return self._is_rooted or self.connected_as_root
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2016-06-23 14:55:19 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def needs_su(self):
|
|
|
|
return not self.connected_as_root and self.is_rooted
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def kernel_version(self):
|
2018-10-30 15:05:03 +00:00
|
|
|
return KernelVersion(self.execute('{} uname -r -v'.format(quote(self.busybox))).strip())
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2021-04-07 17:23:26 +01:00
|
|
|
@property
|
|
|
|
def hostid(self):
|
|
|
|
return int(self.execute('{} hostid'.format(self.busybox)).strip(), 16)
|
|
|
|
|
2021-04-07 17:27:47 +01:00
|
|
|
@property
|
|
|
|
def hostname(self):
|
|
|
|
return self.execute('{} hostname'.format(self.busybox)).strip()
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
def os_version(self): # pylint: disable=no-self-use
|
|
|
|
return {}
|
|
|
|
|
2019-11-28 09:23:41 +00:00
|
|
|
@property
|
|
|
|
def model(self):
|
|
|
|
return self.platform.model
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
def abi(self): # pylint: disable=no-self-use
|
|
|
|
return None
|
|
|
|
|
2017-07-14 17:41:16 +01:00
|
|
|
@property
|
|
|
|
def supported_abi(self):
|
|
|
|
return [self.abi]
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def cpuinfo(self):
|
|
|
|
return Cpuinfo(self.execute('cat /proc/cpuinfo'))
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def number_of_cpus(self):
|
|
|
|
num_cpus = 0
|
|
|
|
corere = re.compile(r'^\s*cpu\d+\s*$')
|
2019-05-24 11:15:46 +01:00
|
|
|
output = self.execute('ls /sys/devices/system/cpu', as_root=self.is_rooted)
|
2015-10-09 09:30:04 +01:00
|
|
|
for entry in output.split():
|
|
|
|
if corere.match(entry):
|
|
|
|
num_cpus += 1
|
|
|
|
return num_cpus
|
|
|
|
|
2019-11-22 10:05:03 -05:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def number_of_nodes(self):
|
2020-01-20 17:15:58 +00:00
|
|
|
cmd = 'cd /sys/devices/system/node && {busybox} find . -maxdepth 1'.format(busybox=quote(self.busybox))
|
2020-01-20 17:19:34 +00:00
|
|
|
try:
|
|
|
|
output = self.execute(cmd, as_root=self.is_rooted)
|
|
|
|
except TargetStableError:
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
nodere = re.compile(r'^\./node\d+\s*$')
|
|
|
|
num_nodes = 0
|
|
|
|
for entry in output.splitlines():
|
|
|
|
if nodere.match(entry):
|
|
|
|
num_nodes += 1
|
|
|
|
return num_nodes
|
2019-11-22 10:05:03 -05:00
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def list_nodes_cpus(self):
|
|
|
|
nodes_cpus = []
|
|
|
|
for node in range(self.number_of_nodes):
|
|
|
|
path = self.path.join('/sys/devices/system/node/node{}/cpulist'.format(node))
|
|
|
|
output = self.read_value(path)
|
|
|
|
nodes_cpus.append(ranges_to_list(output))
|
|
|
|
return nodes_cpus
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def config(self):
|
|
|
|
try:
|
|
|
|
return KernelConfig(self.execute('zcat /proc/config.gz'))
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError:
|
2021-09-27 14:50:08 +01:00
|
|
|
for path in ['/boot/config-$({} uname -r)'.format(self.busybox), '/boot/config']:
|
2015-10-09 09:30:04 +01:00
|
|
|
try:
|
2021-09-27 14:50:08 +01:00
|
|
|
return KernelConfig(self.execute('cat {}'.format(path)))
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError:
|
2015-10-09 09:30:04 +01:00
|
|
|
pass
|
|
|
|
return KernelConfig('')
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def user(self):
|
|
|
|
return self.getenv('USER')
|
|
|
|
|
2018-11-02 10:56:19 +00:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def page_size_kb(self):
|
|
|
|
cmd = "cat /proc/self/smaps | {0} grep KernelPageSize | {0} head -n 1 | {0} awk '{{ print $2 }}'"
|
2020-11-04 17:56:12 +00:00
|
|
|
return int(self.execute(cmd.format(self.busybox)) or 0)
|
2018-11-02 10:56:19 +00:00
|
|
|
|
2017-10-03 16:47:11 +01:00
|
|
|
@property
|
|
|
|
def shutils(self):
|
|
|
|
if self._shutils is None:
|
|
|
|
self._setup_shutils()
|
|
|
|
return self._shutils
|
|
|
|
|
2020-01-15 17:16:47 +00:00
|
|
|
@tls_property
|
|
|
|
def _conn(self):
|
|
|
|
return self.get_connection()
|
|
|
|
|
|
|
|
# Add a basic property that does not require calling to get the value
|
|
|
|
conn = _conn.basic_property
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def __init__(self,
|
|
|
|
connection_settings=None,
|
|
|
|
platform=None,
|
|
|
|
working_directory=None,
|
|
|
|
executables_directory=None,
|
|
|
|
connect=True,
|
|
|
|
modules=None,
|
|
|
|
load_default_modules=True,
|
|
|
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
2016-12-07 15:11:32 +00:00
|
|
|
conn_cls=None,
|
2018-06-29 16:01:43 +01:00
|
|
|
is_container=False
|
2015-10-09 09:30:04 +01:00
|
|
|
):
|
2020-01-15 17:16:47 +00:00
|
|
|
|
2019-08-27 14:28:23 +01:00
|
|
|
self._is_rooted = None
|
2015-10-09 09:30:04 +01:00
|
|
|
self.connection_settings = connection_settings or {}
|
2017-01-31 13:11:03 +00:00
|
|
|
# Set self.platform: either it's given directly (by platform argument)
|
|
|
|
# or it's given in the connection_settings argument
|
|
|
|
# If neither, create default Platform()
|
|
|
|
if platform is None:
|
|
|
|
self.platform = self.connection_settings.get('platform', Platform())
|
|
|
|
else:
|
|
|
|
self.platform = platform
|
|
|
|
# Check if the user hasn't given two different platforms
|
|
|
|
if 'platform' in self.connection_settings:
|
|
|
|
if connection_settings['platform'] is not platform:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError('Platform specified in connection_settings '
|
2017-01-31 13:11:03 +00:00
|
|
|
'({}) differs from that directly passed '
|
|
|
|
'({})!)'
|
|
|
|
.format(connection_settings['platform'],
|
|
|
|
self.platform))
|
|
|
|
self.connection_settings['platform'] = self.platform
|
2015-10-09 09:30:04 +01:00
|
|
|
self.working_directory = working_directory
|
|
|
|
self.executables_directory = executables_directory
|
|
|
|
self.modules = modules or []
|
|
|
|
self.load_default_modules = load_default_modules
|
2018-06-13 17:13:08 +01:00
|
|
|
self.shell_prompt = bytes_regex(shell_prompt)
|
2016-12-07 15:11:32 +00:00
|
|
|
self.conn_cls = conn_cls
|
2018-06-29 16:01:43 +01:00
|
|
|
self.is_container = is_container
|
2015-10-09 09:30:04 +01:00
|
|
|
self.logger = logging.getLogger(self.__class__.__name__)
|
|
|
|
self._installed_binaries = {}
|
|
|
|
self._installed_modules = {}
|
|
|
|
self._cache = {}
|
2017-10-03 16:47:11 +01:00
|
|
|
self._shutils = None
|
2018-07-11 17:30:45 +01:00
|
|
|
self._file_transfer_cache = None
|
2015-10-09 09:30:04 +01:00
|
|
|
self.busybox = None
|
|
|
|
|
|
|
|
if load_default_modules:
|
|
|
|
module_lists = [self.default_modules]
|
|
|
|
else:
|
|
|
|
module_lists = []
|
|
|
|
module_lists += [self.modules, self.platform.modules]
|
|
|
|
self.modules = merge_lists(*module_lists, duplicates='first')
|
|
|
|
self._update_modules('early')
|
|
|
|
if connect:
|
|
|
|
self.connect()
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
def __copy__(self):
|
|
|
|
new = self.__class__.__new__(self.__class__)
|
|
|
|
new.__dict__ = self.__dict__.copy()
|
|
|
|
# Avoid sharing the connection instance with the original target, so
|
|
|
|
# that each target can live its own independent life
|
|
|
|
del new.__dict__['_conn']
|
|
|
|
return new
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
# connection and initialization
|
|
|
|
|
2018-03-02 16:03:43 +00:00
|
|
|
def connect(self, timeout=None, check_boot_completed=True):
|
2015-10-09 09:30:04 +01:00
|
|
|
self.platform.init_target_connection(self)
|
2020-01-15 17:16:47 +00:00
|
|
|
# Forcefully set the thread-local value for the connection, with the
|
|
|
|
# timeout we want
|
|
|
|
self.conn = self.get_connection(timeout=timeout)
|
2018-03-02 16:03:43 +00:00
|
|
|
if check_boot_completed:
|
|
|
|
self.wait_boot_complete(timeout)
|
2020-01-17 17:47:24 +00:00
|
|
|
self.check_connection()
|
2015-11-18 17:32:26 +00:00
|
|
|
self._resolve_paths()
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('mkdir -p {}'.format(quote(self.working_directory)))
|
|
|
|
self.execute('mkdir -p {}'.format(quote(self.executables_directory)))
|
2020-09-10 13:19:48 +01:00
|
|
|
self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'), timeout=30)
|
|
|
|
self.conn.busybox = self.busybox
|
2015-10-09 09:30:04 +01:00
|
|
|
self.platform.update_from_target(self)
|
2015-10-12 12:37:11 +01:00
|
|
|
self._update_modules('connected')
|
2015-10-09 09:30:04 +01:00
|
|
|
if self.platform.big_core and self.load_default_modules:
|
|
|
|
self._install_module(get_module('bl'))
|
|
|
|
|
2020-01-17 17:47:24 +00:00
|
|
|
def check_connection(self):
|
|
|
|
"""
|
|
|
|
Check that the connection works without obvious issues.
|
|
|
|
"""
|
|
|
|
out = self.execute('true', as_root=False)
|
|
|
|
if out.strip():
|
|
|
|
raise TargetStableError('The shell seems to not be functional and adds content to stderr: {}'.format(out))
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def disconnect(self):
|
2020-01-15 17:16:47 +00:00
|
|
|
connections = self._conn.get_all_values()
|
|
|
|
for conn in connections:
|
2015-10-09 09:30:04 +01:00
|
|
|
conn.close()
|
|
|
|
|
|
|
|
def get_connection(self, timeout=None):
|
2018-07-11 17:30:45 +01:00
|
|
|
if self.conn_cls is None:
|
2016-12-07 15:11:32 +00:00
|
|
|
raise ValueError('Connection class not specified on Target creation.')
|
2015-10-09 09:30:04 +01:00
|
|
|
return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
|
|
|
|
|
2018-03-02 16:03:43 +00:00
|
|
|
def wait_boot_complete(self, timeout=10):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def setup(self, executables=None):
|
2017-10-03 16:47:11 +01:00
|
|
|
self._setup_shutils()
|
2015-11-27 16:35:57 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
for host_exe in (executables or []): # pylint: disable=superfluous-parens
|
|
|
|
self.install(host_exe)
|
|
|
|
|
2017-01-31 13:11:03 +00:00
|
|
|
# Check for platform dependent setup procedures
|
|
|
|
self.platform.setup(self)
|
|
|
|
|
2016-05-13 18:15:51 +01:00
|
|
|
# Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
|
|
|
|
self._update_modules('setup')
|
|
|
|
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('mkdir -p {}'.format(quote(self._file_transfer_cache)))
|
2017-12-01 13:51:14 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def reboot(self, hard=False, connect=True, timeout=180):
|
|
|
|
if hard:
|
|
|
|
if not self.has('hard_reset'):
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError('Hard reset not supported for this target.')
|
2015-10-09 09:30:04 +01:00
|
|
|
self.hard_reset() # pylint: disable=no-member
|
|
|
|
else:
|
|
|
|
if not self.is_connected:
|
|
|
|
message = 'Cannot reboot target becuase it is disconnected. ' +\
|
|
|
|
'Either connect() first, or specify hard=True ' +\
|
|
|
|
'(in which case, a hard_reset module must be installed)'
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetTransientError(message)
|
2015-10-09 09:30:04 +01:00
|
|
|
self.reset()
|
2016-02-25 10:28:45 +00:00
|
|
|
# Wait a fixed delay before starting polling to give the target time to
|
|
|
|
# shut down, otherwise, might create the connection while it's still shutting
|
2018-01-11 15:20:12 +00:00
|
|
|
# down resulting in subsequent connection failing.
|
2016-02-25 10:28:45 +00:00
|
|
|
self.logger.debug('Waiting for target to power down...')
|
|
|
|
reset_delay = 20
|
|
|
|
time.sleep(reset_delay)
|
|
|
|
timeout = max(timeout - reset_delay, 10)
|
2015-10-09 09:30:04 +01:00
|
|
|
if self.has('boot'):
|
|
|
|
self.boot() # pylint: disable=no-member
|
2019-08-27 14:24:50 +01:00
|
|
|
self.conn.connected_as_root = None
|
2015-10-09 09:30:04 +01:00
|
|
|
if connect:
|
|
|
|
self.connect(timeout=timeout)
|
|
|
|
|
|
|
|
# file transfer
|
|
|
|
|
2020-06-16 11:56:25 +01:00
|
|
|
@contextmanager
|
|
|
|
def _xfer_cache_path(self, name):
|
|
|
|
"""
|
|
|
|
Context manager to provide a unique path in the transfer cache with the
|
|
|
|
basename of the given name.
|
|
|
|
"""
|
|
|
|
# Use a UUID to avoid race conditions on the target side
|
|
|
|
xfer_uuid = uuid.uuid4().hex
|
|
|
|
folder = self.path.join(self._file_transfer_cache, xfer_uuid)
|
|
|
|
# Make sure basename will work on folders too
|
|
|
|
name = os.path.normpath(name)
|
|
|
|
# Ensure the name is relative so that os.path.join() will actually
|
|
|
|
# join the paths rather than ignoring the first one.
|
|
|
|
name = './{}'.format(os.path.basename(name))
|
|
|
|
|
|
|
|
check_rm = False
|
|
|
|
try:
|
|
|
|
self.makedirs(folder)
|
|
|
|
# Don't check the exit code as the folder might not even exist
|
|
|
|
# before this point, if creating it failed
|
|
|
|
check_rm = True
|
|
|
|
yield self.path.join(folder, name)
|
|
|
|
finally:
|
|
|
|
self.execute('rm -rf -- {}'.format(quote(folder)), check_exit_code=check_rm)
|
|
|
|
|
2021-08-12 11:33:36 +01:00
|
|
|
def _prepare_xfer(self, action, sources, dest, pattern=None, as_root=False):
|
2020-06-16 11:56:25 +01:00
|
|
|
"""
|
|
|
|
Check the sanity of sources and destination and prepare the ground for
|
|
|
|
transfering multiple sources.
|
|
|
|
"""
|
2021-08-12 11:33:36 +01:00
|
|
|
|
|
|
|
once = functools.lru_cache(maxsize=None)
|
|
|
|
|
|
|
|
_target_cache = {}
|
|
|
|
def target_paths_kind(paths):
|
|
|
|
def process(x):
|
|
|
|
x = x.strip()
|
|
|
|
if x == 'notexist':
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return x
|
|
|
|
|
|
|
|
_paths = [
|
|
|
|
path
|
|
|
|
for path in paths
|
|
|
|
if path not in _target_cache
|
|
|
|
]
|
|
|
|
if _paths:
|
|
|
|
cmd = '; '.join(
|
|
|
|
'if [ -d {path} ]; then echo dir; elif [ -e {path} ]; then echo file; else echo notexist; fi'.format(
|
|
|
|
path=quote(path)
|
|
|
|
)
|
|
|
|
for path in _paths
|
|
|
|
)
|
|
|
|
res = self.execute(cmd)
|
|
|
|
_target_cache.update(zip(_paths, map(process, res.split())))
|
|
|
|
|
|
|
|
return [
|
|
|
|
_target_cache[path]
|
|
|
|
for path in paths
|
|
|
|
]
|
|
|
|
|
|
|
|
_host_cache = {}
|
|
|
|
def host_paths_kind(paths):
|
|
|
|
def path_kind(path):
|
|
|
|
if os.path.isdir(path):
|
|
|
|
return 'dir'
|
|
|
|
elif os.path.exists(path):
|
|
|
|
return 'file'
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
for path in paths:
|
|
|
|
if path not in _host_cache:
|
|
|
|
_host_cache[path] = path_kind(path)
|
|
|
|
|
|
|
|
return [
|
|
|
|
_host_cache[path]
|
|
|
|
for path in paths
|
|
|
|
]
|
|
|
|
|
|
|
|
# TODO: Target.remove() and Target.makedirs() would probably benefit
|
|
|
|
# from being implemented by connections, with the current
|
|
|
|
# implementation in ConnectionBase. This would allow SshConnection to
|
|
|
|
# use SFTP for these operations, which should be cheaper than
|
|
|
|
# Target.execute()
|
2020-06-16 11:56:25 +01:00
|
|
|
if action == 'push':
|
|
|
|
src_excep = HostError
|
2021-08-12 11:33:36 +01:00
|
|
|
src_path_kind = host_paths_kind
|
|
|
|
|
|
|
|
dst_mkdir = once(self.makedirs)
|
|
|
|
dst_path_join = self.path.join
|
|
|
|
dst_paths_kind = target_paths_kind
|
|
|
|
dst_remove_file = once(functools.partial(self.remove, as_root=as_root))
|
|
|
|
elif action == 'pull':
|
2020-06-16 11:56:25 +01:00
|
|
|
src_excep = TargetStableError
|
2021-08-12 11:33:36 +01:00
|
|
|
src_path_kind = target_paths_kind
|
|
|
|
|
|
|
|
dst_mkdir = once(functools.partial(os.makedirs, exist_ok=True))
|
|
|
|
dst_path_join = os.path.join
|
|
|
|
dst_paths_kind = host_paths_kind
|
|
|
|
dst_remove_file = once(os.remove)
|
|
|
|
else:
|
|
|
|
raise ValueError('Unknown action "{}"'.format(action))
|
|
|
|
|
|
|
|
def rewrite_dst(src, dst):
|
|
|
|
new_dst = dst_path_join(dst, os.path.basename(src))
|
|
|
|
|
|
|
|
src_kind, = src_path_kind([src])
|
|
|
|
# Batch both checks to avoid a costly extra execute()
|
|
|
|
dst_kind, new_dst_kind = dst_paths_kind([dst, new_dst])
|
|
|
|
|
|
|
|
if src_kind == 'file':
|
|
|
|
if dst_kind == 'dir':
|
|
|
|
if new_dst_kind == 'dir':
|
|
|
|
raise IsADirectoryError(new_dst)
|
|
|
|
if new_dst_kind == 'file':
|
|
|
|
dst_remove_file(new_dst)
|
|
|
|
return new_dst
|
|
|
|
else:
|
|
|
|
return new_dst
|
|
|
|
elif dst_kind == 'file':
|
|
|
|
dst_remove_file(dst)
|
|
|
|
return dst
|
|
|
|
else:
|
|
|
|
dst_mkdir(os.path.dirname(dst))
|
|
|
|
return dst
|
|
|
|
elif src_kind == 'dir':
|
|
|
|
if dst_kind == 'dir':
|
|
|
|
# Do not allow writing over an existing folder
|
|
|
|
if new_dst_kind == 'dir':
|
|
|
|
raise FileExistsError(new_dst)
|
|
|
|
if new_dst_kind == 'file':
|
|
|
|
raise FileExistsError(new_dst)
|
|
|
|
else:
|
|
|
|
return new_dst
|
|
|
|
elif dst_kind == 'file':
|
|
|
|
raise FileExistsError(dst_kind)
|
|
|
|
else:
|
|
|
|
dst_mkdir(os.path.dirname(dst))
|
|
|
|
return dst
|
2020-06-16 11:56:25 +01:00
|
|
|
else:
|
2021-08-12 11:33:36 +01:00
|
|
|
raise FileNotFoundError(src)
|
|
|
|
|
|
|
|
if pattern:
|
|
|
|
if not sources:
|
|
|
|
raise src_excep('No file matching source pattern: {}'.format(pattern))
|
|
|
|
|
|
|
|
if dst_path_exists(dest) and not dst_is_dir(dest):
|
|
|
|
raise NotADirectoryError('A folder dest is required for multiple matches but destination is a file: {}'.format(dest))
|
2020-06-16 11:56:25 +01:00
|
|
|
|
2021-08-12 15:25:58 +01:00
|
|
|
# TODO: since rewrite_dst() will currently return a different path for
|
|
|
|
# each source, it will not bring anything. In order to be useful,
|
|
|
|
# connections need to be able to understand that if the destination is
|
|
|
|
# an empty folder, the source is supposed to be transfered into it with
|
|
|
|
# the same basename.
|
|
|
|
return groupby_value({
|
2021-08-12 11:33:36 +01:00
|
|
|
src: rewrite_dst(src, dest)
|
|
|
|
for src in sources
|
2021-08-12 15:25:58 +01:00
|
|
|
})
|
2020-06-16 11:56:25 +01:00
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2020-06-16 11:56:25 +01:00
|
|
|
def push(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
|
2021-08-10 17:32:09 +01:00
|
|
|
source = str(source)
|
|
|
|
dest = str(dest)
|
|
|
|
|
2020-06-16 11:56:25 +01:00
|
|
|
sources = glob.glob(source) if globbing else [source]
|
2021-08-12 11:33:36 +01:00
|
|
|
mapping = self._prepare_xfer('push', sources, dest, pattern=source if globbing else None, as_root=as_root)
|
2020-06-16 11:56:25 +01:00
|
|
|
|
|
|
|
def do_push(sources, dest):
|
|
|
|
return self.conn.push(sources, dest, timeout=timeout)
|
|
|
|
|
|
|
|
if as_root:
|
2021-08-12 15:25:58 +01:00
|
|
|
for sources, dest in mapping.items():
|
|
|
|
for source in sources:
|
|
|
|
with self._xfer_cache_path(source) as device_tempfile:
|
|
|
|
do_push([source], device_tempfile)
|
|
|
|
self.execute("mv -f -- {} {}".format(quote(device_tempfile), quote(dest)), as_root=True)
|
2017-12-01 13:51:14 +00:00
|
|
|
else:
|
2021-08-12 15:25:58 +01:00
|
|
|
for sources, dest in mapping.items():
|
|
|
|
do_push(sources, dest)
|
2020-06-16 11:56:25 +01:00
|
|
|
|
|
|
|
def _expand_glob(self, pattern, **kwargs):
|
|
|
|
"""
|
|
|
|
Expand the given path globbing pattern on the target using the shell
|
|
|
|
globbing.
|
|
|
|
"""
|
|
|
|
# Since we split the results based on new lines, forbid them in the
|
|
|
|
# pattern
|
|
|
|
if '\n' in pattern:
|
|
|
|
raise ValueError(r'Newline character \n are not allowed in globbing patterns')
|
|
|
|
|
|
|
|
# If the pattern is in fact a plain filename, skip the expansion on the
|
|
|
|
# target to avoid an unncessary command execution.
|
|
|
|
#
|
|
|
|
# fnmatch char list from: https://docs.python.org/3/library/fnmatch.html
|
|
|
|
special_chars = ['*', '?', '[', ']']
|
|
|
|
if not any(char in pattern for char in special_chars):
|
|
|
|
return [pattern]
|
|
|
|
|
|
|
|
# Characters to escape that are impacting parameter splitting, since we
|
|
|
|
# want the pattern to be given in one piece. Unfortunately, there is no
|
|
|
|
# fool-proof way of doing that without also escaping globbing special
|
|
|
|
# characters such as wildcard which would defeat the entire purpose of
|
|
|
|
# that function.
|
|
|
|
for c in [' ', "'", '"']:
|
|
|
|
pattern = pattern.replace(c, '\\' + c)
|
|
|
|
|
|
|
|
cmd = "exec printf '%s\n' {}".format(pattern)
|
|
|
|
# Make sure to use the same shell everywhere for the path globbing,
|
|
|
|
# ensuring consistent results no matter what is the default platform
|
|
|
|
# shell
|
|
|
|
cmd = '{} sh -c {} 2>/dev/null'.format(quote(self.busybox), quote(cmd))
|
|
|
|
# On some shells, match failure will make the command "return" a
|
|
|
|
# non-zero code, even though the command was not actually called
|
|
|
|
result = self.execute(cmd, strip_colors=False, check_exit_code=False, **kwargs)
|
|
|
|
paths = result.splitlines()
|
|
|
|
if not paths:
|
|
|
|
raise TargetStableError('No file matching: {}'.format(pattern))
|
|
|
|
|
|
|
|
return paths
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2021-08-12 18:13:37 +01:00
|
|
|
def pull(self, source, dest, as_root=False, timeout=None, globbing=False, via_temp=False): # pylint: disable=arguments-differ
|
2021-08-10 17:32:09 +01:00
|
|
|
source = str(source)
|
|
|
|
dest = str(dest)
|
|
|
|
|
2020-06-16 11:56:25 +01:00
|
|
|
if globbing:
|
|
|
|
sources = self._expand_glob(source, as_root=as_root)
|
|
|
|
else:
|
|
|
|
sources = [source]
|
|
|
|
|
2021-08-12 18:13:37 +01:00
|
|
|
# The SSH server might not have the right permissions to read the file,
|
|
|
|
# so use a temporary copy instead.
|
|
|
|
via_temp |= as_root
|
|
|
|
|
2021-08-12 11:33:36 +01:00
|
|
|
mapping = self._prepare_xfer('pull', sources, dest, pattern=source if globbing else None, as_root=as_root)
|
2020-06-16 11:56:25 +01:00
|
|
|
|
|
|
|
def do_pull(sources, dest):
|
|
|
|
self.conn.pull(sources, dest, timeout=timeout)
|
|
|
|
|
2021-08-12 18:13:37 +01:00
|
|
|
if via_temp:
|
2021-08-12 15:25:58 +01:00
|
|
|
for sources, dest in mapping.items():
|
|
|
|
for source in sources:
|
|
|
|
with self._xfer_cache_path(source) as device_tempfile:
|
2021-08-12 18:13:37 +01:00
|
|
|
self.execute("cp -r -- {} {}".format(quote(source), quote(device_tempfile)), as_root=as_root)
|
|
|
|
self.execute("{} chmod 0644 -- {}".format(self.busybox, quote(device_tempfile)), as_root=as_root)
|
2021-08-12 15:25:58 +01:00
|
|
|
do_pull([device_tempfile], dest)
|
2017-12-01 13:51:14 +00:00
|
|
|
else:
|
2021-08-12 15:25:58 +01:00
|
|
|
for sources, dest in mapping.items():
|
|
|
|
do_pull(sources, dest)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2018-01-08 14:06:32 +00:00
|
|
|
def get_directory(self, source_dir, dest, as_root=False):
|
2017-05-17 17:13:33 +01:00
|
|
|
""" Pull a directory from the device, after compressing dir """
|
|
|
|
# Create all file names
|
|
|
|
tar_file_name = source_dir.lstrip(self.path.sep).replace(self.path.sep, '.')
|
|
|
|
# Host location of dir
|
|
|
|
outdir = os.path.join(dest, tar_file_name)
|
|
|
|
# Host location of archive
|
2018-07-11 17:30:45 +01:00
|
|
|
tar_file_name = '{}.tar'.format(tar_file_name)
|
|
|
|
tmpfile = os.path.join(dest, tar_file_name)
|
2017-05-17 17:13:33 +01:00
|
|
|
|
2018-01-08 14:06:32 +00:00
|
|
|
# If root is required, use tmp location for tar creation.
|
2020-06-17 12:59:10 +01:00
|
|
|
tar_file_cm = self._xfer_cache_path if as_root else nullcontext
|
2018-01-08 14:06:32 +00:00
|
|
|
|
2017-05-17 17:13:33 +01:00
|
|
|
# Does the folder exist?
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('ls -la {}'.format(quote(source_dir)), as_root=as_root)
|
2020-06-17 12:59:10 +01:00
|
|
|
|
|
|
|
with tar_file_cm(tar_file_name) as tar_file_name:
|
|
|
|
# Try compressing the folder
|
|
|
|
try:
|
|
|
|
self.execute('{} tar -cvf {} {}'.format(
|
|
|
|
quote(self.busybox), quote(tar_file_name), quote(source_dir)
|
|
|
|
), as_root=as_root)
|
|
|
|
except TargetStableError:
|
|
|
|
self.logger.debug('Failed to run tar command on target! ' \
|
|
|
|
'Not pulling directory {}'.format(source_dir))
|
|
|
|
# Pull the file
|
|
|
|
if not os.path.exists(dest):
|
|
|
|
os.mkdir(dest)
|
|
|
|
self.pull(tar_file_name, tmpfile)
|
|
|
|
# Decompress
|
|
|
|
with tarfile.open(tmpfile, 'r') as f:
|
|
|
|
f.extractall(outdir)
|
|
|
|
os.remove(tmpfile)
|
2017-05-17 17:13:33 +01:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
# execution
|
|
|
|
|
2021-03-23 11:01:56 +00:00
|
|
|
def _prepare_cmd(self, command, force_locale):
|
2019-09-03 12:44:43 +01:00
|
|
|
# Force the locale if necessary for more predictable output
|
|
|
|
if force_locale:
|
|
|
|
# Use an explicit export so that the command is allowed to be any
|
|
|
|
# shell statement, rather than just a command invocation
|
|
|
|
command = 'export LC_ALL={} && {}'.format(quote(force_locale), command)
|
|
|
|
|
2019-05-24 15:41:40 +01:00
|
|
|
# Ensure to use deployed command when availables
|
|
|
|
if self.executables_directory:
|
2019-09-03 14:26:13 +01:00
|
|
|
command = "export PATH={}:$PATH && {}".format(quote(self.executables_directory), command)
|
2019-09-03 12:44:43 +01:00
|
|
|
|
2021-03-23 11:01:56 +00:00
|
|
|
return command
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2021-03-23 11:01:56 +00:00
|
|
|
def execute(self, command, timeout=None, check_exit_code=True,
|
|
|
|
as_root=False, strip_colors=True, will_succeed=False,
|
|
|
|
force_locale='C'):
|
|
|
|
|
|
|
|
command = self._prepare_cmd(command, force_locale)
|
2018-10-30 10:00:54 +00:00
|
|
|
return self.conn.execute(command, timeout=timeout,
|
|
|
|
check_exit_code=check_exit_code, as_root=as_root,
|
|
|
|
strip_colors=strip_colors, will_succeed=will_succeed)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2021-03-23 11:01:56 +00:00
|
|
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False,
|
2021-03-24 10:19:23 +00:00
|
|
|
force_locale='C', timeout=None):
|
2021-03-23 11:01:56 +00:00
|
|
|
command = self._prepare_cmd(command, force_locale)
|
2021-03-24 10:19:23 +00:00
|
|
|
bg_cmd = self.conn.background(command, stdout, stderr, as_root)
|
|
|
|
if timeout is not None:
|
|
|
|
timer = threading.Timer(timeout, function=bg_cmd.cancel)
|
|
|
|
timer.daemon = True
|
|
|
|
timer.start()
|
|
|
|
return bg_cmd
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
|
2018-03-01 15:26:43 +00:00
|
|
|
redirect_stderr=False, as_root=False, timeout=30):
|
2015-10-09 09:30:04 +01:00
|
|
|
"""
|
|
|
|
Executes the specified binary under the specified conditions.
|
|
|
|
|
|
|
|
:binary: binary to execute. Must be present and executable on the device.
|
|
|
|
:args: arguments to be passed to the binary. The can be either a list or
|
|
|
|
a string.
|
|
|
|
:in_directory: execute the binary in the specified directory. This must
|
|
|
|
be an absolute path.
|
|
|
|
:on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
|
|
|
|
case, it will be interpreted as the mask), a list of ``ints``, in which
|
|
|
|
case this will be interpreted as the list of cpus, or string, which
|
|
|
|
will be interpreted as a comma-separated list of cpu ranges, e.g.
|
|
|
|
``"0,4-7"``.
|
|
|
|
:as_root: Specify whether the command should be run as root
|
|
|
|
:timeout: If the invocation does not terminate within this number of seconds,
|
|
|
|
a ``TimeoutError`` exception will be raised. Set to ``None`` if the
|
|
|
|
invocation should not timeout.
|
|
|
|
|
2016-11-15 16:58:57 +00:00
|
|
|
:returns: output of command.
|
2015-10-09 09:30:04 +01:00
|
|
|
"""
|
|
|
|
command = binary
|
|
|
|
if args:
|
|
|
|
if isiterable(args):
|
|
|
|
args = ' '.join(args)
|
|
|
|
command = '{} {}'.format(command, args)
|
|
|
|
if on_cpus:
|
|
|
|
on_cpus = bitmask(on_cpus)
|
2018-10-30 15:05:03 +00:00
|
|
|
command = '{} taskset 0x{:x} {}'.format(quote(self.busybox), on_cpus, command)
|
2015-10-09 09:30:04 +01:00
|
|
|
if in_directory:
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'cd {} && {}'.format(quote(in_directory), command)
|
2018-03-01 15:26:43 +00:00
|
|
|
if redirect_stderr:
|
|
|
|
command = '{} 2>&1'.format(command)
|
2015-10-09 09:30:04 +01:00
|
|
|
return self.execute(command, as_root=as_root, timeout=timeout)
|
|
|
|
|
2017-07-06 16:01:15 +01:00
|
|
|
def background_invoke(self, binary, args=None, in_directory=None,
|
|
|
|
on_cpus=None, as_root=False):
|
|
|
|
"""
|
|
|
|
Executes the specified binary as a background task under the
|
|
|
|
specified conditions.
|
|
|
|
|
|
|
|
:binary: binary to execute. Must be present and executable on the device.
|
|
|
|
:args: arguments to be passed to the binary. The can be either a list or
|
|
|
|
a string.
|
|
|
|
:in_directory: execute the binary in the specified directory. This must
|
|
|
|
be an absolute path.
|
|
|
|
:on_cpus: taskset the binary to these CPUs. This may be a single ``int`` (in which
|
|
|
|
case, it will be interpreted as the mask), a list of ``ints``, in which
|
|
|
|
case this will be interpreted as the list of cpus, or string, which
|
|
|
|
will be interpreted as a comma-separated list of cpu ranges, e.g.
|
|
|
|
``"0,4-7"``.
|
|
|
|
:as_root: Specify whether the command should be run as root
|
|
|
|
|
|
|
|
:returns: the subprocess instance handling that command
|
|
|
|
"""
|
|
|
|
command = binary
|
|
|
|
if args:
|
|
|
|
if isiterable(args):
|
|
|
|
args = ' '.join(args)
|
|
|
|
command = '{} {}'.format(command, args)
|
|
|
|
if on_cpus:
|
|
|
|
on_cpus = bitmask(on_cpus)
|
2018-10-30 15:05:03 +00:00
|
|
|
command = '{} taskset 0x{:x} {}'.format(quote(self.busybox), on_cpus, command)
|
2017-07-06 16:01:15 +01:00
|
|
|
if in_directory:
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'cd {} && {}'.format(quote(in_directory), command)
|
2017-07-06 16:01:15 +01:00
|
|
|
return self.background(command, as_root=as_root)
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def kick_off(self, command, as_root=False):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
# sysfs interaction
|
|
|
|
|
|
|
|
def read_value(self, path, kind=None):
|
2018-10-30 15:05:03 +00:00
|
|
|
output = self.execute('cat {}'.format(quote(path)), as_root=self.needs_su).strip() # pylint: disable=E1103
|
2015-10-09 09:30:04 +01:00
|
|
|
if kind:
|
|
|
|
return kind(output)
|
|
|
|
else:
|
|
|
|
return output
|
|
|
|
|
|
|
|
def read_int(self, path):
|
|
|
|
return self.read_value(path, kind=integer)
|
|
|
|
|
|
|
|
def read_bool(self, path):
|
|
|
|
return self.read_value(path, kind=boolean)
|
|
|
|
|
2019-05-09 16:24:29 +01:00
|
|
|
@contextmanager
|
|
|
|
def revertable_write_value(self, path, value, verify=True):
|
|
|
|
orig_value = self.read_value(path)
|
|
|
|
try:
|
|
|
|
self.write_value(path, value, verify)
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
self.write_value(path, orig_value, verify)
|
|
|
|
|
|
|
|
def batch_revertable_write_value(self, kwargs_list):
|
|
|
|
return batch_contextmanager(self.revertable_write_value, kwargs_list)
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def write_value(self, path, value, verify=True):
|
|
|
|
value = str(value)
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('echo {} > {}'.format(quote(value), quote(path)), check_exit_code=False, as_root=True)
|
2015-10-09 09:30:04 +01:00
|
|
|
if verify:
|
|
|
|
output = self.read_value(path)
|
|
|
|
if not output == value:
|
|
|
|
message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError(message)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def reset(self):
|
|
|
|
try:
|
2016-06-23 14:55:19 +01:00
|
|
|
self.execute('reboot', as_root=self.needs_su, timeout=2)
|
2020-06-24 17:04:07 +01:00
|
|
|
except (TargetError, subprocess.CalledProcessError):
|
2015-10-09 09:30:04 +01:00
|
|
|
# on some targets "reboot" doesn't return gracefully
|
|
|
|
pass
|
2019-08-27 14:24:50 +01:00
|
|
|
self.conn.connected_as_root = None
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2018-02-28 10:29:26 +00:00
|
|
|
def check_responsive(self, explode=True):
|
2015-10-09 09:30:04 +01:00
|
|
|
try:
|
|
|
|
self.conn.execute('ls /', timeout=5)
|
2019-12-05 15:51:38 +00:00
|
|
|
return True
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except (DevlibTransientError, subprocess.CalledProcessError):
|
2018-02-28 10:29:26 +00:00
|
|
|
if explode:
|
2018-04-20 17:04:26 +01:00
|
|
|
raise TargetNotRespondingError('Target {} is not responding'.format(self.conn.name))
|
2019-12-05 15:51:38 +00:00
|
|
|
return False
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
# process management
|
|
|
|
|
|
|
|
def kill(self, pid, signal=None, as_root=False):
|
|
|
|
signal_string = '-s {}'.format(signal) if signal else ''
|
2021-07-26 13:57:44 +01:00
|
|
|
self.execute('{} kill {} {}'.format(self.busybox, signal_string, pid), as_root=as_root)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def killall(self, process_name, signal=None, as_root=False):
|
|
|
|
for pid in self.get_pids_of(process_name):
|
2017-01-30 11:14:36 +00:00
|
|
|
try:
|
|
|
|
self.kill(pid, signal=signal, as_root=as_root)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError:
|
2017-01-30 11:14:36 +00:00
|
|
|
pass
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def get_pids_of(self, process_name):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def ps(self, **kwargs):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
# files
|
|
|
|
|
2020-06-18 11:23:00 +01:00
|
|
|
def makedirs(self, path):
|
2020-07-21 17:16:04 +01:00
|
|
|
self.execute('mkdir -p {}'.format(quote(path)))
|
2020-06-18 11:23:00 +01:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def file_exists(self, filepath):
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'if [ -e {} ]; then echo 1; else echo 0; fi'
|
|
|
|
output = self.execute(command.format(quote(filepath)), as_root=self.is_rooted)
|
2016-11-23 13:44:00 +00:00
|
|
|
return boolean(output.strip())
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2016-02-15 15:07:19 +00:00
|
|
|
def directory_exists(self, filepath):
|
2018-10-30 15:05:03 +00:00
|
|
|
output = self.execute('if [ -d {} ]; then echo 1; else echo 0; fi'.format(quote(filepath)))
|
2016-02-15 15:07:19 +00:00
|
|
|
# output from ssh my contain part of the expression in the buffer,
|
|
|
|
# split out everything except the last word.
|
|
|
|
return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def list_file_systems(self):
|
|
|
|
output = self.execute('mount')
|
|
|
|
fstab = []
|
|
|
|
for line in output.split('\n'):
|
|
|
|
line = line.strip()
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
match = FSTAB_ENTRY_REGEX.search(line)
|
|
|
|
if match:
|
|
|
|
fstab.append(FstabEntry(match.group(1), match.group(2),
|
|
|
|
match.group(3), match.group(4),
|
|
|
|
None, None))
|
|
|
|
else: # assume pre-M Android
|
|
|
|
fstab.append(FstabEntry(*line.split()))
|
|
|
|
return fstab
|
|
|
|
|
|
|
|
def list_directory(self, path, as_root=False):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def get_workpath(self, name):
|
|
|
|
return self.path.join(self.working_directory, name)
|
|
|
|
|
|
|
|
def tempfile(self, prefix='', suffix=''):
|
|
|
|
names = tempfile._get_candidate_names() # pylint: disable=W0212
|
2018-05-30 15:58:32 +01:00
|
|
|
for _ in range(tempfile.TMP_MAX):
|
|
|
|
name = next(names)
|
2015-10-09 09:30:04 +01:00
|
|
|
path = self.get_workpath(prefix + name + suffix)
|
|
|
|
if not self.file_exists(path):
|
|
|
|
return path
|
|
|
|
raise IOError('No usable temporary filename found')
|
|
|
|
|
|
|
|
def remove(self, path, as_root=False):
|
2020-06-18 11:26:58 +01:00
|
|
|
self.execute('rm -rf -- {}'.format(quote(path)), as_root=as_root)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
# misc
|
|
|
|
def core_cpus(self, core):
|
|
|
|
return [i for i, c in enumerate(self.core_names) if c == core]
|
|
|
|
|
|
|
|
def list_online_cpus(self, core=None):
|
|
|
|
path = self.path.join('/sys/devices/system/cpu/online')
|
|
|
|
output = self.read_value(path)
|
|
|
|
all_online = ranges_to_list(output)
|
|
|
|
if core:
|
|
|
|
cpus = self.core_cpus(core)
|
|
|
|
if not cpus:
|
|
|
|
raise ValueError(core)
|
|
|
|
return [o for o in all_online if o in cpus]
|
|
|
|
else:
|
|
|
|
return all_online
|
|
|
|
|
|
|
|
def list_offline_cpus(self):
|
|
|
|
online = self.list_online_cpus()
|
2018-05-30 15:58:32 +01:00
|
|
|
return [c for c in range(self.number_of_cpus)
|
2015-10-09 09:30:04 +01:00
|
|
|
if c not in online]
|
|
|
|
|
|
|
|
def getenv(self, variable):
|
|
|
|
return self.execute('echo ${}'.format(variable)).rstrip('\r\n')
|
|
|
|
|
|
|
|
def capture_screen(self, filepath):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def install(self, filepath, timeout=None, with_name=None):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
def uninstall(self, name):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2016-02-15 15:09:27 +00:00
|
|
|
def get_installed(self, name, search_system_binaries=True):
|
|
|
|
# Check user installed binaries first
|
2015-11-18 18:07:47 +00:00
|
|
|
if self.file_exists(self.executables_directory):
|
|
|
|
if name in self.list_directory(self.executables_directory):
|
|
|
|
return self.path.join(self.executables_directory, name)
|
2016-02-15 15:09:27 +00:00
|
|
|
# Fall back to binaries in PATH
|
|
|
|
if search_system_binaries:
|
|
|
|
for path in self.getenv('PATH').split(self.path.pathsep):
|
|
|
|
try:
|
|
|
|
if name in self.list_directory(path):
|
|
|
|
return self.path.join(path, name)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError:
|
2018-01-11 15:20:12 +00:00
|
|
|
pass # directory does not exist or no executable permissions
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
which = get_installed
|
|
|
|
|
2019-09-12 14:10:05 +01:00
|
|
|
def install_if_needed(self, host_path, search_system_binaries=True, timeout=None):
|
2016-02-15 15:11:38 +00:00
|
|
|
|
|
|
|
binary_path = self.get_installed(os.path.split(host_path)[1],
|
|
|
|
search_system_binaries=search_system_binaries)
|
|
|
|
if not binary_path:
|
2019-09-12 14:10:05 +01:00
|
|
|
binary_path = self.install(host_path, timeout=timeout)
|
2016-02-15 15:11:38 +00:00
|
|
|
return binary_path
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def is_installed(self, name):
|
|
|
|
return bool(self.get_installed(name))
|
|
|
|
|
|
|
|
def bin(self, name):
|
|
|
|
return self._installed_binaries.get(name, name)
|
|
|
|
|
|
|
|
def has(self, modname):
|
|
|
|
return hasattr(self, identifier(modname))
|
|
|
|
|
2016-01-27 16:34:26 +00:00
|
|
|
def lsmod(self):
|
|
|
|
lines = self.execute('lsmod').splitlines()
|
|
|
|
entries = []
|
|
|
|
for line in lines[1:]: # first line is the header
|
|
|
|
if not line.strip():
|
|
|
|
continue
|
|
|
|
parts = line.split()
|
|
|
|
name = parts[0]
|
|
|
|
size = int(parts[1])
|
|
|
|
use_count = int(parts[2])
|
|
|
|
if len(parts) > 3:
|
2016-01-27 17:02:59 +00:00
|
|
|
used_by = ''.join(parts[3:]).split(',')
|
2016-01-27 16:34:26 +00:00
|
|
|
else:
|
|
|
|
used_by = []
|
|
|
|
entries.append(LsmodEntry(name, size, use_count, used_by))
|
|
|
|
return entries
|
|
|
|
|
2016-01-27 17:02:59 +00:00
|
|
|
def insmod(self, path):
|
|
|
|
target_path = self.get_workpath(os.path.basename(path))
|
|
|
|
self.push(path, target_path)
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('insmod {}'.format(quote(target_path)), as_root=True)
|
2016-01-27 17:02:59 +00:00
|
|
|
|
2016-07-14 11:00:24 +01:00
|
|
|
|
|
|
|
def extract(self, path, dest=None):
|
|
|
|
"""
|
2018-01-11 15:20:12 +00:00
|
|
|
Extract the specified on-target file. The extraction method to be used
|
2016-07-14 11:00:24 +01:00
|
|
|
(unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
|
|
|
|
If ``dest`` is specified, it must be an existing directory on target;
|
|
|
|
the extracted contents will be placed there.
|
|
|
|
|
2018-01-11 15:20:12 +00:00
|
|
|
Note that, depending on the archive file format (and therefore the
|
2016-07-14 11:00:24 +01:00
|
|
|
extraction method used), the original archive file may or may not exist
|
|
|
|
after the extraction.
|
|
|
|
|
|
|
|
The return value is the path to the extracted contents. In case of
|
|
|
|
gunzip and bunzip2, this will be path to the extracted file; for tar
|
|
|
|
and uzip, this will be the directory with the extracted file(s)
|
2018-01-11 15:20:12 +00:00
|
|
|
(``dest`` if it was specified otherwise, the directory that contained
|
2016-07-14 11:00:24 +01:00
|
|
|
the archive).
|
|
|
|
|
|
|
|
"""
|
|
|
|
for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
|
|
|
|
'.tgz', '.tbz', '.tbz2']:
|
|
|
|
if path.endswith(ending):
|
|
|
|
return self._extract_archive(path, 'tar xf {} -C {}', dest)
|
|
|
|
|
|
|
|
ext = self.path.splitext(path)[1]
|
|
|
|
if ext in ['.bz', '.bz2']:
|
|
|
|
return self._extract_file(path, 'bunzip2 -f {}', dest)
|
|
|
|
elif ext == '.gz':
|
|
|
|
return self._extract_file(path, 'gunzip -f {}', dest)
|
|
|
|
elif ext == '.zip':
|
|
|
|
return self._extract_archive(path, 'unzip {} -d {}', dest)
|
|
|
|
else:
|
|
|
|
raise ValueError('Unknown compression format: {}'.format(ext))
|
|
|
|
|
2017-05-12 11:54:31 +01:00
|
|
|
def sleep(self, duration):
|
|
|
|
timeout = duration + 10
|
|
|
|
self.execute('sleep {}'.format(duration), timeout=timeout)
|
|
|
|
|
2019-01-31 11:11:00 +00:00
|
|
|
def read_tree_tar_flat(self, path, depth=1, check_exit_code=True,
|
2019-01-25 13:25:05 +00:00
|
|
|
decode_unicode=True, strip_null_chars=True):
|
|
|
|
command = 'read_tree_tgz_b64 {} {} {}'.format(quote(path), depth,
|
|
|
|
quote(self.working_directory))
|
2017-10-03 16:28:09 +01:00
|
|
|
output = self._execute_util(command, as_root=self.is_rooted,
|
|
|
|
check_exit_code=check_exit_code)
|
2018-08-23 09:57:56 +01:00
|
|
|
|
2019-01-25 13:25:05 +00:00
|
|
|
result = {}
|
|
|
|
|
|
|
|
# Unpack the archive in memory
|
|
|
|
tar_gz = base64.b64decode(output)
|
|
|
|
tar_gz_bytes = io.BytesIO(tar_gz)
|
|
|
|
tar_buf = gzip.GzipFile(fileobj=tar_gz_bytes).read()
|
|
|
|
tar_bytes = io.BytesIO(tar_buf)
|
|
|
|
with tarfile.open(fileobj=tar_bytes) as tar:
|
|
|
|
for member in tar.getmembers():
|
|
|
|
try:
|
|
|
|
content_f = tar.extractfile(member)
|
|
|
|
# ignore exotic members like sockets
|
|
|
|
except Exception:
|
|
|
|
continue
|
|
|
|
# if it is a file and not a folder
|
|
|
|
if content_f:
|
|
|
|
content = content_f.read()
|
|
|
|
if decode_unicode:
|
|
|
|
try:
|
|
|
|
content = content.decode('utf-8').strip()
|
|
|
|
if strip_null_chars:
|
|
|
|
content = content.replace('\x00', '').strip()
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
content = ''
|
|
|
|
|
|
|
|
name = self.path.join(path, member.name)
|
|
|
|
result[name] = content
|
2018-08-23 09:57:56 +01:00
|
|
|
|
2017-10-03 16:28:09 +01:00
|
|
|
return result
|
|
|
|
|
2019-01-31 11:11:00 +00:00
|
|
|
def read_tree_values_flat(self, path, depth=1, check_exit_code=True):
|
|
|
|
command = 'read_tree_values {} {}'.format(quote(path), depth)
|
|
|
|
output = self._execute_util(command, as_root=self.is_rooted,
|
|
|
|
check_exit_code=check_exit_code)
|
|
|
|
|
|
|
|
accumulator = defaultdict(list)
|
|
|
|
for entry in output.strip().split('\n'):
|
|
|
|
if ':' not in entry:
|
|
|
|
continue
|
|
|
|
path, value = entry.strip().split(':', 1)
|
|
|
|
accumulator[path].append(value)
|
|
|
|
|
|
|
|
result = {k: '\n'.join(v).strip() for k, v in accumulator.items()}
|
|
|
|
return result
|
|
|
|
|
2019-01-25 13:25:05 +00:00
|
|
|
def read_tree_values(self, path, depth=1, dictcls=dict,
|
2019-01-31 11:11:00 +00:00
|
|
|
check_exit_code=True, tar=False, decode_unicode=True,
|
2019-01-25 13:25:05 +00:00
|
|
|
strip_null_chars=True):
|
|
|
|
"""
|
|
|
|
Reads the content of all files under a given tree
|
|
|
|
|
|
|
|
:path: path to the tree
|
|
|
|
:depth: maximum tree depth to read
|
|
|
|
:dictcls: type of the dict used to store the results
|
|
|
|
:check_exit_code: raise an exception if the shutil command fails
|
2019-01-31 11:11:00 +00:00
|
|
|
:tar: fetch the entire tree using tar rather than just the value (more
|
|
|
|
robust but slower in some use-cases)
|
|
|
|
:decode_unicode: decode the content of tar-ed files as utf-8
|
2019-01-25 13:25:05 +00:00
|
|
|
:strip_null_chars: remove '\x00' chars from the content of utf-8
|
|
|
|
decoded files
|
|
|
|
|
|
|
|
:returns: a tree-like dict with the content of files as leafs
|
|
|
|
"""
|
2019-01-31 11:11:00 +00:00
|
|
|
if not tar:
|
|
|
|
value_map = self.read_tree_values_flat(path, depth, check_exit_code)
|
|
|
|
else:
|
|
|
|
value_map = self.read_tree_tar_flat(path, depth, check_exit_code,
|
|
|
|
decode_unicode,
|
|
|
|
strip_null_chars)
|
2018-01-11 15:20:12 +00:00
|
|
|
return _build_path_tree(value_map, path, self.path.sep, dictcls)
|
2017-10-03 16:28:09 +01:00
|
|
|
|
2019-10-21 17:58:32 +01:00
|
|
|
def install_module(self, mod, **params):
|
|
|
|
mod = get_module(mod)
|
|
|
|
if mod.stage == 'early':
|
|
|
|
msg = 'Module {} cannot be installed after device setup has already occoured.'
|
|
|
|
raise TargetStableError(msg)
|
|
|
|
|
|
|
|
if mod.probe(self):
|
|
|
|
self._install_module(mod, **params)
|
|
|
|
else:
|
|
|
|
msg = 'Module {} is not supported by the target'.format(mod.name)
|
|
|
|
raise TargetStableError(msg)
|
|
|
|
|
2016-07-14 11:00:24 +01:00
|
|
|
# internal methods
|
|
|
|
|
2017-10-03 16:47:11 +01:00
|
|
|
def _setup_shutils(self):
|
|
|
|
shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
|
2018-01-18 13:18:27 +00:00
|
|
|
tmp_dir = tempfile.mkdtemp()
|
|
|
|
shutils_ofile = os.path.join(tmp_dir, 'shutils')
|
2017-10-03 16:47:11 +01:00
|
|
|
shell_path = '/bin/sh'
|
|
|
|
if self.os == 'android':
|
|
|
|
shell_path = '/system/bin/sh'
|
|
|
|
with open(shutils_ifile) as fh:
|
|
|
|
lines = fh.readlines()
|
|
|
|
with open(shutils_ofile, 'w') as ofile:
|
|
|
|
for line in lines:
|
|
|
|
line = line.replace("__DEVLIB_SHELL__", shell_path)
|
|
|
|
line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
|
|
|
|
ofile.write(line)
|
2017-12-12 13:36:27 +00:00
|
|
|
self._shutils = self.install(shutils_ofile)
|
|
|
|
os.remove(shutils_ofile)
|
2018-01-18 13:18:27 +00:00
|
|
|
os.rmdir(tmp_dir)
|
2017-10-03 16:47:11 +01:00
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2017-05-12 11:48:19 +01:00
|
|
|
def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
|
|
|
|
command = '{} {}'.format(self.shutils, command)
|
|
|
|
return self.conn.execute(command, timeout, check_exit_code, as_root)
|
|
|
|
|
2016-07-14 11:00:24 +01:00
|
|
|
def _extract_archive(self, path, cmd, dest=None):
|
|
|
|
cmd = '{} ' + cmd # busybox
|
|
|
|
if dest:
|
|
|
|
extracted = dest
|
|
|
|
else:
|
|
|
|
extracted = self.path.dirname(path)
|
2018-10-30 15:05:03 +00:00
|
|
|
cmdtext = cmd.format(quote(self.busybox), quote(path), quote(extracted))
|
2016-07-14 11:00:24 +01:00
|
|
|
self.execute(cmdtext)
|
|
|
|
return extracted
|
|
|
|
|
|
|
|
def _extract_file(self, path, cmd, dest=None):
|
|
|
|
cmd = '{} ' + cmd # busybox
|
2018-10-30 15:05:03 +00:00
|
|
|
cmdtext = cmd.format(quote(self.busybox), quote(path))
|
2016-07-14 11:00:24 +01:00
|
|
|
self.execute(cmdtext)
|
|
|
|
extracted = self.path.splitext(path)[0]
|
|
|
|
if dest:
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('mv -f {} {}'.format(quote(extracted), quote(dest)))
|
2016-07-14 11:00:24 +01:00
|
|
|
if dest.endswith('/'):
|
|
|
|
extracted = self.path.join(dest, self.path.basename(extracted))
|
|
|
|
else:
|
|
|
|
extracted = dest
|
|
|
|
return extracted
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def _update_modules(self, stage):
|
2018-10-31 16:33:34 +00:00
|
|
|
for mod_name in copy.copy(self.modules):
|
|
|
|
if isinstance(mod_name, dict):
|
|
|
|
mod_name, params = list(mod_name.items())[0]
|
2015-10-09 09:30:04 +01:00
|
|
|
else:
|
|
|
|
params = {}
|
2018-10-31 16:33:34 +00:00
|
|
|
mod = get_module(mod_name)
|
2015-10-09 09:30:04 +01:00
|
|
|
if not mod.stage == stage:
|
|
|
|
continue
|
|
|
|
if mod.probe(self):
|
|
|
|
self._install_module(mod, **params)
|
|
|
|
else:
|
2016-01-18 12:27:48 +00:00
|
|
|
msg = 'Module {} is not supported by the target'.format(mod.name)
|
2018-10-31 16:33:34 +00:00
|
|
|
self.modules.remove(mod_name)
|
2016-01-18 12:27:48 +00:00
|
|
|
if self.load_default_modules:
|
|
|
|
self.logger.debug(msg)
|
|
|
|
else:
|
|
|
|
self.logger.warning(msg)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def _install_module(self, mod, **params):
|
2020-02-18 14:46:51 +00:00
|
|
|
name = mod.name
|
|
|
|
if name not in self._installed_modules:
|
|
|
|
self.logger.debug('Installing module {}'.format(name))
|
2019-10-21 17:58:43 +01:00
|
|
|
try:
|
|
|
|
mod.install(self, **params)
|
|
|
|
except Exception as e:
|
2020-02-18 14:46:51 +00:00
|
|
|
self.logger.error('Module "{}" failed to install on target'.format(name))
|
2019-10-21 17:58:43 +01:00
|
|
|
raise
|
2020-02-18 14:46:51 +00:00
|
|
|
self._installed_modules[name] = mod
|
|
|
|
if name not in self.modules:
|
|
|
|
self.modules.append(name)
|
2015-10-09 09:30:04 +01:00
|
|
|
else:
|
2020-02-18 14:46:51 +00:00
|
|
|
self.logger.debug('Module {} is already installed.'.format(name))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2015-11-18 17:32:26 +00:00
|
|
|
def _resolve_paths(self):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
2017-10-18 11:28:08 +01:00
|
|
|
def is_network_connected(self):
|
|
|
|
self.logger.debug('Checking for internet connectivity...')
|
|
|
|
|
|
|
|
timeout_s = 5
|
|
|
|
# It would be nice to use busybox for this, but that means we'd need
|
|
|
|
# root (ping is usually setuid so it can open raw sockets to send ICMP)
|
|
|
|
command = 'ping -q -c 1 -w {} {} 2>&1'.format(timeout_s,
|
2018-10-30 15:05:03 +00:00
|
|
|
quote(GOOGLE_DNS_SERVER_ADDRESS))
|
2017-10-18 11:28:08 +01:00
|
|
|
|
|
|
|
# We'll use our own retrying mechanism (rather than just using ping's -c
|
|
|
|
# to send multiple packets) so that we don't slow things down in the
|
|
|
|
# 'good' case where the first packet gets echoed really quickly.
|
2017-10-24 16:03:10 +01:00
|
|
|
attempts = 5
|
|
|
|
for _ in range(attempts):
|
2017-10-18 11:28:08 +01:00
|
|
|
try:
|
|
|
|
self.execute(command)
|
|
|
|
return True
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError as e:
|
2017-10-18 11:28:08 +01:00
|
|
|
err = str(e).lower()
|
|
|
|
if '100% packet loss' in err:
|
|
|
|
# We sent a packet but got no response.
|
|
|
|
# Try again - we don't want this to fail just because of a
|
|
|
|
# transient drop in connection quality.
|
|
|
|
self.logger.debug('No ping response from {} after {}s'
|
|
|
|
.format(GOOGLE_DNS_SERVER_ADDRESS, timeout_s))
|
|
|
|
continue
|
|
|
|
elif 'network is unreachable' in err:
|
|
|
|
# No internet connection at all, we can fail straight away
|
|
|
|
self.logger.debug('Network unreachable')
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
# Something else went wrong, we don't know what, raise an
|
|
|
|
# error.
|
|
|
|
raise
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-10-24 16:03:10 +01:00
|
|
|
self.logger.debug('Failed to ping {} after {} attempts'.format(
|
|
|
|
GOOGLE_DNS_SERVER_ADDRESS, attempts))
|
|
|
|
return False
|
|
|
|
|
2018-07-13 12:18:17 +01:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
class LinuxTarget(Target):
|
|
|
|
|
|
|
|
path = posixpath
|
|
|
|
os = 'linux'
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def abi(self):
|
2016-06-16 13:31:53 +01:00
|
|
|
value = self.execute('uname -m').strip()
|
2018-05-30 15:58:32 +01:00
|
|
|
for abi, architectures in ABI_MAP.items():
|
2015-10-09 09:30:04 +01:00
|
|
|
if value in architectures:
|
|
|
|
result = abi
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
result = value
|
|
|
|
return result
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def os_version(self):
|
|
|
|
os_version = {}
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
|
|
|
|
version_files = self.execute(command, check_exit_code=False).strip().split()
|
|
|
|
for vf in version_files:
|
|
|
|
name = self.path.basename(vf)
|
|
|
|
output = self.read_value(vf)
|
|
|
|
os_version[name] = convert_new_lines(output.strip()).replace('\n', ' ')
|
2015-10-09 09:30:04 +01:00
|
|
|
return os_version
|
|
|
|
|
2018-07-13 12:18:17 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def system_id(self):
|
|
|
|
return self._execute_util('get_linux_system_id').strip()
|
|
|
|
|
2016-12-07 15:11:32 +00:00
|
|
|
def __init__(self,
|
|
|
|
connection_settings=None,
|
|
|
|
platform=None,
|
|
|
|
working_directory=None,
|
|
|
|
executables_directory=None,
|
|
|
|
connect=True,
|
|
|
|
modules=None,
|
|
|
|
load_default_modules=True,
|
|
|
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
|
|
|
conn_cls=SshConnection,
|
2018-06-29 16:01:43 +01:00
|
|
|
is_container=False,
|
2016-12-07 15:11:32 +00:00
|
|
|
):
|
|
|
|
super(LinuxTarget, self).__init__(connection_settings=connection_settings,
|
|
|
|
platform=platform,
|
|
|
|
working_directory=working_directory,
|
|
|
|
executables_directory=executables_directory,
|
|
|
|
connect=connect,
|
|
|
|
modules=modules,
|
|
|
|
load_default_modules=load_default_modules,
|
|
|
|
shell_prompt=shell_prompt,
|
2018-06-29 16:01:43 +01:00
|
|
|
conn_cls=conn_cls,
|
|
|
|
is_container=is_container)
|
2016-12-07 15:11:32 +00:00
|
|
|
|
2018-03-07 08:04:08 +00:00
|
|
|
def wait_boot_complete(self, timeout=10):
|
|
|
|
pass
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2015-10-09 09:30:04 +01:00
|
|
|
def kick_off(self, command, as_root=False):
|
2018-10-30 15:52:45 +00:00
|
|
|
command = 'sh -c {} 1>/dev/null 2>/dev/null &'.format(quote(command))
|
2015-10-09 09:30:04 +01:00
|
|
|
return self.conn.execute(command, as_root=as_root)
|
|
|
|
|
|
|
|
def get_pids_of(self, process_name):
|
|
|
|
"""Returns a list of PIDs of all processes with the specified name."""
|
|
|
|
# result should be a column of PIDs with the first row as "PID" header
|
2018-10-30 15:05:03 +00:00
|
|
|
result = self.execute('ps -C {} -o pid'.format(quote(process_name)), # NOQA
|
2015-10-09 09:30:04 +01:00
|
|
|
check_exit_code=False).strip().split()
|
|
|
|
if len(result) >= 2: # at least one row besides the header
|
2018-05-30 15:58:32 +01:00
|
|
|
return list(map(int, result[1:]))
|
2015-10-09 09:30:04 +01:00
|
|
|
else:
|
|
|
|
return []
|
|
|
|
|
2020-07-10 10:12:25 +01:00
|
|
|
def ps(self, threads=False, **kwargs):
|
|
|
|
ps_flags = '-eo'
|
|
|
|
if threads:
|
|
|
|
ps_flags = '-eLo'
|
|
|
|
command = 'ps {} user,pid,tid,ppid,vsize,rss,wchan,pcpu,state,fname'.format(ps_flags)
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
lines = iter(convert_new_lines(self.execute(command)).split('\n'))
|
2018-05-30 15:58:32 +01:00
|
|
|
next(lines) # header
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
result = []
|
|
|
|
for line in lines:
|
2020-07-10 10:12:25 +01:00
|
|
|
parts = re.split(r'\s+', line, maxsplit=9)
|
2015-10-09 09:30:04 +01:00
|
|
|
if parts and parts != ['']:
|
2020-07-10 10:12:25 +01:00
|
|
|
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:6])) + parts[6:])))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
if not kwargs:
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
filtered_result = []
|
|
|
|
for entry in result:
|
2018-05-30 15:58:32 +01:00
|
|
|
if all(getattr(entry, k) == v for k, v in kwargs.items()):
|
2015-10-09 09:30:04 +01:00
|
|
|
filtered_result.append(entry)
|
|
|
|
return filtered_result
|
|
|
|
|
|
|
|
def list_directory(self, path, as_root=False):
|
2018-10-30 15:52:45 +00:00
|
|
|
contents = self.execute('ls -1 {}'.format(quote(path)), as_root=as_root)
|
2015-10-09 09:30:04 +01:00
|
|
|
return [x.strip() for x in contents.split('\n') if x.strip()]
|
|
|
|
|
|
|
|
def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
|
|
|
|
destpath = self.path.join(self.executables_directory,
|
|
|
|
with_name and with_name or self.path.basename(filepath))
|
2019-09-12 14:10:05 +01:00
|
|
|
self.push(filepath, destpath, timeout=timeout)
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('chmod a+x {}'.format(quote(destpath)), timeout=timeout)
|
2015-10-09 09:30:04 +01:00
|
|
|
self._installed_binaries[self.path.basename(destpath)] = destpath
|
|
|
|
return destpath
|
|
|
|
|
|
|
|
def uninstall(self, name):
|
|
|
|
path = self.path.join(self.executables_directory, name)
|
|
|
|
self.remove(path)
|
|
|
|
|
|
|
|
def capture_screen(self, filepath):
|
|
|
|
if not self.is_installed('scrot'):
|
|
|
|
self.logger.debug('Could not take screenshot as scrot is not installed.')
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
|
|
|
|
tmpfile = self.tempfile()
|
2018-04-11 10:48:49 +01:00
|
|
|
cmd = 'DISPLAY=:0.0 scrot {} && {} date -u -Iseconds'
|
2018-10-30 15:05:03 +00:00
|
|
|
ts = self.execute(cmd.format(quote(tmpfile), quote(self.busybox))).strip()
|
2018-04-11 10:48:49 +01:00
|
|
|
filepath = filepath.format(ts=ts)
|
2015-10-09 09:30:04 +01:00
|
|
|
self.pull(tmpfile, filepath)
|
|
|
|
self.remove(tmpfile)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError as e:
|
2015-10-09 09:30:04 +01:00
|
|
|
if "Can't open X dispay." not in e.message:
|
|
|
|
raise e
|
|
|
|
message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
|
|
|
|
self.logger.debug('Could not take screenshot: {}'.format(message))
|
|
|
|
|
2015-11-18 17:32:26 +00:00
|
|
|
def _resolve_paths(self):
|
|
|
|
if self.working_directory is None:
|
2018-09-07 18:35:40 +01:00
|
|
|
self.working_directory = self.path.join(self.execute("pwd").strip(), 'devlib-target')
|
2017-12-01 13:51:14 +00:00
|
|
|
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
|
2015-11-18 17:32:26 +00:00
|
|
|
if self.executables_directory is None:
|
|
|
|
self.executables_directory = self.path.join(self.working_directory, 'bin')
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
class AndroidTarget(Target):
|
|
|
|
|
|
|
|
path = posixpath
|
|
|
|
os = 'android'
|
2016-09-27 12:08:33 +01:00
|
|
|
ls_command = ''
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def abi(self):
|
|
|
|
return self.getprop()['ro.product.cpu.abi'].split('-')[0]
|
|
|
|
|
2017-07-14 17:41:16 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def supported_abi(self):
|
|
|
|
props = self.getprop()
|
|
|
|
result = [props['ro.product.cpu.abi']]
|
|
|
|
if 'ro.product.cpu.abi2' in props:
|
|
|
|
result.append(props['ro.product.cpu.abi2'])
|
|
|
|
if 'ro.product.cpu.abilist' in props:
|
|
|
|
for abi in props['ro.product.cpu.abilist'].split(','):
|
|
|
|
if abi not in result:
|
|
|
|
result.append(abi)
|
|
|
|
|
|
|
|
mapped_result = []
|
|
|
|
for supported_abi in result:
|
2018-05-30 15:58:32 +01:00
|
|
|
for abi, architectures in ABI_MAP.items():
|
2017-07-14 17:41:16 +01:00
|
|
|
found = False
|
|
|
|
if supported_abi in architectures and abi not in mapped_result:
|
|
|
|
mapped_result.append(abi)
|
|
|
|
found = True
|
|
|
|
break
|
|
|
|
if not found and supported_abi not in mapped_result:
|
|
|
|
mapped_result.append(supported_abi)
|
|
|
|
return mapped_result
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def os_version(self):
|
|
|
|
os_version = {}
|
|
|
|
for k, v in self.getprop().iteritems():
|
|
|
|
if k.startswith('ro.build.version'):
|
|
|
|
part = k.split('.')[-1]
|
|
|
|
os_version[part] = v
|
|
|
|
return os_version
|
|
|
|
|
|
|
|
@property
|
|
|
|
def adb_name(self):
|
2020-05-01 17:57:49 +01:00
|
|
|
return getattr(self.conn, 'device', None)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def adb_server(self):
|
|
|
|
return getattr(self.conn, 'adb_server', None)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2016-02-15 15:17:32 +00:00
|
|
|
@property
|
2016-02-23 17:10:01 +00:00
|
|
|
@memoized
|
2016-02-15 15:17:32 +00:00
|
|
|
def android_id(self):
|
|
|
|
"""
|
|
|
|
Get the device's ANDROID_ID. Which is
|
|
|
|
|
|
|
|
"A 64-bit number (as a hex string) that is randomly generated when the user
|
|
|
|
first sets up the device and should remain constant for the lifetime of the
|
|
|
|
user's device."
|
|
|
|
|
|
|
|
.. note:: This will get reset on userdata erasure.
|
|
|
|
|
|
|
|
"""
|
|
|
|
output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
|
|
|
|
return output.split('value=')[-1]
|
|
|
|
|
2018-07-13 12:18:17 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def system_id(self):
|
|
|
|
return self._execute_util('get_android_system_id').strip()
|
|
|
|
|
2017-11-07 09:21:57 +00:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def external_storage(self):
|
|
|
|
return self.execute('echo $EXTERNAL_STORAGE').strip()
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def screen_resolution(self):
|
2018-10-19 18:29:08 +01:00
|
|
|
output = self.execute('dumpsys window displays')
|
2015-10-09 09:30:04 +01:00
|
|
|
match = ANDROID_SCREEN_RESOLUTION_REGEX.search(output)
|
|
|
|
if match:
|
|
|
|
return (int(match.group('width')),
|
|
|
|
int(match.group('height')))
|
|
|
|
else:
|
|
|
|
return (0, 0)
|
|
|
|
|
2016-02-16 17:24:19 +00:00
|
|
|
def __init__(self,
|
|
|
|
connection_settings=None,
|
|
|
|
platform=None,
|
|
|
|
working_directory=None,
|
|
|
|
executables_directory=None,
|
|
|
|
connect=True,
|
|
|
|
modules=None,
|
|
|
|
load_default_modules=True,
|
|
|
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
2016-12-07 15:11:32 +00:00
|
|
|
conn_cls=AdbConnection,
|
2016-02-16 17:24:19 +00:00
|
|
|
package_data_directory="/data/data",
|
2018-06-29 16:01:43 +01:00
|
|
|
is_container=False,
|
2016-02-16 17:24:19 +00:00
|
|
|
):
|
|
|
|
super(AndroidTarget, self).__init__(connection_settings=connection_settings,
|
|
|
|
platform=platform,
|
|
|
|
working_directory=working_directory,
|
|
|
|
executables_directory=executables_directory,
|
|
|
|
connect=connect,
|
|
|
|
modules=modules,
|
|
|
|
load_default_modules=load_default_modules,
|
2016-12-07 15:11:32 +00:00
|
|
|
shell_prompt=shell_prompt,
|
2018-06-29 16:01:43 +01:00
|
|
|
conn_cls=conn_cls,
|
|
|
|
is_container=is_container)
|
2016-02-16 17:24:19 +00:00
|
|
|
self.package_data_directory = package_data_directory
|
2017-10-09 17:08:38 +01:00
|
|
|
self.clear_logcat_lock = threading.Lock()
|
2016-02-16 17:24:19 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def reset(self, fastboot=False): # pylint: disable=arguments-differ
|
|
|
|
try:
|
|
|
|
self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
|
2016-06-23 14:55:19 +01:00
|
|
|
as_root=self.needs_su, timeout=2)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except (DevlibTransientError, subprocess.CalledProcessError):
|
2015-10-09 09:30:04 +01:00
|
|
|
# on some targets "reboot" doesn't return gracefully
|
|
|
|
pass
|
2019-08-27 14:24:50 +01:00
|
|
|
self.conn.connected_as_root = None
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-07-12 12:27:28 +01:00
|
|
|
def wait_boot_complete(self, timeout=10):
|
2015-10-09 09:30:04 +01:00
|
|
|
start = time.time()
|
2017-07-12 12:27:28 +01:00
|
|
|
boot_completed = boolean(self.getprop('sys.boot_completed'))
|
|
|
|
while not boot_completed and timeout >= time.time() - start:
|
|
|
|
time.sleep(5)
|
|
|
|
boot_completed = boolean(self.getprop('sys.boot_completed'))
|
|
|
|
if not boot_completed:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
# Raise a TargetStableError as this usually happens because of
|
|
|
|
# an issue with Android more than a timeout that is too small.
|
|
|
|
raise TargetStableError('Connected but Android did not fully boot.')
|
2017-07-12 12:27:28 +01:00
|
|
|
|
2017-12-12 16:23:00 +00:00
|
|
|
def connect(self, timeout=30, check_boot_completed=True): # pylint: disable=arguments-differ
|
2015-10-09 09:30:04 +01:00
|
|
|
device = self.connection_settings.get('device')
|
2018-03-02 16:03:43 +00:00
|
|
|
super(AndroidTarget, self).connect(timeout=timeout, check_boot_completed=check_boot_completed)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2016-05-27 16:26:30 +01:00
|
|
|
def kick_off(self, command, as_root=None):
|
2015-10-09 09:30:04 +01:00
|
|
|
"""
|
|
|
|
Like execute but closes adb session and returns immediately, leaving the command running on the
|
|
|
|
device (this is different from execute(background=True) which keeps adb connection open and returns
|
|
|
|
a subprocess object).
|
|
|
|
"""
|
2016-05-27 16:26:30 +01:00
|
|
|
if as_root is None:
|
2016-06-23 14:55:19 +01:00
|
|
|
as_root = self.needs_su
|
2015-10-09 09:30:04 +01:00
|
|
|
try:
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'cd {} && {} nohup {} &'.format(quote(self.working_directory), quote(self.busybox), command)
|
2018-07-11 17:30:45 +01:00
|
|
|
self.execute(command, timeout=1, as_root=as_root)
|
2015-10-09 09:30:04 +01:00
|
|
|
except TimeoutError:
|
|
|
|
pass
|
|
|
|
|
2016-09-27 12:08:33 +01:00
|
|
|
def __setup_list_directory(self):
|
|
|
|
# In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
|
|
|
|
# AOSP 7.0 as well, the ls command was changed.
|
|
|
|
# Previous versions default to a single column listing, which is nice and easy to parse.
|
|
|
|
# Newer versions default to a multi-column listing, which is not, but it does support
|
|
|
|
# a '-1' option to get into single column mode. Older versions do not support this option
|
|
|
|
# so we try the new version, and if it fails we use the old version.
|
|
|
|
self.ls_command = 'ls -1'
|
|
|
|
try:
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('ls -1 {}'.format(quote(self.working_directory)), as_root=False)
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError:
|
2016-09-27 12:08:33 +01:00
|
|
|
self.ls_command = 'ls'
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def list_directory(self, path, as_root=False):
|
2016-09-27 12:08:33 +01:00
|
|
|
if self.ls_command == '':
|
|
|
|
self.__setup_list_directory()
|
2018-10-30 15:05:03 +00:00
|
|
|
contents = self.execute('{} {}'.format(self.ls_command, quote(path)), as_root=as_root)
|
2015-10-09 09:30:04 +01:00
|
|
|
return [x.strip() for x in contents.split('\n') if x.strip()]
|
|
|
|
|
|
|
|
def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
|
|
|
|
ext = os.path.splitext(filepath)[1].lower()
|
|
|
|
if ext == '.apk':
|
|
|
|
return self.install_apk(filepath, timeout)
|
|
|
|
else:
|
2019-09-12 14:10:05 +01:00
|
|
|
return self.install_executable(filepath, with_name, timeout)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def uninstall(self, name):
|
|
|
|
if self.package_is_installed(name):
|
|
|
|
self.uninstall_package(name)
|
|
|
|
else:
|
|
|
|
self.uninstall_executable(name)
|
|
|
|
|
|
|
|
def get_pids_of(self, process_name):
|
2017-09-22 17:39:17 +01:00
|
|
|
result = []
|
|
|
|
search_term = process_name[-15:]
|
|
|
|
for entry in self.ps():
|
|
|
|
if search_term in entry.name:
|
|
|
|
result.append(entry.pid)
|
|
|
|
return result
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2020-07-10 10:12:25 +01:00
|
|
|
def ps(self, threads=False, **kwargs):
|
|
|
|
maxsplit = 9 if threads else 8
|
|
|
|
command = 'ps'
|
|
|
|
if threads:
|
|
|
|
command = 'ps -AT'
|
|
|
|
|
|
|
|
lines = iter(convert_new_lines(self.execute(command)).split('\n'))
|
2018-05-30 15:58:32 +01:00
|
|
|
next(lines) # header
|
2015-10-09 09:30:04 +01:00
|
|
|
result = []
|
|
|
|
for line in lines:
|
2020-07-10 10:12:25 +01:00
|
|
|
parts = line.split(None, maxsplit)
|
2017-09-26 13:30:15 +01:00
|
|
|
if not parts:
|
|
|
|
continue
|
2020-07-10 10:12:25 +01:00
|
|
|
|
|
|
|
wchan_missing = False
|
|
|
|
if len(parts) == maxsplit:
|
|
|
|
wchan_missing = True
|
|
|
|
|
|
|
|
if not threads:
|
|
|
|
# Duplicate PID into TID location.
|
|
|
|
parts.insert(2, parts[1])
|
|
|
|
|
|
|
|
if wchan_missing:
|
2017-09-26 13:30:15 +01:00
|
|
|
# wchan was blank; insert an empty field where it should be.
|
2020-07-10 10:12:25 +01:00
|
|
|
parts.insert(6, '')
|
|
|
|
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:6])) + parts[6:])))
|
2015-10-09 09:30:04 +01:00
|
|
|
if not kwargs:
|
|
|
|
return result
|
|
|
|
else:
|
|
|
|
filtered_result = []
|
|
|
|
for entry in result:
|
2018-05-30 15:58:32 +01:00
|
|
|
if all(getattr(entry, k) == v for k, v in kwargs.items()):
|
2015-10-09 09:30:04 +01:00
|
|
|
filtered_result.append(entry)
|
|
|
|
return filtered_result
|
|
|
|
|
|
|
|
def capture_screen(self, filepath):
|
|
|
|
on_device_file = self.path.join(self.working_directory, 'screen_capture.png')
|
2018-04-11 10:48:49 +01:00
|
|
|
cmd = 'screencap -p {} && {} date -u -Iseconds'
|
2018-10-30 15:05:03 +00:00
|
|
|
ts = self.execute(cmd.format(quote(on_device_file), quote(self.busybox))).strip()
|
2018-04-11 10:48:49 +01:00
|
|
|
filepath = filepath.format(ts=ts)
|
2015-10-09 09:30:04 +01:00
|
|
|
self.pull(on_device_file, filepath)
|
|
|
|
self.remove(on_device_file)
|
|
|
|
|
|
|
|
# Android-specific
|
|
|
|
|
2018-06-20 17:59:22 +01:00
|
|
|
def input_tap(self, x, y):
|
|
|
|
command = 'input tap {} {}'
|
|
|
|
self.execute(command.format(x, y))
|
|
|
|
|
|
|
|
def input_tap_pct(self, x, y):
|
2015-10-09 09:30:04 +01:00
|
|
|
width, height = self.screen_resolution
|
2018-06-20 17:59:22 +01:00
|
|
|
|
|
|
|
x = (x * width) // 100
|
|
|
|
y = (y * height) // 100
|
|
|
|
|
|
|
|
self.input_tap(x, y)
|
|
|
|
|
|
|
|
def input_swipe(self, x1, y1, x2, y2):
|
|
|
|
"""
|
|
|
|
Issue a swipe on the screen from (x1, y1) to (x2, y2)
|
|
|
|
Uses absolute screen positions
|
|
|
|
"""
|
2015-10-09 09:30:04 +01:00
|
|
|
command = 'input swipe {} {} {} {}'
|
2018-06-20 17:59:22 +01:00
|
|
|
self.execute(command.format(x1, y1, x2, y2))
|
|
|
|
|
|
|
|
def input_swipe_pct(self, x1, y1, x2, y2):
|
|
|
|
"""
|
|
|
|
Issue a swipe on the screen from (x1, y1) to (x2, y2)
|
|
|
|
Uses percent-based positions
|
|
|
|
"""
|
|
|
|
width, height = self.screen_resolution
|
|
|
|
|
|
|
|
x1 = (x1 * width) // 100
|
|
|
|
y1 = (y1 * height) // 100
|
|
|
|
x2 = (x2 * width) // 100
|
|
|
|
y2 = (y2 * height) // 100
|
|
|
|
|
|
|
|
self.input_swipe(x1, y1, x2, y2)
|
|
|
|
|
|
|
|
def swipe_to_unlock(self, direction="diagonal"):
|
|
|
|
width, height = self.screen_resolution
|
2017-08-10 17:19:13 +01:00
|
|
|
if direction == "diagonal":
|
|
|
|
start = 100
|
|
|
|
stop = width - start
|
|
|
|
swipe_height = height * 2 // 3
|
2018-06-20 17:59:22 +01:00
|
|
|
self.input_swipe(start, swipe_height, stop, 0)
|
2017-08-10 17:19:13 +01:00
|
|
|
elif direction == "horizontal":
|
2017-07-26 11:29:10 +01:00
|
|
|
swipe_height = height * 2 // 3
|
2016-02-15 15:19:47 +00:00
|
|
|
start = 100
|
|
|
|
stop = width - start
|
2018-06-20 17:59:22 +01:00
|
|
|
self.input_swipe(start, swipe_height, stop, swipe_height)
|
2017-07-26 11:29:10 +01:00
|
|
|
elif direction == "vertical":
|
|
|
|
swipe_middle = width / 2
|
|
|
|
swipe_height = height * 2 // 3
|
2018-06-20 17:59:22 +01:00
|
|
|
self.input_swipe(swipe_middle, swipe_height, swipe_middle, 0)
|
2016-02-15 15:19:47 +00:00
|
|
|
else:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError("Invalid swipe direction: {}".format(direction))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def getprop(self, prop=None):
|
|
|
|
props = AndroidProperties(self.execute('getprop'))
|
|
|
|
if prop:
|
|
|
|
return props[prop]
|
|
|
|
return props
|
|
|
|
|
2018-03-21 14:57:05 +00:00
|
|
|
def capture_ui_hierarchy(self, filepath):
|
|
|
|
on_target_file = self.get_workpath('screen_capture.xml')
|
|
|
|
self.execute('uiautomator dump {}'.format(on_target_file))
|
|
|
|
self.pull(on_target_file, filepath)
|
|
|
|
self.remove(on_target_file)
|
|
|
|
|
|
|
|
parsed_xml = xml.dom.minidom.parse(filepath)
|
|
|
|
with open(filepath, 'w') as f:
|
2018-05-30 15:58:32 +01:00
|
|
|
if sys.version_info[0] == 3:
|
|
|
|
f.write(parsed_xml.toprettyxml())
|
|
|
|
else:
|
|
|
|
f.write(parsed_xml.toprettyxml().encode('utf-8'))
|
2018-03-21 14:57:05 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def is_installed(self, name):
|
|
|
|
return super(AndroidTarget, self).is_installed(name) or self.package_is_installed(name)
|
|
|
|
|
|
|
|
def package_is_installed(self, package_name):
|
|
|
|
return package_name in self.list_packages()
|
|
|
|
|
|
|
|
def list_packages(self):
|
|
|
|
output = self.execute('pm list packages')
|
|
|
|
output = output.replace('package:', '')
|
|
|
|
return output.split()
|
|
|
|
|
|
|
|
def get_package_version(self, package):
|
2018-10-30 15:05:03 +00:00
|
|
|
output = self.execute('dumpsys package {}'.format(quote(package)))
|
2015-10-09 09:30:04 +01:00
|
|
|
for line in convert_new_lines(output).split('\n'):
|
|
|
|
if 'versionName' in line:
|
|
|
|
return line.split('=', 1)[1]
|
|
|
|
return None
|
|
|
|
|
2018-03-15 16:34:45 +00:00
|
|
|
def get_package_info(self, package):
|
2018-10-30 15:05:03 +00:00
|
|
|
output = self.execute('pm list packages -f {}'.format(quote(package)))
|
2018-03-15 16:34:45 +00:00
|
|
|
for entry in output.strip().split('\n'):
|
|
|
|
rest, entry_package = entry.rsplit('=', 1)
|
|
|
|
if entry_package != package:
|
|
|
|
continue
|
|
|
|
_, apk_path = rest.split(':')
|
|
|
|
return installed_package_info(apk_path, entry_package)
|
|
|
|
|
2017-05-31 15:56:50 +01:00
|
|
|
def get_sdk_version(self):
|
|
|
|
try:
|
|
|
|
return int(self.getprop('ro.build.version.sdk'))
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
return None
|
|
|
|
|
2017-05-31 15:51:31 +01:00
|
|
|
def install_apk(self, filepath, timeout=None, replace=False, allow_downgrade=False): # pylint: disable=W0221
|
2015-10-09 09:30:04 +01:00
|
|
|
ext = os.path.splitext(filepath)[1].lower()
|
|
|
|
if ext == '.apk':
|
2017-05-31 15:51:31 +01:00
|
|
|
flags = []
|
|
|
|
if replace:
|
|
|
|
flags.append('-r') # Replace existing APK
|
|
|
|
if allow_downgrade:
|
|
|
|
flags.append('-d') # Install the APK even if a newer version is already installed
|
|
|
|
if self.get_sdk_version() >= 23:
|
|
|
|
flags.append('-g') # Grant all runtime permissions
|
|
|
|
self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
|
2019-08-27 14:46:09 +01:00
|
|
|
if isinstance(self.conn, AdbConnection):
|
2020-05-01 17:57:49 +01:00
|
|
|
return adb_command(self.adb_name, "install {} {}".format(' '.join(flags), quote(filepath)),
|
|
|
|
timeout=timeout, adb_server=self.adb_server)
|
2019-08-27 14:46:09 +01:00
|
|
|
else:
|
|
|
|
dev_path = self.get_workpath(filepath.rsplit(os.path.sep, 1)[-1])
|
|
|
|
self.push(quote(filepath), dev_path, timeout=timeout)
|
|
|
|
result = self.execute("pm install {} {}".format(' '.join(flags), quote(dev_path)), timeout=timeout)
|
|
|
|
self.remove(dev_path)
|
|
|
|
return result
|
2015-10-09 09:30:04 +01:00
|
|
|
else:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError('Can\'t install {}: unsupported format.'.format(filepath))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-11-07 09:22:39 +00:00
|
|
|
def grant_package_permission(self, package, permission):
|
|
|
|
try:
|
2018-10-30 15:05:03 +00:00
|
|
|
return self.execute('pm grant {} {}'.format(quote(package), quote(permission)))
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
except TargetStableError as e:
|
2017-11-07 09:22:39 +00:00
|
|
|
if 'is not a changeable permission type' in e.message:
|
|
|
|
pass # Ignore if unchangeable
|
|
|
|
elif 'Unknown permission' in e.message:
|
|
|
|
pass # Ignore if unknown
|
2017-11-07 09:25:43 +00:00
|
|
|
elif 'has not requested permission' in e.message:
|
|
|
|
pass # Ignore if not requested
|
2018-03-14 17:22:58 +00:00
|
|
|
elif 'Operation not allowed' in e.message:
|
|
|
|
pass # Ignore if not allowed
|
2017-11-07 09:22:39 +00:00
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
2017-11-09 17:02:11 +00:00
|
|
|
def refresh_files(self, file_list):
|
|
|
|
"""
|
|
|
|
Depending on the android version and root status, determine the
|
|
|
|
appropriate method of forcing a re-index of the mediaserver cache for a given
|
|
|
|
list of files.
|
|
|
|
"""
|
|
|
|
if self.is_rooted or self.get_sdk_version() < 24: # MM and below
|
|
|
|
common_path = commonprefix(file_list, sep=self.path.sep)
|
|
|
|
self.broadcast_media_mounted(common_path, self.is_rooted)
|
|
|
|
else:
|
|
|
|
for f in file_list:
|
|
|
|
self.broadcast_media_scan_file(f)
|
|
|
|
|
|
|
|
def broadcast_media_scan_file(self, filepath):
|
|
|
|
"""
|
|
|
|
Force a re-index of the mediaserver cache for the specified file.
|
|
|
|
"""
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d {}'
|
|
|
|
self.execute(command.format(quote('file://' + filepath)))
|
2017-11-09 17:02:11 +00:00
|
|
|
|
|
|
|
def broadcast_media_mounted(self, dirpath, as_root=False):
|
|
|
|
"""
|
|
|
|
Force a re-index of the mediaserver cache for the specified directory.
|
|
|
|
"""
|
2018-10-30 15:05:03 +00:00
|
|
|
command = 'am broadcast -a android.intent.action.MEDIA_MOUNTED -d {} '\
|
2017-11-21 13:25:45 +00:00
|
|
|
'-n com.android.providers.media/.MediaScannerReceiver'
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute(command.format(quote('file://'+dirpath)), as_root=as_root)
|
2017-11-09 17:02:11 +00:00
|
|
|
|
2019-09-12 14:10:05 +01:00
|
|
|
def install_executable(self, filepath, with_name=None, timeout=None):
|
2015-10-09 09:30:04 +01:00
|
|
|
self._ensure_executables_directory_is_writable()
|
|
|
|
executable_name = with_name or os.path.basename(filepath)
|
|
|
|
on_device_file = self.path.join(self.working_directory, executable_name)
|
|
|
|
on_device_executable = self.path.join(self.executables_directory, executable_name)
|
2019-09-12 14:10:05 +01:00
|
|
|
self.push(filepath, on_device_file, timeout=timeout)
|
2015-10-09 09:30:04 +01:00
|
|
|
if on_device_file != on_device_executable:
|
2019-09-12 14:10:05 +01:00
|
|
|
self.execute('cp {} {}'.format(quote(on_device_file), quote(on_device_executable)),
|
|
|
|
as_root=self.needs_su, timeout=timeout)
|
2016-06-23 14:55:19 +01:00
|
|
|
self.remove(on_device_file, as_root=self.needs_su)
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute("chmod 0777 {}".format(quote(on_device_executable)), as_root=self.needs_su)
|
2015-10-09 09:30:04 +01:00
|
|
|
self._installed_binaries[executable_name] = on_device_executable
|
|
|
|
return on_device_executable
|
|
|
|
|
|
|
|
def uninstall_package(self, package):
|
2019-08-27 14:46:09 +01:00
|
|
|
if isinstance(self.conn, AdbConnection):
|
2020-05-01 17:57:49 +01:00
|
|
|
adb_command(self.adb_name, "uninstall {}".format(quote(package)), timeout=30,
|
|
|
|
adb_server=self.adb_server)
|
2019-08-27 14:46:09 +01:00
|
|
|
else:
|
|
|
|
self.execute("pm uninstall {}".format(quote(package)), timeout=30)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def uninstall_executable(self, executable_name):
|
|
|
|
on_device_executable = self.path.join(self.executables_directory, executable_name)
|
|
|
|
self._ensure_executables_directory_is_writable()
|
2016-06-23 14:55:19 +01:00
|
|
|
self.remove(on_device_executable, as_root=self.needs_su)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2020-01-17 17:00:59 +00:00
|
|
|
def dump_logcat(self, filepath, filter=None, logcat_format=None, append=False,
|
2021-01-11 09:59:38 +00:00
|
|
|
timeout=60): # pylint: disable=redefined-builtin
|
2016-02-15 15:21:40 +00:00
|
|
|
op = '>>' if append else '>'
|
2018-10-30 15:05:03 +00:00
|
|
|
filtstr = ' -s {}'.format(quote(filter)) if filter else ''
|
2020-01-17 17:00:59 +00:00
|
|
|
formatstr = ' -v {}'.format(quote(logcat_format)) if logcat_format else ''
|
|
|
|
logcat_opts = '-d' + formatstr + filtstr
|
2019-08-27 14:46:09 +01:00
|
|
|
if isinstance(self.conn, AdbConnection):
|
2020-01-17 17:00:59 +00:00
|
|
|
command = 'logcat {} {} {}'.format(logcat_opts, op, quote(filepath))
|
2020-05-01 17:57:49 +01:00
|
|
|
adb_command(self.adb_name, command, timeout=timeout, adb_server=self.adb_server)
|
2019-08-27 14:46:09 +01:00
|
|
|
else:
|
|
|
|
dev_path = self.get_workpath('logcat')
|
2020-01-17 17:00:59 +00:00
|
|
|
command = 'logcat {} {} {}'.format(logcat_opts, op, quote(dev_path))
|
2019-08-27 14:46:09 +01:00
|
|
|
self.execute(command, timeout=timeout)
|
|
|
|
self.pull(dev_path, filepath)
|
|
|
|
self.remove(dev_path)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def clear_logcat(self):
|
2021-07-21 13:58:53 +01:00
|
|
|
locked = self.clear_logcat_lock.acquire(blocking=False)
|
|
|
|
if locked:
|
|
|
|
try:
|
|
|
|
if isinstance(self.conn, AdbConnection):
|
|
|
|
adb_command(self.adb_name, 'logcat -c', timeout=30, adb_server=self.adb_server)
|
|
|
|
else:
|
|
|
|
self.execute('logcat -c', timeout=30)
|
|
|
|
finally:
|
|
|
|
self.clear_logcat_lock.release()
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-09-11 16:58:24 +01:00
|
|
|
def get_logcat_monitor(self, regexps=None):
|
|
|
|
return LogcatMonitor(self, regexps)
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2019-11-07 09:59:48 +00:00
|
|
|
def wait_for_device(self, timeout=30):
|
|
|
|
self.conn.wait_for_device()
|
|
|
|
|
2021-05-12 16:46:40 +01:00
|
|
|
@call_conn
|
2019-11-07 09:59:48 +00:00
|
|
|
def reboot_bootloader(self, timeout=30):
|
|
|
|
self.conn.reboot_bootloader()
|
2017-02-16 13:11:02 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def is_screen_on(self):
|
|
|
|
output = self.execute('dumpsys power')
|
|
|
|
match = ANDROID_SCREEN_STATE_REGEX.search(output)
|
|
|
|
if match:
|
2020-06-05 17:04:37 +01:00
|
|
|
if 'DOZE' in match.group(1).upper():
|
|
|
|
return True
|
2021-09-27 14:45:55 -07:00
|
|
|
if match.group(1) == 'Asleep':
|
|
|
|
return False
|
|
|
|
if match.group(1) == 'Awake':
|
|
|
|
return True
|
2015-10-09 09:30:04 +01:00
|
|
|
return boolean(match.group(1))
|
|
|
|
else:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError('Could not establish screen state.')
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2020-06-05 17:04:37 +01:00
|
|
|
def ensure_screen_is_on(self, verify=True):
|
2015-10-09 09:30:04 +01:00
|
|
|
if not self.is_screen_on():
|
|
|
|
self.execute('input keyevent 26')
|
2020-06-05 17:04:37 +01:00
|
|
|
if verify and not self.is_screen_on():
|
|
|
|
raise TargetStableError('Display cannot be turned on.')
|
|
|
|
|
2020-08-11 12:08:40 +01:00
|
|
|
def ensure_screen_is_on_and_stays(self, verify=True, mode=7):
|
|
|
|
self.ensure_screen_is_on(verify=verify)
|
|
|
|
self.set_stay_on_mode(mode)
|
|
|
|
|
2020-06-05 17:04:37 +01:00
|
|
|
def ensure_screen_is_off(self, verify=True):
|
|
|
|
# Allow 2 attempts to help with cases of ambient display modes
|
|
|
|
# where the first attempt will switch the display fully on.
|
|
|
|
for _ in range(2):
|
|
|
|
if self.is_screen_on():
|
|
|
|
self.execute('input keyevent 26')
|
|
|
|
time.sleep(0.5)
|
|
|
|
if verify and self.is_screen_on():
|
|
|
|
msg = 'Display cannot be turned off. Is always on display enabled?'
|
|
|
|
raise TargetStableError(msg)
|
2017-05-12 11:54:31 +01:00
|
|
|
|
2017-07-06 17:30:12 +01:00
|
|
|
def set_auto_brightness(self, auto_brightness):
|
|
|
|
cmd = 'settings put system screen_brightness_mode {}'
|
|
|
|
self.execute(cmd.format(int(boolean(auto_brightness))))
|
|
|
|
|
|
|
|
def get_auto_brightness(self):
|
|
|
|
cmd = 'settings get system screen_brightness_mode'
|
|
|
|
return boolean(self.execute(cmd).strip())
|
|
|
|
|
|
|
|
def set_brightness(self, value):
|
|
|
|
if not 0 <= value <= 255:
|
|
|
|
msg = 'Invalid brightness "{}"; Must be between 0 and 255'
|
|
|
|
raise ValueError(msg.format(value))
|
|
|
|
self.set_auto_brightness(False)
|
|
|
|
cmd = 'settings put system screen_brightness {}'
|
|
|
|
self.execute(cmd.format(int(value)))
|
|
|
|
|
|
|
|
def get_brightness(self):
|
|
|
|
cmd = 'settings get system screen_brightness'
|
|
|
|
return integer(self.execute(cmd).strip())
|
|
|
|
|
2018-06-20 17:59:22 +01:00
|
|
|
def set_screen_timeout(self, timeout_ms):
|
|
|
|
cmd = 'settings put system screen_off_timeout {}'
|
|
|
|
self.execute(cmd.format(int(timeout_ms)))
|
|
|
|
|
|
|
|
def get_screen_timeout(self):
|
|
|
|
cmd = 'settings get system screen_off_timeout'
|
|
|
|
return int(self.execute(cmd).strip())
|
|
|
|
|
2017-07-06 17:31:02 +01:00
|
|
|
def get_airplane_mode(self):
|
|
|
|
cmd = 'settings get global airplane_mode_on'
|
|
|
|
return boolean(self.execute(cmd).strip())
|
|
|
|
|
2020-08-11 12:08:40 +01:00
|
|
|
def get_stay_on_mode(self):
|
|
|
|
cmd = 'settings get global stay_on_while_plugged_in'
|
|
|
|
return int(self.execute(cmd).strip())
|
|
|
|
|
2017-07-06 17:31:02 +01:00
|
|
|
def set_airplane_mode(self, mode):
|
|
|
|
root_required = self.get_sdk_version() > 23
|
|
|
|
if root_required and not self.is_rooted:
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError('Root is required to toggle airplane mode on Android 7+')
|
2017-10-11 13:09:15 +01:00
|
|
|
mode = int(boolean(mode))
|
2017-07-06 17:31:02 +01:00
|
|
|
cmd = 'settings put global airplane_mode_on {}'
|
2017-10-11 13:09:15 +01:00
|
|
|
self.execute(cmd.format(mode))
|
|
|
|
self.execute('am broadcast -a android.intent.action.AIRPLANE_MODE '
|
|
|
|
'--ez state {}'.format(mode), as_root=root_required)
|
2017-07-06 17:31:02 +01:00
|
|
|
|
2017-07-24 17:44:33 +01:00
|
|
|
def get_auto_rotation(self):
|
|
|
|
cmd = 'settings get system accelerometer_rotation'
|
|
|
|
return boolean(self.execute(cmd).strip())
|
|
|
|
|
|
|
|
def set_auto_rotation(self, autorotate):
|
|
|
|
cmd = 'settings put system accelerometer_rotation {}'
|
|
|
|
self.execute(cmd.format(int(boolean(autorotate))))
|
|
|
|
|
|
|
|
def set_natural_rotation(self):
|
|
|
|
self.set_rotation(0)
|
|
|
|
|
|
|
|
def set_left_rotation(self):
|
|
|
|
self.set_rotation(1)
|
|
|
|
|
|
|
|
def set_inverted_rotation(self):
|
|
|
|
self.set_rotation(2)
|
|
|
|
|
|
|
|
def set_right_rotation(self):
|
|
|
|
self.set_rotation(3)
|
|
|
|
|
|
|
|
def get_rotation(self):
|
2018-11-15 16:03:00 +00:00
|
|
|
output = self.execute('dumpsys input')
|
|
|
|
match = ANDROID_SCREEN_ROTATION_REGEX.search(output)
|
|
|
|
if match:
|
|
|
|
return int(match.group('rotation'))
|
|
|
|
else:
|
2018-02-08 14:23:53 +00:00
|
|
|
return None
|
2017-07-24 17:44:33 +01:00
|
|
|
|
|
|
|
def set_rotation(self, rotation):
|
|
|
|
if not 0 <= rotation <= 3:
|
|
|
|
raise ValueError('Rotation value must be between 0 and 3')
|
|
|
|
self.set_auto_rotation(False)
|
|
|
|
cmd = 'settings put system user_rotation {}'
|
|
|
|
self.execute(cmd.format(rotation))
|
|
|
|
|
2020-08-11 12:08:40 +01:00
|
|
|
def set_stay_on_never(self):
|
|
|
|
self.set_stay_on_mode(0)
|
|
|
|
|
|
|
|
def set_stay_on_while_powered(self):
|
|
|
|
self.set_stay_on_mode(7)
|
|
|
|
|
|
|
|
def set_stay_on_mode(self, mode):
|
|
|
|
if not 0 <= mode <= 7:
|
|
|
|
raise ValueError('Screen stay on mode must be between 0 and 7')
|
|
|
|
cmd = 'settings put global stay_on_while_plugged_in {}'
|
|
|
|
self.execute(cmd.format(mode))
|
|
|
|
|
2018-06-20 17:49:20 +01:00
|
|
|
def open_url(self, url, force_new=False):
|
|
|
|
"""
|
|
|
|
Start a view activity by specifying an URL
|
|
|
|
|
|
|
|
:param url: URL of the item to display
|
|
|
|
:type url: str
|
|
|
|
|
|
|
|
:param force_new: Force the viewing application to be relaunched
|
|
|
|
if it is already running
|
|
|
|
:type force_new: bool
|
|
|
|
"""
|
2018-10-30 15:52:45 +00:00
|
|
|
cmd = 'am start -a android.intent.action.VIEW -d {}'
|
2018-06-20 17:49:20 +01:00
|
|
|
|
|
|
|
if force_new:
|
|
|
|
cmd = cmd + ' -f {}'.format(INTENT_FLAGS['ACTIVITY_NEW_TASK'] |
|
|
|
|
INTENT_FLAGS['ACTIVITY_CLEAR_TASK'])
|
|
|
|
|
2018-10-30 15:52:45 +00:00
|
|
|
self.execute(cmd.format(quote(url)))
|
2018-03-08 13:21:53 +00:00
|
|
|
|
2017-05-12 11:54:31 +01:00
|
|
|
def homescreen(self):
|
|
|
|
self.execute('am start -a android.intent.action.MAIN -c android.intent.category.HOME')
|
|
|
|
|
2015-11-18 17:32:26 +00:00
|
|
|
def _resolve_paths(self):
|
|
|
|
if self.working_directory is None:
|
2017-11-13 17:39:31 +00:00
|
|
|
self.working_directory = self.path.join(self.external_storage, 'devlib-target')
|
2015-11-18 17:32:26 +00:00
|
|
|
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
|
|
|
|
if self.executables_directory is None:
|
2016-02-15 15:28:20 +00:00
|
|
|
self.executables_directory = '/data/local/tmp/bin'
|
2015-11-18 17:32:26 +00:00
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def _ensure_executables_directory_is_writable(self):
|
|
|
|
matched = []
|
|
|
|
for entry in self.list_file_systems():
|
|
|
|
if self.executables_directory.rstrip('/').startswith(entry.mount_point):
|
|
|
|
matched.append(entry)
|
|
|
|
if matched:
|
|
|
|
entry = sorted(matched, key=lambda x: len(x.mount_point))[-1]
|
|
|
|
if 'rw' not in entry.options:
|
2018-10-30 15:05:03 +00:00
|
|
|
self.execute('mount -o rw,remount {} {}'.format(quote(entry.device),
|
|
|
|
quote(entry.mount_point)),
|
2015-10-09 09:30:04 +01:00
|
|
|
as_root=True)
|
|
|
|
else:
|
|
|
|
message = 'Could not find mount point for executables directory {}'
|
exceptions: Classify transient exceptions
Exceptions such as TargetError can sometimes be raised because of a
network issue, which is useful to distinguish from errors caused by a
missing feature for automated testing environments.
The following exceptions are introduced:
* DevlibStableError: raised when a non-transient error is encountered
* TargetStableError
* DevlibTransientError: raised when a transient error is encountered,
including timeouts.
* TargetTransientError
When there is an ambiguity on the type of exception to use, it can be
assumed that the configuration is correct, and therefore it is a
transient error, unless the function is specifically designed to probe a
property of the system. In that case, ambiguity is allowed to be lifted
by assuming a non-transient error, since we expect it to raise an
exception when that property is not met. Such ambiguous case can appear
when checking Android has booted, since we cannot know if this is a
timeout/connection issue, or an actual issue with the Android build or
configuration. Another case are the execute() methods, which can be
expected to fail on purpose. A new parameter will_succeed=False is
added, to automatically turn non transient errors into transient ones if
the caller is 100% sure that the command cannot fail unless there is an
environment issue that is outside of the scope controlled by the user.
devlib now never raises TargetError directly, but one of
TargetStableError or TargetTransientError. External code can therefore
rely on all (indirect) instances TargetError to be in either category.
Most existing uses of TargetError are replaced by TargetStableError.
2018-06-20 15:04:12 +01:00
|
|
|
raise TargetStableError(message.format(self.executables_directory))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2017-02-09 11:53:11 +00:00
|
|
|
_charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def charging_enabled(self):
|
|
|
|
"""
|
|
|
|
Whether drawing power to charge the battery is enabled
|
|
|
|
|
|
|
|
Not all devices have the ability to enable/disable battery charging
|
|
|
|
(e.g. because they don't have a battery). In that case,
|
|
|
|
``charging_enabled`` is None.
|
|
|
|
"""
|
|
|
|
if not self.file_exists(self._charging_enabled_path):
|
|
|
|
return None
|
|
|
|
return self.read_bool(self._charging_enabled_path)
|
|
|
|
|
|
|
|
@charging_enabled.setter
|
|
|
|
def charging_enabled(self, enabled):
|
|
|
|
"""
|
|
|
|
Enable/disable drawing power to charge the battery
|
|
|
|
|
|
|
|
Not all devices have this facility. In that case, do nothing.
|
|
|
|
"""
|
|
|
|
if not self.file_exists(self._charging_enabled_path):
|
|
|
|
return
|
|
|
|
self.write_value(self._charging_enabled_path, int(bool(enabled)))
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
|
2020-07-10 10:12:25 +01:00
|
|
|
PsEntry = namedtuple('PsEntry', 'user pid tid ppid vsize rss wchan pc state name')
|
2016-01-27 16:34:26 +00:00
|
|
|
LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Cpuinfo(object):
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def architecture(self):
|
|
|
|
for section in self.sections:
|
|
|
|
if 'CPU architecture' in section:
|
|
|
|
return section['CPU architecture']
|
|
|
|
if 'architecture' in section:
|
|
|
|
return section['architecture']
|
|
|
|
|
|
|
|
@property
|
|
|
|
@memoized
|
|
|
|
def cpu_names(self):
|
|
|
|
cpu_names = []
|
|
|
|
global_name = None
|
|
|
|
for section in self.sections:
|
|
|
|
if 'processor' in section:
|
|
|
|
if 'CPU part' in section:
|
|
|
|
cpu_names.append(_get_part_name(section))
|
|
|
|
elif 'model name' in section:
|
|
|
|
cpu_names.append(_get_model_name(section))
|
|
|
|
else:
|
|
|
|
cpu_names.append(None)
|
|
|
|
elif 'CPU part' in section:
|
|
|
|
global_name = _get_part_name(section)
|
|
|
|
return [caseless_string(c or global_name) for c in cpu_names]
|
|
|
|
|
|
|
|
def __init__(self, text):
|
|
|
|
self.sections = None
|
|
|
|
self.text = None
|
|
|
|
self.parse(text)
|
|
|
|
|
|
|
|
@memoized
|
|
|
|
def get_cpu_features(self, cpuid=0):
|
|
|
|
global_features = []
|
|
|
|
for section in self.sections:
|
|
|
|
if 'processor' in section:
|
|
|
|
if int(section.get('processor')) != cpuid:
|
|
|
|
continue
|
|
|
|
if 'Features' in section:
|
|
|
|
return section.get('Features').split()
|
2015-12-15 18:07:34 +00:00
|
|
|
elif 'flags' in section:
|
|
|
|
return section.get('flags').split()
|
2015-10-09 09:30:04 +01:00
|
|
|
elif 'Features' in section:
|
|
|
|
global_features = section.get('Features').split()
|
2015-12-15 18:07:34 +00:00
|
|
|
elif 'flags' in section:
|
|
|
|
global_features = section.get('flags').split()
|
2015-10-09 09:30:04 +01:00
|
|
|
return global_features
|
|
|
|
|
|
|
|
def parse(self, text):
|
|
|
|
self.sections = []
|
|
|
|
current_section = {}
|
|
|
|
self.text = text.strip()
|
|
|
|
for line in self.text.split('\n'):
|
|
|
|
line = line.strip()
|
|
|
|
if line:
|
|
|
|
key, value = line.split(':', 1)
|
|
|
|
current_section[key.strip()] = value.strip()
|
|
|
|
else: # not line
|
|
|
|
self.sections.append(current_section)
|
|
|
|
current_section = {}
|
|
|
|
self.sections.append(current_section)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return 'CpuInfo({})'.format(self.cpu_names)
|
|
|
|
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
|
|
|
|
|
|
class KernelVersion(object):
|
2017-02-20 17:57:51 +00:00
|
|
|
"""
|
|
|
|
Class representing the version of a target kernel
|
|
|
|
|
|
|
|
Not expected to work for very old (pre-3.0) kernel version numbers.
|
|
|
|
|
|
|
|
:ivar release: Version number/revision string. Typical output of
|
|
|
|
``uname -r``
|
|
|
|
:type release: str
|
|
|
|
:ivar version: Extra version info (aside from ``release``) reported by
|
|
|
|
``uname``
|
|
|
|
:type version: str
|
|
|
|
:ivar version_number: Main version number (e.g. 3 for Linux 3.18)
|
|
|
|
:type version_number: int
|
|
|
|
:ivar major: Major version number (e.g. 18 for Linux 3.18)
|
|
|
|
:type major: int
|
|
|
|
:ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
|
|
|
|
be None
|
|
|
|
:type minor: int
|
|
|
|
:ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
|
|
|
|
:type rc: int
|
2020-02-28 10:10:23 +00:00
|
|
|
:ivar commits: Number of additional commits on the branch. May be None.
|
|
|
|
:type commits: int
|
2017-02-20 17:57:51 +00:00
|
|
|
:ivar sha1: Kernel git revision hash, if available (otherwise None)
|
|
|
|
:type sha1: str
|
2017-02-20 17:52:56 +00:00
|
|
|
|
|
|
|
:ivar parts: Tuple of version number components. Can be used for
|
|
|
|
lexicographically comparing kernel versions.
|
|
|
|
:type parts: tuple(int)
|
2017-02-20 17:57:51 +00:00
|
|
|
"""
|
2015-10-09 09:30:04 +01:00
|
|
|
def __init__(self, version_string):
|
|
|
|
if ' #' in version_string:
|
|
|
|
release, version = version_string.split(' #')
|
|
|
|
self.release = release
|
|
|
|
self.version = version
|
|
|
|
elif version_string.startswith('#'):
|
|
|
|
self.release = ''
|
|
|
|
self.version = version_string
|
|
|
|
else:
|
|
|
|
self.release = version_string
|
|
|
|
self.version = ''
|
|
|
|
|
2017-02-17 15:28:07 +00:00
|
|
|
self.version_number = None
|
|
|
|
self.major = None
|
|
|
|
self.minor = None
|
|
|
|
self.sha1 = None
|
|
|
|
self.rc = None
|
2020-02-28 10:10:23 +00:00
|
|
|
self.commits = None
|
2017-02-17 15:28:07 +00:00
|
|
|
match = KVERSION_REGEX.match(version_string)
|
|
|
|
if match:
|
2017-02-20 17:51:17 +00:00
|
|
|
groups = match.groupdict()
|
|
|
|
self.version_number = int(groups['version'])
|
|
|
|
self.major = int(groups['major'])
|
|
|
|
if groups['minor'] is not None:
|
|
|
|
self.minor = int(groups['minor'])
|
|
|
|
if groups['rc'] is not None:
|
|
|
|
self.rc = int(groups['rc'])
|
2020-02-28 10:10:23 +00:00
|
|
|
if groups['commits'] is not None:
|
|
|
|
self.commits = int(groups['commits'])
|
2017-02-20 17:51:17 +00:00
|
|
|
if groups['sha1'] is not None:
|
|
|
|
self.sha1 = match.group('sha1')
|
2017-02-17 15:28:07 +00:00
|
|
|
|
2017-02-20 17:52:56 +00:00
|
|
|
self.parts = (self.version_number, self.major, self.minor)
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
def __str__(self):
|
|
|
|
return '{} {}'.format(self.release, self.version)
|
|
|
|
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
|
|
|
2019-02-13 14:16:55 +00:00
|
|
|
class HexInt(long):
|
2018-12-17 11:53:52 +00:00
|
|
|
"""
|
|
|
|
Subclass of :class:`int` that uses hexadecimal formatting by default.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __new__(cls, val=0, base=16):
|
|
|
|
super_new = super(HexInt, cls).__new__
|
|
|
|
if isinstance(val, Number):
|
|
|
|
return super_new(cls, val)
|
|
|
|
else:
|
|
|
|
return super_new(cls, val, base=base)
|
|
|
|
|
|
|
|
def __str__(self):
|
2019-02-13 14:16:55 +00:00
|
|
|
return hex(self).strip('L')
|
2018-12-17 11:53:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
class KernelConfigTristate(Enum):
|
|
|
|
YES = 'y'
|
|
|
|
NO = 'n'
|
|
|
|
MODULE = 'm'
|
|
|
|
|
|
|
|
def __bool__(self):
|
|
|
|
"""
|
|
|
|
Allow using this enum to represent bool Kconfig type, although it is
|
|
|
|
technically different from tristate.
|
|
|
|
"""
|
|
|
|
return self in (self.YES, self.MODULE)
|
|
|
|
|
|
|
|
def __nonzero__(self):
|
|
|
|
"""
|
|
|
|
For Python 2.x compatibility.
|
|
|
|
"""
|
|
|
|
return self.__bool__()
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
@classmethod
|
|
|
|
def from_str(cls, str_):
|
|
|
|
for state in cls:
|
|
|
|
if state.value == str_:
|
|
|
|
return state
|
|
|
|
raise ValueError('No kernel config tristate value matches "{}"'.format(str_))
|
|
|
|
|
|
|
|
|
|
|
|
class TypedKernelConfig(Mapping):
|
|
|
|
"""
|
|
|
|
Mapping-like typed version of :class:`KernelConfig`.
|
|
|
|
|
|
|
|
Values are either :class:`str`, :class:`int`,
|
|
|
|
:class:`KernelConfigTristate`, or :class:`HexInt`. ``hex`` Kconfig type is
|
|
|
|
mapped to :class:`HexInt` and ``bool`` to :class:`KernelConfigTristate`.
|
|
|
|
"""
|
2015-10-09 09:30:04 +01:00
|
|
|
not_set_regex = re.compile(r'# (\S+) is not set')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_config_name(name):
|
|
|
|
name = name.upper()
|
|
|
|
if not name.startswith('CONFIG_'):
|
|
|
|
name = 'CONFIG_' + name
|
|
|
|
return name
|
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
def __init__(self, mapping=None):
|
|
|
|
mapping = mapping if mapping is not None else {}
|
|
|
|
self._config = {
|
|
|
|
# Ensure we use the canonical name of the config keys for internal
|
|
|
|
# representation
|
|
|
|
self.get_config_name(k): v
|
|
|
|
for k, v in dict(mapping).items()
|
|
|
|
}
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
@classmethod
|
|
|
|
def from_str(cls, text):
|
|
|
|
"""
|
|
|
|
Build a :class:`TypedKernelConfig` out of the string content of a
|
|
|
|
Kconfig file.
|
|
|
|
"""
|
|
|
|
return cls(cls._parse_text(text))
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _val_to_str(val):
|
|
|
|
"Convert back values to Kconfig-style string value"
|
|
|
|
# Special case the gracefully handle the output of get()
|
|
|
|
if val is None:
|
|
|
|
return None
|
|
|
|
elif isinstance(val, KernelConfigTristate):
|
|
|
|
return val.value
|
|
|
|
elif isinstance(val, basestring):
|
2019-01-30 14:26:05 +00:00
|
|
|
return '"{}"'.format(val.strip('"'))
|
2018-12-17 11:53:52 +00:00
|
|
|
else:
|
|
|
|
return str(val)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '\n'.join(
|
|
|
|
'{}={}'.format(k, self._val_to_str(v))
|
|
|
|
for k, v in self.items()
|
|
|
|
)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _parse_val(k, v):
|
|
|
|
"""
|
|
|
|
Parse a value of types handled by Kconfig:
|
|
|
|
* string
|
|
|
|
* bool
|
|
|
|
* tristate
|
|
|
|
* hex
|
|
|
|
* int
|
|
|
|
|
|
|
|
Since bool cannot be distinguished from tristate, tristate is
|
|
|
|
always used. :meth:`KernelConfigTristate.__bool__` will allow using
|
|
|
|
it as a bool though, so it should not impact user code.
|
|
|
|
"""
|
|
|
|
if not v:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Handle "string" type
|
|
|
|
if v.startswith('"'):
|
|
|
|
# Strip enclosing "
|
|
|
|
return v[1:-1]
|
|
|
|
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
# Handles "bool" and "tristate" types
|
|
|
|
return KernelConfigTristate.from_str(v)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Handles "int" type
|
|
|
|
return int(v)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Handles "hex" type
|
|
|
|
return HexInt(v)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# If no type could be parsed
|
|
|
|
raise ValueError('Could not parse Kconfig key: {}={}'.format(
|
|
|
|
k, v
|
|
|
|
), k, v
|
|
|
|
)
|
2018-12-17 12:24:00 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _parse_text(cls, text):
|
|
|
|
config = {}
|
2018-12-17 11:53:52 +00:00
|
|
|
for line in text.splitlines():
|
2015-10-09 09:30:04 +01:00
|
|
|
line = line.strip()
|
2018-12-17 14:57:29 +00:00
|
|
|
|
|
|
|
# skip empty lines
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
|
2015-10-09 09:30:04 +01:00
|
|
|
if line.startswith('#'):
|
2018-12-17 12:24:00 +00:00
|
|
|
match = cls.not_set_regex.search(line)
|
2015-10-09 09:30:04 +01:00
|
|
|
if match:
|
2018-12-17 14:57:29 +00:00
|
|
|
value = 'n'
|
|
|
|
name = match.group(1)
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
else:
|
2015-10-09 09:30:04 +01:00
|
|
|
name, value = line.split('=', 1)
|
2018-12-17 14:57:29 +00:00
|
|
|
|
|
|
|
name = cls.get_config_name(name.strip())
|
2018-12-17 11:53:52 +00:00
|
|
|
value = cls._parse_val(name, value.strip())
|
|
|
|
config[name] = value
|
2018-12-17 12:24:00 +00:00
|
|
|
return config
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
def __getitem__(self, name):
|
2018-06-08 18:15:10 +01:00
|
|
|
name = self.get_config_name(name)
|
2018-12-17 11:53:52 +00:00
|
|
|
try:
|
|
|
|
return self._config[name]
|
|
|
|
except KeyError:
|
2019-01-02 14:57:06 +00:00
|
|
|
raise KernelConfigKeyError(
|
|
|
|
"{} is not exposed in kernel config".format(name),
|
|
|
|
name
|
|
|
|
)
|
2018-06-08 18:15:10 +01:00
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
def __iter__(self):
|
|
|
|
return iter(self._config)
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
return len(self._config)
|
|
|
|
|
|
|
|
def __contains__(self, name):
|
|
|
|
name = self.get_config_name(name)
|
|
|
|
return name in self._config
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def like(self, name):
|
|
|
|
regex = re.compile(name, re.I)
|
2018-12-17 11:53:52 +00:00
|
|
|
return {
|
|
|
|
k: v for k, v in self.items()
|
|
|
|
if regex.search(k)
|
|
|
|
}
|
|
|
|
|
|
|
|
def is_enabled(self, name):
|
|
|
|
return self.get(name) is KernelConfigTristate.YES
|
|
|
|
|
|
|
|
def is_module(self, name):
|
|
|
|
return self.get(name) is KernelConfigTristate.MODULE
|
|
|
|
|
|
|
|
def is_not_set(self, name):
|
|
|
|
return self.get(name) is KernelConfigTristate.NO
|
|
|
|
|
|
|
|
def has(self, name):
|
|
|
|
return self.is_enabled(name) or self.is_module(name)
|
|
|
|
|
|
|
|
|
|
|
|
class KernelConfig(object):
|
|
|
|
"""
|
|
|
|
Backward compatibility shim on top of :class:`TypedKernelConfig`.
|
|
|
|
|
|
|
|
This class does not provide a Mapping API and only return string values.
|
|
|
|
"""
|
2019-07-17 11:42:41 +01:00
|
|
|
@staticmethod
|
|
|
|
def get_config_name(name):
|
|
|
|
return TypedKernelConfig.get_config_name(name)
|
2018-12-17 11:53:52 +00:00
|
|
|
|
|
|
|
def __init__(self, text):
|
|
|
|
# Expose typed_config as a non-private attribute, so that user code
|
|
|
|
# needing it can get it from any existing producer of KernelConfig.
|
|
|
|
self.typed_config = TypedKernelConfig.from_str(text)
|
|
|
|
# Expose the original text for backward compatibility
|
|
|
|
self.text = text
|
|
|
|
|
2019-07-17 11:46:55 +01:00
|
|
|
def __bool__(self):
|
|
|
|
return bool(self.typed_config)
|
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
not_set_regex = TypedKernelConfig.not_set_regex
|
|
|
|
|
|
|
|
def iteritems(self):
|
|
|
|
for k, v in self.typed_config.items():
|
|
|
|
yield (k, self.typed_config._val_to_str(v))
|
|
|
|
|
2019-01-30 16:44:06 +00:00
|
|
|
items = iteritems
|
|
|
|
|
2018-12-17 11:53:52 +00:00
|
|
|
def get(self, name, strict=False):
|
|
|
|
if strict:
|
|
|
|
val = self.typed_config[name]
|
|
|
|
else:
|
|
|
|
val = self.typed_config.get(name)
|
|
|
|
|
|
|
|
return self.typed_config._val_to_str(val)
|
|
|
|
|
|
|
|
def like(self, name):
|
|
|
|
return {
|
|
|
|
k: self.typed_config._val_to_str(v)
|
|
|
|
for k, v in self.typed_config.like(name).items()
|
|
|
|
}
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def is_enabled(self, name):
|
2018-12-17 11:53:52 +00:00
|
|
|
return self.typed_config.is_enabled(name)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def is_module(self, name):
|
2018-12-17 11:53:52 +00:00
|
|
|
return self.typed_config.is_module(name)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def is_not_set(self, name):
|
2018-12-17 11:53:52 +00:00
|
|
|
return self.typed_config.is_not_set(name)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
def has(self, name):
|
2018-12-17 11:53:52 +00:00
|
|
|
return self.typed_config.has(name)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
class LocalLinuxTarget(LinuxTarget):
|
|
|
|
|
2016-12-07 15:11:32 +00:00
|
|
|
def __init__(self,
|
|
|
|
connection_settings=None,
|
|
|
|
platform=None,
|
|
|
|
working_directory=None,
|
|
|
|
executables_directory=None,
|
|
|
|
connect=True,
|
|
|
|
modules=None,
|
|
|
|
load_default_modules=True,
|
|
|
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
|
|
|
conn_cls=LocalConnection,
|
2018-06-29 16:01:43 +01:00
|
|
|
is_container=False,
|
2016-12-07 15:11:32 +00:00
|
|
|
):
|
|
|
|
super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
|
|
|
|
platform=platform,
|
|
|
|
working_directory=working_directory,
|
|
|
|
executables_directory=executables_directory,
|
|
|
|
connect=connect,
|
|
|
|
modules=modules,
|
|
|
|
load_default_modules=load_default_modules,
|
|
|
|
shell_prompt=shell_prompt,
|
2018-06-29 16:01:43 +01:00
|
|
|
conn_cls=conn_cls,
|
|
|
|
is_container=is_container)
|
2015-10-09 09:30:04 +01:00
|
|
|
|
2015-11-18 17:32:26 +00:00
|
|
|
def _resolve_paths(self):
|
2015-10-09 09:30:04 +01:00
|
|
|
if self.working_directory is None:
|
2018-01-16 14:25:06 +00:00
|
|
|
self.working_directory = '/tmp/devlib-target'
|
2017-12-12 12:17:14 +00:00
|
|
|
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
|
2015-10-09 09:30:04 +01:00
|
|
|
if self.executables_directory is None:
|
2018-01-16 14:25:06 +00:00
|
|
|
self.executables_directory = '/tmp/devlib-target/bin'
|
2015-10-09 09:30:04 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _get_model_name(section):
|
|
|
|
name_string = section['model name']
|
|
|
|
parts = name_string.split('@')[0].strip().split()
|
|
|
|
return ' '.join([p for p in parts
|
|
|
|
if '(' not in p and p != 'CPU'])
|
|
|
|
|
|
|
|
|
|
|
|
def _get_part_name(section):
|
|
|
|
implementer = section.get('CPU implementer', '0x0')
|
|
|
|
part = section['CPU part']
|
|
|
|
variant = section.get('CPU variant', '0x0')
|
2018-05-30 15:58:32 +01:00
|
|
|
name = get_cpu_name(*list(map(integer, [implementer, part, variant])))
|
2015-10-09 09:30:04 +01:00
|
|
|
if name is None:
|
|
|
|
name = '{}/{}/{}'.format(implementer, part, variant)
|
|
|
|
return name
|
2017-10-03 16:28:09 +01:00
|
|
|
|
|
|
|
|
|
|
|
def _build_path_tree(path_map, basepath, sep=os.path.sep, dictcls=dict):
|
|
|
|
"""
|
|
|
|
Convert a flat mapping of paths to values into a nested structure of
|
|
|
|
dict-line object (``dict``'s by default), mirroring the directory hierarchy
|
|
|
|
represented by the paths relative to ``basepath``.
|
|
|
|
|
|
|
|
"""
|
|
|
|
def process_node(node, path, value):
|
|
|
|
parts = path.split(sep, 1)
|
|
|
|
if len(parts) == 1: # leaf
|
|
|
|
node[parts[0]] = value
|
|
|
|
else: # branch
|
|
|
|
if parts[0] not in node:
|
|
|
|
node[parts[0]] = dictcls()
|
|
|
|
process_node(node[parts[0]], parts[1], value)
|
|
|
|
|
|
|
|
relpath_map = {os.path.relpath(p, basepath): v
|
2018-05-30 15:58:32 +01:00
|
|
|
for p, v in path_map.items()}
|
2017-10-03 16:28:09 +01:00
|
|
|
|
2018-05-30 15:58:32 +01:00
|
|
|
if len(relpath_map) == 1 and list(relpath_map.keys())[0] == '.':
|
|
|
|
result = list(relpath_map.values())[0]
|
2017-10-03 16:28:09 +01:00
|
|
|
else:
|
|
|
|
result = dictcls()
|
2018-05-30 15:58:32 +01:00
|
|
|
for path, value in relpath_map.items():
|
2017-10-03 16:28:09 +01:00
|
|
|
process_node(result, path, value)
|
|
|
|
|
|
|
|
return result
|
2017-12-21 17:08:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ChromeOsTarget(LinuxTarget):
|
|
|
|
|
|
|
|
os = 'chromeos'
|
|
|
|
|
2018-07-11 17:30:45 +01:00
|
|
|
# pylint: disable=too-many-locals
|
2017-12-21 17:08:11 +00:00
|
|
|
def __init__(self,
|
|
|
|
connection_settings=None,
|
|
|
|
platform=None,
|
|
|
|
working_directory=None,
|
|
|
|
executables_directory=None,
|
|
|
|
android_working_directory=None,
|
|
|
|
android_executables_directory=None,
|
|
|
|
connect=True,
|
|
|
|
modules=None,
|
|
|
|
load_default_modules=True,
|
|
|
|
shell_prompt=DEFAULT_SHELL_PROMPT,
|
|
|
|
package_data_directory="/data/data",
|
2018-06-29 16:01:43 +01:00
|
|
|
is_container=False
|
2017-12-21 17:08:11 +00:00
|
|
|
):
|
|
|
|
|
|
|
|
self.supports_android = None
|
|
|
|
self.android_container = None
|
|
|
|
|
|
|
|
# Pull out ssh connection settings
|
|
|
|
ssh_conn_params = ['host', 'username', 'password', 'keyfile',
|
2020-05-13 11:01:27 +01:00
|
|
|
'port', 'timeout', 'sudo_cmd',
|
2020-09-10 13:19:48 +01:00
|
|
|
'strict_host_check', 'use_scp',
|
|
|
|
'total_timeout', 'poll_transfers',
|
|
|
|
'start_transfer_poll_delay']
|
2017-12-21 17:08:11 +00:00
|
|
|
self.ssh_connection_settings = {}
|
|
|
|
for setting in ssh_conn_params:
|
|
|
|
if connection_settings.get(setting, None):
|
|
|
|
self.ssh_connection_settings[setting] = connection_settings[setting]
|
|
|
|
|
|
|
|
super(ChromeOsTarget, self).__init__(connection_settings=self.ssh_connection_settings,
|
|
|
|
platform=platform,
|
|
|
|
working_directory=working_directory,
|
|
|
|
executables_directory=executables_directory,
|
|
|
|
connect=False,
|
|
|
|
modules=modules,
|
|
|
|
load_default_modules=load_default_modules,
|
|
|
|
shell_prompt=shell_prompt,
|
2018-06-29 16:01:43 +01:00
|
|
|
conn_cls=SshConnection,
|
|
|
|
is_container=is_container)
|
2017-12-21 17:08:11 +00:00
|
|
|
|
|
|
|
# We can't determine if the target supports android until connected to the linux host so
|
|
|
|
# create unconditionally.
|
|
|
|
# Pull out adb connection settings
|
|
|
|
adb_conn_params = ['device', 'adb_server', 'timeout']
|
|
|
|
self.android_connection_settings = {}
|
|
|
|
for setting in adb_conn_params:
|
|
|
|
if connection_settings.get(setting, None):
|
|
|
|
self.android_connection_settings[setting] = connection_settings[setting]
|
|
|
|
|
|
|
|
# If adb device is not explicitly specified use same as ssh host
|
|
|
|
if not connection_settings.get('device', None):
|
|
|
|
self.android_connection_settings['device'] = connection_settings.get('host', None)
|
|
|
|
|
|
|
|
self.android_container = AndroidTarget(connection_settings=self.android_connection_settings,
|
|
|
|
platform=platform,
|
|
|
|
working_directory=android_working_directory,
|
|
|
|
executables_directory=android_executables_directory,
|
|
|
|
connect=False,
|
|
|
|
modules=[], # Only use modules with linux target
|
|
|
|
load_default_modules=False,
|
|
|
|
shell_prompt=shell_prompt,
|
|
|
|
conn_cls=AdbConnection,
|
2018-06-29 16:01:43 +01:00
|
|
|
package_data_directory=package_data_directory,
|
|
|
|
is_container=True)
|
2017-12-21 17:08:11 +00:00
|
|
|
if connect:
|
|
|
|
self.connect()
|
|
|
|
|
|
|
|
def __getattr__(self, attr):
|
|
|
|
"""
|
|
|
|
By default use the linux target methods and attributes however,
|
|
|
|
if not present, use android implementation if available.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return super(ChromeOsTarget, self).__getattribute__(attr)
|
|
|
|
except AttributeError:
|
|
|
|
if hasattr(self.android_container, attr):
|
|
|
|
return getattr(self.android_container, attr)
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
2018-03-02 16:03:43 +00:00
|
|
|
def connect(self, timeout=30, check_boot_completed=True):
|
|
|
|
super(ChromeOsTarget, self).connect(timeout, check_boot_completed)
|
2017-12-21 17:08:11 +00:00
|
|
|
|
|
|
|
# Assume device supports android apps if container directory is present
|
|
|
|
if self.supports_android is None:
|
|
|
|
self.supports_android = self.directory_exists('/opt/google/containers/android/')
|
|
|
|
|
|
|
|
if self.supports_android:
|
|
|
|
self.android_container.connect(timeout)
|
|
|
|
else:
|
|
|
|
self.android_container = None
|
|
|
|
|
|
|
|
def _resolve_paths(self):
|
|
|
|
if self.working_directory is None:
|
|
|
|
self.working_directory = '/mnt/stateful_partition/devlib-target'
|
|
|
|
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
|
|
|
|
if self.executables_directory is None:
|
|
|
|
self.executables_directory = self.path.join(self.working_directory, 'bin')
|