mirror of
https://github.com/ARM-software/devlib.git
synced 2025-09-22 20:01:53 +01:00
Compare commits
100 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5116d46141 | ||
|
beb3b011bd | ||
|
bf4e242129 | ||
|
b1538fd184 | ||
|
5b37dfc50b | ||
|
a948982700 | ||
|
d300b9e57f | ||
|
81db8200e2 | ||
|
9e9af8c6de | ||
|
5473031ab7 | ||
|
a82db5ed37 | ||
|
1381944e5b | ||
|
822c50273f | ||
|
8f3200679c | ||
|
2cfb076e4c | ||
|
98bc0a31e1 | ||
|
345a9ed199 | ||
|
1fc9f6cc94 | ||
|
4194b1dd5e | ||
|
ef2d1a6fa4 | ||
|
33397649b6 | ||
|
ebf1c1a2e1 | ||
|
1d1ba7811d | ||
|
dc7faf46e4 | ||
|
0498017bf0 | ||
|
b2950686a7 | ||
|
f2b5f85dab | ||
|
c0f26e536a | ||
|
1a02f77fdd | ||
|
117686996b | ||
|
8695344969 | ||
|
f23fbd22b6 | ||
|
24e6de67ae | ||
|
07bbf902ba | ||
|
590069f01f | ||
|
bef1ec3afc | ||
|
0c72763d2a | ||
|
2129d85422 | ||
|
80bddf38a2 | ||
|
00f3f5f690 | ||
|
bc9478c324 | ||
|
9a2c413372 | ||
|
3cb2793e51 | ||
|
1ad2e895b3 | ||
|
3d5a164338 | ||
|
af8c47151e | ||
|
20d1eabaf0 | ||
|
45ee68fdd4 | ||
|
b52462440c | ||
|
bae741dc81 | ||
|
b717deb8e4 | ||
|
ccde9de257 | ||
|
c25852b210 | ||
|
f7b7aaf527 | ||
|
569e4bd057 | ||
|
07cad78046 | ||
|
21cb10f550 | ||
|
d2aea077b4 | ||
|
d464053546 | ||
|
cfb28c47c0 | ||
|
b941c6c5a6 | ||
|
ea9f9c878b | ||
|
4f10387688 | ||
|
a4f9231707 | ||
|
3c85738f0d | ||
|
45881b9f0d | ||
|
a8ff622f33 | ||
|
fcd2439b50 | ||
|
3709e06b5c | ||
|
7c8573a416 | ||
|
6f1ffee2b7 | ||
|
7ade1b8bcc | ||
|
3c28c280de | ||
|
b9d50ec164 | ||
|
7780cfdd5c | ||
|
7c79a040b7 | ||
|
779b0cbc77 | ||
|
b6cab6467d | ||
|
ec0a5884c0 | ||
|
7f5e0f5b4d | ||
|
7e682ed97d | ||
|
62e24c5764 | ||
|
eb6fa93845 | ||
|
9d5d70564f | ||
|
922686a348 | ||
|
98e2e51d09 | ||
|
92e16ee873 | ||
|
72ded188fa | ||
|
dcab0b3718 | ||
|
37a6b4f96d | ||
|
1ddbb75e74 | ||
|
696dec9b91 | ||
|
17374cf2b4 | ||
|
9661c6bff3 | ||
|
0aeb5bc409 | ||
|
a5640502ac | ||
|
6fe78b4d47 | ||
|
5bda1c0eee | ||
|
0465a75c56 | ||
|
795c0f233f |
10
README.rst
10
README.rst
@@ -14,6 +14,16 @@ Installation
|
||||
sudo -H pip install devlib
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
``devlib`` should install all dependencies automatically, however if you run
|
||||
into issues please ensure you are using that latest version of pip.
|
||||
|
||||
On some systems there may additional steps required to install the dependency
|
||||
``paramiko`` please consult the `module documentation <http://www.paramiko.org/installing.html>`_
|
||||
for more information.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
|
BIN
devlib/bin/arm64/get_clock_boottime
Executable file
BIN
devlib/bin/arm64/get_clock_boottime
Executable file
Binary file not shown.
BIN
devlib/bin/armeabi/get_clock_boottime
Executable file
BIN
devlib/bin/armeabi/get_clock_boottime
Executable file
Binary file not shown.
@@ -227,6 +227,8 @@ class FtraceCollector(CollectorBase):
|
||||
self._set_buffer_size()
|
||||
self.target.execute('{} reset'.format(self.target_binary),
|
||||
as_root=True, timeout=TIMEOUT)
|
||||
if self.functions:
|
||||
self.target.write_value(self.function_profile_file, 0, verify=False)
|
||||
self._reset_needed = False
|
||||
|
||||
def start(self):
|
||||
@@ -290,7 +292,7 @@ class FtraceCollector(CollectorBase):
|
||||
def stop(self):
|
||||
# Disable kernel function profiling
|
||||
if self.functions and self.tracer is None:
|
||||
self.target.execute('echo 1 > {}'.format(self.function_profile_file),
|
||||
self.target.execute('echo 0 > {}'.format(self.function_profile_file),
|
||||
as_root=True)
|
||||
if 'cpufreq' in self.target.modules:
|
||||
self.logger.debug('Trace CPUFreq frequencies')
|
||||
|
@@ -22,9 +22,10 @@ from devlib.utils.android import LogcatMonitor
|
||||
|
||||
class LogcatCollector(CollectorBase):
|
||||
|
||||
def __init__(self, target, regexps=None):
|
||||
def __init__(self, target, regexps=None, logcat_format=None):
|
||||
super(LogcatCollector, self).__init__(target)
|
||||
self.regexps = regexps
|
||||
self.logcat_format = logcat_format
|
||||
self.output_path = None
|
||||
self._collecting = False
|
||||
self._prev_log = None
|
||||
@@ -49,7 +50,7 @@ class LogcatCollector(CollectorBase):
|
||||
"""
|
||||
if self.output_path is None:
|
||||
raise RuntimeError("Output path was not set.")
|
||||
self._monitor = LogcatMonitor(self.target, self.regexps)
|
||||
self._monitor = LogcatMonitor(self.target, self.regexps, logcat_format=self.logcat_format)
|
||||
if self._prev_log:
|
||||
# Append new data collection to previous collection
|
||||
self._monitor.start(self._prev_log)
|
||||
|
@@ -236,7 +236,7 @@ class PerfCollector(CollectorBase):
|
||||
self.target.execute(report_command)
|
||||
|
||||
def _validate_events(self, events):
|
||||
available_events_string = self.target.execute('{} list'.format(self.perf_type))
|
||||
available_events_string = self.target.execute('{} list | {} cat'.format(self.perf_type, self.target.busybox))
|
||||
available_events = available_events_string.splitlines()
|
||||
for available_event in available_events:
|
||||
if available_event == '':
|
||||
|
@@ -33,7 +33,7 @@ class SerialTraceCollector(CollectorBase):
|
||||
self.serial_port = serial_port
|
||||
self.baudrate = baudrate
|
||||
self.timeout = timeout
|
||||
self.output_path - None
|
||||
self.output_path = None
|
||||
|
||||
self._serial_target = None
|
||||
self._conn = None
|
||||
@@ -54,7 +54,7 @@ class SerialTraceCollector(CollectorBase):
|
||||
if self.output_path is None:
|
||||
raise RuntimeError("Output path was not set.")
|
||||
|
||||
self._outfile_fh = open(self.output_path, 'w')
|
||||
self._outfile_fh = open(self.output_path, 'wb')
|
||||
start_marker = "-------- Starting serial logging --------\n"
|
||||
self._outfile_fh.write(start_marker.encode('utf-8'))
|
||||
|
||||
|
523
devlib/connection.py
Normal file
523
devlib/connection.py
Normal file
@@ -0,0 +1,523 @@
|
||||
# Copyright 2019 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.
|
||||
#
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from weakref import WeakSet
|
||||
from shlex import quote
|
||||
from time import monotonic
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
|
||||
from devlib.utils.misc import InitCheckpoint
|
||||
|
||||
_KILL_TIMEOUT = 3
|
||||
|
||||
|
||||
def _kill_pgid_cmd(pgid, sig):
|
||||
return 'kill -{} -{}'.format(sig.value, pgid)
|
||||
|
||||
|
||||
class ConnectionBase(InitCheckpoint):
|
||||
"""
|
||||
Base class for all connections.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._current_bg_cmds = WeakSet()
|
||||
self._closed = False
|
||||
self._close_lock = threading.Lock()
|
||||
self.busybox = None
|
||||
|
||||
def cancel_running_command(self):
|
||||
bg_cmds = set(self._current_bg_cmds)
|
||||
for bg_cmd in bg_cmds:
|
||||
bg_cmd.cancel()
|
||||
|
||||
@abstractmethod
|
||||
def _close(self):
|
||||
"""
|
||||
Close the connection.
|
||||
|
||||
The public :meth:`close` method makes sure that :meth:`_close` will
|
||||
only be called once, and will serialize accesses to it if it happens to
|
||||
be called from multiple threads at once.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
# Locking the closing allows any thread to safely call close() as long
|
||||
# as the connection can be closed from a thread that is not the one it
|
||||
# started its life in.
|
||||
with self._close_lock:
|
||||
if not self._closed:
|
||||
self._close()
|
||||
self._closed = True
|
||||
|
||||
# Ideally, that should not be relied upon but that will improve the chances
|
||||
# of the connection being properly cleaned up when it's not in use anymore.
|
||||
def __del__(self):
|
||||
# Since __del__ will be called if an exception is raised in __init__
|
||||
# (e.g. we cannot connect), we only run close() when we are sure
|
||||
# __init__ has completed successfully.
|
||||
if self.initialized:
|
||||
self.close()
|
||||
|
||||
|
||||
class BackgroundCommand(ABC):
|
||||
"""
|
||||
Allows managing a running background command using a subset of the
|
||||
:class:`subprocess.Popen` API.
|
||||
|
||||
Instances of this class can be used as context managers, with the same
|
||||
semantic as :class:`subprocess.Popen`.
|
||||
"""
|
||||
@abstractmethod
|
||||
def send_signal(self, sig):
|
||||
"""
|
||||
Send a POSIX signal to the background command's process group ID
|
||||
(PGID).
|
||||
|
||||
:param signal: Signal to send.
|
||||
:type signal: signal.Signals
|
||||
"""
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Send SIGKILL to the background command.
|
||||
"""
|
||||
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``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def wait(self):
|
||||
"""
|
||||
Block until the background command completes, and return its exit code.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def poll(self):
|
||||
"""
|
||||
Return exit code if the command has exited, None otherwise.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stdin(self):
|
||||
"""
|
||||
File-like object connected to the background's command stdin.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stdout(self):
|
||||
"""
|
||||
File-like object connected to the background's command stdout.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stderr(self):
|
||||
"""
|
||||
File-like object connected to the background's command stderr.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def pid(self):
|
||||
"""
|
||||
Process Group ID (PGID) of the background command.
|
||||
|
||||
Since the command is usually wrapped in shell processes for IO
|
||||
redirections, sudo etc, the PID cannot be assumed to be the actual PID
|
||||
of the command passed by the user. It's is guaranteed to be a PGID
|
||||
instead, which means signals sent to it as such will target all
|
||||
subprocesses involved in executing that command.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def close(self):
|
||||
"""
|
||||
Close all opened streams and then wait for command completion.
|
||||
|
||||
:returns: Exit code of the command.
|
||||
|
||||
.. note:: If the command is writing to its stdout/stderr, it might be
|
||||
blocked on that and die when the streams are closed.
|
||||
"""
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.close()
|
||||
|
||||
|
||||
class PopenBackgroundCommand(BackgroundCommand):
|
||||
"""
|
||||
:class:`subprocess.Popen`-based background command.
|
||||
"""
|
||||
|
||||
def __init__(self, popen):
|
||||
self.popen = popen
|
||||
|
||||
def send_signal(self, sig):
|
||||
return os.killpg(self.popen.pid, sig)
|
||||
|
||||
@property
|
||||
def stdin(self):
|
||||
return self.popen.stdin
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
return self.popen.stdout
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
return self.popen.stderr
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
return self.popen.pid
|
||||
|
||||
def wait(self):
|
||||
return self.popen.wait()
|
||||
|
||||
def poll(self):
|
||||
return self.popen.poll()
|
||||
|
||||
def cancel(self, kill_timeout=_KILL_TIMEOUT):
|
||||
popen = self.popen
|
||||
os.killpg(os.getpgid(popen.pid), signal.SIGTERM)
|
||||
try:
|
||||
popen.wait(timeout=_KILL_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(os.getpgid(popen.pid), signal.SIGKILL)
|
||||
|
||||
def close(self):
|
||||
self.popen.__exit__(None, None, None)
|
||||
return self.popen.returncode
|
||||
|
||||
def __enter__(self):
|
||||
self.popen.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.popen.__exit__(*args, **kwargs)
|
||||
|
||||
|
||||
class ParamikoBackgroundCommand(BackgroundCommand):
|
||||
"""
|
||||
:mod:`paramiko`-based background command.
|
||||
"""
|
||||
def __init__(self, conn, chan, pid, as_root, stdin, stdout, stderr, redirect_thread):
|
||||
self.chan = chan
|
||||
self.as_root = as_root
|
||||
self.conn = conn
|
||||
self._pid = pid
|
||||
self._stdin = stdin
|
||||
self._stdout = stdout
|
||||
self._stderr = stderr
|
||||
self.redirect_thread = redirect_thread
|
||||
|
||||
def send_signal(self, sig):
|
||||
# If the command has already completed, we don't want to send a signal
|
||||
# to another process that might have gotten that PID in the meantime.
|
||||
if self.poll() is not None:
|
||||
return
|
||||
# Use -PGID to target a process group rather than just the process
|
||||
# itself
|
||||
cmd = _kill_pgid_cmd(self.pid, sig)
|
||||
self.conn.execute(cmd, as_root=self.as_root)
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
return self._pid
|
||||
|
||||
def wait(self):
|
||||
return self.chan.recv_exit_status()
|
||||
|
||||
def poll(self):
|
||||
if self.chan.exit_status_ready():
|
||||
return self.wait()
|
||||
else:
|
||||
return None
|
||||
|
||||
def cancel(self, kill_timeout=_KILL_TIMEOUT):
|
||||
self.send_signal(signal.SIGTERM)
|
||||
# Check if the command terminated quickly
|
||||
time.sleep(10e-3)
|
||||
# Otherwise wait for the full timeout and kill it
|
||||
if self.poll() is None:
|
||||
time.sleep(kill_timeout)
|
||||
self.send_signal(signal.SIGKILL)
|
||||
self.wait()
|
||||
|
||||
@property
|
||||
def stdin(self):
|
||||
return self._stdin
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
return self._stdout
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
return self._stderr
|
||||
|
||||
def close(self):
|
||||
for x in (self.stdin, self.stdout, self.stderr):
|
||||
if x is not None:
|
||||
x.close()
|
||||
|
||||
exit_code = self.wait()
|
||||
thread = self.redirect_thread
|
||||
if thread:
|
||||
thread.join()
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
class AdbBackgroundCommand(BackgroundCommand):
|
||||
"""
|
||||
``adb``-based background command.
|
||||
"""
|
||||
|
||||
def __init__(self, conn, adb_popen, pid, as_root):
|
||||
self.conn = conn
|
||||
self.as_root = as_root
|
||||
self.adb_popen = adb_popen
|
||||
self._pid = pid
|
||||
|
||||
def send_signal(self, sig):
|
||||
self.conn.execute(
|
||||
_kill_pgid_cmd(self.pid, sig),
|
||||
as_root=self.as_root,
|
||||
)
|
||||
|
||||
@property
|
||||
def stdin(self):
|
||||
return self.adb_popen.stdin
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
return self.adb_popen.stdout
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
return self.adb_popen.stderr
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
return self._pid
|
||||
|
||||
def wait(self):
|
||||
return self.adb_popen.wait()
|
||||
|
||||
def poll(self):
|
||||
return self.adb_popen.poll()
|
||||
|
||||
def cancel(self, kill_timeout=_KILL_TIMEOUT):
|
||||
self.send_signal(signal.SIGTERM)
|
||||
try:
|
||||
self.adb_popen.wait(timeout=_KILL_TIMEOUT)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_signal(signal.SIGKILL)
|
||||
self.adb_popen.kill()
|
||||
|
||||
def close(self):
|
||||
self.adb_popen.__exit__(None, None, None)
|
||||
return self.adb_popen.returncode
|
||||
|
||||
def __enter__(self):
|
||||
self.adb_popen.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args, **kwargs):
|
||||
self.adb_popen.__exit__(*args, **kwargs)
|
||||
|
||||
|
||||
class TransferManagerBase(ABC):
|
||||
|
||||
def _pull_dest_size(self, dest):
|
||||
if os.path.isdir(dest):
|
||||
return sum(
|
||||
os.stat(os.path.join(dirpath, f)).st_size
|
||||
for dirpath, _, fnames in os.walk(dest)
|
||||
for f in fnames
|
||||
)
|
||||
else:
|
||||
return os.stat(dest).st_size
|
||||
return 0
|
||||
|
||||
def _push_dest_size(self, dest):
|
||||
cmd = '{} du -s {}'.format(quote(self.conn.busybox), quote(dest))
|
||||
out = self.conn.execute(cmd)
|
||||
try:
|
||||
return int(out.split()[0])
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
def __init__(self, conn, poll_period, start_transfer_poll_delay, total_timeout):
|
||||
self.conn = conn
|
||||
self.poll_period = poll_period
|
||||
self.total_timeout = total_timeout
|
||||
self.start_transfer_poll_delay = start_transfer_poll_delay
|
||||
|
||||
self.logger = logging.getLogger('FileTransfer')
|
||||
self.managing = threading.Event()
|
||||
self.transfer_started = threading.Event()
|
||||
self.transfer_completed = threading.Event()
|
||||
self.transfer_aborted = threading.Event()
|
||||
|
||||
self.monitor_thread = None
|
||||
self.sources = None
|
||||
self.dest = None
|
||||
self.direction = None
|
||||
|
||||
@abstractmethod
|
||||
def _cancel(self):
|
||||
pass
|
||||
|
||||
def cancel(self, reason=None):
|
||||
msg = 'Cancelling file transfer {} -> {}'.format(self.sources, self.dest)
|
||||
if reason is not None:
|
||||
msg += ' due to \'{}\''.format(reason)
|
||||
self.logger.warning(msg)
|
||||
self.transfer_aborted.set()
|
||||
self._cancel()
|
||||
|
||||
@abstractmethod
|
||||
def isactive(self):
|
||||
pass
|
||||
|
||||
@contextmanager
|
||||
def manage(self, sources, dest, direction):
|
||||
try:
|
||||
self.sources, self.dest, self.direction = sources, dest, direction
|
||||
m_thread = threading.Thread(target=self._monitor)
|
||||
|
||||
self.transfer_completed.clear()
|
||||
self.transfer_aborted.clear()
|
||||
self.transfer_started.set()
|
||||
|
||||
m_thread.start()
|
||||
yield self
|
||||
except BaseException:
|
||||
self.cancel(reason='exception during transfer')
|
||||
raise
|
||||
finally:
|
||||
self.transfer_completed.set()
|
||||
self.transfer_started.set()
|
||||
m_thread.join()
|
||||
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)
|
||||
while not self.transfer_completed.wait(self.poll_period):
|
||||
if not self.isactive():
|
||||
self.cancel(reason='transfer inactive')
|
||||
elif monotonic() - start_t > self.total_timeout:
|
||||
self.cancel(reason='transfer timed out')
|
||||
|
||||
|
||||
class PopenTransferManager(TransferManagerBase):
|
||||
|
||||
def __init__(self, conn, poll_period=30, start_transfer_poll_delay=30, total_timeout=3600):
|
||||
super().__init__(conn, poll_period, start_transfer_poll_delay, total_timeout)
|
||||
self.transfer = None
|
||||
self.last_sample = None
|
||||
|
||||
def _cancel(self):
|
||||
if self.transfer:
|
||||
self.transfer.cancel()
|
||||
self.transfer = None
|
||||
|
||||
def isactive(self):
|
||||
size_fn = self._push_dest_size if self.direction == 'push' else self._pull_dest_size
|
||||
curr_size = size_fn(self.dest)
|
||||
self.logger.debug('Polled file transfer, destination size {}'.format(curr_size))
|
||||
active = True if self.last_sample is None else curr_size > self.last_sample
|
||||
self.last_sample = curr_size
|
||||
return active
|
||||
|
||||
def set_transfer_and_wait(self, popen_bg_cmd):
|
||||
self.transfer = popen_bg_cmd
|
||||
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():
|
||||
raise TimeoutError(self.transfer.popen.args)
|
||||
|
||||
|
||||
class SSHTransferManager(TransferManagerBase):
|
||||
|
||||
def __init__(self, conn, poll_period=30, start_transfer_poll_delay=30, total_timeout=3600):
|
||||
super().__init__(conn, poll_period, start_transfer_poll_delay, total_timeout)
|
||||
self.transferer = None
|
||||
self.progressed = False
|
||||
self.transferred = None
|
||||
self.to_transfer = None
|
||||
|
||||
def _cancel(self):
|
||||
self.transferer.close()
|
||||
|
||||
def isactive(self):
|
||||
progressed = self.progressed
|
||||
self.progressed = False
|
||||
msg = 'Polled transfer: {}% [{}B/{}B]'
|
||||
pc = format((self.transferred / self.to_transfer) * 100, '.2f')
|
||||
self.logger.debug(msg.format(pc, self.transferred, self.to_transfer))
|
||||
return progressed
|
||||
|
||||
@contextmanager
|
||||
def manage(self, sources, dest, direction, transferer):
|
||||
with super().manage(sources, dest, direction):
|
||||
try:
|
||||
self.progressed = False
|
||||
self.transferer = transferer # SFTPClient or SCPClient
|
||||
yield self
|
||||
except socket.error as e:
|
||||
if self.transfer_aborted.is_set():
|
||||
self.transfer_aborted.clear()
|
||||
method = 'SCP' if self.conn.use_scp else 'SFTP'
|
||||
raise TimeoutError('{} {}: {} -> {}'.format(method, self.direction, sources, self.dest))
|
||||
else:
|
||||
raise e
|
||||
|
||||
def progress_cb(self, *args):
|
||||
if self.transfer_started.is_set():
|
||||
self.progressed = True
|
||||
if len(args) == 3: # For SCPClient callbacks
|
||||
self.transferred = args[2]
|
||||
self.to_transfer = args[1]
|
||||
elif len(args) == 2: # For SFTPClient callbacks
|
||||
self.transferred = args[0]
|
||||
self.to_transfer = args[1]
|
@@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from glob import iglob
|
||||
import glob
|
||||
import os
|
||||
import signal
|
||||
import shutil
|
||||
@@ -24,9 +24,12 @@ from pipes import quote
|
||||
|
||||
from devlib.exception import TargetTransientError, TargetStableError
|
||||
from devlib.utils.misc import check_output
|
||||
from devlib.connection import ConnectionBase, PopenBackgroundCommand
|
||||
|
||||
|
||||
PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
|
||||
|
||||
|
||||
# pylint: disable=redefined-outer-name
|
||||
def kill_children(pid, signal=signal.SIGKILL):
|
||||
with open('/proc/{0}/task/{0}/children'.format(pid), 'r') as fd:
|
||||
@@ -34,9 +37,11 @@ def kill_children(pid, signal=signal.SIGKILL):
|
||||
kill_children(cpid, signal)
|
||||
os.kill(cpid, signal)
|
||||
|
||||
class LocalConnection(object):
|
||||
|
||||
class LocalConnection(ConnectionBase):
|
||||
|
||||
name = 'local'
|
||||
host = 'localhost'
|
||||
|
||||
@property
|
||||
def connected_as_root(self):
|
||||
@@ -52,41 +57,55 @@ class LocalConnection(object):
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, platform=None, keep_password=True, unrooted=False,
|
||||
password=None, timeout=None):
|
||||
super().__init__()
|
||||
self._connected_as_root = None
|
||||
self.logger = logging.getLogger('local_connection')
|
||||
self.keep_password = keep_password
|
||||
self.unrooted = unrooted
|
||||
self.password = password
|
||||
|
||||
def push(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||
self.logger.debug('cp {} {}'.format(source, dest))
|
||||
shutil.copy(source, dest)
|
||||
|
||||
def pull(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||
self.logger.debug('cp {} {}'.format(source, dest))
|
||||
if ('*' in source or '?' in source) and os.path.isdir(dest):
|
||||
# Pull all files matching a wildcard expression
|
||||
for each_source in iglob(source):
|
||||
shutil.copy(each_source, dest)
|
||||
def _copy_path(self, source, dest):
|
||||
self.logger.debug('copying {} to {}'.format(source, dest))
|
||||
if os.path.isdir(source):
|
||||
# Behave similarly as cp, scp, adb push, etc. by creating a new
|
||||
# folder instead of merging hierarchies
|
||||
if os.path.exists(dest):
|
||||
dest = os.path.join(dest, os.path.basename(os.path.normpath(src)))
|
||||
|
||||
# Use distutils copy_tree since it behaves the same as
|
||||
# shutils.copytree except that it won't fail if some folders
|
||||
# already exist.
|
||||
#
|
||||
# Mirror the behavior of all other targets which only copy the
|
||||
# content without metadata
|
||||
copy_tree(source, dest, preserve_mode=False, preserve_times=False)
|
||||
else:
|
||||
if os.path.isdir(source):
|
||||
# Use distutils to allow copying into an existing directory structure.
|
||||
copy_tree(source, dest)
|
||||
else:
|
||||
shutil.copy(source, dest)
|
||||
shutil.copy(source, dest)
|
||||
|
||||
def _copy_paths(self, sources, dest):
|
||||
for source in sources:
|
||||
self._copy_path(source, dest)
|
||||
|
||||
def push(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||
self._copy_paths(sources, dest)
|
||||
|
||||
def pull(self, sources, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
|
||||
self._copy_paths(sources, dest)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(self, command, timeout=None, check_exit_code=True,
|
||||
as_root=False, strip_colors=True, will_succeed=False):
|
||||
self.logger.debug(command)
|
||||
if as_root and not self.connected_as_root:
|
||||
use_sudo = as_root and not self.connected_as_root
|
||||
if use_sudo:
|
||||
if self.unrooted:
|
||||
raise TargetStableError('unrooted')
|
||||
password = self._get_password()
|
||||
command = 'echo {} | sudo -S -- sh -c '.format(quote(password)) + quote(command)
|
||||
command = "echo {} | sudo -p ' ' -S -- sh -c {}".format(quote(password), quote(command))
|
||||
ignore = None if check_exit_code else 'all'
|
||||
try:
|
||||
return check_output(command, shell=True, timeout=timeout, ignore=ignore)[0]
|
||||
stdout, stderr = check_output(command, shell=True, timeout=timeout, ignore=ignore)
|
||||
except subprocess.CalledProcessError as e:
|
||||
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'.format(
|
||||
e.returncode, command, e.output)
|
||||
@@ -95,15 +114,38 @@ class LocalConnection(object):
|
||||
else:
|
||||
raise TargetStableError(message)
|
||||
|
||||
# Remove the one-character prompt of sudo -S -p
|
||||
if use_sudo and stderr:
|
||||
stderr = stderr[1:]
|
||||
|
||||
return stdout + stderr
|
||||
|
||||
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||
if as_root and not self.connected_as_root:
|
||||
if self.unrooted:
|
||||
raise TargetStableError('unrooted')
|
||||
password = self._get_password()
|
||||
command = 'echo {} | sudo -S '.format(quote(password)) + command
|
||||
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
|
||||
# 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))
|
||||
|
||||
def close(self):
|
||||
# Make sure to get a new PGID so PopenBackgroundCommand() can kill
|
||||
# all sub processes that could be started without troubles.
|
||||
def preexec_fn():
|
||||
os.setpgrp()
|
||||
|
||||
popen = subprocess.Popen(
|
||||
command,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
shell=True,
|
||||
preexec_fn=preexec_fn,
|
||||
)
|
||||
bg_cmd = PopenBackgroundCommand(popen)
|
||||
self._current_bg_cmds.add(bg_cmd)
|
||||
return bg_cmd
|
||||
|
||||
def _close(self):
|
||||
pass
|
||||
|
||||
def cancel_running_command(self):
|
||||
|
@@ -16,8 +16,10 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from itertools import chain
|
||||
import time
|
||||
from itertools import chain, zip_longest
|
||||
|
||||
from devlib.host import PACKAGE_BIN_DIRECTORY
|
||||
from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
|
||||
from devlib.exception import HostError
|
||||
from devlib.utils.csvutil import csvwriter, create_reader
|
||||
@@ -45,7 +47,8 @@ class DaqInstrument(Instrument):
|
||||
dv_range=0.2,
|
||||
sample_rate_hz=10000,
|
||||
channel_map=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23),
|
||||
keep_raw=False
|
||||
keep_raw=False,
|
||||
time_as_clock_boottime=True
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
super(DaqInstrument, self).__init__(target)
|
||||
@@ -53,6 +56,7 @@ class DaqInstrument(Instrument):
|
||||
self._need_reset = True
|
||||
self._raw_files = []
|
||||
self.tempdir = None
|
||||
self.target_boottime_clock_at_start = 0.0
|
||||
if DaqClient is None:
|
||||
raise HostError('Could not import "daqpower": {}'.format(import_error_mesg))
|
||||
if labels is None:
|
||||
@@ -76,11 +80,30 @@ class DaqInstrument(Instrument):
|
||||
channel_map=channel_map,
|
||||
labels=labels)
|
||||
self.sample_rate_hz = sample_rate_hz
|
||||
self.time_as_clock_boottime = time_as_clock_boottime
|
||||
|
||||
self.add_channel('Time', 'time')
|
||||
for label in labels:
|
||||
for kind in ['power', 'voltage']:
|
||||
self.add_channel(label, kind)
|
||||
|
||||
if time_as_clock_boottime:
|
||||
host_path = os.path.join(PACKAGE_BIN_DIRECTORY, self.target.abi,
|
||||
'get_clock_boottime')
|
||||
self.clock_boottime_cmd = self.target.install_if_needed(host_path,
|
||||
search_system_binaries=False)
|
||||
|
||||
def calculate_boottime_offset(self):
|
||||
time_before = time.time()
|
||||
out = self.target.execute(self.clock_boottime_cmd)
|
||||
time_after = time.time()
|
||||
|
||||
remote_clock_boottime = float(out)
|
||||
propagation_delay = (time_after - time_before) / 2
|
||||
boottime_at_end = remote_clock_boottime + propagation_delay
|
||||
|
||||
return time_after - boottime_at_end
|
||||
|
||||
def reset(self, sites=None, kinds=None, channels=None):
|
||||
super(DaqInstrument, self).reset(sites, kinds, channels)
|
||||
self.daq_client.close()
|
||||
@@ -90,9 +113,19 @@ class DaqInstrument(Instrument):
|
||||
|
||||
def start(self):
|
||||
if self._need_reset:
|
||||
self.reset()
|
||||
# Preserve channel order
|
||||
self.reset(channels=self.channels.keys())
|
||||
|
||||
if self.time_as_clock_boottime:
|
||||
target_boottime_offset = self.calculate_boottime_offset()
|
||||
time_start = time.time()
|
||||
|
||||
self.daq_client.start()
|
||||
|
||||
if self.time_as_clock_boottime:
|
||||
time_end = time.time()
|
||||
self.target_boottime_clock_at_start = (time_start + time_end) / 2 - target_boottime_offset
|
||||
|
||||
def stop(self):
|
||||
self.daq_client.stop()
|
||||
self._need_reset = True
|
||||
@@ -118,32 +151,32 @@ class DaqInstrument(Instrument):
|
||||
site_readers[site] = reader
|
||||
file_handles.append(fh)
|
||||
except KeyError:
|
||||
message = 'Could not get DAQ trace for {}; Obtained traces are in {}'
|
||||
raise HostError(message.format(site, self.tempdir))
|
||||
if not site.startswith("Time"):
|
||||
message = 'Could not get DAQ trace for {}; Obtained traces are in {}'
|
||||
raise HostError(message.format(site, self.tempdir))
|
||||
|
||||
# The first row is the headers
|
||||
channel_order = []
|
||||
channel_order = ['Time_time']
|
||||
for site, reader in site_readers.items():
|
||||
channel_order.extend(['{}_{}'.format(site, kind)
|
||||
for kind in next(reader)])
|
||||
|
||||
def _read_next_rows():
|
||||
parts = []
|
||||
for reader in site_readers.values():
|
||||
try:
|
||||
parts.extend(next(reader))
|
||||
except StopIteration:
|
||||
parts.extend([None, None])
|
||||
return list(chain(parts))
|
||||
def _read_rows():
|
||||
row_iter = zip_longest(*site_readers.values(), fillvalue=(None, None))
|
||||
for raw_row in row_iter:
|
||||
raw_row = list(chain.from_iterable(raw_row))
|
||||
raw_row.insert(0, _read_rows.row_time_s)
|
||||
yield raw_row
|
||||
_read_rows.row_time_s += 1.0 / self.sample_rate_hz
|
||||
|
||||
_read_rows.row_time_s = self.target_boottime_clock_at_start
|
||||
|
||||
with csvwriter(outfile) as writer:
|
||||
field_names = [c.label for c in self.active_channels]
|
||||
writer.writerow(field_names)
|
||||
raw_row = _read_next_rows()
|
||||
while any(raw_row):
|
||||
for raw_row in _read_rows():
|
||||
row = [raw_row[channel_order.index(f)] for f in field_names]
|
||||
writer.writerow(row)
|
||||
raw_row = _read_next_rows()
|
||||
|
||||
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
|
||||
finally:
|
||||
@@ -156,5 +189,5 @@ class DaqInstrument(Instrument):
|
||||
def teardown(self):
|
||||
self.daq_client.close()
|
||||
if not self.keep_raw:
|
||||
if os.path.isdir(self.tempdir):
|
||||
if self.tempdir and os.path.isdir(self.tempdir):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
@@ -212,7 +212,7 @@ class CpufreqModule(Module):
|
||||
|
||||
@memoized
|
||||
def list_frequencies(self, cpu):
|
||||
"""Returns a list of frequencies supported by the cpu or an empty list
|
||||
"""Returns a sorted list of frequencies supported by the cpu or an empty list
|
||||
if not could be found."""
|
||||
if isinstance(cpu, int):
|
||||
cpu = 'cpu{}'.format(cpu)
|
||||
@@ -234,7 +234,7 @@ class CpufreqModule(Module):
|
||||
raise
|
||||
|
||||
available_frequencies = list(map(int, reversed([f for f, _ in zip(out_iter, out_iter)])))
|
||||
return available_frequencies
|
||||
return sorted(available_frequencies)
|
||||
|
||||
@memoized
|
||||
def get_max_available_frequency(self, cpu):
|
||||
|
@@ -15,6 +15,9 @@
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
from past.builtins import basestring
|
||||
|
||||
from operator import attrgetter
|
||||
from pprint import pformat
|
||||
|
||||
from devlib.module import Module
|
||||
from devlib.utils.types import integer, boolean
|
||||
|
||||
@@ -96,40 +99,35 @@ class Cpuidle(Module):
|
||||
|
||||
def __init__(self, target):
|
||||
super(Cpuidle, self).__init__(target)
|
||||
self._states = {}
|
||||
|
||||
basepath = '/sys/devices/system/cpu/'
|
||||
values_tree = self.target.read_tree_values(basepath, depth=4, check_exit_code=False)
|
||||
i = 0
|
||||
cpu_id = 'cpu{}'.format(i)
|
||||
while cpu_id in values_tree:
|
||||
cpu_node = values_tree[cpu_id]
|
||||
|
||||
if 'cpuidle' in cpu_node:
|
||||
idle_node = cpu_node['cpuidle']
|
||||
self._states[cpu_id] = []
|
||||
j = 0
|
||||
state_id = 'state{}'.format(j)
|
||||
while state_id in idle_node:
|
||||
state_node = idle_node[state_id]
|
||||
state = CpuidleState(
|
||||
self._states = {
|
||||
cpu_name: sorted(
|
||||
(
|
||||
CpuidleState(
|
||||
self.target,
|
||||
index=j,
|
||||
path=self.target.path.join(basepath, cpu_id, 'cpuidle', state_id),
|
||||
# state_name is formatted as "state42"
|
||||
index=int(state_name[len('state'):]),
|
||||
path=self.target.path.join(basepath, cpu_name, 'cpuidle', state_name),
|
||||
name=state_node['name'],
|
||||
desc=state_node['desc'],
|
||||
power=int(state_node['power']),
|
||||
latency=int(state_node['latency']),
|
||||
residency=int(state_node['residency']) if 'residency' in state_node else None,
|
||||
)
|
||||
msg = 'Adding {} state {}: {} {}'
|
||||
self.logger.debug(msg.format(cpu_id, j, state.name, state.desc))
|
||||
self._states[cpu_id].append(state)
|
||||
j += 1
|
||||
state_id = 'state{}'.format(j)
|
||||
for state_name, state_node in cpu_node['cpuidle'].items()
|
||||
if state_name.startswith('state')
|
||||
),
|
||||
key=attrgetter('index'),
|
||||
)
|
||||
|
||||
i += 1
|
||||
cpu_id = 'cpu{}'.format(i)
|
||||
for cpu_name, cpu_node in values_tree.items()
|
||||
if cpu_name.startswith('cpu') and 'cpuidle' in cpu_node
|
||||
}
|
||||
|
||||
self.logger.debug('Adding cpuidle states:\n{}'.format(pformat(self._states)))
|
||||
|
||||
def get_states(self, cpu=0):
|
||||
if isinstance(cpu, int):
|
||||
@@ -174,6 +172,6 @@ class Cpuidle(Module):
|
||||
|
||||
def get_governor(self):
|
||||
path = self.target.path.join(self.root_path, 'current_governor_ro')
|
||||
if not self.target.path.exists(path):
|
||||
if not self.target.file_exists(path):
|
||||
path = self.target.path.join(self.root_path, 'current_governor')
|
||||
return self.target.read_value(path)
|
||||
|
@@ -130,7 +130,7 @@ class VexpressBootModule(BootModule):
|
||||
init_dtr=0) as tty:
|
||||
self.get_through_early_boot(tty)
|
||||
self.perform_boot_sequence(tty)
|
||||
self.wait_for_android_prompt(tty)
|
||||
self.wait_for_shell_prompt(tty)
|
||||
|
||||
def perform_boot_sequence(self, tty):
|
||||
raise NotImplementedError()
|
||||
@@ -159,8 +159,8 @@ class VexpressBootModule(BootModule):
|
||||
menu.wait(timeout=self.timeout)
|
||||
return menu
|
||||
|
||||
def wait_for_android_prompt(self, tty):
|
||||
self.logger.debug('Waiting for the Android prompt.')
|
||||
def wait_for_shell_prompt(self, tty):
|
||||
self.logger.debug('Waiting for the shell prompt.')
|
||||
tty.expect(self.target.shell_prompt, timeout=self.timeout)
|
||||
# This delay is needed to allow the platform some time to finish
|
||||
# initilizing; querying the ip address too early from connect() may
|
||||
|
@@ -90,9 +90,6 @@ class VersatileExpressPlatform(Platform):
|
||||
def _init_android_target(self, target):
|
||||
if target.connection_settings.get('device') is None:
|
||||
addr = self._get_target_ip_address(target)
|
||||
if sys.version_info[0] == 3:
|
||||
# Convert bytes to string for Python3 compatibility
|
||||
addr = addr.decode("utf-8")
|
||||
target.connection_settings['device'] = addr + ':5555'
|
||||
|
||||
def _init_linux_target(self, target):
|
||||
@@ -108,7 +105,7 @@ class VersatileExpressPlatform(Platform):
|
||||
init_dtr=0) as tty:
|
||||
tty.sendline('su') # this is, apprently, required to query network device
|
||||
# info by name on recent Juno builds...
|
||||
self.logger.debug('Waiting for the Android shell prompt.')
|
||||
self.logger.debug('Waiting for the shell prompt.')
|
||||
tty.expect(target.shell_prompt)
|
||||
|
||||
self.logger.debug('Waiting for IP address...')
|
||||
@@ -119,7 +116,7 @@ class VersatileExpressPlatform(Platform):
|
||||
time.sleep(1)
|
||||
try:
|
||||
tty.expect(r'inet ([1-9]\d*.\d+.\d+.\d+)', timeout=10)
|
||||
return tty.match.group(1)
|
||||
return tty.match.group(1).decode('utf-8')
|
||||
except pexpect.TIMEOUT:
|
||||
pass # We have our own timeout -- see below.
|
||||
if (time.time() - wait_start_time) > self.ready_timeout:
|
||||
|
389
devlib/target.py
389
devlib/target.py
@@ -15,7 +15,9 @@
|
||||
|
||||
import io
|
||||
import base64
|
||||
import functools
|
||||
import gzip
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
@@ -26,6 +28,7 @@ import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import threading
|
||||
import uuid
|
||||
import xml.dom.minidom
|
||||
import copy
|
||||
from collections import namedtuple, defaultdict
|
||||
@@ -47,25 +50,25 @@ from devlib.platform import Platform
|
||||
from devlib.exception import (DevlibTransientError, TargetStableError,
|
||||
TargetNotRespondingError, TimeoutError,
|
||||
TargetTransientError, KernelConfigKeyError,
|
||||
TargetError) # pylint: disable=redefined-builtin
|
||||
TargetError, HostError) # pylint: disable=redefined-builtin
|
||||
from devlib.utils.ssh import SshConnection
|
||||
from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, adb_disconnect, INTENT_FLAGS
|
||||
from devlib.utils.misc import memoized, isiterable, convert_new_lines
|
||||
from devlib.utils.misc import commonprefix, merge_lists
|
||||
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list
|
||||
from devlib.utils.misc import batch_contextmanager
|
||||
from devlib.utils.misc import batch_contextmanager, tls_property, nullcontext
|
||||
from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string, bytes_regex
|
||||
|
||||
|
||||
FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
|
||||
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
|
||||
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF|DOZE)',
|
||||
re.IGNORECASE)
|
||||
ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'cur=(?P<width>\d+)x(?P<height>\d+)')
|
||||
ANDROID_SCREEN_ROTATION_REGEX = re.compile(r'orientation=(?P<rotation>[0-3])')
|
||||
DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root|juno)@?.*:[/~]\S* *[#$] ',
|
||||
re.MULTILINE)
|
||||
KVERSION_REGEX = re.compile(
|
||||
r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(.*-g(?P<sha1>[0-9a-fA-F]{7,}))?'
|
||||
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,}))?'
|
||||
)
|
||||
|
||||
GOOGLE_DNS_SERVER_ADDRESS = '8.8.8.8'
|
||||
@@ -167,13 +170,18 @@ class Target(object):
|
||||
@property
|
||||
@memoized
|
||||
def number_of_nodes(self):
|
||||
num_nodes = 0
|
||||
nodere = re.compile(r'^\s*node\d+\s*$')
|
||||
output = self.execute('ls /sys/devices/system/node', as_root=self.is_rooted)
|
||||
for entry in output.split():
|
||||
if nodere.match(entry):
|
||||
num_nodes += 1
|
||||
return num_nodes
|
||||
cmd = 'cd /sys/devices/system/node && {busybox} find . -maxdepth 1'.format(busybox=quote(self.busybox))
|
||||
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
|
||||
|
||||
@property
|
||||
@memoized
|
||||
@@ -207,17 +215,7 @@ class Target(object):
|
||||
@memoized
|
||||
def page_size_kb(self):
|
||||
cmd = "cat /proc/self/smaps | {0} grep KernelPageSize | {0} head -n 1 | {0} awk '{{ print $2 }}'"
|
||||
return int(self.execute(cmd.format(self.busybox)))
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
if self._connections:
|
||||
tid = id(threading.current_thread())
|
||||
if tid not in self._connections:
|
||||
self._connections[tid] = self.get_connection()
|
||||
return self._connections[tid]
|
||||
else:
|
||||
return None
|
||||
return int(self.execute(cmd.format(self.busybox)) or 0)
|
||||
|
||||
@property
|
||||
def shutils(self):
|
||||
@@ -225,6 +223,13 @@ class Target(object):
|
||||
self._setup_shutils()
|
||||
return self._shutils
|
||||
|
||||
@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
|
||||
|
||||
def __init__(self,
|
||||
connection_settings=None,
|
||||
platform=None,
|
||||
@@ -237,6 +242,7 @@ class Target(object):
|
||||
conn_cls=None,
|
||||
is_container=False
|
||||
):
|
||||
|
||||
self._is_rooted = None
|
||||
self.connection_settings = connection_settings or {}
|
||||
# Set self.platform: either it's given directly (by platform argument)
|
||||
@@ -266,7 +272,6 @@ class Target(object):
|
||||
self._installed_binaries = {}
|
||||
self._installed_modules = {}
|
||||
self._cache = {}
|
||||
self._connections = {}
|
||||
self._shutils = None
|
||||
self._file_transfer_cache = None
|
||||
self.busybox = None
|
||||
@@ -285,23 +290,34 @@ class Target(object):
|
||||
|
||||
def connect(self, timeout=None, check_boot_completed=True):
|
||||
self.platform.init_target_connection(self)
|
||||
tid = id(threading.current_thread())
|
||||
self._connections[tid] = self.get_connection(timeout=timeout)
|
||||
# Forcefully set the thread-local value for the connection, with the
|
||||
# timeout we want
|
||||
self.conn = self.get_connection(timeout=timeout)
|
||||
if check_boot_completed:
|
||||
self.wait_boot_complete(timeout)
|
||||
self.check_connection()
|
||||
self._resolve_paths()
|
||||
self.execute('mkdir -p {}'.format(quote(self.working_directory)))
|
||||
self.execute('mkdir -p {}'.format(quote(self.executables_directory)))
|
||||
self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
|
||||
self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'), timeout=30)
|
||||
self.conn.busybox = self.busybox
|
||||
self.platform.update_from_target(self)
|
||||
self._update_modules('connected')
|
||||
if self.platform.big_core and self.load_default_modules:
|
||||
self._install_module(get_module('bl'))
|
||||
|
||||
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))
|
||||
|
||||
def disconnect(self):
|
||||
for conn in self._connections.values():
|
||||
connections = self._conn.get_all_values()
|
||||
for conn in connections:
|
||||
conn.close()
|
||||
self._connections = {}
|
||||
|
||||
def get_connection(self, timeout=None):
|
||||
if self.conn_cls is None:
|
||||
@@ -352,25 +368,137 @@ class Target(object):
|
||||
|
||||
# file transfer
|
||||
|
||||
def push(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
|
||||
if not as_root:
|
||||
self.conn.push(source, dest, timeout=timeout)
|
||||
else:
|
||||
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
|
||||
self.execute("mkdir -p {}".format(quote(self.path.dirname(device_tempfile))))
|
||||
self.conn.push(source, device_tempfile, timeout=timeout)
|
||||
self.execute("cp {} {}".format(quote(device_tempfile), quote(dest)), as_root=True)
|
||||
@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))
|
||||
|
||||
def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
|
||||
if not as_root:
|
||||
self.conn.pull(source, dest, timeout=timeout)
|
||||
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)
|
||||
|
||||
def _prepare_xfer(self, action, sources, dest):
|
||||
"""
|
||||
Check the sanity of sources and destination and prepare the ground for
|
||||
transfering multiple sources.
|
||||
"""
|
||||
if action == 'push':
|
||||
src_excep = HostError
|
||||
dst_excep = TargetStableError
|
||||
dst_path_exists = self.file_exists
|
||||
dst_is_dir = self.directory_exists
|
||||
dst_mkdir = self.makedirs
|
||||
|
||||
for source in sources:
|
||||
if not os.path.exists(source):
|
||||
raise HostError('No such file "{}"'.format(source))
|
||||
else:
|
||||
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
|
||||
self.execute("mkdir -p {}".format(quote(self.path.dirname(device_tempfile))))
|
||||
self.execute("cp -r {} {}".format(quote(source), quote(device_tempfile)), as_root=True)
|
||||
self.execute("chmod 0644 {}".format(quote(device_tempfile)), as_root=True)
|
||||
self.conn.pull(device_tempfile, dest, timeout=timeout)
|
||||
self.execute("rm -r {}".format(quote(device_tempfile)), as_root=True)
|
||||
src_excep = TargetStableError
|
||||
dst_excep = HostError
|
||||
dst_path_exists = os.path.exists
|
||||
dst_is_dir = os.path.isdir
|
||||
dst_mkdir = functools.partial(os.makedirs, exist_ok=True)
|
||||
|
||||
if not sources:
|
||||
raise src_excep('No file matching: {}'.format(source))
|
||||
elif len(sources) > 1:
|
||||
if dst_path_exists(dest):
|
||||
if not dst_is_dir(dest):
|
||||
raise dst_excep('A folder dest is required for multiple matches but destination is a file: {}'.format(dest))
|
||||
else:
|
||||
dst_mkdir(dest)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def do_push(sources, dest):
|
||||
return self.conn.push(sources, dest, timeout=timeout)
|
||||
|
||||
if as_root:
|
||||
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)
|
||||
else:
|
||||
do_push(sources, dest)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
else:
|
||||
sources = [source]
|
||||
|
||||
self._prepare_xfer('pull', sources, dest)
|
||||
|
||||
def do_pull(sources, dest):
|
||||
self.conn.pull(sources, dest, timeout=timeout)
|
||||
|
||||
if as_root:
|
||||
for source in sources:
|
||||
with self._xfer_cache_path(source) as device_tempfile:
|
||||
self.execute("cp -r -- {} {}".format(quote(source), quote(device_tempfile)), as_root=True)
|
||||
self.execute("{} chmod 0644 -- {}".format(self.busybox, quote(device_tempfile)), as_root=True)
|
||||
do_pull([device_tempfile], dest)
|
||||
else:
|
||||
do_pull(sources, dest)
|
||||
|
||||
def get_directory(self, source_dir, dest, as_root=False):
|
||||
""" Pull a directory from the device, after compressing dir """
|
||||
@@ -383,27 +511,28 @@ class Target(object):
|
||||
tmpfile = os.path.join(dest, tar_file_name)
|
||||
|
||||
# If root is required, use tmp location for tar creation.
|
||||
if as_root:
|
||||
tar_file_name = self.path.join(self._file_transfer_cache, tar_file_name)
|
||||
tar_file_cm = self._xfer_cache_path if as_root else nullcontext
|
||||
|
||||
# Does the folder exist?
|
||||
self.execute('ls -la {}'.format(quote(source_dir)), as_root=as_root)
|
||||
# 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
|
||||
f = tarfile.open(tmpfile, 'r')
|
||||
f.extractall(outdir)
|
||||
os.remove(tmpfile)
|
||||
|
||||
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)
|
||||
|
||||
# execution
|
||||
|
||||
@@ -538,7 +667,7 @@ class Target(object):
|
||||
def reset(self):
|
||||
try:
|
||||
self.execute('reboot', as_root=self.needs_su, timeout=2)
|
||||
except (DevlibTransientError, subprocess.CalledProcessError):
|
||||
except (TargetError, subprocess.CalledProcessError):
|
||||
# on some targets "reboot" doesn't return gracefully
|
||||
pass
|
||||
self.conn.connected_as_root = None
|
||||
@@ -573,6 +702,9 @@ class Target(object):
|
||||
|
||||
# files
|
||||
|
||||
def makedirs(self, path):
|
||||
self.execute('mkdir -p {}'.format(quote(path)))
|
||||
|
||||
def file_exists(self, filepath):
|
||||
command = 'if [ -e {} ]; then echo 1; else echo 0; fi'
|
||||
output = self.execute(command.format(quote(filepath)), as_root=self.is_rooted)
|
||||
@@ -616,7 +748,7 @@ class Target(object):
|
||||
raise IOError('No usable temporary filename found')
|
||||
|
||||
def remove(self, path, as_root=False):
|
||||
self.execute('rm -rf {}'.format(quote(path)), as_root=as_root)
|
||||
self.execute('rm -rf -- {}'.format(quote(path)), as_root=as_root)
|
||||
|
||||
# misc
|
||||
def core_cpus(self, core):
|
||||
@@ -901,16 +1033,19 @@ class Target(object):
|
||||
self.logger.warning(msg)
|
||||
|
||||
def _install_module(self, mod, **params):
|
||||
if mod.name not in self._installed_modules:
|
||||
self.logger.debug('Installing module {}'.format(mod.name))
|
||||
name = mod.name
|
||||
if name not in self._installed_modules:
|
||||
self.logger.debug('Installing module {}'.format(name))
|
||||
try:
|
||||
mod.install(self, **params)
|
||||
except Exception as e:
|
||||
self.logger.error('Module "{}" failed to install on target'.format(mod.name))
|
||||
self.logger.error('Module "{}" failed to install on target'.format(name))
|
||||
raise
|
||||
self._installed_modules[mod.name] = mod
|
||||
self._installed_modules[name] = mod
|
||||
if name not in self.modules:
|
||||
self.modules.append(name)
|
||||
else:
|
||||
self.logger.debug('Module {} is already installed.'.format(mod.name))
|
||||
self.logger.debug('Module {} is already installed.'.format(name))
|
||||
|
||||
def _resolve_paths(self):
|
||||
raise NotImplementedError()
|
||||
@@ -1029,16 +1164,20 @@ class LinuxTarget(Target):
|
||||
else:
|
||||
return []
|
||||
|
||||
def ps(self, **kwargs):
|
||||
command = 'ps -eo user,pid,ppid,vsize,rss,wchan,pcpu,state,fname'
|
||||
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)
|
||||
|
||||
lines = iter(convert_new_lines(self.execute(command)).split('\n'))
|
||||
next(lines) # header
|
||||
|
||||
result = []
|
||||
for line in lines:
|
||||
parts = re.split(r'\s+', line, maxsplit=8)
|
||||
parts = re.split(r'\s+', line, maxsplit=9)
|
||||
if parts and parts != ['']:
|
||||
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:5])) + parts[5:])))
|
||||
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:6])) + parts[6:])))
|
||||
|
||||
if not kwargs:
|
||||
return result
|
||||
@@ -1138,7 +1277,11 @@ class AndroidTarget(Target):
|
||||
|
||||
@property
|
||||
def adb_name(self):
|
||||
return self.conn.device
|
||||
return getattr(self.conn, 'device', None)
|
||||
|
||||
@property
|
||||
def adb_server(self):
|
||||
return getattr(self.conn, 'adb_server', None)
|
||||
|
||||
@property
|
||||
@memoized
|
||||
@@ -1281,18 +1424,32 @@ class AndroidTarget(Target):
|
||||
result.append(entry.pid)
|
||||
return result
|
||||
|
||||
def ps(self, **kwargs):
|
||||
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
|
||||
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'))
|
||||
next(lines) # header
|
||||
result = []
|
||||
for line in lines:
|
||||
parts = line.split(None, 8)
|
||||
parts = line.split(None, maxsplit)
|
||||
if not parts:
|
||||
continue
|
||||
if len(parts) == 8:
|
||||
|
||||
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:
|
||||
# wchan was blank; insert an empty field where it should be.
|
||||
parts.insert(5, '')
|
||||
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:5])) + parts[5:])))
|
||||
parts.insert(6, '')
|
||||
result.append(PsEntry(*(parts[0:1] + list(map(int, parts[1:6])) + parts[6:])))
|
||||
if not kwargs:
|
||||
return result
|
||||
else:
|
||||
@@ -1429,7 +1586,8 @@ class AndroidTarget(Target):
|
||||
flags.append('-g') # Grant all runtime permissions
|
||||
self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
|
||||
if isinstance(self.conn, AdbConnection):
|
||||
return adb_command(self.adb_name, "install {} {}".format(' '.join(flags), quote(filepath)), timeout=timeout)
|
||||
return adb_command(self.adb_name, "install {} {}".format(' '.join(flags), quote(filepath)),
|
||||
timeout=timeout, adb_server=self.adb_server)
|
||||
else:
|
||||
dev_path = self.get_workpath(filepath.rsplit(os.path.sep, 1)[-1])
|
||||
self.push(quote(filepath), dev_path, timeout=timeout)
|
||||
@@ -1498,7 +1656,8 @@ class AndroidTarget(Target):
|
||||
|
||||
def uninstall_package(self, package):
|
||||
if isinstance(self.conn, AdbConnection):
|
||||
adb_command(self.adb_name, "uninstall {}".format(quote(package)), timeout=30)
|
||||
adb_command(self.adb_name, "uninstall {}".format(quote(package)), timeout=30,
|
||||
adb_server=self.adb_server)
|
||||
else:
|
||||
self.execute("pm uninstall {}".format(quote(package)), timeout=30)
|
||||
|
||||
@@ -1507,15 +1666,18 @@ class AndroidTarget(Target):
|
||||
self._ensure_executables_directory_is_writable()
|
||||
self.remove(on_device_executable, as_root=self.needs_su)
|
||||
|
||||
def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
|
||||
def dump_logcat(self, filepath, filter=None, logcat_format=None, append=False,
|
||||
timeout=30): # 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 ''
|
||||
logcat_opts = '-d' + formatstr + filtstr
|
||||
if isinstance(self.conn, AdbConnection):
|
||||
command = 'logcat -d{} {} {}'.format(filtstr, op, quote(filepath))
|
||||
adb_command(self.adb_name, command, timeout=timeout)
|
||||
command = 'logcat {} {} {}'.format(logcat_opts, op, quote(filepath))
|
||||
adb_command(self.adb_name, command, timeout=timeout, adb_server=self.adb_server)
|
||||
else:
|
||||
dev_path = self.get_workpath('logcat')
|
||||
command = 'logcat -d{} {} {}'.format(filtstr, op, quote(dev_path))
|
||||
command = 'logcat {} {} {}'.format(logcat_opts, op, quote(dev_path))
|
||||
self.execute(command, timeout=timeout)
|
||||
self.pull(dev_path, filepath)
|
||||
self.remove(dev_path)
|
||||
@@ -1523,7 +1685,7 @@ class AndroidTarget(Target):
|
||||
def clear_logcat(self):
|
||||
with self.clear_logcat_lock:
|
||||
if isinstance(self.conn, AdbConnection):
|
||||
adb_command(self.adb_name, 'logcat -c', timeout=30)
|
||||
adb_command(self.adb_name, 'logcat -c', timeout=30, adb_server=self.adb_server)
|
||||
else:
|
||||
self.execute('logcat -c', timeout=30)
|
||||
|
||||
@@ -1540,17 +1702,32 @@ class AndroidTarget(Target):
|
||||
output = self.execute('dumpsys power')
|
||||
match = ANDROID_SCREEN_STATE_REGEX.search(output)
|
||||
if match:
|
||||
if 'DOZE' in match.group(1).upper():
|
||||
return True
|
||||
return boolean(match.group(1))
|
||||
else:
|
||||
raise TargetStableError('Could not establish screen state.')
|
||||
|
||||
def ensure_screen_is_on(self):
|
||||
def ensure_screen_is_on(self, verify=True):
|
||||
if not self.is_screen_on():
|
||||
self.execute('input keyevent 26')
|
||||
if verify and not self.is_screen_on():
|
||||
raise TargetStableError('Display cannot be turned on.')
|
||||
|
||||
def ensure_screen_is_off(self):
|
||||
if self.is_screen_on():
|
||||
self.execute('input keyevent 26')
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def set_auto_brightness(self, auto_brightness):
|
||||
cmd = 'settings put system screen_brightness_mode {}'
|
||||
@@ -1584,6 +1761,10 @@ class AndroidTarget(Target):
|
||||
cmd = 'settings get global airplane_mode_on'
|
||||
return boolean(self.execute(cmd).strip())
|
||||
|
||||
def get_stay_on_mode(self):
|
||||
cmd = 'settings get global stay_on_while_plugged_in'
|
||||
return int(self.execute(cmd).strip())
|
||||
|
||||
def set_airplane_mode(self, mode):
|
||||
root_required = self.get_sdk_version() > 23
|
||||
if root_required and not self.is_rooted:
|
||||
@@ -1629,6 +1810,18 @@ class AndroidTarget(Target):
|
||||
cmd = 'settings put system user_rotation {}'
|
||||
self.execute(cmd.format(rotation))
|
||||
|
||||
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))
|
||||
|
||||
def open_url(self, url, force_new=False):
|
||||
"""
|
||||
Start a view activity by specifying an URL
|
||||
@@ -1700,7 +1893,7 @@ class AndroidTarget(Target):
|
||||
self.write_value(self._charging_enabled_path, int(bool(enabled)))
|
||||
|
||||
FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
|
||||
PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
|
||||
PsEntry = namedtuple('PsEntry', 'user pid tid ppid vsize rss wchan pc state name')
|
||||
LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
|
||||
|
||||
|
||||
@@ -1795,6 +1988,8 @@ class KernelVersion(object):
|
||||
:type minor: int
|
||||
:ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
|
||||
:type rc: int
|
||||
:ivar commits: Number of additional commits on the branch. May be None.
|
||||
:type commits: int
|
||||
:ivar sha1: Kernel git revision hash, if available (otherwise None)
|
||||
:type sha1: str
|
||||
|
||||
@@ -1819,6 +2014,7 @@ class KernelVersion(object):
|
||||
self.minor = None
|
||||
self.sha1 = None
|
||||
self.rc = None
|
||||
self.commits = None
|
||||
match = KVERSION_REGEX.match(version_string)
|
||||
if match:
|
||||
groups = match.groupdict()
|
||||
@@ -1828,6 +2024,8 @@ class KernelVersion(object):
|
||||
self.minor = int(groups['minor'])
|
||||
if groups['rc'] is not None:
|
||||
self.rc = int(groups['rc'])
|
||||
if groups['commits'] is not None:
|
||||
self.commits = int(groups['commits'])
|
||||
if groups['sha1'] is not None:
|
||||
self.sha1 = match.group('sha1')
|
||||
|
||||
@@ -2205,7 +2403,10 @@ class ChromeOsTarget(LinuxTarget):
|
||||
|
||||
# Pull out ssh connection settings
|
||||
ssh_conn_params = ['host', 'username', 'password', 'keyfile',
|
||||
'port', 'password_prompt', 'timeout', 'sudo_cmd']
|
||||
'port', 'timeout', 'sudo_cmd',
|
||||
'strict_host_check', 'use_scp',
|
||||
'total_timeout', 'poll_transfers',
|
||||
'start_transfer_poll_delay']
|
||||
self.ssh_connection_settings = {}
|
||||
for setting in ssh_conn_params:
|
||||
if connection_settings.get(setting, None):
|
||||
|
@@ -19,6 +19,7 @@ Utility functions for working with Android devices through adb.
|
||||
|
||||
"""
|
||||
# pylint: disable=E1103
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -30,6 +31,7 @@ from collections import defaultdict
|
||||
import pexpect
|
||||
import xml.etree.ElementTree
|
||||
import zipfile
|
||||
import uuid
|
||||
|
||||
try:
|
||||
from shlex import quote
|
||||
@@ -37,13 +39,15 @@ except ImportError:
|
||||
from pipes import quote
|
||||
|
||||
from devlib.exception import TargetTransientError, TargetStableError, HostError
|
||||
from devlib.utils.misc import check_output, which, ABI_MAP
|
||||
from devlib.utils.misc import check_output, which, ABI_MAP, redirect_streams, get_subprocess
|
||||
from devlib.connection import ConnectionBase, AdbBackgroundCommand, PopenBackgroundCommand, PopenTransferManager
|
||||
|
||||
|
||||
logger = logging.getLogger('android')
|
||||
|
||||
MAX_ATTEMPTS = 5
|
||||
AM_START_ERROR = re.compile(r"Error: Activity.*")
|
||||
AAPT_BADGING_OUTPUT = re.compile(r"no dump ((file)|(apk)) specified", re.IGNORECASE)
|
||||
|
||||
# See:
|
||||
# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
|
||||
@@ -91,6 +95,7 @@ android_home = None
|
||||
platform_tools = None
|
||||
adb = None
|
||||
aapt = None
|
||||
aapt_version = None
|
||||
fastboot = None
|
||||
|
||||
|
||||
@@ -150,7 +155,11 @@ class ApkInfo(object):
|
||||
self.version_code = None
|
||||
self.native_code = None
|
||||
self.permissions = []
|
||||
self.parse(path)
|
||||
self._apk_path = None
|
||||
self._activities = None
|
||||
self._methods = None
|
||||
if path:
|
||||
self.parse(path)
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
def parse(self, apk_path):
|
||||
@@ -195,8 +204,10 @@ class ApkInfo(object):
|
||||
@property
|
||||
def activities(self):
|
||||
if self._activities is None:
|
||||
cmd = [aapt, 'dump', 'xmltree', self._apk_path,
|
||||
'AndroidManifest.xml']
|
||||
cmd = [aapt, 'dump', 'xmltree', self._apk_path]
|
||||
if aapt_version == 2:
|
||||
cmd += ['--file']
|
||||
cmd += ['AndroidManifest.xml']
|
||||
matched_activities = self.activity_regex.finditer(self._run(cmd))
|
||||
self._activities = [m.group('name') for m in matched_activities]
|
||||
return self._activities
|
||||
@@ -204,21 +215,26 @@ class ApkInfo(object):
|
||||
@property
|
||||
def methods(self):
|
||||
if self._methods is None:
|
||||
with zipfile.ZipFile(self._apk_path, 'r') as z:
|
||||
extracted = z.extract('classes.dex', tempfile.gettempdir())
|
||||
|
||||
dexdump = os.path.join(os.path.dirname(aapt), 'dexdump')
|
||||
command = [dexdump, '-l', 'xml', extracted]
|
||||
dump = self._run(command)
|
||||
# Only try to extract once
|
||||
self._methods = []
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
with zipfile.ZipFile(self._apk_path, 'r') as z:
|
||||
try:
|
||||
extracted = z.extract('classes.dex', tmp_dir)
|
||||
except KeyError:
|
||||
return []
|
||||
dexdump = os.path.join(os.path.dirname(aapt), 'dexdump')
|
||||
command = [dexdump, '-l', 'xml', extracted]
|
||||
dump = self._run(command)
|
||||
|
||||
xml_tree = xml.etree.ElementTree.fromstring(dump)
|
||||
|
||||
package = next(i for i in xml_tree.iter('package')
|
||||
if i.attrib['name'] == self.package)
|
||||
package = next((i for i in xml_tree.iter('package')
|
||||
if i.attrib['name'] == self.package), None)
|
||||
|
||||
self._methods = [(meth.attrib['name'], klass.attrib['name'])
|
||||
for klass in package.iter('class')
|
||||
for meth in klass.iter('method')]
|
||||
for meth in klass.iter('method')] if package else []
|
||||
return self._methods
|
||||
|
||||
def _run(self, command):
|
||||
@@ -233,7 +249,7 @@ class ApkInfo(object):
|
||||
return output
|
||||
|
||||
|
||||
class AdbConnection(object):
|
||||
class AdbConnection(ConnectionBase):
|
||||
|
||||
# maintains the count of parallel active connections to a device, so that
|
||||
# adb disconnect is not invoked untill all connections are closed
|
||||
@@ -251,53 +267,62 @@ class AdbConnection(object):
|
||||
@property
|
||||
def connected_as_root(self):
|
||||
if self._connected_as_root[self.device] is None:
|
||||
result = self.execute('id')
|
||||
self._connected_as_root[self.device] = 'uid=0(' in result
|
||||
result = self.execute('id')
|
||||
self._connected_as_root[self.device] = 'uid=0(' in result
|
||||
return self._connected_as_root[self.device]
|
||||
|
||||
@connected_as_root.setter
|
||||
def connected_as_root(self, state):
|
||||
self._connected_as_root[self.device] = state
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def __init__(self, device=None, timeout=None, platform=None, adb_server=None,
|
||||
adb_as_root=False):
|
||||
adb_as_root=False, connection_attempts=MAX_ATTEMPTS,
|
||||
poll_transfers=False,
|
||||
start_transfer_poll_delay=30,
|
||||
total_transfer_timeout=3600,
|
||||
transfer_poll_period=30,):
|
||||
super().__init__()
|
||||
self.timeout = timeout if timeout is not None else self.default_timeout
|
||||
if device is None:
|
||||
device = adb_get_device(timeout=timeout, adb_server=adb_server)
|
||||
self.device = device
|
||||
self.adb_server = adb_server
|
||||
self.adb_as_root = adb_as_root
|
||||
self.poll_transfers = poll_transfers
|
||||
if poll_transfers:
|
||||
transfer_opts = {'start_transfer_poll_delay': start_transfer_poll_delay,
|
||||
'total_timeout': total_transfer_timeout,
|
||||
'poll_period': transfer_poll_period,
|
||||
}
|
||||
self.transfer_mgr = PopenTransferManager(self, **transfer_opts) if poll_transfers else None
|
||||
if self.adb_as_root:
|
||||
self.adb_root(enable=True)
|
||||
adb_connect(self.device)
|
||||
adb_connect(self.device, adb_server=self.adb_server, attempts=connection_attempts)
|
||||
AdbConnection.active_connections[self.device] += 1
|
||||
self._setup_ls()
|
||||
self._setup_su()
|
||||
|
||||
def push(self, source, dest, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
command = "push {} {}".format(quote(source), quote(dest))
|
||||
if not os.path.exists(source):
|
||||
raise HostError('No such file "{}"'.format(source))
|
||||
return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
|
||||
def push(self, sources, dest, timeout=None):
|
||||
return self._push_pull('push', sources, dest, timeout)
|
||||
|
||||
def pull(self, source, dest, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
# Pull all files matching a wildcard expression
|
||||
if os.path.isdir(dest) and \
|
||||
('*' in source or '?' in source):
|
||||
command = 'shell {} {}'.format(self.ls_command, source)
|
||||
output = adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
|
||||
for line in output.splitlines():
|
||||
command = "pull {} {}".format(quote(line.strip()), quote(dest))
|
||||
adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
|
||||
return
|
||||
command = "pull {} {}".format(quote(source), quote(dest))
|
||||
return adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
|
||||
def pull(self, sources, dest, timeout=None):
|
||||
return self._push_pull('pull', sources, dest, timeout)
|
||||
|
||||
def _push_pull(self, action, sources, dest, timeout):
|
||||
paths = sources + [dest]
|
||||
|
||||
# Quote twice to avoid expansion by host shell, then ADB globbing
|
||||
do_quote = lambda x: quote(glob.escape(x))
|
||||
paths = ' '.join(map(do_quote, paths))
|
||||
|
||||
command = "{} {}".format(action, paths)
|
||||
if timeout or not self.poll_transfers:
|
||||
adb_command(self.device, command, timeout=timeout, adb_server=self.adb_server)
|
||||
else:
|
||||
with self.transfer_mgr.manage(sources, dest, action):
|
||||
bg_cmd = adb_command_background(self.device, command, adb_server=self.adb_server)
|
||||
self.transfer_mgr.set_transfer_and_wait(bg_cmd)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def execute(self, command, timeout=None, check_exit_code=False,
|
||||
@@ -312,14 +337,26 @@ class AdbConnection(object):
|
||||
raise
|
||||
|
||||
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||
return adb_background_shell(self.device, command, stdout, stderr, as_root, adb_server=self.adb_server)
|
||||
bg_cmd = self._background(command, stdout, stderr, as_root)
|
||||
self._current_bg_cmds.add(bg_cmd)
|
||||
return bg_cmd
|
||||
|
||||
def close(self):
|
||||
def _background(self, command, stdout, stderr, as_root):
|
||||
adb_shell, pid = adb_background_shell(self, command, stdout, stderr, as_root)
|
||||
bg_cmd = AdbBackgroundCommand(
|
||||
conn=self,
|
||||
adb_popen=adb_shell,
|
||||
pid=pid,
|
||||
as_root=as_root
|
||||
)
|
||||
return bg_cmd
|
||||
|
||||
def _close(self):
|
||||
AdbConnection.active_connections[self.device] -= 1
|
||||
if AdbConnection.active_connections[self.device] <= 0:
|
||||
if self.adb_as_root:
|
||||
self.adb_root(self.device, enable=False)
|
||||
adb_disconnect(self.device)
|
||||
self.adb_root(enable=False)
|
||||
adb_disconnect(self.device, self.adb_server)
|
||||
del AdbConnection.active_connections[self.device]
|
||||
|
||||
def cancel_running_command(self):
|
||||
@@ -330,16 +367,16 @@ class AdbConnection(object):
|
||||
|
||||
def adb_root(self, enable=True):
|
||||
cmd = 'root' if enable else 'unroot'
|
||||
output = adb_command(self.device, cmd, timeout=30)
|
||||
output = adb_command(self.device, cmd, timeout=30, adb_server=self.adb_server)
|
||||
if 'cannot run as root in production builds' in output:
|
||||
raise TargetStableError(output)
|
||||
AdbConnection._connected_as_root[self.device] = enable
|
||||
|
||||
def wait_for_device(self, timeout=30):
|
||||
adb_command(self.device, 'wait-for-device', timeout)
|
||||
adb_command(self.device, 'wait-for-device', timeout, self.adb_server)
|
||||
|
||||
def reboot_bootloader(self, timeout=30):
|
||||
adb_command(self.device, 'reboot-bootloader', timeout)
|
||||
adb_command(self.device, 'reboot-bootloader', timeout, self.adb_server)
|
||||
|
||||
# Again, we need to handle boards where the default output format from ls is
|
||||
# single column *and* boards where the default output is multi-column.
|
||||
@@ -423,7 +460,7 @@ def adb_get_device(timeout=None, adb_server=None):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
|
||||
def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS, adb_server=None):
|
||||
_check_env()
|
||||
tries = 0
|
||||
output = None
|
||||
@@ -436,11 +473,12 @@ def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
|
||||
# adb connection may have gone "stale", resulting in adb blocking
|
||||
# indefinitely when making calls to the device. To avoid this,
|
||||
# always disconnect first.
|
||||
adb_disconnect(device)
|
||||
command = 'adb connect {}'.format(quote(device))
|
||||
adb_disconnect(device, adb_server)
|
||||
adb_cmd = get_adb_command(None, 'connect', adb_server)
|
||||
command = '{} {}'.format(adb_cmd, quote(device))
|
||||
logger.debug(command)
|
||||
output, _ = check_output(command, shell=True, timeout=timeout)
|
||||
if _ping(device):
|
||||
if _ping(device, adb_server):
|
||||
break
|
||||
time.sleep(10)
|
||||
else: # did not connect to the device
|
||||
@@ -450,22 +488,23 @@ def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
|
||||
raise HostError(message)
|
||||
|
||||
|
||||
def adb_disconnect(device):
|
||||
def adb_disconnect(device, adb_server=None):
|
||||
_check_env()
|
||||
if not device:
|
||||
return
|
||||
if ":" in device and device in adb_list_devices():
|
||||
command = "adb disconnect " + device
|
||||
if ":" in device and device in adb_list_devices(adb_server):
|
||||
adb_cmd = get_adb_command(None, 'disconnect', adb_server)
|
||||
command = "{} {}".format(adb_cmd, device)
|
||||
logger.debug(command)
|
||||
retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
|
||||
if retval:
|
||||
raise TargetTransientError('"{}" returned {}'.format(command, retval))
|
||||
|
||||
|
||||
def _ping(device):
|
||||
def _ping(device, adb_server=None):
|
||||
_check_env()
|
||||
device_string = ' -s {}'.format(quote(device)) if device else ''
|
||||
command = "adb{} shell \"ls /data/local/tmp > /dev/null\"".format(device_string)
|
||||
adb_cmd = get_adb_command(device, 'shell', adb_server)
|
||||
command = "{} {}".format(adb_cmd, quote('ls /data/local/tmp > /dev/null'))
|
||||
logger.debug(command)
|
||||
result = subprocess.call(command, stderr=subprocess.PIPE, shell=True)
|
||||
if not result: # pylint: disable=simplifiable-if-statement
|
||||
@@ -496,7 +535,7 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
|
||||
|
||||
logger.debug(' '.join(quote(part) for part in parts))
|
||||
try:
|
||||
raw_output, _ = check_output(parts, timeout, shell=False, combined_output=True)
|
||||
raw_output, error = check_output(parts, timeout, shell=False)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise TargetStableError(str(e))
|
||||
|
||||
@@ -516,8 +555,8 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
|
||||
if exit_code.isdigit():
|
||||
if int(exit_code):
|
||||
message = ('Got exit code {}\nfrom target command: {}\n'
|
||||
'OUTPUT: {}')
|
||||
raise TargetStableError(message.format(exit_code, command, output))
|
||||
'OUTPUT: {}\nSTDERR: {}\n')
|
||||
raise TargetStableError(message.format(exit_code, command, output, error))
|
||||
elif re_search:
|
||||
message = 'Could not start activity; got the following:\n{}'
|
||||
raise TargetStableError(message.format(re_search[0]))
|
||||
@@ -528,30 +567,50 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
|
||||
else:
|
||||
message = 'adb has returned early; did not get an exit code. '\
|
||||
'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
|
||||
'-----'
|
||||
raise TargetTransientError(message.format(raw_output))
|
||||
'-----\nSTDERR:\n-----\n{}\n-----'
|
||||
raise TargetTransientError(message.format(raw_output, error))
|
||||
|
||||
return output
|
||||
return output + error
|
||||
|
||||
|
||||
def adb_background_shell(device, command,
|
||||
def adb_background_shell(conn, command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
as_root=False,
|
||||
adb_server=None):
|
||||
as_root=False):
|
||||
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
|
||||
device = conn.device
|
||||
adb_server = conn.adb_server
|
||||
|
||||
_check_env()
|
||||
stdout, stderr, command = redirect_streams(stdout, stderr, command)
|
||||
if as_root:
|
||||
command = 'echo {} | su'.format(quote(command))
|
||||
|
||||
device_string = ' -H {}'.format(adb_server) if adb_server else ''
|
||||
device_string += ' -s {}'.format(device) if device else ''
|
||||
full_command = 'adb{} shell {}'.format(device_string, quote(command))
|
||||
logger.debug(full_command)
|
||||
return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
|
||||
# Attach a unique UUID to the command line so it can be looked for without
|
||||
# any ambiguity with ps
|
||||
uuid_ = uuid.uuid4().hex
|
||||
uuid_var = 'BACKGROUND_COMMAND_UUID={}'.format(uuid_)
|
||||
command = "{} sh -c {}".format(uuid_var, quote(command))
|
||||
|
||||
def adb_kill_server(self, timeout=30):
|
||||
adb_command(None, 'kill-server', timeout)
|
||||
adb_cmd = get_adb_command(device, 'shell', adb_server)
|
||||
full_command = '{} {}'.format(adb_cmd, quote(command))
|
||||
logger.debug(full_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))
|
||||
ps_out = conn.execute(find_pid)
|
||||
pids = [
|
||||
int(line.strip().split(' ', 1)[0])
|
||||
for line in ps_out.splitlines()
|
||||
]
|
||||
# The line we are looking for is the first one, since it was started before
|
||||
# any look up command
|
||||
pid = sorted(pids)[0]
|
||||
return (p, pid)
|
||||
|
||||
def adb_kill_server(timeout=30, adb_server=None):
|
||||
adb_command(None, 'kill-server', timeout, adb_server)
|
||||
|
||||
def adb_list_devices(adb_server=None):
|
||||
output = adb_command(None, 'devices', adb_server=adb_server)
|
||||
@@ -571,12 +630,22 @@ def get_adb_command(device, command, adb_server=None):
|
||||
device_string += ' -s {}'.format(device) if device else ''
|
||||
return "adb{} {}".format(device_string, command)
|
||||
|
||||
|
||||
def adb_command(device, command, timeout=None, adb_server=None):
|
||||
full_command = get_adb_command(device, command, adb_server)
|
||||
logger.debug(full_command)
|
||||
output, _ = check_output(full_command, timeout, shell=True)
|
||||
return output
|
||||
|
||||
|
||||
def adb_command_background(device, command, adb_server=None):
|
||||
full_command = get_adb_command(device, command, adb_server)
|
||||
logger.debug(full_command)
|
||||
proc = get_subprocess(full_command, shell=True)
|
||||
cmd = PopenBackgroundCommand(proc)
|
||||
return cmd
|
||||
|
||||
|
||||
def grant_app_permissions(target, package):
|
||||
"""
|
||||
Grant an app all the permissions it may ask for
|
||||
@@ -584,7 +653,7 @@ def grant_app_permissions(target, package):
|
||||
dumpsys = target.execute('dumpsys package {}'.format(package))
|
||||
|
||||
permissions = re.search(
|
||||
'requested permissions:\s*(?P<permissions>(android.permission.+\s*)+)', dumpsys
|
||||
r'requested permissions:\s*(?P<permissions>(android.permission.+\s*)+)', dumpsys
|
||||
)
|
||||
if permissions is None:
|
||||
return
|
||||
@@ -604,8 +673,10 @@ class _AndroidEnvironment(object):
|
||||
def __init__(self):
|
||||
self.android_home = None
|
||||
self.platform_tools = None
|
||||
self.build_tools = None
|
||||
self.adb = None
|
||||
self.aapt = None
|
||||
self.aapt_version = None
|
||||
self.fastboot = None
|
||||
|
||||
|
||||
@@ -631,28 +702,73 @@ def _initialize_without_android_home(env):
|
||||
_init_common(env)
|
||||
return env
|
||||
|
||||
|
||||
def _init_common(env):
|
||||
_discover_build_tools(env)
|
||||
_discover_aapt(env)
|
||||
|
||||
def _discover_build_tools(env):
|
||||
logger.debug('ANDROID_HOME: {}'.format(env.android_home))
|
||||
build_tools_directory = os.path.join(env.android_home, 'build-tools')
|
||||
if not os.path.isdir(build_tools_directory):
|
||||
msg = '''ANDROID_HOME ({}) does not appear to have valid Android SDK install
|
||||
(cannot find build-tools)'''
|
||||
raise HostError(msg.format(env.android_home))
|
||||
versions = os.listdir(build_tools_directory)
|
||||
for version in reversed(sorted(versions)):
|
||||
aapt_path = os.path.join(build_tools_directory, version, 'aapt')
|
||||
if os.path.isfile(aapt_path):
|
||||
logger.debug('Using aapt for version {}'.format(version))
|
||||
env.aapt = aapt_path
|
||||
break
|
||||
else:
|
||||
raise HostError('aapt not found. Please make sure at least one Android '
|
||||
'platform is installed.')
|
||||
if os.path.isdir(build_tools_directory):
|
||||
env.build_tools = build_tools_directory
|
||||
|
||||
def _check_supported_aapt2(binary):
|
||||
# At time of writing the version argument of aapt2 is not helpful as
|
||||
# the output is only a placeholder that does not distinguish between versions
|
||||
# with and without support for badging. Unfortunately aapt has been
|
||||
# deprecated and fails to parse some valid apks so we will try to favour
|
||||
# aapt2 if possible else will fall back to aapt.
|
||||
# Try to execute the badging command and check if we get an expected error
|
||||
# message as opposed to an unknown command error to determine if we have a
|
||||
# suitable version.
|
||||
cmd = '{} dump badging'.format(binary)
|
||||
result = subprocess.run(cmd.encode('utf-8'), shell=True, stderr=subprocess.PIPE)
|
||||
supported = bool(AAPT_BADGING_OUTPUT.search(result.stderr.decode('utf-8')))
|
||||
msg = 'Found a {} aapt2 binary at: {}'
|
||||
logger.debug(msg.format('supported' if supported else 'unsupported', binary))
|
||||
return supported
|
||||
|
||||
def _discover_aapt(env):
|
||||
if env.build_tools:
|
||||
aapt_path = ''
|
||||
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):
|
||||
aapt2_path = os.path.join(env.build_tools, version, 'aapt2')
|
||||
if not aapt_path and not os.path.isfile(aapt_path):
|
||||
aapt_path = os.path.join(env.build_tools, version, 'aapt')
|
||||
aapt_version = 1
|
||||
break
|
||||
|
||||
# Use aapt2 only if present and we have a suitable version
|
||||
if aapt2_path and _check_supported_aapt2(aapt2_path):
|
||||
aapt_path = aapt2_path
|
||||
aapt_version = 2
|
||||
|
||||
# Use the aapt version discoverted from build tools.
|
||||
if aapt_path:
|
||||
logger.debug('Using {} for version {}'.format(aapt_path, version))
|
||||
env.aapt = aapt_path
|
||||
env.aapt_version = aapt_version
|
||||
return
|
||||
|
||||
# Try detecting aapt2 and aapt from PATH
|
||||
if not env.aapt:
|
||||
aapt2_path = which('aapt2')
|
||||
if _check_supported_aapt2(aapt2_path):
|
||||
env.aapt = aapt2_path
|
||||
env.aapt_version = 2
|
||||
else:
|
||||
env.aapt = which('aapt')
|
||||
env.aapt_version = 1
|
||||
|
||||
if not env.aapt:
|
||||
raise HostError('aapt/aapt2 not found. Please make sure it is avaliable in PATH'
|
||||
' or at least one Android platform is installed')
|
||||
|
||||
def _check_env():
|
||||
global android_home, platform_tools, adb, aapt # pylint: disable=W0603
|
||||
global android_home, platform_tools, adb, aapt, aapt_version # pylint: disable=W0603
|
||||
if not android_home:
|
||||
android_home = os.getenv('ANDROID_HOME')
|
||||
if android_home:
|
||||
@@ -663,6 +779,7 @@ def _check_env():
|
||||
platform_tools = _env.platform_tools
|
||||
adb = _env.adb
|
||||
aapt = _env.aapt
|
||||
aapt_version = _env.aapt_version
|
||||
|
||||
class LogcatMonitor(object):
|
||||
"""
|
||||
@@ -681,11 +798,12 @@ class LogcatMonitor(object):
|
||||
def logfile(self):
|
||||
return self._logfile
|
||||
|
||||
def __init__(self, target, regexps=None):
|
||||
def __init__(self, target, regexps=None, logcat_format=None):
|
||||
super(LogcatMonitor, self).__init__()
|
||||
|
||||
self.target = target
|
||||
self._regexps = regexps
|
||||
self._logcat_format = logcat_format
|
||||
self._logcat = None
|
||||
self._logfile = None
|
||||
|
||||
@@ -699,7 +817,7 @@ class LogcatMonitor(object):
|
||||
if outfile:
|
||||
self._logfile = open(outfile, 'w')
|
||||
else:
|
||||
self._logfile = tempfile.NamedTemporaryFile()
|
||||
self._logfile = tempfile.NamedTemporaryFile(mode='w')
|
||||
|
||||
self.target.clear_logcat()
|
||||
|
||||
@@ -717,12 +835,16 @@ class LogcatMonitor(object):
|
||||
else:
|
||||
logcat_cmd = '{} | grep {}'.format(logcat_cmd, quote(regexp))
|
||||
|
||||
logcat_cmd = get_adb_command(self.target.conn.device, logcat_cmd)
|
||||
if self._logcat_format:
|
||||
logcat_cmd = "{} -v {}".format(logcat_cmd, quote(self._logcat_format))
|
||||
|
||||
logcat_cmd = get_adb_command(self.target.conn.device, logcat_cmd, self.target.adb_server)
|
||||
|
||||
logger.debug('logcat command ="{}"'.format(logcat_cmd))
|
||||
self._logcat = pexpect.spawn(logcat_cmd, logfile=self._logfile)
|
||||
self._logcat = pexpect.spawn(logcat_cmd, logfile=self._logfile, encoding='utf-8')
|
||||
|
||||
def stop(self):
|
||||
self.flush_log()
|
||||
self._logcat.terminate()
|
||||
self._logfile.close()
|
||||
|
||||
@@ -730,6 +852,12 @@ class LogcatMonitor(object):
|
||||
"""
|
||||
Return the list of lines found by the monitor
|
||||
"""
|
||||
self.flush_log()
|
||||
|
||||
with open(self._logfile.name) as fh:
|
||||
return [line for line in fh]
|
||||
|
||||
def flush_log(self):
|
||||
# Unless we tell pexect to 'expect' something, it won't read from
|
||||
# logcat's buffer or write into our logfile. We'll need to force it to
|
||||
# read any pending logcat output.
|
||||
@@ -760,9 +888,6 @@ class LogcatMonitor(object):
|
||||
# printed anything since pexpect last read from its buffer.
|
||||
break
|
||||
|
||||
with open(self._logfile.name) as fh:
|
||||
return [line for line in fh]
|
||||
|
||||
def clear_log(self):
|
||||
with open(self._logfile.name, 'w') as _:
|
||||
pass
|
||||
|
@@ -18,7 +18,7 @@ import logging
|
||||
from devlib.utils.types import numeric
|
||||
|
||||
|
||||
GEM5STATS_FIELD_REGEX = re.compile("^(?P<key>[^- ]\S*) +(?P<value>[^#]+).+$")
|
||||
GEM5STATS_FIELD_REGEX = re.compile(r"^(?P<key>[^- ]\S*) +(?P<value>[^#]+).+$")
|
||||
GEM5STATS_DUMP_HEAD = '---------- Begin Simulation Statistics ----------'
|
||||
GEM5STATS_DUMP_TAIL = '---------- End Simulation Statistics ----------'
|
||||
GEM5STATS_ROI_NUMBER = 8
|
||||
|
@@ -20,9 +20,10 @@ Miscellaneous functions that don't fit anywhere else.
|
||||
"""
|
||||
from __future__ import division
|
||||
from contextlib import contextmanager
|
||||
from functools import partial, reduce
|
||||
from functools import partial, reduce, wraps
|
||||
from itertools import groupby
|
||||
from operator import itemgetter
|
||||
from weakref import WeakKeyDictionary, WeakSet
|
||||
|
||||
import ctypes
|
||||
import functools
|
||||
@@ -45,6 +46,11 @@ try:
|
||||
except AttributeError:
|
||||
from contextlib2 import ExitStack
|
||||
|
||||
try:
|
||||
from shlex import quote
|
||||
except ImportError:
|
||||
from pipes import quote
|
||||
|
||||
from past.builtins import basestring
|
||||
|
||||
# pylint: disable=redefined-builtin
|
||||
@@ -136,9 +142,6 @@ def get_cpu_name(implementer, part, variant):
|
||||
|
||||
|
||||
def preexec_function():
|
||||
# Ignore the SIGINT signal by setting the handler to the standard
|
||||
# signal handler SIG_IGN.
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
# Change process group in case we have to kill the subprocess and all of
|
||||
# its children later.
|
||||
# TODO: this is Unix-specific; would be good to find an OS-agnostic way
|
||||
@@ -152,10 +155,22 @@ check_output_logger = logging.getLogger('check_output')
|
||||
check_output_lock = threading.Lock()
|
||||
|
||||
|
||||
def check_output(command, timeout=None, ignore=None, inputtext=None,
|
||||
combined_output=False, **kwargs):
|
||||
"""This is a version of subprocess.check_output that adds a timeout parameter to kill
|
||||
the subprocess if it does not return within the specified time."""
|
||||
def get_subprocess(command, **kwargs):
|
||||
if 'stdout' in kwargs:
|
||||
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||
with check_output_lock:
|
||||
process = subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
preexec_fn=preexec_function,
|
||||
**kwargs)
|
||||
return process
|
||||
|
||||
|
||||
def check_subprocess_output(process, timeout=None, ignore=None, inputtext=None):
|
||||
output = None
|
||||
error = None
|
||||
# pylint: disable=too-many-branches
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
@@ -164,49 +179,35 @@ def check_output(command, timeout=None, ignore=None, inputtext=None,
|
||||
elif not isinstance(ignore, list) and ignore != 'all':
|
||||
message = 'Invalid value for ignore parameter: "{}"; must be an int or a list'
|
||||
raise ValueError(message.format(ignore))
|
||||
if 'stdout' in kwargs:
|
||||
raise ValueError('stdout argument not allowed, it will be overridden.')
|
||||
|
||||
def callback(pid):
|
||||
try:
|
||||
check_output_logger.debug('{} timed out; sending SIGKILL'.format(pid))
|
||||
os.killpg(pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass # process may have already terminated.
|
||||
|
||||
with check_output_lock:
|
||||
stderr = subprocess.STDOUT if combined_output else subprocess.PIPE
|
||||
process = subprocess.Popen(command,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=stderr,
|
||||
stdin=subprocess.PIPE,
|
||||
preexec_fn=preexec_function,
|
||||
**kwargs)
|
||||
|
||||
if timeout:
|
||||
timer = threading.Timer(timeout, callback, [process.pid, ])
|
||||
timer.start()
|
||||
|
||||
try:
|
||||
output, error = process.communicate(inputtext)
|
||||
if sys.version_info[0] == 3:
|
||||
# Currently errors=replace is needed as 0x8c throws an error
|
||||
output = output.decode(sys.stdout.encoding or 'utf-8', "replace")
|
||||
if error:
|
||||
error = error.decode(sys.stderr.encoding or 'utf-8', "replace")
|
||||
finally:
|
||||
if timeout:
|
||||
timer.cancel()
|
||||
output, error = process.communicate(inputtext, timeout=timeout)
|
||||
except subprocess.TimeoutExpired as e:
|
||||
timeout_expired = e
|
||||
else:
|
||||
timeout_expired = None
|
||||
|
||||
# Currently errors=replace is needed as 0x8c throws an error
|
||||
output = output.decode(sys.stdout.encoding or 'utf-8', "replace") if output else ''
|
||||
error = error.decode(sys.stderr.encoding or 'utf-8', "replace") if error else ''
|
||||
|
||||
if timeout_expired:
|
||||
raise TimeoutError(process.args, output='\n'.join([output, error]))
|
||||
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
if retcode == -9: # killed, assume due to timeout callback
|
||||
raise TimeoutError(command, output='\n'.join([output or '', error or '']))
|
||||
elif ignore != 'all' and retcode not in ignore:
|
||||
raise subprocess.CalledProcessError(retcode, command, output='\n'.join([output or '', error or '']))
|
||||
if retcode and ignore != 'all' and retcode not in ignore:
|
||||
raise subprocess.CalledProcessError(retcode, process.args, output='\n'.join([output, error]))
|
||||
|
||||
return output, error
|
||||
|
||||
|
||||
def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
|
||||
"""This is a version of subprocess.check_output that adds a timeout parameter to kill
|
||||
the subprocess if it does not return within the specified time."""
|
||||
process = get_subprocess(command, **kwargs)
|
||||
return check_subprocess_output(process, timeout=timeout, ignore=ignore, inputtext=inputtext)
|
||||
|
||||
|
||||
def walk_modules(path):
|
||||
"""
|
||||
Given package name, return a list of all modules (including submodules, etc)
|
||||
@@ -244,6 +245,32 @@ def walk_modules(path):
|
||||
mods.append(submod)
|
||||
return mods
|
||||
|
||||
def redirect_streams(stdout, stderr, command):
|
||||
"""
|
||||
Update a command to redirect a given stream to /dev/null if it's
|
||||
``subprocess.DEVNULL``.
|
||||
|
||||
:return: A tuple (stdout, stderr, command) with stream set to ``subprocess.PIPE``
|
||||
if the `stream` parameter was set to ``subprocess.DEVNULL``.
|
||||
"""
|
||||
def redirect(stream, redirection):
|
||||
if stream == subprocess.DEVNULL:
|
||||
suffix = '{}/dev/null'.format(redirection)
|
||||
elif stream == subprocess.STDOUT:
|
||||
suffix = '{}&1'.format(redirection)
|
||||
# Indicate that there is nothing to monitor for stderr anymore
|
||||
# since it's merged into stdout
|
||||
stream = subprocess.DEVNULL
|
||||
else:
|
||||
suffix = ''
|
||||
|
||||
return (stream, suffix)
|
||||
|
||||
stdout, suffix1 = redirect(stdout, '>')
|
||||
stderr, suffix2 = redirect(stderr, '2>')
|
||||
|
||||
command = 'sh -c {} {} {}'.format(quote(command), suffix1, suffix2)
|
||||
return (stdout, stderr, command)
|
||||
|
||||
def ensure_directory_exists(dirpath):
|
||||
"""A filter for directory paths to ensure they exist."""
|
||||
@@ -468,7 +495,7 @@ def escape_spaces(text):
|
||||
|
||||
.. note:: :func:`pipes.quote` should be favored where possible.
|
||||
"""
|
||||
return text.replace(' ', '\ ')
|
||||
return text.replace(' ', '\\ ')
|
||||
|
||||
|
||||
def getch(count=1):
|
||||
@@ -718,3 +745,184 @@ def batch_contextmanager(f, kwargs_list):
|
||||
for kwargs in kwargs_list:
|
||||
stack.enter_context(f(**kwargs))
|
||||
yield
|
||||
|
||||
|
||||
@contextmanager
|
||||
def nullcontext(enter_result=None):
|
||||
"""
|
||||
Backport of Python 3.7 ``contextlib.nullcontext``
|
||||
|
||||
This context manager does nothing, so it can be used as a default
|
||||
placeholder for code that needs to select at runtime what context manager
|
||||
to use.
|
||||
|
||||
:param enter_result: Object that will be bound to the target of the with
|
||||
statement, or `None` if nothing is specified.
|
||||
:type enter_result: object
|
||||
"""
|
||||
yield enter_result
|
||||
|
||||
|
||||
class tls_property:
|
||||
"""
|
||||
Use it like `property` decorator, but the result will be memoized per
|
||||
thread. When the owning thread dies, the values for that thread will be
|
||||
destroyed.
|
||||
|
||||
In order to get the values, it's necessary to call the object
|
||||
given by the property. This is necessary in order to be able to add methods
|
||||
to that object, like :meth:`_BoundTLSProperty.get_all_values`.
|
||||
|
||||
Values can be set and deleted as well, which will be a thread-local set.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.factory.__name__
|
||||
|
||||
def __init__(self, factory):
|
||||
self.factory = factory
|
||||
# Lock accesses to shared WeakKeyDictionary and WeakSet
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
return _BoundTLSProperty(self, instance, owner)
|
||||
|
||||
def _get_value(self, instance, owner):
|
||||
tls, values = self._get_tls(instance)
|
||||
try:
|
||||
return tls.value
|
||||
except AttributeError:
|
||||
# Bind the method to `instance`
|
||||
f = self.factory.__get__(instance, owner)
|
||||
obj = f()
|
||||
tls.value = obj
|
||||
# Since that's a WeakSet, values will be removed automatically once
|
||||
# the threading.local variable that holds them is destroyed
|
||||
with self.lock:
|
||||
values.add(obj)
|
||||
return obj
|
||||
|
||||
def _get_all_values(self, instance, owner):
|
||||
with self.lock:
|
||||
# Grab a reference to all the objects at the time of the call by
|
||||
# using a regular set
|
||||
tls, values = self._get_tls(instance=instance)
|
||||
return set(values)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
tls, values = self._get_tls(instance)
|
||||
tls.value = value
|
||||
with self.lock:
|
||||
values.add(value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
tls, values = self._get_tls(instance)
|
||||
with self.lock:
|
||||
values.discard(tls.value)
|
||||
del tls.value
|
||||
|
||||
def _get_tls(self, instance):
|
||||
dct = instance.__dict__
|
||||
name = self.name
|
||||
try:
|
||||
# Using instance.__dict__[self.name] is safe as
|
||||
# getattr(instance, name) will return the property instead, as
|
||||
# the property is a descriptor
|
||||
tls = dct[name]
|
||||
except KeyError:
|
||||
with self.lock:
|
||||
# Double check after taking the lock to avoid a race
|
||||
if name not in dct:
|
||||
tls = (threading.local(), WeakSet())
|
||||
dct[name] = tls
|
||||
|
||||
return tls
|
||||
|
||||
@property
|
||||
def basic_property(self):
|
||||
"""
|
||||
Return a basic property that can be used to access the TLS value
|
||||
without having to call it first.
|
||||
|
||||
The drawback is that it's not possible to do anything over than
|
||||
getting/setting/deleting.
|
||||
"""
|
||||
def getter(instance, owner=None):
|
||||
prop = self.__get__(instance, owner)
|
||||
return prop()
|
||||
|
||||
return property(getter, self.__set__, self.__delete__)
|
||||
|
||||
class _BoundTLSProperty:
|
||||
"""
|
||||
Simple proxy object to allow either calling it to get the TLS value, or get
|
||||
some other informations by calling methods.
|
||||
"""
|
||||
def __init__(self, tls_property, instance, owner):
|
||||
self.tls_property = tls_property
|
||||
self.instance = instance
|
||||
self.owner = owner
|
||||
|
||||
def __call__(self):
|
||||
return self.tls_property._get_value(
|
||||
instance=self.instance,
|
||||
owner=self.owner,
|
||||
)
|
||||
|
||||
def get_all_values(self):
|
||||
"""
|
||||
Returns all the thread-local values currently in use in the process for
|
||||
that property for that instance.
|
||||
"""
|
||||
return self.tls_property._get_all_values(
|
||||
instance=self.instance,
|
||||
owner=self.owner,
|
||||
)
|
||||
|
||||
|
||||
class InitCheckpointMeta(type):
|
||||
"""
|
||||
Metaclass providing an ``initialized`` 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__``.
|
||||
"""
|
||||
def __new__(metacls, name, bases, dct, **kwargs):
|
||||
cls = super().__new__(metacls, name, bases, dct, **kwargs)
|
||||
init_f = cls.__init__
|
||||
|
||||
@wraps(init_f)
|
||||
def init_wrapper(self, *args, **kwargs):
|
||||
self.initialized = False
|
||||
|
||||
# Track the nesting of super()__init__ to set initialized=True only
|
||||
# when the outer level is finished
|
||||
try:
|
||||
stack = self._init_stack
|
||||
except AttributeError:
|
||||
stack = []
|
||||
self._init_stack = stack
|
||||
|
||||
stack.append(init_f)
|
||||
try:
|
||||
x = init_f(self, *args, **kwargs)
|
||||
finally:
|
||||
stack.pop()
|
||||
|
||||
if not stack:
|
||||
self.initialized = True
|
||||
del self._init_stack
|
||||
|
||||
return x
|
||||
|
||||
cls.__init__ = init_wrapper
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
class InitCheckpoint(metaclass=InitCheckpointMeta):
|
||||
"""
|
||||
Inherit from this class to set the :class:`InitCheckpointMeta` metaclass.
|
||||
"""
|
||||
pass
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ from subprocess import Popen, PIPE
|
||||
|
||||
VersionTuple = namedtuple('Version', ['major', 'minor', 'revision', 'dev'])
|
||||
|
||||
version = VersionTuple(1, 2, 0, '')
|
||||
version = VersionTuple(1, 3, 0, '')
|
||||
|
||||
|
||||
def get_devlib_version():
|
||||
|
@@ -3,16 +3,17 @@ Connection
|
||||
|
||||
A :class:`Connection` abstracts an actual physical connection to a device. The
|
||||
first connection is created when :func:`Target.connect` method is called. If a
|
||||
:class:`Target` is used in a multi-threaded environment, it will maintain a
|
||||
connection for each thread in which it is invoked. This allows the same target
|
||||
object to be used in parallel in multiple threads.
|
||||
:class:`~devlib.target.Target` is used in a multi-threaded environment, it will
|
||||
maintain a connection for each thread in which it is invoked. This allows
|
||||
the same target object to be used in parallel in multiple threads.
|
||||
|
||||
:class:`Connection`\ s will be automatically created and managed by
|
||||
:class:`Target`\ s, so there is usually no reason to create one manually.
|
||||
Instead, configuration for a :class:`Connection` is passed as
|
||||
`connection_settings` parameter when creating a :class:`Target`. The connection
|
||||
to be used target is also specified on instantiation by `conn_cls` parameter,
|
||||
though all concrete :class:`Target` implementations will set an appropriate
|
||||
:class:`~devlib.target.Target`\ s, so there is usually no reason to create one
|
||||
manually. Instead, configuration for a :class:`Connection` is passed as
|
||||
`connection_settings` parameter when creating a
|
||||
:class:`~devlib.target.Target`. The connection to be used target is also
|
||||
specified on instantiation by `conn_cls` parameter, though all concrete
|
||||
:class:`~devlib.target.Target` implementations will set an appropriate
|
||||
default, so there is typically no need to specify this explicitly.
|
||||
|
||||
:class:`Connection` classes are not a part of an inheritance hierarchy, i.e.
|
||||
@@ -20,25 +21,25 @@ they do not derive from a common base. Instead, a :class:`Connection` is any
|
||||
class that implements the following methods.
|
||||
|
||||
|
||||
.. method:: push(self, source, dest, timeout=None)
|
||||
.. method:: push(self, sources, dest, timeout=None)
|
||||
|
||||
Transfer a file from the host machine to the connected device.
|
||||
Transfer a list of files from the host machine to the connected device.
|
||||
|
||||
:param source: path of to the file on the host
|
||||
:param dest: path of to the file on the connected device.
|
||||
:param timeout: timeout (in seconds) for the transfer; if the transfer does
|
||||
not complete within this period, an exception will be raised.
|
||||
:param sources: list of paths on the host
|
||||
:param dest: path to the file or folder on the connected device.
|
||||
:param timeout: timeout (in seconds) for the transfer of each file; if the
|
||||
transfer does not complete within this period, an exception will be
|
||||
raised.
|
||||
|
||||
.. method:: pull(self, source, dest, timeout=None)
|
||||
.. method:: pull(self, sources, dest, timeout=None)
|
||||
|
||||
Transfer a file, or files matching a glob pattern, from the connected device
|
||||
to the host machine.
|
||||
Transfer a list of files from the connected device to the host machine.
|
||||
|
||||
:param source: path of to the file on the connected device. If ``dest`` is a
|
||||
directory, may be a glob pattern.
|
||||
:param dest: path of to the file on the host
|
||||
:param timeout: timeout (in seconds) for the transfer; if the transfer does
|
||||
not complete within this period, an exception will be raised.
|
||||
:param sources: list of paths on the connected device.
|
||||
:param dest: path to the file or folder on the host
|
||||
:param timeout: timeout (in seconds) for the transfer for each file; if the
|
||||
transfer does not complete within this period, an exception will be
|
||||
raised.
|
||||
|
||||
.. method:: execute(self, command, timeout=None, check_exit_code=False, as_root=False, strip_colors=True, will_succeed=False)
|
||||
|
||||
@@ -58,7 +59,7 @@ class that implements the following methods.
|
||||
:param will_succeed: The command is assumed to always succeed, unless there is
|
||||
an issue in the environment like the loss of network connectivity. That
|
||||
will make the method always raise an instance of a subclass of
|
||||
:class:`DevlibTransientError' when the command fails, instead of a
|
||||
:class:`DevlibTransientError` when the command fails, instead of a
|
||||
:class:`DevlibStableError`.
|
||||
|
||||
.. method:: background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False)
|
||||
@@ -76,7 +77,7 @@ class that implements the following methods.
|
||||
|
||||
.. note:: This **will block the connection** until the command completes.
|
||||
|
||||
.. note:: The above methods are directly wrapped by :class:`Target` methods,
|
||||
.. note:: The above methods are directly wrapped by :class:`~devlib.target.Target` methods,
|
||||
however note that some of the defaults are different.
|
||||
|
||||
.. method:: cancel_running_command(self)
|
||||
@@ -100,7 +101,12 @@ class that implements the following methods.
|
||||
Connection Types
|
||||
----------------
|
||||
|
||||
.. class:: AdbConnection(device=None, timeout=None, adb_server=None, adb_as_root=False)
|
||||
|
||||
.. module:: devlib.utils.android
|
||||
|
||||
.. class:: AdbConnection(device=None, timeout=None, adb_server=None, adb_as_root=False, connection_attempts=MAX_ATTEMPTS,\
|
||||
poll_transfers=False, start_transfer_poll_delay=30, total_transfer_timeout=3600,\
|
||||
transfer_poll_period=30)
|
||||
|
||||
A connection to an android device via ``adb`` (Android Debug Bridge).
|
||||
``adb`` is part of the Android SDK (though stand-alone versions are also
|
||||
@@ -115,11 +121,35 @@ Connection Types
|
||||
is raised.
|
||||
:param adb_server: Allows specifying the address of the adb server to use.
|
||||
:param adb_as_root: Specify whether the adb server should be restarted in root mode.
|
||||
:param connection_attempts: Specify how many connection attempts, 10 seconds
|
||||
apart, should be attempted to connect to the device.
|
||||
Defaults to 5.
|
||||
:param poll_transfers: Specify whether file transfers should be polled. Polling
|
||||
monitors the progress of file transfers and periodically
|
||||
checks whether they have stalled, attempting to cancel
|
||||
the transfers prematurely if so.
|
||||
:param start_transfer_poll_delay: If transfers are polled, specify the length of
|
||||
time after a transfer has started before polling
|
||||
should start.
|
||||
:param total_transfer_timeout: If transfers are polled, specify the total amount of time
|
||||
to elapse before the transfer is cancelled, regardless
|
||||
of its activity.
|
||||
:param transfer_poll_period: If transfers are polled, specify the period at which
|
||||
the transfers are sampled for activity. Too small values
|
||||
may cause the destination size to appear the same over
|
||||
one or more sample periods, causing improper transfer
|
||||
cancellation.
|
||||
|
||||
|
||||
.. class:: SshConnection(host, username, password=None, keyfile=None, port=None,\
|
||||
timeout=None, password_prompt=None, \
|
||||
sudo_cmd="sudo -- sh -c {}", options=None)
|
||||
|
||||
.. module:: devlib.utils.ssh
|
||||
|
||||
.. 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,
|
||||
start_transfer_poll_delay=30, total_transfer_timeout=3600,\
|
||||
transfer_poll_period=30)
|
||||
|
||||
A connection to a device on the network over SSH.
|
||||
|
||||
@@ -127,6 +157,9 @@ Connection Types
|
||||
:param username: username for SSH login
|
||||
:param password: password for the SSH connection
|
||||
|
||||
.. note:: To connect to a system without a password this
|
||||
parameter should be set to an empty string otherwise
|
||||
ssh key authentication will be attempted.
|
||||
.. note:: In order to user password-based authentication,
|
||||
``sshpass`` utility must be installed on the
|
||||
system.
|
||||
@@ -141,12 +174,26 @@ Connection Types
|
||||
:param timeout: Timeout for the connection in seconds. If a connection
|
||||
cannot be established within this time, an error will be
|
||||
raised.
|
||||
:param password_prompt: A string with the password prompt used by
|
||||
``sshpass``. Set this if your version of ``sshpass``
|
||||
uses something other than ``"[sudo] password"``.
|
||||
: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 options: A dictionary with extra ssh configuration options.
|
||||
|
||||
: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
|
||||
checks whether they have stalled, attempting to cancel
|
||||
the transfers prematurely if so.
|
||||
:param start_transfer_poll_delay: If transfers are polled, specify the length of
|
||||
time after a transfer has started before polling
|
||||
should start.
|
||||
:param total_transfer_timeout: If transfers are polled, specify the total amount of time
|
||||
to elapse before the transfer is cancelled, regardless
|
||||
of its activity.
|
||||
:param transfer_poll_period: If transfers are polled, specify the period at which
|
||||
the transfers are sampled for activity. Too small values
|
||||
may cause the destination size to appear the same over
|
||||
one or more sample periods, causing improper transfer
|
||||
cancellation.
|
||||
|
||||
.. class:: TelnetConnection(host, username, password=None, port=None,\
|
||||
timeout=None, password_prompt=None,\
|
||||
@@ -179,6 +226,7 @@ Connection Types
|
||||
connection to reduce the possibility of clashes).
|
||||
This parameter is ignored for SSH connections.
|
||||
|
||||
.. module:: devlib.host
|
||||
|
||||
.. class:: LocalConnection(keep_password=True, unrooted=False, password=None)
|
||||
|
||||
@@ -194,6 +242,9 @@ Connection Types
|
||||
prompting for it.
|
||||
|
||||
|
||||
.. module:: devlib.utils.ssh
|
||||
:noindex:
|
||||
|
||||
.. class:: Gem5Connection(platform, host=None, username=None, password=None,\
|
||||
timeout=None, password_prompt=None,\
|
||||
original_prompt=None)
|
||||
@@ -202,7 +253,7 @@ Connection Types
|
||||
|
||||
.. note:: Some of the following input parameters are optional and will be ignored during
|
||||
initialisation. They were kept to keep the analogy with a :class:`TelnetConnection`
|
||||
(i.e. ``host``, `username``, ``password``, ``port``,
|
||||
(i.e. ``host``, ``username``, ``password``, ``port``,
|
||||
``password_prompt`` and ``original_promp``)
|
||||
|
||||
|
||||
@@ -212,7 +263,7 @@ Connection Types
|
||||
will be ignored, the gem5 simulation needs to be
|
||||
on the same host the user is currently on, so if
|
||||
the host given as input parameter is not the
|
||||
same as the actual host, a ``TargetStableError``
|
||||
same as the actual host, a :class:`TargetStableError`
|
||||
will be raised to prevent confusion.
|
||||
|
||||
:param username: Username in the simulated system
|
||||
@@ -238,14 +289,14 @@ The only methods discussed below are those that will be overwritten by the
|
||||
|
||||
A connection to a gem5 simulation that emulates a Linux system.
|
||||
|
||||
.. method:: _login_to_device(self)
|
||||
.. method:: _login_to_device(self)
|
||||
|
||||
Login to the gem5 simulated system.
|
||||
Login to the gem5 simulated system.
|
||||
|
||||
.. class:: AndroidGem5Connection
|
||||
|
||||
A connection to a gem5 simulation that emulates an Android system.
|
||||
|
||||
.. method:: _wait_for_boot(self)
|
||||
.. method:: _wait_for_boot(self)
|
||||
|
||||
Wait for the gem5 simulated system to have booted and finished the booting animation.
|
||||
Wait for the gem5 simulated system to have booted and finished the booting animation.
|
||||
|
@@ -1,7 +1,6 @@
|
||||
Derived Measurements
|
||||
=====================
|
||||
|
||||
|
||||
The ``DerivedMeasurements`` API provides a consistent way of performing post
|
||||
processing on a provided :class:`MeasurementCsv` file.
|
||||
|
||||
@@ -35,6 +34,8 @@ API
|
||||
Derived Measurements
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. module:: devlib.derived
|
||||
|
||||
.. class:: DerivedMeasurements
|
||||
|
||||
The ``DerivedMeasurements`` class provides an API for post-processing
|
||||
@@ -102,17 +103,20 @@ Available Derived Measurements
|
||||
Energy
|
||||
~~~~~~
|
||||
|
||||
.. module:: devlib.derived.energy
|
||||
|
||||
.. class:: DerivedEnergyMeasurements
|
||||
|
||||
The ``DerivedEnergyMeasurements`` class is used to calculate average power and
|
||||
cumulative energy for each site if the required data is present.
|
||||
The ``DerivedEnergyMeasurements`` class is used to calculate average power
|
||||
and cumulative energy for each site if the required data is present.
|
||||
|
||||
The calculation of cumulative energy can occur in 3 ways. If a
|
||||
``site`` contains ``energy`` results, the first and last measurements are extracted
|
||||
and the delta calculated. If not, a ``timestamp`` channel will be used to calculate
|
||||
the energy from the power channel, failing back to using the sample rate attribute
|
||||
of the :class:`MeasurementCsv` file if timestamps are not available. If neither
|
||||
timestamps or a sample rate are available then an error will be raised.
|
||||
The calculation of cumulative energy can occur in 3 ways. If a ``site``
|
||||
contains ``energy`` results, the first and last measurements are extracted
|
||||
and the delta calculated. If not, a ``timestamp`` channel will be used to
|
||||
calculate the energy from the power channel, failing back to using the sample
|
||||
rate attribute of the :class:`MeasurementCsv` file if timestamps are not
|
||||
available. If neither timestamps or a sample rate are available then an error
|
||||
will be raised.
|
||||
|
||||
|
||||
.. method:: DerivedEnergyMeasurements.process(measurement_csv)
|
||||
@@ -128,6 +132,8 @@ Energy
|
||||
FPS / Rendering
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. module:: devlib.derived.fps
|
||||
|
||||
.. class:: DerivedGfxInfoStats(drop_threshold=5, suffix='-fps', filename=None, outdir=None)
|
||||
|
||||
Produces FPS (frames-per-second) and other derived statistics from
|
||||
|
@@ -3,6 +3,8 @@
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
.. module:: devlib
|
||||
|
||||
Welcome to devlib documentation
|
||||
===============================
|
||||
|
||||
|
@@ -5,9 +5,9 @@ Instrumentation
|
||||
|
||||
The ``Instrument`` API provide a consistent way of collecting measurements from
|
||||
a target. Measurements are collected via an instance of a class derived from
|
||||
:class:`Instrument`. An ``Instrument`` allows collection of measurement from one
|
||||
or more channels. An ``Instrument`` may support ``INSTANTANEOUS`` or
|
||||
``CONTINUOUS`` collection, or both.
|
||||
:class:`~devlib.instrument.Instrument`. An ``Instrument`` allows collection of
|
||||
measurement from one or more channels. An ``Instrument`` may support
|
||||
``INSTANTANEOUS`` or ``CONTINUOUS`` collection, or both.
|
||||
|
||||
Example
|
||||
-------
|
||||
@@ -50,6 +50,8 @@ Android target.
|
||||
API
|
||||
---
|
||||
|
||||
.. module:: devlib.instrument
|
||||
|
||||
Instrument
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -122,14 +124,16 @@ Instrument
|
||||
Take a single measurement from ``active_channels``. Returns a list of
|
||||
:class:`Measurement` objects (one for each active channel).
|
||||
|
||||
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||
.. note:: This method is only implemented by
|
||||
:class:`~devlib.instrument.Instrument`\ s that
|
||||
support ``INSTANTANEOUS`` measurement.
|
||||
|
||||
.. method:: Instrument.start()
|
||||
|
||||
Starts collecting measurements from ``active_channels``.
|
||||
|
||||
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||
.. note:: This method is only implemented by
|
||||
:class:`~devlib.instrument.Instrument`\ s that
|
||||
support ``CONTINUOUS`` measurement.
|
||||
|
||||
.. method:: Instrument.stop()
|
||||
@@ -137,7 +141,8 @@ Instrument
|
||||
Stops collecting measurements from ``active_channels``. Must be called after
|
||||
:func:`start()`.
|
||||
|
||||
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||
.. note:: This method is only implemented by
|
||||
:class:`~devlib.instrument.Instrument`\ s that
|
||||
support ``CONTINUOUS`` measurement.
|
||||
|
||||
.. method:: Instrument.get_data(outfile)
|
||||
@@ -148,9 +153,9 @@ Instrument
|
||||
``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns
|
||||
will be the same as the order of channels in ``Instrument.active_channels``.
|
||||
|
||||
If reporting timestamps, one channel must have a ``site`` named ``"timestamp"``
|
||||
and a ``kind`` of a :class:`MeasurmentType` of an appropriate time unit which will
|
||||
be used, if appropriate, during any post processing.
|
||||
If reporting timestamps, one channel must have a ``site`` named
|
||||
``"timestamp"`` and a ``kind`` of a :class:`MeasurmentType` of an appropriate
|
||||
time unit which will be used, if appropriate, during any post processing.
|
||||
|
||||
.. note:: Currently supported time units are seconds, milliseconds and
|
||||
microseconds, other units can also be used if an appropriate
|
||||
@@ -160,7 +165,8 @@ Instrument
|
||||
that can be used to stream :class:`Measurement`\ s lists (similar to what is
|
||||
returned by ``take_measurement()``.
|
||||
|
||||
.. note:: This method is only implemented by :class:`Instrument`\ s that
|
||||
.. note:: This method is only implemented by
|
||||
:class:`~devlib.instrument.Instrument`\ s that
|
||||
support ``CONTINUOUS`` measurement.
|
||||
|
||||
.. method:: Instrument.get_raw()
|
||||
@@ -185,7 +191,8 @@ Instrument
|
||||
|
||||
Sample rate of the instrument in Hz. Assumed to be the same for all channels.
|
||||
|
||||
.. note:: This attribute is only provided by :class:`Instrument`\ s that
|
||||
.. note:: This attribute is only provided by
|
||||
:class:`~devlib.instrument.Instrument`\ s that
|
||||
support ``CONTINUOUS`` measurement.
|
||||
|
||||
Instrument Channel
|
||||
@@ -194,8 +201,8 @@ Instrument Channel
|
||||
.. class:: InstrumentChannel(name, site, measurement_type, \*\*attrs)
|
||||
|
||||
An :class:`InstrumentChannel` describes a single type of measurement that may
|
||||
be collected by an :class:`Instrument`. A channel is primarily defined by a
|
||||
``site`` and a ``measurement_type``.
|
||||
be collected by an :class:`~devlib.instrument.Instrument`. A channel is
|
||||
primarily defined by a ``site`` and a ``measurement_type``.
|
||||
|
||||
A ``site`` indicates where on the target a measurement is collected from
|
||||
(e.g. a voltage rail or location of a sensor).
|
||||
@@ -488,12 +495,13 @@ voltage (see previous figure), samples are retrieved at a frequency of
|
||||
|
||||
where :math:`T_X` is the integration time for the :math:`X` voltage.
|
||||
|
||||
As described below (:meth:`BaylibreAcmeInstrument.reset`), the integration
|
||||
times for the bus and shunt voltage can be set separately which allows a
|
||||
tradeoff of accuracy between signals. This is particularly useful as the shunt
|
||||
voltage returned by the INA226 has a higher resolution than the bus voltage
|
||||
(2.5 μV and 1.25 mV LSB, respectively) and therefore would benefit more from a
|
||||
longer integration time.
|
||||
As described below (:meth:`BaylibreAcmeInstrument.reset
|
||||
<devlib.instrument.baylibre_acme.BaylibreAcmeInstrument.reset>`), the
|
||||
integration times for the bus and shunt voltage can be set separately which
|
||||
allows a tradeoff of accuracy between signals. This is particularly useful as
|
||||
the shunt voltage returned by the INA226 has a higher resolution than the bus
|
||||
voltage (2.5 μV and 1.25 mV LSB, respectively) and therefore would benefit more
|
||||
from a longer integration time.
|
||||
|
||||
As an illustration, consider the following sampled sine wave and notice how
|
||||
increasing the integration time (of the bus voltage in this case) "smoothes"
|
||||
@@ -601,8 +609,9 @@ Buffer-based transactions
|
||||
|
||||
Samples made available by the INA226 are retrieved by the BBB and stored in a
|
||||
buffer which is sent back to the host once it is full (see
|
||||
``buffer_samples_count`` in :meth:`BaylibreAcmeInstrument.setup` for setting
|
||||
its size). Therefore, the larger the buffer is, the longer it takes to be
|
||||
``buffer_samples_count`` in :meth:`BaylibreAcmeInstrument.setup
|
||||
<devlib.instrument.baylibre_acme.BaylibreAcmeInstrument.setup>` for setting its
|
||||
size). Therefore, the larger the buffer is, the longer it takes to be
|
||||
transmitted back but the less often it has to be transmitted. To illustrate
|
||||
this, consider the following graphs showing the time difference between
|
||||
successive samples in a retrieved signal when the size of the buffer changes:
|
||||
@@ -624,6 +633,8 @@ given by `libiio (the Linux IIO interface)`_ however only the network-based one
|
||||
has been tested. For the other classes, please refer to the official IIO
|
||||
documentation for the meaning of their constructor parameters.
|
||||
|
||||
.. module:: devlib.instrument.baylibre_acme
|
||||
|
||||
.. class:: BaylibreAcmeInstrument(target=None, iio_context=None, use_base_iio_context=False, probe_names=None)
|
||||
|
||||
Base class wrapper for the ACME instrument which itself is a wrapper for the
|
||||
|
@@ -1,11 +1,13 @@
|
||||
.. module:: devlib.module
|
||||
|
||||
.. _modules:
|
||||
|
||||
Modules
|
||||
=======
|
||||
|
||||
Modules add additional functionality to the core :class:`Target` interface.
|
||||
Usually, it is support for specific subsystems on the target. Modules are
|
||||
instantiated as attributes of the :class:`Target` instance.
|
||||
Modules add additional functionality to the core :class:`~devlib.target.Target`
|
||||
interface. Usually, it is support for specific subsystems on the target. Modules
|
||||
are instantiated as attributes of the :class:`~devlib.target.Target` instance.
|
||||
|
||||
hotplug
|
||||
-------
|
||||
@@ -28,6 +30,8 @@ interface to this subsystem
|
||||
# Make sure all cpus are online
|
||||
target.hotplug.online_all()
|
||||
|
||||
.. module:: devlib.module.cpufreq
|
||||
|
||||
cpufreq
|
||||
-------
|
||||
|
||||
@@ -132,6 +136,9 @@ policies (governors). The ``devlib`` module exposes the following interface
|
||||
``1`` or ``"cpu1"``).
|
||||
:param frequency: Frequency to set.
|
||||
|
||||
|
||||
.. module:: devlib.module.cupidle
|
||||
|
||||
cpuidle
|
||||
-------
|
||||
|
||||
@@ -167,11 +174,15 @@ cpuidle
|
||||
You can also call ``enable()`` or ``disable()`` on :class:`CpuidleState` objects
|
||||
returned by get_state(s).
|
||||
|
||||
.. module:: devlib.module.cgroups
|
||||
|
||||
cgroups
|
||||
-------
|
||||
|
||||
TODO
|
||||
|
||||
.. module:: devlib.module.hwmon
|
||||
|
||||
hwmon
|
||||
-----
|
||||
|
||||
@@ -187,8 +198,8 @@ Modules implement discrete, optional pieces of functionality ("optional" in the
|
||||
sense that the functionality may or may not be present on the target device, or
|
||||
that it may or may not be necessary for a particular application).
|
||||
|
||||
Every module (ultimately) derives from :class:`Module` class. A module must
|
||||
define the following class attributes:
|
||||
Every module (ultimately) derives from :class:`devlib.module.Module` class. A
|
||||
module must define the following class attributes:
|
||||
|
||||
:name: A unique name for the module. This cannot clash with any of the existing
|
||||
names and must be a valid Python identifier, but is otherwise free-form.
|
||||
@@ -204,14 +215,16 @@ define the following class attributes:
|
||||
which case the module's ``name`` will be treated as its
|
||||
``kind`` as well.
|
||||
|
||||
:stage: This defines when the module will be installed into a :class:`Target`.
|
||||
Currently, the following values are allowed:
|
||||
:stage: This defines when the module will be installed into a
|
||||
:class:`~devlib.target.Target`. Currently, the following values are
|
||||
allowed:
|
||||
|
||||
:connected: The module is installed after a connection to the target has
|
||||
been established. This is the default.
|
||||
:early: The module will be installed when a :class:`Target` is first
|
||||
created. This should be used for modules that do not rely on a
|
||||
live connection to the target.
|
||||
:early: The module will be installed when a
|
||||
:class:`~devlib.target.Target` is first created. This should be
|
||||
used for modules that do not rely on a live connection to the
|
||||
target.
|
||||
:setup: The module will be installed after initial setup of the device
|
||||
has been performed. This allows the module to utilize assets
|
||||
deployed during the setup stage for example 'Busybox'.
|
||||
@@ -220,8 +233,8 @@ Additionally, a module must implement a static (or class) method :func:`probe`:
|
||||
|
||||
.. method:: Module.probe(target)
|
||||
|
||||
This method takes a :class:`Target` instance and returns ``True`` if this
|
||||
module is supported by that target, or ``False`` otherwise.
|
||||
This method takes a :class:`~devlib.target.Target` instance and returns
|
||||
``True`` if this module is supported by that target, or ``False`` otherwise.
|
||||
|
||||
.. note:: If the module ``stage`` is ``"early"``, this method cannot assume
|
||||
that a connection has been established (i.e. it can only access
|
||||
@@ -231,9 +244,9 @@ Installation and invocation
|
||||
***************************
|
||||
|
||||
The default installation method will create an instance of a module (the
|
||||
:class:`Target` instance being the sole argument) and assign it to the target
|
||||
instance attribute named after the module's ``kind`` (or ``name`` if ``kind`` is
|
||||
``None``).
|
||||
:class:`~devlib.target.Target` instance being the sole argument) and assign it
|
||||
to the target instance attribute named after the module's ``kind`` (or
|
||||
``name`` if ``kind`` is ``None``).
|
||||
|
||||
It is possible to change the installation procedure for a module by overriding
|
||||
the default :func:`install` method. The method must have the following
|
||||
@@ -344,10 +357,11 @@ FlashModule
|
||||
Module Registration
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Modules are specified on :class:`Target` or :class:`Platform` creation by name.
|
||||
In order to find the class associated with the name, the module needs to be
|
||||
registered with ``devlib``. This is accomplished by passing the module class
|
||||
into :func:`register_module` method once it is defined.
|
||||
Modules are specified on :class:`~devlib.target.Target` or
|
||||
:class:`~devlib.platform.Platform` creation by name. In order to find the class
|
||||
associated with the name, the module needs to be registered with ``devlib``.
|
||||
This is accomplished by passing the module class into :func:`register_module`
|
||||
method once it is defined.
|
||||
|
||||
.. note:: If you're wiring a module to be included as part of ``devlib`` code
|
||||
base, you can place the file with the module class under
|
||||
|
@@ -1,25 +1,26 @@
|
||||
Overview
|
||||
========
|
||||
|
||||
A :class:`Target` instance serves as the main interface to the target device.
|
||||
A :class:`~devlib.target.Target` instance serves as the main interface to the target device.
|
||||
There are currently four target interfaces:
|
||||
|
||||
- :class:`LinuxTarget` for interacting with Linux devices over SSH.
|
||||
- :class:`AndroidTarget` for interacting with Android devices over adb.
|
||||
- :class:`ChromeOsTarget`: for interacting with ChromeOS devices over SSH, and their Android containers over adb.
|
||||
- :class:`LocalLinuxTarget`: for interacting with the local Linux host.
|
||||
- :class:`~devlib.target.LinuxTarget` for interacting with Linux devices over SSH.
|
||||
- :class:`~devlib.target.AndroidTarget` for interacting with Android devices over adb.
|
||||
- :class:`~devlib.target.ChromeOsTarget`: for interacting with ChromeOS devices
|
||||
over SSH, and their Android containers over adb.
|
||||
- :class:`~devlib.target.LocalLinuxTarget`: for interacting with the local Linux host.
|
||||
|
||||
They all work in more-or-less the same way, with the major difference being in
|
||||
how connection settings are specified; though there may also be a few APIs
|
||||
specific to a particular target type (e.g. :class:`AndroidTarget` exposes
|
||||
methods for working with logcat).
|
||||
specific to a particular target type (e.g. :class:`~devlib.target.AndroidTarget`
|
||||
exposes methods for working with logcat).
|
||||
|
||||
|
||||
Acquiring a Target
|
||||
------------------
|
||||
|
||||
To create an interface to your device, you just need to instantiate one of the
|
||||
:class:`Target` derivatives listed above, and pass it the right
|
||||
:class:`~devlib.target.Target` derivatives listed above, and pass it the right
|
||||
``connection_settings``. Code snippet below gives a typical example of
|
||||
instantiating each of the three target types.
|
||||
|
||||
@@ -46,21 +47,22 @@ instantiating each of the three target types.
|
||||
t3 = AndroidTarget(connection_settings={'device': '0123456789abcde'})
|
||||
|
||||
Instantiating a target may take a second or two as the remote device will be
|
||||
queried to initialize :class:`Target`'s internal state. If you would like to
|
||||
create a :class:`Target` instance but not immediately connect to the remote
|
||||
device, you can pass ``connect=False`` parameter. If you do that, you would have
|
||||
to then explicitly call ``t.connect()`` before you can interact with the device.
|
||||
queried to initialize :class:`~devlib.target.Target`'s internal state. If you
|
||||
would like to create a :class:`~devlib.target.Target` instance but not
|
||||
immediately connect to the remote device, you can pass ``connect=False``
|
||||
parameter. If you do that, you would have to then explicitly call
|
||||
``t.connect()`` before you can interact with the device.
|
||||
|
||||
There are a few additional parameters you can pass in instantiation besides
|
||||
``connection_settings``, but they are usually unnecessary. Please see
|
||||
:class:`Target` API documentation for more details.
|
||||
:class:`~devlib.target.Target` API documentation for more details.
|
||||
|
||||
Target Interface
|
||||
----------------
|
||||
|
||||
This is a quick overview of the basic interface to the device. See
|
||||
:class:`Target` API documentation for the full list of supported methods and
|
||||
more detailed documentation.
|
||||
:class:`~devlib.target.Target` API documentation for the full list of supported
|
||||
methods and more detailed documentation.
|
||||
|
||||
One-time Setup
|
||||
~~~~~~~~~~~~~~
|
||||
@@ -166,15 +168,16 @@ Process Control
|
||||
# PsEntry records.
|
||||
entries = t.ps()
|
||||
# e.g. print virtual memory sizes of all running sshd processes:
|
||||
print ', '.join(str(e.vsize) for e in entries if e.name == 'sshd')
|
||||
print(', '.join(str(e.vsize) for e in entries if e.name == 'sshd'))
|
||||
|
||||
|
||||
More...
|
||||
~~~~~~~
|
||||
|
||||
As mentioned previously, the above is not intended to be exhaustive
|
||||
documentation of the :class:`Target` interface. Please refer to the API
|
||||
documentation for the full list of attributes and methods and their parameters.
|
||||
documentation of the :class:`~devlib.target.Target` interface. Please refer to
|
||||
the API documentation for the full list of attributes and methods and their
|
||||
parameters.
|
||||
|
||||
Super User Privileges
|
||||
---------------------
|
||||
@@ -238,6 +241,8 @@ complete. Retrying it or bailing out is therefore a responsability of the caller
|
||||
|
||||
The hierarchy is as follows:
|
||||
|
||||
.. module:: devlib.exception
|
||||
|
||||
- :class:`DevlibError`
|
||||
|
||||
- :class:`WorkerThreadError`
|
||||
@@ -287,7 +292,7 @@ Modules
|
||||
Additional functionality is exposed via modules. Modules are initialized as
|
||||
attributes of a target instance. By default, ``hotplug``, ``cpufreq``,
|
||||
``cpuidle``, ``cgroups`` and ``hwmon`` will attempt to load on target; additional
|
||||
modules may be specified when creating a :class:`Target` instance.
|
||||
modules may be specified when creating a :class:`~devlib.target.Target` instance.
|
||||
|
||||
A module will probe the target for support before attempting to load. So if the
|
||||
underlying platform does not support particular functionality (e.g. the kernel
|
||||
|
@@ -1,14 +1,17 @@
|
||||
.. module:: devlib.platform
|
||||
|
||||
.. _platform:
|
||||
|
||||
Platform
|
||||
========
|
||||
|
||||
:class:`Platform`\ s describe the system underlying the OS. They encapsulate
|
||||
hardware- and firmware-specific details. In most cases, the generic
|
||||
:class:`Platform` class, which gets used if a platform is not explicitly
|
||||
specified on :class:`Target` creation, will be sufficient. It will automatically
|
||||
query as much platform information (such CPU topology, hardware model, etc) if
|
||||
it was not specified explicitly by the user.
|
||||
:class:`~devlib.platform.Platform`\ s describe the system underlying the OS.
|
||||
They encapsulate hardware- and firmware-specific details. In most cases, the
|
||||
generic :class:`~devlib.platform.Platform` class, which gets used if a
|
||||
platform is not explicitly specified on :class:`~devlib.target.Target`
|
||||
creation, will be sufficient. It will automatically query as much platform
|
||||
information (such CPU topology, hardware model, etc) if it was not specified
|
||||
explicitly by the user.
|
||||
|
||||
|
||||
.. class:: Platform(name=None, core_names=None, core_clusters=None,\
|
||||
@@ -31,6 +34,7 @@ it was not specified explicitly by the user.
|
||||
platform (e.g. for handling flashing, rebooting, etc). These
|
||||
would be added to the Target's modules. (See :ref:`modules`\ ).
|
||||
|
||||
.. module:: devlib.platform.arm
|
||||
|
||||
Versatile Express
|
||||
-----------------
|
||||
@@ -38,8 +42,8 @@ Versatile Express
|
||||
The generic platform may be extended to support hardware- or
|
||||
infrastructure-specific functionality. Platforms exist for ARM
|
||||
VersatileExpress-based :class:`Juno` and :class:`TC2` development boards. In
|
||||
addition to the standard :class:`Platform` parameters above, these platforms
|
||||
support additional configuration:
|
||||
addition to the standard :class:`~devlib.platform.Platform` parameters above,
|
||||
these platforms support additional configuration:
|
||||
|
||||
|
||||
.. class:: VersatileExpressPlatform
|
||||
@@ -116,43 +120,53 @@ support additional configuration:
|
||||
Gem5 Simulation Platform
|
||||
------------------------
|
||||
|
||||
By initialising a Gem5SimulationPlatform, devlib will start a gem5 simulation (based upon the
|
||||
arguments the user provided) and then connect to it using :class:`Gem5Connection`.
|
||||
Using the methods discussed above, some methods of the :class:`Target` will be altered
|
||||
slightly to better suit gem5.
|
||||
By initialising a Gem5SimulationPlatform, devlib will start a gem5 simulation
|
||||
(based upon the arguments the user provided) and then connect to it using
|
||||
:class:`~devlib.utils.ssh.Gem5Connection`. Using the methods discussed above,
|
||||
some methods of the :class:`~devlib.target.Target` will be altered slightly to
|
||||
better suit gem5.
|
||||
|
||||
.. module:: devlib.platform.gem5
|
||||
|
||||
.. class:: Gem5SimulationPlatform(name, host_output_dir, gem5_bin, gem5_args, gem5_virtio, gem5_telnet_port=None)
|
||||
|
||||
During initialisation the gem5 simulation will be kicked off (based upon the arguments
|
||||
provided by the user) and the telnet port used by the gem5 simulation will be intercepted
|
||||
and stored for use by the :class:`Gem5Connection`.
|
||||
During initialisation the gem5 simulation will be kicked off (based upon the
|
||||
arguments provided by the user) and the telnet port used by the gem5
|
||||
simulation will be intercepted and stored for use by the
|
||||
:class:`~devlib.utils.ssh.Gem5Connection`.
|
||||
|
||||
:param name: Platform name
|
||||
|
||||
:param host_output_dir: Path on the host where the gem5 outputs will be placed (e.g. stats file)
|
||||
:param host_output_dir: Path on the host where the gem5 outputs will be
|
||||
placed (e.g. stats file)
|
||||
|
||||
:param gem5_bin: gem5 binary
|
||||
|
||||
:param gem5_args: Arguments to be passed onto gem5 such as config file etc.
|
||||
|
||||
:param gem5_virtio: Arguments to be passed onto gem5 in terms of the virtIO device used
|
||||
to transfer files between the host and the gem5 simulated system.
|
||||
:param gem5_virtio: Arguments to be passed onto gem5 in terms of the virtIO
|
||||
device used to transfer files between the host and the gem5 simulated
|
||||
system.
|
||||
|
||||
:param gem5_telnet_port: Not yet in use as it would be used in future implementations
|
||||
of devlib in which the user could use the platform to pick
|
||||
up an existing and running simulation.
|
||||
:param gem5_telnet_port: Not yet in use as it would be used in future
|
||||
implementations of devlib in which the user could
|
||||
use the platform to pick up an existing and running
|
||||
simulation.
|
||||
|
||||
|
||||
.. method:: Gem5SimulationPlatform.init_target_connection([target])
|
||||
|
||||
Based upon the OS defined in the :class:`Target`, the type of :class:`Gem5Connection`
|
||||
will be set (:class:`AndroidGem5Connection` or :class:`AndroidGem5Connection`).
|
||||
Based upon the OS defined in the :class:`~devlib.target.Target`, the type of
|
||||
:class:`~devlib.utils.ssh.Gem5Connection` will be set
|
||||
(:class:`~devlib.utils.ssh.AndroidGem5Connection` or
|
||||
:class:`~devlib.utils.ssh.AndroidGem5Connection`).
|
||||
|
||||
.. method:: Gem5SimulationPlatform.update_from_target([target])
|
||||
|
||||
This method provides specific setup procedures for a gem5 simulation. First of all, the m5
|
||||
binary will be installed on the guest (if it is not present). Secondly, three methods
|
||||
in the :class:`Target` will be monkey-patched:
|
||||
This method provides specific setup procedures for a gem5 simulation. First
|
||||
of all, the m5 binary will be installed on the guest (if it is not present).
|
||||
Secondly, three methods in the :class:`~devlib.target.Target` will be
|
||||
monkey-patched:
|
||||
|
||||
- **reboot**: this is not supported in gem5
|
||||
- **reset**: this is not supported in gem5
|
||||
@@ -160,7 +174,7 @@ slightly to better suit gem5.
|
||||
monkey-patched method will first try to
|
||||
transfer the existing screencaps.
|
||||
In case that does not work, it will fall back
|
||||
to the original :class:`Target` implementation
|
||||
to the original :class:`~devlib.target.Target` implementation
|
||||
of :func:`capture_screen`.
|
||||
|
||||
Finally, it will call the parent implementation of :func:`update_from_target`.
|
||||
|
237
doc/target.rst
237
doc/target.rst
@@ -1,57 +1,62 @@
|
||||
.. module:: devlib.target
|
||||
|
||||
Target
|
||||
======
|
||||
|
||||
|
||||
.. class:: Target(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=None)
|
||||
|
||||
:class:`Target` is the primary interface to the remote device. All interactions
|
||||
with the device are performed via a :class:`Target` instance, either
|
||||
directly, or via its modules or a wrapper interface (such as an
|
||||
:class:`Instrument`).
|
||||
:class:`~devlib.target.Target` is the primary interface to the remote
|
||||
device. All interactions with the device are performed via a
|
||||
:class:`~devlib.target.Target` instance, either directly, or via its
|
||||
modules or a wrapper interface (such as an
|
||||
:class:`~devlib.instrument.Instrument`).
|
||||
|
||||
:param connection_settings: A ``dict`` that specifies how to connect to the remote
|
||||
device. Its contents depend on the specific :class:`Target` type (used see
|
||||
:param connection_settings: A ``dict`` that specifies how to connect to the
|
||||
remote device. Its contents depend on the specific
|
||||
:class:`~devlib.target.Target` type (used see
|
||||
:ref:`connection-types`\ ).
|
||||
|
||||
:param platform: A :class:`Target` defines interactions at Operating System level. A
|
||||
:class:`Platform` describes the underlying hardware (such as CPUs
|
||||
available). If a :class:`Platform` instance is not specified on
|
||||
:class:`Target` creation, one will be created automatically and it will
|
||||
dynamically probe the device to discover as much about the underlying
|
||||
hardware as it can. See also :ref:`platform`\ .
|
||||
:param platform: A :class:`~devlib.target.Target` defines interactions at
|
||||
Operating System level. A :class:`~devlib.platform.Platform` describes
|
||||
the underlying hardware (such as CPUs available). If a
|
||||
:class:`~devlib.platform.Platform` instance is not specified on
|
||||
:class:`~devlib.target.Target` creation, one will be created
|
||||
automatically and it will dynamically probe the device to discover
|
||||
as much about the underlying hardware as it can. See also
|
||||
:ref:`platform`\ .
|
||||
|
||||
:param working_directory: This is primary location for on-target file system
|
||||
interactions performed by ``devlib``. This location *must* be readable and
|
||||
writable directly (i.e. without sudo) by the connection's user account.
|
||||
It may or may not allow execution. This location will be created,
|
||||
if necessary, during ``setup()``.
|
||||
interactions performed by ``devlib``. This location *must* be readable
|
||||
and writable directly (i.e. without sudo) by the connection's user
|
||||
account. It may or may not allow execution. This location will be
|
||||
created, if necessary, during :meth:`setup()`.
|
||||
|
||||
If not explicitly specified, this will be set to a default value
|
||||
depending on the type of :class:`Target`
|
||||
depending on the type of :class:`~devlib.target.Target`
|
||||
|
||||
:param executables_directory: This is the location to which ``devlib`` will
|
||||
install executable binaries (either during ``setup()`` or via an
|
||||
explicit ``install()`` call). This location *must* support execution
|
||||
install executable binaries (either during :meth:`setup()` or via an
|
||||
explicit :meth:`install()` call). This location *must* support execution
|
||||
(obviously). It should also be possible to write to this location,
|
||||
possibly with elevated privileges (i.e. on a rooted Linux target, it
|
||||
should be possible to write here with sudo, but not necessarily directly
|
||||
by the connection's account). This location will be created,
|
||||
if necessary, during ``setup()``.
|
||||
by the connection's account). This location will be created, if
|
||||
necessary, during :meth:`setup()`.
|
||||
|
||||
This location does *not* need to be same as the system's executables
|
||||
location. In fact, to prevent devlib from overwriting system's defaults,
|
||||
it better if this is a separate location, if possible.
|
||||
|
||||
If not explicitly specified, this will be set to a default value
|
||||
depending on the type of :class:`Target`
|
||||
depending on the type of :class:`~devlib.target.Target`
|
||||
|
||||
:param connect: Specifies whether a connections should be established to the
|
||||
target. If this is set to ``False``, then ``connect()`` must be
|
||||
explicitly called later on before the :class:`Target` instance can be
|
||||
used.
|
||||
target. If this is set to ``False``, then :meth:`connect()` must be
|
||||
explicitly called later on before the :class:`~devlib.target.Target`
|
||||
instance can be used.
|
||||
|
||||
:param modules: a list of additional modules to be installed. Some modules will
|
||||
try to install by default (if supported by the underlying target).
|
||||
:param modules: a list of additional modules to be installed. Some modules
|
||||
will try to install by default (if supported by the underlying target).
|
||||
Current default modules are ``hotplug``, ``cpufreq``, ``cpuidle``,
|
||||
``cgroups``, and ``hwmon`` (See :ref:`modules`\ ).
|
||||
|
||||
@@ -59,40 +64,40 @@ Target
|
||||
|
||||
:param load_default_modules: If set to ``False``, default modules listed
|
||||
above will *not* attempt to load. This may be used to either speed up
|
||||
target instantiation (probing for initializing modules takes a bit of time)
|
||||
or if there is an issue with one of the modules on a particular device
|
||||
(the rest of the modules will then have to be explicitly specified in
|
||||
the ``modules``).
|
||||
target instantiation (probing for initializing modules takes a bit of
|
||||
time) or if there is an issue with one of the modules on a particular
|
||||
device (the rest of the modules will then have to be explicitly
|
||||
specified in the ``modules``).
|
||||
|
||||
:param shell_prompt: This is a regular expression that matches the shell
|
||||
prompted on the target. This may be used by some modules that establish
|
||||
auxiliary connections to a target over UART.
|
||||
|
||||
:param conn_cls: This is the type of connection that will be used to communicate
|
||||
with the device.
|
||||
:param conn_cls: This is the type of connection that will be used to
|
||||
communicate with the device.
|
||||
|
||||
.. attribute:: Target.core_names
|
||||
|
||||
This is a list containing names of CPU cores on the target, in the order in
|
||||
which they are index by the kernel. This is obtained via the underlying
|
||||
:class:`Platform`.
|
||||
:class:`~devlib.platform.Platform`.
|
||||
|
||||
.. attribute:: Target.core_clusters
|
||||
|
||||
Some devices feature heterogeneous core configurations (such as ARM
|
||||
big.LITTLE). This is a list that maps CPUs onto underlying clusters.
|
||||
(Usually, but not always, clusters correspond to groups of CPUs with the same
|
||||
name). This is obtained via the underlying :class:`Platform`.
|
||||
name). This is obtained via the underlying :class:`~devlib.platform.Platform`.
|
||||
|
||||
.. attribute:: Target.big_core
|
||||
|
||||
This is the name of the cores that are the "big"s in an ARM big.LITTLE
|
||||
configuration. This is obtained via the underlying :class:`Platform`.
|
||||
configuration. This is obtained via the underlying :class:`~devlib.platform.Platform`.
|
||||
|
||||
.. attribute:: Target.little_core
|
||||
|
||||
This is the name of the cores that are the "little"s in an ARM big.LITTLE
|
||||
configuration. This is obtained via the underlying :class:`Platform`.
|
||||
configuration. This is obtained via the underlying :class:`~devlib.platform.Platform`.
|
||||
|
||||
.. attribute:: Target.is_connected
|
||||
|
||||
@@ -152,11 +157,11 @@ Target
|
||||
|
||||
The underlying connection object. This will be ``None`` if an active
|
||||
connection does not exist (e.g. if ``connect=False`` as passed on
|
||||
initialization and ``connect()`` has not been called).
|
||||
initialization and :meth:`connect()` has not been called).
|
||||
|
||||
.. note:: a :class:`Target` will automatically create a connection per
|
||||
thread. This will always be set to the connection for the current
|
||||
thread.
|
||||
.. note:: a :class:`~devlib.target.Target` will automatically create a
|
||||
connection per thread. This will always be set to the connection
|
||||
for the current thread.
|
||||
|
||||
.. method:: Target.connect([timeout])
|
||||
|
||||
@@ -176,19 +181,20 @@ Target
|
||||
being executed.
|
||||
|
||||
This should *not* be used to establish an initial connection; use
|
||||
``connect()`` instead.
|
||||
:meth:`connect()` instead.
|
||||
|
||||
.. note:: :class:`Target` will automatically create a connection per
|
||||
thread, so you don't normally need to use this explicitly in
|
||||
.. note:: :class:`~devlib.target.Target` will automatically create a connection
|
||||
per thread, so you don't normally need to use this explicitly in
|
||||
threaded code. This is generally useful if you want to perform a
|
||||
blocking operation (e.g. using ``background()``) while at the same
|
||||
blocking operation (e.g. using :class:`background()`) while at the same
|
||||
time doing something else in the same host-side thread.
|
||||
|
||||
.. method:: Target.setup([executables])
|
||||
|
||||
This will perform an initial one-time set up of a device for devlib
|
||||
interaction. This involves deployment of tools relied on the :class:`Target`,
|
||||
creation of working locations on the device, etc.
|
||||
interaction. This involves deployment of tools relied on the
|
||||
:class:`~devlib.target.Target`, creation of working locations on the device,
|
||||
etc.
|
||||
|
||||
Usually, it is enough to call this method once per new device, as its effects
|
||||
will persist across reboots. However, it is safe to call this method multiple
|
||||
@@ -212,25 +218,43 @@ Target
|
||||
operations during reboot process to detect if the reboot has failed and
|
||||
the device has hung.
|
||||
|
||||
.. method:: Target.push(source, dest [,as_root , timeout])
|
||||
.. method:: Target.push(source, dest [,as_root , timeout, globbing])
|
||||
|
||||
Transfer a file from the host machine to the target device.
|
||||
|
||||
:param source: path of to the file on the host
|
||||
:param dest: path of to the file on the 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).
|
||||
|
||||
: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.
|
||||
:param globbing: If ``True``, the ``source`` is interpreted as a globbing
|
||||
pattern instead of being take as-is. If the pattern has mulitple
|
||||
matches, ``dest`` must be a folder (or will be created as such if it
|
||||
does not exists yet).
|
||||
|
||||
.. method:: Target.pull(source, dest [, as_root, timeout])
|
||||
.. method:: Target.pull(source, dest [, as_root, timeout, globbing])
|
||||
|
||||
Transfer a file from the target device to the host machine.
|
||||
|
||||
:param source: path of to the file on the target
|
||||
:param dest: path of to the file on the host
|
||||
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).
|
||||
|
||||
:param source: path on the target
|
||||
:param dest: path on the host
|
||||
: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.
|
||||
:param globbing: If ``True``, the ``source`` is interpreted as a globbing
|
||||
pattern instead of being take as-is. If the pattern has mulitple
|
||||
matches, ``dest`` must be a folder (or will be created as such if it
|
||||
does not exists yet).
|
||||
|
||||
.. method:: Target.execute(command [, timeout [, check_exit_code [, as_root [, strip_colors [, will_succeed [, force_locale]]]]]])
|
||||
|
||||
@@ -281,31 +305,31 @@ Target
|
||||
a string.
|
||||
:param in_directory: execute the binary in the specified directory. This must
|
||||
be an absolute path.
|
||||
:param 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"``.
|
||||
:param 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"``.
|
||||
:param as_root: Specify whether the command should be run as root
|
||||
:param timeout: If this is specified and invocation does not terminate within this number
|
||||
of seconds, an exception will be raised.
|
||||
|
||||
.. method:: Target.background_invoke(binary [, args [, in_directory [, on_cpus [, as_root ]]]])
|
||||
|
||||
Execute the specified binary on target (must already be installed) as a background
|
||||
task, under the specified conditions and return the :class:`subprocess.Popen`
|
||||
instance for the command.
|
||||
Execute the specified binary on target (must already be installed) as a
|
||||
background task, under the specified conditions and return the
|
||||
:class:`subprocess.Popen` instance for the command.
|
||||
|
||||
:param binary: binary to execute. Must be present and executable on the device.
|
||||
:param args: arguments to be passed to the binary. The can be either a list or
|
||||
a string.
|
||||
:param in_directory: execute the binary in the specified directory. This must
|
||||
be an absolute path.
|
||||
:param 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"``.
|
||||
:param 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"``.
|
||||
:param as_root: Specify whether the command should be run as root
|
||||
|
||||
.. method:: Target.kick_off(command [, as_root])
|
||||
@@ -361,7 +385,7 @@ Target
|
||||
multiple files at once, leaving them in their original state on exit. If one
|
||||
write fails, all the already-performed writes will be reverted as well.
|
||||
|
||||
.. method:: Target.read_tree_values(path, depth=1, dictcls=dict, [, tar [, decode_unicode [, strip_null_char ]]]):
|
||||
.. method:: Target.read_tree_values(path, depth=1, dictcls=dict, [, tar [, decode_unicode [, strip_null_char ]]])
|
||||
|
||||
Read values of all sysfs (or similar) file nodes under ``path``, traversing
|
||||
up to the maximum depth ``depth``.
|
||||
@@ -386,7 +410,7 @@ Target
|
||||
:param decode_unicode: decode the content of tar-ed files as utf-8
|
||||
:param strip_null_char: remove null chars from utf-8 decoded files
|
||||
|
||||
.. method:: Target.read_tree_values_flat(path, depth=1):
|
||||
.. method:: Target.read_tree_values_flat(path, depth=1)
|
||||
|
||||
Read values of all sysfs (or similar) file nodes under ``path``, traversing
|
||||
up to the maximum depth ``depth``.
|
||||
@@ -430,6 +454,10 @@ Target
|
||||
Return a list of :class:`PsEntry` instances for all running processes on the
|
||||
system.
|
||||
|
||||
.. method:: Target.makedirs(self, path)
|
||||
|
||||
Create a directory at the given path and all its ancestors if needed.
|
||||
|
||||
.. method:: Target.file_exists(self, filepath)
|
||||
|
||||
Returns ``True`` if the specified path exists on the target and ``False``
|
||||
@@ -553,16 +581,35 @@ Target
|
||||
Installs an additional module to the target after the initial setup has been
|
||||
performed.
|
||||
|
||||
Linux Target
|
||||
------------
|
||||
|
||||
.. class:: LinuxTarget(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, is_container=False,)
|
||||
|
||||
:class:`LinuxTarget` is a subclass of :class:`~devlib.target.Target`
|
||||
with customisations specific to a device running linux.
|
||||
|
||||
|
||||
Local Linux Target
|
||||
------------------
|
||||
|
||||
.. class:: LocalLinuxTarget(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, is_container=False,)
|
||||
|
||||
:class:`LocalLinuxTarget` is a subclass of
|
||||
:class:`~devlib.target.LinuxTarget` with customisations specific to using
|
||||
the host machine running linux as the target.
|
||||
|
||||
|
||||
Android Target
|
||||
---------------
|
||||
|
||||
.. class:: AndroidTarget(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=AdbConnection, package_data_directory="/data/data")
|
||||
|
||||
:class:`AndroidTarget` is a subclass of :class:`Target` with additional features specific to a device running Android.
|
||||
:class:`AndroidTarget` is a subclass of :class:`~devlib.target.Target` with
|
||||
additional features specific to a device running Android.
|
||||
|
||||
:param package_data_directory: This is the location of the data stored
|
||||
for installed Android packages on the device.
|
||||
:param package_data_directory: This is the location of the data stored for
|
||||
installed Android packages on the device.
|
||||
|
||||
.. method:: AndroidTarget.set_rotation(rotation)
|
||||
|
||||
@@ -635,25 +682,57 @@ Android Target
|
||||
Returns ``True`` if the targets auto brightness is currently
|
||||
enabled and ``False`` otherwise.
|
||||
|
||||
.. method:: AndroidTarget.ensure_screen_is_off()
|
||||
.. method:: AndroidTarget.set_stay_on_never()
|
||||
|
||||
Sets the stay-on mode to ``0``, where the screen will turn off
|
||||
as standard after the timeout.
|
||||
|
||||
.. method:: AndroidTarget.set_stay_on_while_powered()
|
||||
|
||||
Sets the stay-on mode to ``7``, where the screen will stay on
|
||||
while the device is charging
|
||||
|
||||
.. method:: AndroidTarget.set_stay_on_mode(mode)
|
||||
|
||||
Sets the stay-on mode to the specified number between ``0`` and
|
||||
``7`` (inclusive).
|
||||
|
||||
.. method:: AndroidTarget.get_stay_on_mode()
|
||||
|
||||
Returns an integer between ``0`` and ``7`` representing the current
|
||||
stay-on mode of the device.
|
||||
|
||||
.. method:: AndroidTarget.ensure_screen_is_off(verify=True)
|
||||
|
||||
Checks if the devices screen is on and if so turns it off.
|
||||
If ``verify`` is set to ``True`` then a ``TargetStableError``
|
||||
will be raise if the display cannot be turned off. E.g. if
|
||||
always on mode is enabled.
|
||||
|
||||
.. method:: AndroidTarget.ensure_screen_is_on()
|
||||
.. method:: AndroidTarget.ensure_screen_is_on(verify=True)
|
||||
|
||||
Checks if the devices screen is off and if so turns it on.
|
||||
If ``verify`` is set to ``True`` then a ``TargetStableError``
|
||||
will be raise if the display cannot be turned on.
|
||||
|
||||
.. method:: AndroidTarget.ensure_screen_is_on_and_stays(verify=True, mode=7)
|
||||
|
||||
Calls ``AndroidTarget.ensure_screen_is_on(verify)`` then additionally
|
||||
sets the screen stay on mode to ``mode``.
|
||||
|
||||
.. method:: AndroidTarget.is_screen_on()
|
||||
|
||||
Returns ``True`` if the targets screen is currently on and ``False``
|
||||
otherwise.
|
||||
otherwise. If the display is in a "Doze" mode or similar always on state,
|
||||
this will return ``True``.
|
||||
|
||||
.. method:: AndroidTarget.wait_for_target(timeout=30)
|
||||
.. method:: AndroidTarget.wait_for_device(timeout=30)
|
||||
|
||||
Returns when the devices becomes available withing the given timeout
|
||||
otherwise returns a ``TimeoutError``.
|
||||
|
||||
.. method:: AndroidTarget.reboot_bootloader(timeout=30)
|
||||
|
||||
Attempts to reboot the target into it's bootloader.
|
||||
|
||||
.. method:: AndroidTarget.homescreen()
|
||||
@@ -687,9 +766,9 @@ ChromeOS Target
|
||||
:class:`ChromeOsTarget` if the device supports android otherwise only the
|
||||
:class:`LinuxTarget` methods will be available.
|
||||
|
||||
:param working_directory: This is the location of the working
|
||||
directory to be used for the Linux target container. If not specified will
|
||||
default to ``"/mnt/stateful_partition/devlib-target"``.
|
||||
:param working_directory: This is the location of the working directory to
|
||||
be used for the Linux target container. If not specified will default to
|
||||
``"/mnt/stateful_partition/devlib-target"``.
|
||||
|
||||
:param android_working_directory: This is the location of the working
|
||||
directory to be used for the android container. If not specified it will
|
||||
|
2
setup.py
2
setup.py
@@ -82,6 +82,8 @@ params = dict(
|
||||
'python-dateutil', # converting between UTC and local time.
|
||||
'pexpect>=3.3', # Send/recieve to/from device
|
||||
'pyserial', # Serial port interface
|
||||
'paramiko', # SSH connection
|
||||
'scp', # SSH connection file transfers
|
||||
'wrapt', # Basic for construction of decorator functions
|
||||
'future', # Python 2-3 compatibility
|
||||
'enum34;python_version<"3.4"', # Enums for Python < 3.4
|
||||
|
6
src/get_clock_boottime/Makefile
Normal file
6
src/get_clock_boottime/Makefile
Normal file
@@ -0,0 +1,6 @@
|
||||
CFLAGS=-Wall --pedantic-errors -O2 -static
|
||||
|
||||
all: get_clock_boottime
|
||||
|
||||
get_clock_boottime: get_clock_boottime.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
18
src/get_clock_boottime/get_clock_boottime.c
Normal file
18
src/get_clock_boottime/get_clock_boottime.c
Normal file
@@ -0,0 +1,18 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
int main(void) {
|
||||
int ret;
|
||||
struct timespec tp;
|
||||
|
||||
ret = clock_gettime(CLOCK_BOOTTIME, &tp);
|
||||
if (ret) {
|
||||
perror("clock_gettime()");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
printf("%ld.%ld\n", tp.tv_sec, tp.tv_nsec);
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
Reference in New Issue
Block a user