1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-09-22 20:01:53 +01:00

50 Commits
v1.3 ... v1.3.2

Author SHA1 Message Date
Marc Bonnici
e6c52c49ff version: Bump revision number 2021-07-23 15:42:13 +01:00
Marc Bonnici
6825130e48 connection: Use busybox implementation of kill
Some target implementations of kill do not support killing
process groups so use the busybox implementation for greater
portability.
2021-07-23 12:35:51 +01:00
Douglas Raillard
80c0e37d11 utils/misc: Use an RLock in tls_property
Allow reentrancy of the lock to fix a deadlock that can occur if
self._get_tls() is called while holding the lock.
2021-07-21 16:44:49 +01:00
Douglas Raillard
f523afda95 target: Fix deadlock in Target.clear_logcat()
Ensure that only once clear_logcat() call is active at once, and just
ignore reentrant calls.
2021-07-21 16:44:49 +01:00
Douglas Raillard
b64ec714a0 utils/misc: Use RLock for check_output_lock
Using a threading.Lock leads to a deadlock in some circumstances.
2021-07-21 16:44:49 +01:00
Valentin Schneider
6249c06b44 modules/sched: Add awareness of new debug directory root
Scheduler debug information is being unified under /sys/kernel/debug/sched
for Linux v5.13. Plug in awareness for the new path while still trying the
old one(s) for backwards compatibility.
2021-07-12 15:16:59 +01:00
Marc Bonnici
3af3463c3c utils/ssh: Fix paramiko streams
Ensure that we use the input stream for reading.
2021-06-29 13:44:14 +01:00
Marc Bonnici
7065847f77 utils/ssh: Fix paramiko stdin
Ensure that we open the stdin stream for writing instead
of read only.
2021-06-29 13:44:14 +01:00
douglas-raillard-arm
79783fa09a target: Create new connection for reentrant calls
When Target.conn property is required while the current connection is
already in use, provide a fresh connection to avoid deadlocks. This is
enabled by the @call_conn decorator that is used on all Target methods
that use self.conn directly.
2021-06-03 17:24:50 +01:00
douglas-raillard-arm
796536d67d hotplug: Verify hotplug.online_all()
Check that all CPUs are effectively online after a call to
target.hotplug.online_all(), as hotplug issues are common and failure to
bring back up a CPU can be quite problematic.
2021-06-03 17:24:43 +01:00
douglas-raillard-arm
b9374d530e ssh: Raise explicit exception when SFTP is not available
When SFTP is not available on OpenSSH, paramiko will raise a generic
exception:

    paramiko.ssh_exception.SSHException: EOF during negotiation

In order to make it easier to debug, raise a TargetStableError telling
the user to enable SFTP on their server. On OpenSSH, this means
installing the sftp subsystem and enabling it in sshd_config.
2021-05-11 09:39:53 +01:00
Javi Merino
34e51e7230 collector/perf: raise an error if report_options or report_sample_options are specified when not using perf/simpleperf record 2021-04-27 10:40:06 +01:00
Marc Bonnici
fa595e1a3d version: Dev version bump 2021-04-19 11:02:53 +01:00
Marc Bonnici
78938cf243 version: Bump revision number 2021-04-19 11:02:53 +01:00
Marc Bonnici
4941a7183a setup.py: Update project description 2021-04-19 10:58:59 +01:00
Marc Bonnici
3ab9d23a4a setup.py: Add long description to package
Use the readme as the long description, this will be
displayed on the PyPi page.
2021-04-19 10:58:59 +01:00
Marc Bonnici
5cf18a7b3c docs: Add note to hostid about PowerPc64 devices
Add caveat for the hostid on PowerPC64 devices due to the library
used when linking the included binary.
2021-04-19 10:46:46 +01:00
Benjamin Crawford
5bfeae08f4 bin: update aarch64 and x86_64 busybox binaries
The original BusyBox binaries were statically linked
against glibc, which caused segmentation faults to occur
on __nss_* calls. This switches the libc implementation
to uClibc in both cases.

busybox ver bump to 1.32.1 for arm64 and x86_64

update x86_64-linux-uclibc busybox

update aarch64-linux-uclibc busybox
2021-04-16 18:25:06 +01:00
Marc Bonnici
a87a1df0fb bin/busybox: Update ppc64le busybox
Update the ppc64le busybox implementation to v1.32.1
to match other architecture binaries.

Note: This binary is linked with musl as glibc has been
reported to cause issues however uclibc does not appear
to be a support build configuration.
2021-04-16 18:24:12 +01:00
Marc Bonnici
3bf763688e bin/busybox: Update 32 bit busybox binaries
Update the arm32 and add i368 v1.32.1 busybox binaries statically linked
with uclibc rather than the previous version with glibc which
caused segfaults on some devices.
2021-04-16 18:24:12 +01:00
Vincent Donnefort
a5cced85ce module/hotplug: Extend hotplug support with 'fail' and 'states' interfaces
The Linux HP path is composed of a series of states the CPU has to execute
to perform a complete hotplug or hotunplug. Devlib now exposes this
information. get_state() gives the current state of a CPU. get_states()
gives the list of all the states that composes the HP path.

The Linux HP 'fail' interface allows to simulate a failure during the
hotplug path. It helps to test the rollback mechanism. Devlib exposes this
interface with the method fail().
2021-04-12 13:38:42 +01:00
Javi Merino
9f55ae7603 connection: PopenTransferManager: Reset last_sample when we start a new transfer
last_sample is initialized in the PopenTransferManager constructor.
However, there is only one PopenTransferManager instance, which is
initialized during the construction of AdbConnection.  Afterwards, the
transfer manager is reused for every file transfer, without going
through __init__().  Therefore, after pushing/pulling a big file, the
next file transfer has compares the current size to the last sample of
the previous file transfer.  This makes it believe that the transfer
is inactive.

Reinitialize last_sample every time we start a new transfer to avoid
this.
2021-04-09 18:44:55 +01:00
Marc Bonnici
e7bafd6e5b version: Bump dev version
Exposing additional target properties so bump the development
version accordingly.
2021-04-07 18:21:33 +01:00
Marc Bonnici
ca84124fae doc/target: Fix Typo 2021-04-07 18:21:33 +01:00
Marc Bonnici
1f41853341 docs/target: Add new Target attributes 2021-04-07 18:21:33 +01:00
Marc Bonnici
82a2f7d8b6 target: Expose hostname as target property 2021-04-07 18:21:33 +01:00
Marc Bonnici
2a633b783a target: Expose hostid as target property 2021-04-07 18:21:33 +01:00
douglas-raillard-arm
b6d1863e77 collector/dmesg: DmesgCollector: Avoid not rooted targets
Fail early if the target is not rooted, as root is required by
DmesgCollector.start() anyway.
2021-04-07 10:55:27 +01:00
Vincent Donnefort
bbc891341c module/cpufreq: Warn when cpuinfo doesn't reflect the cpufreq request
The cpufreq/scaling_* files reflect the policy configuration from a kernel
point of view. The actual frequency at which the CPU is running can be
found in cpuinfo_cur_freq. Warn when the requested frequency does not
match the actual one.

As the kernel does not provide any guarantee that the requested frequency
will be actually used (due to other part of the system such as thermal or
the firmware), printing a warning instead of raising an error seems more
suitable here.
2021-04-07 10:55:08 +01:00
Marc Bonnici
d14df074ee utils/android: Fix Typo 2021-03-31 11:03:59 +01:00
Marc Bonnici
81f9ee2c50 utils/android: Use busybox implementation of ps
On some older versions of ps the flags used to look up
the pids are not supported and results in no output.
Use the busybox implementation for consistency.
2021-03-31 11:03:59 +01:00
Marc Bonnici
09d0a0f500 docs: Fix Formatting / typos 2021-03-31 11:02:49 +01:00
Robert Freeman
fe2fe3ae04 collector/perf: run simpleperf report-sample in the target if requested
If simpleperf record is called with "-f N", we may want to run
"simpleperf report-sample" on the output to dump the periodic
records.  Let the PerfCollector() run report-sample in the target,
similar to how we run report.
2021-03-31 10:03:33 +01:00
Robert Freeman
4859e818fb collector/perf: Only kill sleep if running perf stat
The stop() command of the PerfCollector kills all sleep commands in
the target, saying "We hope that no other important sleep is
on-going".  This is only needed if we are running "perf stat",
simpleperf does not need this.  Background the
perf/simpleperf command and only kill all sleeps in that specific case.
2021-03-31 10:03:33 +01:00
douglas-raillard-arm
5d342044a2 host and ssh: Fix sudo invocation
Add -k to sudo invocation to avoid using cached credentials.

If cached credentials is used, sudo will not write a prompt again,
leading to the stderr fixup code to remove a char from stderr output.
2021-03-24 19:06:05 +00:00
douglas-raillard-arm
d953377ff3 doc: Extend doc of Target.background()
Add doc for parameters "force_locale" and "timeout".
2021-03-24 19:05:50 +00:00
douglas-raillard-arm
4f2d9fa66d target: Add Target.background(timeout=...) parameter
Use a Timer daemonic thread to cancel the command after the given
timeout.
2021-03-24 19:05:50 +00:00
douglas-raillard-arm
4e44863777 connection: Un-hardcode _KILL_TIMEOUT
Replace erroneous use of _KILL_TIMEOUT constant by the kill_timeout
parameter.
2021-03-23 15:40:28 +00:00
douglas-raillard-arm
6cabad14d0 connection: Make ConnectionBase.cancel() more robust
Check with poll() if the command is already finished first, to avoid
sending SIGKILL to unrelated processes due to PID recycling.

The race window still exists between the call to poll() and _cancel(),
but is reduced a great deal.
2021-03-23 15:40:28 +00:00
douglas-raillard-arm
31f7c1e8f9 connection: Remove trailing whitespace 2021-03-23 15:40:28 +00:00
douglas-raillard-arm
3bc98f855b target: Align Target.background() LC_ALL and PATH
Make Target.background() behave like Target.execute():

* Set LC_ALL if force_locale is provided (defaults to "C")
* Add the target bin folder to PATH
2021-03-23 15:40:28 +00:00
douglas-raillard-arm
d2b80ccaf9 modules/sched: Fix sched domain flags parsing
Recent kernels can have a space-separated list of textual flags rather
than a bitfield packed in an int.
2021-03-12 17:56:01 +00:00
douglas-raillard-arm
552040f390 devlib/collector/dmesg: handle CONFIG_SECURITY_DMESG_RESTRICT
Some kernels compiled with CONFIG_SECURITY_DMESG_RESTRICT can restrict
reading the dmesg buffer to root user as a security hardening measure.
Detect this case and use root accordingly.
2021-02-12 12:11:07 +00:00
Javi Merino
0d259be01b collector/perf: Run perf as root if the target is rooted
If you want to collect events by event id (eg. in simpleperf, "rNNN"
with NNN a number), you must run it as root.  Otherwise, simpleperf
fails with a SIGABRT.

Run simpleperf as root by default if the target is rooted to avoid
this.
2021-02-02 11:30:09 +00:00
Marc Bonnici
792101819a utils/version: Prevent installation failure on systems without git
On systems that do not have git installed devlib will currently fail
to install with a FileNotFound Exception. If git is not present then
we will not have a commit hash so just ignore this error.
2021-01-12 17:53:27 +00:00
Javi Merino
3b8317d42e target: increase dump_logcat timeout
If WA is connected to a phone via a slow connection, dump_logcat() may
timeout when dumping logcat after the job has finished:

    2021-01-11 09:38:16,277 DEBUG       android:         adb -s X.Y.Z.X:5555 logcat -d -v threadtime > wa_output/wk1-wkld-1/logcat.log
    2021-01-11 09:38:46,317 DEBUG        signal:         Sending error-logged from <ErrorSignalHandler (DEBUG)>
    2021-01-11 09:38:46,318 DEBUG        signal:         Disconnecting <bound method Executor._error_signalled_callback of executor> from error-logged(<class 'louie.sender.Any'>)
    2021-01-11 09:38:46,317 ERROR        signal:         Timed out: adb -s X.Y.Z.X:5555 logcat -d -v threadtime > wa_output/wk1-wkld-1/logcat.log
    2021-01-11 09:38:46,317 ERROR        signal:         OUTPUT:
    2021-01-11 09:38:46,317 ERROR        signal:
    2021-01-11 09:38:46,317 ERROR        signal:

Increase the timeout to prevent this.
2021-01-11 10:11:42 +00:00
Marc Bonnici
e3da419e5b utils/android: Switch to using the lxml module
Using dexdump from versions 30.0.1-30.0.3 of Android build tools
does not produce valid XML caused by certain APKs
Use the lxml module for parsing xml as it is more robust and
better equipped to handle errors in input.
2021-01-08 17:22:12 +00:00
Marc Bonnici
e251b158b2 utils/android: Reorder imports 2021-01-08 17:22:12 +00:00
Marc Bonnici
c0a5765da5 utils/android: Fix aapt discovery with unexpected structure
If there is an additional file or directory in the `build_tools` directory
then WA can fail to find a working version of aapt(2), ensure that at least
one of the binaries is a valid file path.
2021-01-08 17:22:12 +00:00
Marc Bonnici
b32f15bbdb utils/version: Bump to dev version 2020-12-11 16:42:57 +00:00
20 changed files with 487 additions and 135 deletions

Binary file not shown.

Binary file not shown.

BIN
devlib/bin/ppc64le/busybox Executable file → Normal file

Binary file not shown.

BIN
devlib/bin/x86/busybox Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -20,6 +20,8 @@ from datetime import timedelta
from devlib.collector import (CollectorBase, CollectorOutput,
CollectorOutputEntry)
from devlib.target import KernelConfigTristate
from devlib.exception import TargetStableError
class KernelLogEntry(object):
@@ -152,6 +154,10 @@ class DmesgCollector(CollectorBase):
def __init__(self, target, level=LOG_LEVELS[-1], facility='kern'):
super(DmesgCollector, self).__init__(target)
if not target.is_rooted:
raise TargetStableError('Cannot collect dmesg on non-rooted target')
self.output_path = None
if level not in self.LOG_LEVELS:
@@ -167,6 +173,8 @@ class DmesgCollector(CollectorBase):
self.basic_dmesg = '--force-prefix' not in \
self.target.execute('dmesg -h', check_exit_code=False)
self.facility = facility
self.needs_root = bool(target.config.typed_config.get(
'CONFIG_SECURITY_DMESG_RESTRICT', KernelConfigTristate.NO))
self.reset()
@property
@@ -178,7 +186,7 @@ class DmesgCollector(CollectorBase):
def start(self):
self.reset()
# Empty the dmesg ring buffer
# Empty the dmesg ring buffer. This requires root in all cases
self.target.execute('dmesg -c', as_root=True)
def stop(self):
@@ -195,7 +203,7 @@ class DmesgCollector(CollectorBase):
facility=self.facility,
)
self.dmesg_out = self.target.execute(cmd)
self.dmesg_out = self.target.execute(cmd, as_root=self.needs_root)
def set_output(self, output_path):
self.output_path = output_path

View File

@@ -24,8 +24,9 @@ from devlib.collector import (CollectorBase, CollectorOutput,
from devlib.utils.misc import ensure_file_directory_exists as _f
PERF_COMMAND_TEMPLATE = '{binary} {command} {options} {events} sleep 1000 > {outfile} 2>&1 '
PERF_STAT_COMMAND_TEMPLATE = '{binary} {command} {options} {events} {sleep_cmd} > {outfile} 2>&1 '
PERF_REPORT_COMMAND_TEMPLATE= '{binary} report {options} -i {datafile} > {outfile} 2>&1 '
PERF_REPORT_SAMPLE_COMMAND_TEMPLATE= '{binary} report-sample {options} -i {datafile} > {outfile} '
PERF_RECORD_COMMAND_TEMPLATE= '{binary} record {options} {events} -o {outfile}'
PERF_DEFAULT_EVENTS = [
@@ -90,12 +91,16 @@ class PerfCollector(CollectorBase):
events=None,
optionstring=None,
report_options=None,
run_report_sample=False,
report_sample_options=None,
labels=None,
force_install=False):
super(PerfCollector, self).__init__(target)
self.force_install = force_install
self.labels = labels
self.report_options = report_options
self.run_report_sample = run_report_sample
self.report_sample_options = report_sample_options
self.output_path = None
# Validate parameters
@@ -121,6 +126,10 @@ class PerfCollector(CollectorBase):
self.command = command
else:
raise ValueError('Unsupported perf command, must be stat or record')
if report_options and (command != 'record'):
raise ValueError('report_options specified, but command is not record')
if report_sample_options and (command != 'record'):
raise ValueError('report_sample_options specified, but command is not record')
self.binary = self.target.get_installed(self.perf_type)
if self.force_install or not self.binary:
@@ -138,17 +147,20 @@ class PerfCollector(CollectorBase):
self.target.remove(filepath)
filepath = self._get_target_file(label, 'rpt')
self.target.remove(filepath)
filepath = self._get_target_file(label, 'rptsamples')
self.target.remove(filepath)
def start(self):
for command in self.commands:
self.target.kick_off(command)
self.target.background(command, as_root=self.target.is_rooted)
def stop(self):
self.target.killall(self.perf_type, signal='SIGINT',
as_root=self.target.is_rooted)
# perf doesn't transmit the signal to its sleep call so handled here:
self.target.killall('sleep', as_root=self.target.is_rooted)
# NB: we hope that no other "important" sleep is on-going
if self.perf_type == "perf" and self.command == "stat":
# perf doesn't transmit the signal to its sleep call so handled here:
self.target.killall('sleep', as_root=self.target.is_rooted)
# NB: we hope that no other "important" sleep is on-going
def set_output(self, output_path):
self.output_path = output_path
@@ -164,6 +176,9 @@ class PerfCollector(CollectorBase):
self._wait_for_data_file_write(label, self.output_path)
path = self._pull_target_file_to_host(label, 'rpt', self.output_path)
output.append(CollectorOutputEntry(path, 'file'))
if self.run_report_sample:
report_samples_path = self._pull_target_file_to_host(label, 'rptsamples', self.output_path)
output.append(CollectorOutputEntry(report_samples_path, 'file'))
else:
path = self._pull_target_file_to_host(label, 'out', self.output_path)
output.append(CollectorOutputEntry(path, 'file'))
@@ -188,10 +203,12 @@ class PerfCollector(CollectorBase):
def _build_perf_stat_command(self, options, events, label):
event_string = ' '.join(['-e {}'.format(e) for e in events])
command = PERF_COMMAND_TEMPLATE.format(binary = self.binary,
sleep_cmd = 'sleep 1000' if self.perf_type == 'perf' else ''
command = PERF_STAT_COMMAND_TEMPLATE.format(binary = self.binary,
command = self.command,
options = options or '',
events = event_string,
sleep_cmd = sleep_cmd,
outfile = self._get_target_file(label, 'out'))
return command
@@ -202,6 +219,13 @@ class PerfCollector(CollectorBase):
outfile=self._get_target_file(label, 'rpt'))
return command
def _build_perf_report_sample_command(self, label):
command = PERF_REPORT_SAMPLE_COMMAND_TEMPLATE.format(binary=self.binary,
options=self.report_sample_options or '',
datafile=self._get_target_file(label, 'data'),
outfile=self._get_target_file(label, 'rptsamples'))
return command
def _build_perf_record_command(self, options, label):
event_string = ' '.join(['-e {}'.format(e) for e in self.events])
command = PERF_RECORD_COMMAND_TEMPLATE.format(binary=self.binary,
@@ -234,6 +258,9 @@ class PerfCollector(CollectorBase):
data_file_finished_writing = True
report_command = self._build_perf_report_command(self.report_options, label)
self.target.execute(report_command)
if self.run_report_sample:
report_sample_command = self._build_perf_report_sample_command(label)
self.target.execute(report_sample_command)
def _validate_events(self, events):
available_events_string = self.target.execute('{} list | {} cat'.format(self.perf_type, self.target.busybox))

View File

@@ -33,8 +33,8 @@ from devlib.utils.misc import InitCheckpoint
_KILL_TIMEOUT = 3
def _kill_pgid_cmd(pgid, sig):
return 'kill -{} -{}'.format(sig.value, pgid)
def _kill_pgid_cmd(pgid, sig, busybox):
return '{} kill -{} -{}'.format(busybox, sig.value, pgid)
class ConnectionBase(InitCheckpoint):
@@ -105,12 +105,20 @@ class BackgroundCommand(ABC):
"""
self.send_signal(signal.SIGKILL)
@abstractmethod
def cancel(self, kill_timeout=_KILL_TIMEOUT):
"""
Try to gracefully terminate the process by sending ``SIGTERM``, then
waiting for ``kill_timeout`` to send ``SIGKILL``.
"""
if self.poll() is None:
self._cancel(kill_timeout=kill_timeout)
@abstractmethod
def _cancel(self, kill_timeout):
"""
Method to override in subclasses to implement :meth:`cancel`.
"""
pass
@abstractmethod
def wait(self):
@@ -209,11 +217,11 @@ class PopenBackgroundCommand(BackgroundCommand):
def poll(self):
return self.popen.poll()
def cancel(self, kill_timeout=_KILL_TIMEOUT):
def _cancel(self, kill_timeout):
popen = self.popen
os.killpg(os.getpgid(popen.pid), signal.SIGTERM)
try:
popen.wait(timeout=_KILL_TIMEOUT)
popen.wait(timeout=kill_timeout)
except subprocess.TimeoutExpired:
os.killpg(os.getpgid(popen.pid), signal.SIGKILL)
@@ -250,7 +258,7 @@ class ParamikoBackgroundCommand(BackgroundCommand):
return
# Use -PGID to target a process group rather than just the process
# itself
cmd = _kill_pgid_cmd(self.pid, sig)
cmd = _kill_pgid_cmd(self.pid, sig, self.conn.busybox)
self.conn.execute(cmd, as_root=self.as_root)
@property
@@ -266,7 +274,7 @@ class ParamikoBackgroundCommand(BackgroundCommand):
else:
return None
def cancel(self, kill_timeout=_KILL_TIMEOUT):
def _cancel(self, kill_timeout):
self.send_signal(signal.SIGTERM)
# Check if the command terminated quickly
time.sleep(10e-3)
@@ -314,7 +322,7 @@ class AdbBackgroundCommand(BackgroundCommand):
def send_signal(self, sig):
self.conn.execute(
_kill_pgid_cmd(self.pid, sig),
_kill_pgid_cmd(self.pid, sig, self.conn.busybox),
as_root=self.as_root,
)
@@ -340,10 +348,10 @@ class AdbBackgroundCommand(BackgroundCommand):
def poll(self):
return self.adb_popen.poll()
def cancel(self, kill_timeout=_KILL_TIMEOUT):
def _cancel(self, kill_timeout):
self.send_signal(signal.SIGTERM)
try:
self.adb_popen.wait(timeout=_KILL_TIMEOUT)
self.adb_popen.wait(timeout=kill_timeout)
except subprocess.TimeoutExpired:
self.send_signal(signal.SIGKILL)
self.adb_popen.kill()
@@ -436,7 +444,7 @@ class TransferManagerBase(ABC):
self.transfer_started.clear()
self.transfer_completed.clear()
self.transfer_aborted.clear()
def _monitor(self):
start_t = monotonic()
self.transfer_completed.wait(self.start_transfer_poll_delay)
@@ -458,6 +466,7 @@ class PopenTransferManager(TransferManagerBase):
if self.transfer:
self.transfer.cancel()
self.transfer = None
self.last_sample = None
def isactive(self):
size_fn = self._push_dest_size if self.direction == 'push' else self._pull_dest_size
@@ -469,8 +478,9 @@ class PopenTransferManager(TransferManagerBase):
def set_transfer_and_wait(self, popen_bg_cmd):
self.transfer = popen_bg_cmd
self.last_sample = None
ret = self.transfer.wait()
if ret and not self.transfer_aborted.is_set():
raise subprocess.CalledProcessError(ret, self.transfer.popen.args)
elif self.transfer_aborted.is_set():
@@ -520,4 +530,4 @@ class SSHTransferManager(TransferManagerBase):
self.to_transfer = args[1]
elif len(args) == 2: # For SFTPClient callbacks
self.transferred = args[0]
self.to_transfer = args[1]
self.to_transfer = args[1]

View File

@@ -102,7 +102,7 @@ class LocalConnection(ConnectionBase):
if self.unrooted:
raise TargetStableError('unrooted')
password = self._get_password()
command = "echo {} | sudo -p ' ' -S -- sh -c {}".format(quote(password), quote(command))
command = "echo {} | sudo -k -p ' ' -S -- sh -c {}".format(quote(password), quote(command))
ignore = None if check_exit_code else 'all'
try:
stdout, stderr = check_output(command, shell=True, timeout=timeout, ignore=ignore)
@@ -127,7 +127,7 @@ class LocalConnection(ConnectionBase):
password = self._get_password()
# The sudo prompt will add a space on stderr, but we cannot filter
# it out here
command = "echo {} | sudo -p ' ' -S -- sh -c {}".format(quote(password), quote(command))
command = "echo {} | sudo -k -p ' ' -S -- sh -c {}".format(quote(password), quote(command))
# Make sure to get a new PGID so PopenBackgroundCommand() can kill
# all sub processes that could be started without troubles.

View File

@@ -301,7 +301,7 @@ class CpufreqModule(Module):
except ValueError:
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
def get_frequency(self, cpu):
def get_frequency(self, cpu, cpuinfo=False):
"""
Returns the current frequency currently set for the specified CPU.
@@ -309,12 +309,18 @@ class CpufreqModule(Module):
try to read the current frequency and the following exception will be
raised ::
:param cpuinfo: Read the value in the cpuinfo interface that reflects
the actual running frequency.
:raises: TargetStableError if for some reason the frequency could not be read.
"""
if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu)
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_cur_freq'.format(cpu)
sysfile = '/sys/devices/system/cpu/{}/cpufreq/{}'.format(
cpu,
'cpuinfo_cur_freq' if cpuinfo else 'scaling_cur_freq')
return self.target.read_int(sysfile)
def set_frequency(self, cpu, frequency, exact=True):
@@ -350,6 +356,10 @@ class CpufreqModule(Module):
raise TargetStableError('Can\'t set {} frequency; governor must be "userspace"'.format(cpu))
sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_setspeed'.format(cpu)
self.target.write_value(sysfile, value, verify=False)
cpuinfo = self.get_frequency(cpu, cpuinfo=True)
if cpuinfo != value:
self.logger.warning(
'The cpufreq value has not been applied properly cpuinfo={} request={}'.format(cpuinfo, value))
except ValueError:
raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))

View File

@@ -14,6 +14,7 @@
#
from devlib.module import Module
from devlib.exception import TargetTransientError
class HotplugModule(Module):
@@ -39,9 +40,13 @@ class HotplugModule(Module):
return [cpu for cpu in range(self.target.number_of_cpus)
if self.target.file_exists(self._cpu_path(self.target, cpu))]
def online_all(self):
def online_all(self, verify=True):
self.target._execute_util('hotplug_online_all', # pylint: disable=protected-access
as_root=self.target.is_rooted)
if verify:
offline = set(self.target.list_offline_cpus())
if offline:
raise TargetTransientError('The following CPUs failed to come back online: {}'.format(offline))
def online(self, *args):
for cpu in args:
@@ -57,3 +62,23 @@ class HotplugModule(Module):
return
value = 1 if online else 0
self.target.write_value(path, value)
def _get_path(self, path):
return self.target.path.join(self.base_path,
path)
def fail(self, cpu, state):
path = self._get_path('cpu{}/hotplug/fail'.format(cpu))
return self.target.write_value(path, state)
def get_state(self, cpu):
path = self._get_path('cpu{}/hotplug/state'.format(cpu))
return self.target.read_value(path)
def get_states(self):
path = self._get_path('hotplug/states')
states_string = self.target.read_value(path)
return dict(
map(str.strip, string.split(':', 1))
for string in states_string.strip().splitlines()
)

View File

@@ -15,14 +15,13 @@
import logging
import re
from enum import Enum
from past.builtins import basestring
from devlib.module import Module
from devlib.utils.misc import memoized
from devlib.utils.types import boolean
from devlib.exception import TargetStableError
class SchedProcFSNode(object):
"""
@@ -147,43 +146,44 @@ class SchedProcFSNode(object):
self._dyn_attrs[key] = self._build_node(key, nodes[key])
class DocInt(int):
# See https://stackoverflow.com/a/50473952/5096023
def __new__(cls, value, doc):
new = super(DocInt, cls).__new__(cls, value)
new.__doc__ = doc
return new
class SchedDomainFlag(DocInt, Enum):
class _SchedDomainFlag:
"""
Represents a sched domain flag
Backward-compatible emulation of the former :class:`enum.Enum` that will
work on recent kernels with dynamic sched domain flags name and no value
exposed.
"""
# pylint: disable=bad-whitespace
# Domain flags obtained from include/linux/sched/topology.h on v4.17
# https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux/+/v4.17/include/linux/sched/topology.h#20
SD_LOAD_BALANCE = 0x0001, "Do load balancing on this domain"
SD_BALANCE_NEWIDLE = 0x0002, "Balance when about to become idle"
SD_BALANCE_EXEC = 0x0004, "Balance on exec"
SD_BALANCE_FORK = 0x0008, "Balance on fork, clone"
SD_BALANCE_WAKE = 0x0010, "Balance on wakeup"
SD_WAKE_AFFINE = 0x0020, "Wake task to waking CPU"
SD_ASYM_CPUCAPACITY = 0x0040, "Groups have different max cpu capacities"
SD_SHARE_CPUCAPACITY = 0x0080, "Domain members share cpu capacity"
SD_SHARE_POWERDOMAIN = 0x0100, "Domain members share power domain"
SD_SHARE_PKG_RESOURCES = 0x0200, "Domain members share cpu pkg resources"
SD_SERIALIZE = 0x0400, "Only a single load balancing instance"
SD_ASYM_PACKING = 0x0800, "Place busy groups earlier in the domain"
SD_PREFER_SIBLING = 0x1000, "Prefer to place tasks in a sibling domain"
SD_OVERLAP = 0x2000, "Sched_domains of this level overlap"
SD_NUMA = 0x4000, "Cross-node balancing"
# Only defined in Android
# https://android.googlesource.com/kernel/common/+/android-4.14/include/linux/sched/topology.h#29
SD_SHARE_CAP_STATES = 0x8000, "(Android only) Domain members share capacity state"
@classmethod
def check_version(cls, target, logger):
_INSTANCES = {}
"""
Dictionary storing the instances so that they can be compared with ``is``
operator.
"""
def __new__(cls, name, value, doc=None):
self = super().__new__(cls)
self.name = name
self._value = value
self.__doc__ = doc
return cls._INSTANCES.setdefault(self, self)
def __eq__(self, other):
# We *have to* check for "value" as well, otherwise it will be
# impossible to keep in the same set 2 instances with differing values.
return self.name == other.name and self._value == other._value
def __hash__(self):
return hash((self.name, self._value))
@property
def value(self):
value = self._value
if value is None:
raise AttributeError('The kernel does not expose the sched domain flag values')
else:
return value
@staticmethod
def check_version(target, logger):
"""
Check the target and see if its kernel version matches our view of the world
"""
@@ -197,38 +197,139 @@ class SchedDomainFlag(DocInt, Enum):
"but target is running v{}".format(ref_parts, parts)
)
def __str__(self):
return self.name
def __repr__(self):
return '<SchedDomainFlag: {}>'.format(self.name)
class _SchedDomainFlagMeta(type):
"""
Metaclass of :class:`SchedDomainFlag`.
Provides some level of emulation of :class:`enum.Enum` behavior for
backward compatibility.
"""
@property
def _flags(self):
return [
attr
for name, attr in self.__dict__.items()
if name.startswith('SD_')
]
def __getitem__(self, i):
return self._flags[i]
def __len__(self):
return len(self._flags)
# These would be provided by collections.abc.Sequence, but using it on a
# metaclass seems to have issues around __init_subclass__
def __iter__(self):
return iter(self._flags)
def __reversed__(self):
return reversed(self._flags)
def __contains__(self, x):
return x in self._flags
@property
def __members__(self):
return {flag.name: flag for flag in self._flags}
class SchedDomainFlag(_SchedDomainFlag, metaclass=_SchedDomainFlagMeta):
"""
Represents a sched domain flag.
.. note:: ``SD_*`` class attributes are deprecated, new code should never
test a given flag against one of these attributes with ``is`` (.e.g ``x
is SchedDomainFlag.SD_LOAD_BALANCE``. This is because the
``SD_LOAD_BALANCE`` flag exists in two flavors that are not equal: one
with a value (the class attribute) and one without (dynamically created
when parsing flags for new kernels). Old code ran on old kernels should
work fine though.
"""
# pylint: disable=bad-whitespace
# Domain flags obtained from include/linux/sched/topology.h on v4.17
# https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux/+/v4.17/include/linux/sched/topology.h#20
SD_LOAD_BALANCE = _SchedDomainFlag("SD_LOAD_BALANCE", 0x0001, "Do load balancing on this domain")
SD_BALANCE_NEWIDLE = _SchedDomainFlag("SD_BALANCE_NEWIDLE", 0x0002, "Balance when about to become idle")
SD_BALANCE_EXEC = _SchedDomainFlag("SD_BALANCE_EXEC", 0x0004, "Balance on exec")
SD_BALANCE_FORK = _SchedDomainFlag("SD_BALANCE_FORK", 0x0008, "Balance on fork, clone")
SD_BALANCE_WAKE = _SchedDomainFlag("SD_BALANCE_WAKE", 0x0010, "Balance on wakeup")
SD_WAKE_AFFINE = _SchedDomainFlag("SD_WAKE_AFFINE", 0x0020, "Wake task to waking CPU")
SD_ASYM_CPUCAPACITY = _SchedDomainFlag("SD_ASYM_CPUCAPACITY", 0x0040, "Groups have different max cpu capacities")
SD_SHARE_CPUCAPACITY = _SchedDomainFlag("SD_SHARE_CPUCAPACITY", 0x0080, "Domain members share cpu capacity")
SD_SHARE_POWERDOMAIN = _SchedDomainFlag("SD_SHARE_POWERDOMAIN", 0x0100, "Domain members share power domain")
SD_SHARE_PKG_RESOURCES = _SchedDomainFlag("SD_SHARE_PKG_RESOURCES", 0x0200, "Domain members share cpu pkg resources")
SD_SERIALIZE = _SchedDomainFlag("SD_SERIALIZE", 0x0400, "Only a single load balancing instance")
SD_ASYM_PACKING = _SchedDomainFlag("SD_ASYM_PACKING", 0x0800, "Place busy groups earlier in the domain")
SD_PREFER_SIBLING = _SchedDomainFlag("SD_PREFER_SIBLING", 0x1000, "Prefer to place tasks in a sibling domain")
SD_OVERLAP = _SchedDomainFlag("SD_OVERLAP", 0x2000, "Sched_domains of this level overlap")
SD_NUMA = _SchedDomainFlag("SD_NUMA", 0x4000, "Cross-node balancing")
# Only defined in Android
# https://android.googlesource.com/kernel/common/+/android-4.14/include/linux/sched/topology.h#29
SD_SHARE_CAP_STATES = _SchedDomainFlag("SD_SHARE_CAP_STATES", 0x8000, "(Android only) Domain members share capacity state")
class SchedDomain(SchedProcFSNode):
"""
Represents a sched domain as seen through procfs
"""
def __init__(self, nodes):
super(SchedDomain, self).__init__(nodes)
super().__init__(nodes)
obj_flags = set()
for flag in list(SchedDomainFlag):
if self.flags & flag.value == flag.value:
obj_flags.add(flag)
flags = self.flags
# Recent kernels now have a space-separated list of flags instead of a
# packed bitfield
if isinstance(flags, str):
flags = {
_SchedDomainFlag(name=name, value=None)
for name in flags.split()
}
else:
def has_flag(flags, flag):
return flags & flag.value == flag.value
self.flags = obj_flags
flags = {
flag
for flag in SchedDomainFlag
if has_flag(flags, flag)
}
self.flags = flags
def _select_path(target, paths, name):
for p in paths:
if target.file_exists(p):
return p
raise TargetStableError('No {} found. Tried: {}'.format(name, ', '.join(paths)))
class SchedProcFSData(SchedProcFSNode):
"""
Root class for creating & storing SchedProcFSNode instances
"""
_read_depth = 6
sched_domain_root = '/proc/sys/kernel/sched_domain'
@classmethod
def get_data_root(cls, target):
# Location differs depending on kernel version
paths = ['/sys/kernel/debug/sched/domains/', '/proc/sys/kernel/sched_domain']
return _select_path(target, paths, "sched_domain debug directory")
@staticmethod
def available(target):
path = SchedProcFSData.sched_domain_root
cpus = target.list_directory(path) if target.file_exists(path) else []
try:
path = SchedProcFSData.get_data_root(target)
except TargetStableError:
return False
cpus = target.list_directory(path)
if not cpus:
return False
@@ -242,7 +343,7 @@ class SchedProcFSData(SchedProcFSNode):
def __init__(self, target, path=None):
if path is None:
path = self.sched_domain_root
path = SchedProcFSData.get_data_root(target)
procfs = target.read_tree_values(path, depth=self._read_depth)
super(SchedProcFSData, self).__init__(procfs)
@@ -275,6 +376,15 @@ class SchedModule(Module):
return schedproc or debug or dmips
def __init__(self, target):
super().__init__(target)
@classmethod
def get_sched_features_path(cls, target):
# Location differs depending on kernel version
paths = ['/sys/kernel/debug/sched/features', '/sys/kernel/debug/sched_features']
return _select_path(target, paths, "sched_features file")
def get_kernel_attributes(self, matching=None, check_exit_code=True):
"""
Get the value of scheduler attributes.
@@ -331,12 +441,12 @@ class SchedModule(Module):
def target_has_debug(cls, target):
if target.config.get('SCHED_DEBUG') != 'y':
return False
return target.file_exists('/sys/kernel/debug/sched_features')
@property
@memoized
def has_debug(self):
return self.target_has_debug(self.target)
try:
cls.get_sched_features_path(target)
return True
except TargetStableError:
return False
def get_features(self):
"""
@@ -344,9 +454,7 @@ class SchedModule(Module):
:returns: a dictionary of features and their "is enabled" status
"""
if not self.has_debug:
raise RuntimeError("sched_features not available")
feats = self.target.read_value('/sys/kernel/debug/sched_features')
feats = self.target.read_value(self.get_sched_features_path(self.target))
features = {}
for feat in feats.split():
value = True
@@ -366,13 +474,11 @@ class SchedModule(Module):
:raise ValueError: if the specified enable value is not bool
:raise RuntimeError: if the specified feature cannot be set
"""
if not self.has_debug:
raise RuntimeError("sched_features not available")
feature = feature.upper()
feat_value = feature
if not boolean(enable):
feat_value = 'NO_' + feat_value
self.target.write_value('/sys/kernel/debug/sched_features',
self.target.write_value(self.get_sched_features_path(self.target),
feat_value, verify=False)
if not verify:
return
@@ -384,10 +490,10 @@ class SchedModule(Module):
def get_cpu_sd_info(self, cpu):
"""
:returns: An object view of /proc/sys/kernel/sched_domain/cpu<cpu>/*
:returns: An object view of the sched_domain debug directory of 'cpu'
"""
path = self.target.path.join(
SchedProcFSData.sched_domain_root,
SchedProcFSData.get_data_root(self.target),
"cpu{}".format(cpu)
)
@@ -395,7 +501,7 @@ class SchedModule(Module):
def get_sd_info(self):
"""
:returns: An object view of /proc/sys/kernel/sched_domain/*
:returns: An object view of the entire sched_domain debug directory
"""
return SchedProcFSData(self.target)

View File

@@ -76,6 +76,48 @@ GOOGLE_DNS_SERVER_ADDRESS = '8.8.8.8'
installed_package_info = namedtuple('installed_package_info', 'apk_path package')
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
class Target(object):
path = None
@@ -135,6 +177,14 @@ class Target(object):
def kernel_version(self):
return KernelVersion(self.execute('{} uname -r -v'.format(quote(self.busybox))).strip())
@property
def hostid(self):
return int(self.execute('{} hostid'.format(self.busybox)).strip(), 16)
@property
def hostname(self):
return self.execute('{} hostname'.format(self.busybox)).strip()
@property
def os_version(self): # pylint: disable=no-self-use
return {}
@@ -286,6 +336,14 @@ class Target(object):
if connect:
self.connect()
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
# connection and initialization
def connect(self, timeout=None, check_boot_completed=True):
@@ -425,6 +483,7 @@ class Target(object):
dst_mkdir(dest)
@call_conn
def push(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
sources = glob.glob(source) if globbing else [source]
self._prepare_xfer('push', sources, dest)
@@ -480,6 +539,7 @@ class Target(object):
return paths
@call_conn
def pull(self, source, dest, as_root=False, timeout=None, globbing=False): # pylint: disable=arguments-differ
if globbing:
sources = self._expand_glob(source, as_root=as_root)
@@ -536,10 +596,7 @@ class Target(object):
# execution
def execute(self, command, timeout=None, check_exit_code=True,
as_root=False, strip_colors=True, will_succeed=False,
force_locale='C'):
def _prepare_cmd(self, command, force_locale):
# 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
@@ -550,12 +607,28 @@ class Target(object):
if self.executables_directory:
command = "export PATH={}:$PATH && {}".format(quote(self.executables_directory), command)
return command
@call_conn
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)
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)
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
return self.conn.background(command, stdout, stderr, as_root)
@call_conn
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False,
force_locale='C', timeout=None):
command = self._prepare_cmd(command, force_locale)
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
def invoke(self, binary, args=None, in_directory=None, on_cpus=None,
redirect_stderr=False, as_root=False, timeout=30):
@@ -672,6 +745,7 @@ class Target(object):
pass
self.conn.connected_as_root = None
@call_conn
def check_responsive(self, explode=True):
try:
self.conn.execute('ls /', timeout=5)
@@ -986,6 +1060,7 @@ class Target(object):
os.remove(shutils_ofile)
os.rmdir(tmp_dir)
@call_conn
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)
@@ -1150,6 +1225,7 @@ class LinuxTarget(Target):
def wait_boot_complete(self, timeout=10):
pass
@call_conn
def kick_off(self, command, as_root=False):
command = 'sh -c {} 1>/dev/null 2>/dev/null &'.format(quote(command))
return self.conn.execute(command, as_root=as_root)
@@ -1667,7 +1743,7 @@ class AndroidTarget(Target):
self.remove(on_device_executable, as_root=self.needs_su)
def dump_logcat(self, filepath, filter=None, logcat_format=None, append=False,
timeout=30): # pylint: disable=redefined-builtin
timeout=60): # pylint: disable=redefined-builtin
op = '>>' if append else '>'
filtstr = ' -s {}'.format(quote(filter)) if filter else ''
formatstr = ' -v {}'.format(quote(logcat_format)) if logcat_format else ''
@@ -1683,18 +1759,24 @@ class AndroidTarget(Target):
self.remove(dev_path)
def clear_logcat(self):
with self.clear_logcat_lock:
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)
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()
def get_logcat_monitor(self, regexps=None):
return LogcatMonitor(self, regexps)
@call_conn
def wait_for_device(self, timeout=30):
self.conn.wait_for_device()
@call_conn
def reboot_bootloader(self, timeout=30):
self.conn.reboot_bootloader()

View File

@@ -20,18 +20,20 @@ Utility functions for working with Android devices through adb.
"""
# pylint: disable=E1103
import glob
import os
import re
import sys
import time
import logging
import tempfile
import subprocess
from collections import defaultdict
import os
import pexpect
import xml.etree.ElementTree
import zipfile
import re
import subprocess
import sys
import tempfile
import time
import uuid
import zipfile
from collections import defaultdict
from io import StringIO
from lxml import etree
try:
from shlex import quote
@@ -227,7 +229,10 @@ class ApkInfo(object):
command = [dexdump, '-l', 'xml', extracted]
dump = self._run(command)
xml_tree = xml.etree.ElementTree.fromstring(dump)
# Dexdump from build tools v30.0.X does not seem to produce
# valid xml from certain APKs so ignore errors and attempt to recover.
parser = etree.XMLParser(encoding='utf-8', recover=True)
xml_tree = etree.parse(StringIO(dump), parser)
package = next((i for i in xml_tree.iter('package')
if i.attrib['name'] == self.package), None)
@@ -577,7 +582,7 @@ def adb_background_shell(conn, command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
as_root=False):
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
"""Runs the specified command in a subprocess, returning the the Popen object."""
device = conn.device
adb_server = conn.adb_server
@@ -598,7 +603,7 @@ def adb_background_shell(conn, command,
p = subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
# Out of band PID lookup, to avoid conflicting needs with stdout redirection
find_pid = 'ps -A -o pid,args | grep {}'.format(quote(uuid_var))
find_pid = '{} ps -A -o pid,args | grep {}'.format(conn.busybox, quote(uuid_var))
ps_out = conn.execute(find_pid)
pids = [
int(line.strip().split(' ', 1)[0])
@@ -734,11 +739,13 @@ def _discover_aapt(env):
aapt2_path = ''
versions = os.listdir(env.build_tools)
for version in reversed(sorted(versions)):
if not aapt2_path and not os.path.isfile(aapt2_path):
if not os.path.isfile(aapt2_path):
aapt2_path = os.path.join(env.build_tools, version, 'aapt2')
if not aapt_path and not os.path.isfile(aapt_path):
if not os.path.isfile(aapt_path):
aapt_path = os.path.join(env.build_tools, version, 'aapt')
aapt_version = 1
# Use latest available version for aapt/appt2 but ensure at least one is valid.
if os.path.isfile(aapt2_path) or os.path.isfile(aapt_path):
break
# Use aapt2 only if present and we have a suitable version

View File

@@ -37,6 +37,7 @@ import string
import subprocess
import sys
import threading
import types
import wrapt
import warnings
@@ -152,7 +153,7 @@ def preexec_function():
check_output_logger = logging.getLogger('check_output')
# Popen is not thread safe. If two threads attempt to call it at the same time,
# one may lock up. See https://bugs.python.org/issue12739.
check_output_lock = threading.Lock()
check_output_lock = threading.RLock()
def get_subprocess(command, **kwargs):
@@ -783,7 +784,7 @@ class tls_property:
def __init__(self, factory):
self.factory = factory
# Lock accesses to shared WeakKeyDictionary and WeakSet
self.lock = threading.Lock()
self.lock = threading.RLock()
def __get__(self, instance, owner=None):
return _BoundTLSProperty(self, instance, owner)
@@ -883,10 +884,14 @@ class _BoundTLSProperty:
class InitCheckpointMeta(type):
"""
Metaclass providing an ``initialized`` boolean attributes on instances.
Metaclass providing an ``initialized`` and ``is_in_use`` boolean attributes
on instances.
``initialized`` is set to ``True`` once the ``__init__`` constructor has
returned. It will deal cleanly with nested calls to ``super().__init__``.
``is_in_use`` is set to ``True`` when an instance method is being called.
This allows to detect reentrance.
"""
def __new__(metacls, name, bases, dct, **kwargs):
cls = super().__new__(metacls, name, bases, dct, **kwargs)
@@ -895,6 +900,7 @@ class InitCheckpointMeta(type):
@wraps(init_f)
def init_wrapper(self, *args, **kwargs):
self.initialized = False
self.is_in_use = False
# Track the nesting of super()__init__ to set initialized=True only
# when the outer level is finished
@@ -918,6 +924,45 @@ class InitCheckpointMeta(type):
cls.__init__ = init_wrapper
# Set the is_in_use attribute to allow external code to detect if the
# methods are about to be re-entered.
def make_wrapper(f):
if f is None:
return None
@wraps(f)
def wrapper(self, *args, **kwargs):
f_ = f.__get__(self, self.__class__)
initial_state = self.is_in_use
try:
self.is_in_use = True
return f_(*args, **kwargs)
finally:
self.is_in_use = initial_state
return wrapper
# This will not decorate methods defined in base classes, but we cannot
# use inspect.getmembers() as it uses __get__ to bind the attributes to
# the class, making staticmethod indistinguishible from instance
# methods.
for name, attr in cls.__dict__.items():
# Only wrap the methods (exposed as functions), not things like
# classmethod or staticmethod
if (
name not in ('__init__', '__new__') and
isinstance(attr, types.FunctionType)
):
setattr(cls, name, make_wrapper(attr))
elif isinstance(attr, property):
prop = property(
fget=make_wrapper(attr.fget),
fset=make_wrapper(attr.fset),
fdel=make_wrapper(attr.fdel),
doc=attr.__doc__,
)
setattr(cls, name, prop)
return cls

View File

@@ -59,7 +59,7 @@ from devlib.connection import (ConnectionBase, ParamikoBackgroundCommand, PopenB
SSHTransferManager)
DEFAULT_SSH_SUDO_COMMAND = "sudo -p ' ' -S -- sh -c {}"
DEFAULT_SSH_SUDO_COMMAND = "sudo -k -p ' ' -S -- sh -c {}"
ssh = None
@@ -466,7 +466,13 @@ class SshConnection(SshConnectionBase):
return self.transfer_mgr.progress_cb if self.transfer_mgr is not None else None
def _get_sftp(self, timeout):
sftp = self.client.open_sftp()
try:
sftp = self.client.open_sftp()
except paramiko.ssh_exception.SSHException as e:
if 'EOF during negotiation' in str(e):
raise TargetStableError('The SSH server does not support SFTP. Please install and enable appropriate module.') from e
else:
raise
sftp.get_channel().settimeout(timeout)
return sftp
@@ -654,7 +660,7 @@ class SshConnection(SshConnectionBase):
# Read are not buffered so we will always get the data as soon as
# they arrive
return (
channel.makefile_stdin(),
channel.makefile_stdin('w', 0),
channel.makefile(),
channel.makefile_stderr(),
)
@@ -685,11 +691,11 @@ class SshConnection(SshConnectionBase):
w = os.fdopen(w, 'wb')
# Turn a file descriptor into a file-like object
elif isinstance(stream_out, int) and stream_out >= 0:
r = os.fdopen(stream_out, 'rb')
r = os.fdopen(stream_in, 'rb')
w = os.fdopen(stream_out, 'wb')
# file-like object
else:
r = stream_out
r = stream_in
w = stream_out
return (r, w)

View File

@@ -21,7 +21,7 @@ from subprocess import Popen, PIPE
VersionTuple = namedtuple('Version', ['major', 'minor', 'revision', 'dev'])
version = VersionTuple(1, 3, 0, '')
version = VersionTuple(1, 3, 2, '')
def get_devlib_version():
@@ -33,8 +33,11 @@ def get_devlib_version():
def get_commit():
p = Popen(['git', 'rev-parse', 'HEAD'], cwd=os.path.dirname(__file__),
stdout=PIPE, stderr=PIPE)
try:
p = Popen(['git', 'rev-parse', 'HEAD'], cwd=os.path.dirname(__file__),
stdout=PIPE, stderr=PIPE)
except FileNotFoundError:
return None
std, _ = p.communicate()
p.wait()
if p.returncode:

View File

@@ -147,7 +147,7 @@ Connection Types
.. class:: SshConnection(host, username, password=None, keyfile=None, port=22,\
timeout=None, platform=None, \
sudo_cmd="sudo -- sh -c {}", strict_host_check=True, \
use_scp=False, poll_transfers=False,
use_scp=False, poll_transfers=False, \
start_transfer_poll_delay=30, total_transfer_timeout=3600,\
transfer_poll_period=30)
@@ -177,7 +177,7 @@ Connection Types
:param platform: Specify the platform to be used. The generic :class:`~devlib.platform.Platform`
class is used by default.
:param sudo_cmd: Specify the format of the command used to grant sudo access.
:param strict_host_check: Specify the ssh connection parameter ``StrictHostKeyChecking``,
:param strict_host_check: Specify the ssh connection parameter ``StrictHostKeyChecking``,
:param use_scp: Use SCP for file transfers, defaults to SFTP.
:param poll_transfers: Specify whether file transfers should be polled. Polling
monitors the progress of file transfers and periodically

View File

@@ -125,10 +125,21 @@ Target
This is a dict that contains a mapping of OS version elements to their
values. This mapping is OS-specific.
.. attribute:: Target.hostname
A string containing the hostname of the target.
.. attribute:: Target.hostid
A numerical id used to represent the identity of the target.
.. note:: Currently on 64-bit PowerPC devices this id will always be 0. This is
due to the included busybox binary being linked with musl.
.. attribute:: Target.system_id
A unique identifier for the system running on the target. This identifier is
intended to be uninque for the combination of hardware, kernel, and file
intended to be unique for the combination of hardware, kernel, and file
system.
.. attribute:: Target.model
@@ -225,15 +236,16 @@ Target
If transfer polling is supported (ADB connections and SSH connections),
``poll_transfers`` is set in the connection, and a timeout is not specified,
the push will be polled for activity. Inactive transfers will be
cancelled. (See :ref:`connection-types`\ for more information on polling).
cancelled. (See :ref:`connection-types` for more information on polling).
:param source: path on the host
:param dest: path on the target
:param as_root: whether root is required. Defaults to false.
:param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised.
not complete within this period, an exception will be raised. Leave unset
to utilise transfer polling if enabled.
:param globbing: If ``True``, the ``source`` is interpreted as a globbing
pattern instead of being take as-is. If the pattern has mulitple
pattern instead of being take as-is. If the pattern has multiple
matches, ``dest`` must be a folder (or will be created as such if it
does not exists yet).
@@ -244,7 +256,7 @@ Target
If transfer polling is supported (ADB connections and SSH connections),
``poll_transfers`` is set in the connection, and a timeout is not specified,
the pull will be polled for activity. Inactive transfers will be
cancelled. (See :ref:`connection-types`\ for more information on polling).
cancelled. (See :ref:`connection-types` for more information on polling).
:param source: path on the target
:param dest: path on the host
@@ -252,7 +264,7 @@ Target
:param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised.
:param globbing: If ``True``, the ``source`` is interpreted as a globbing
pattern instead of being take as-is. If the pattern has mulitple
pattern instead of being take as-is. If the pattern has multiple
matches, ``dest`` must be a folder (or will be created as such if it
does not exists yet).
@@ -280,7 +292,7 @@ Target
command to get predictable output that can be more safely parsed.
If ``None``, no locale is prepended.
.. method:: Target.background(command [, stdout [, stderr [, as_root]]])
.. method:: Target.background(command [, stdout [, stderr [, as_root, [, force_locale [, timeout]]])
Execute the command on the target, invoking it via subprocess on the host.
This will return :class:`subprocess.Popen` instance for the command.
@@ -292,6 +304,12 @@ Target
this may be used to redirect it to an alternative file handle.
:param as_root: The command will be executed as root. This will fail on
unrooted targets.
:param force_locale: Prepend ``LC_ALL=<force_locale>`` in front of the
command to get predictable output that can be more safely parsed.
If ``None``, no locale is prepended.
:param timeout: Timeout (in seconds) for the execution of the command. When
the timeout expires, :meth:`BackgroundCommand.cancel` is executed to
terminate the command.
.. note:: This **will block the connection** until the command completes.
@@ -700,7 +718,7 @@ Android Target
.. method:: AndroidTarget.get_stay_on_mode()
Returns an integer between ``0`` and ``7`` representing the current
stay-on mode of the device.
stay-on mode of the device.
.. method:: AndroidTarget.ensure_screen_is_off(verify=True)

View File

@@ -69,9 +69,13 @@ for root, dirs, files in os.walk(devlib_dir):
filepaths = [os.path.join(root, f) for f in files]
data_files[package_name].extend([os.path.relpath(f, package_dir) for f in filepaths])
with open("README.rst", "r") as fh:
long_description = fh.read()
params = dict(
name='devlib',
description='A framework for automating workload execution and measurment collection on ARM devices.',
description='A library for interacting with and instrumentation of remote devices.',
long_description=long_description,
version=__version__,
packages=packages,
package_data=data_files,
@@ -92,6 +96,7 @@ params = dict(
'numpy; python_version>="3"',
'pandas<=0.24.2; python_version<"3"',
'pandas; python_version>"3"',
'lxml', # More robust xml parsing
],
extras_require={
'daq': ['daqpower>=2'],