mirror of
https://github.com/ARM-software/devlib.git
synced 2025-01-31 02:00:45 +00:00
connections: Unify BackgroundCommand API and use paramiko for SSH
* Unify the behavior of background commands in connections.BackgroundCommand(). This implements a subset of subprocess.Popen class, with a unified behavior across all connection types * Implement the SSH connection using paramiko rather than pxssh.
This commit is contained in:
parent
eb6fa93845
commit
62e24c5764
351
devlib/connection.py
Normal file
351
devlib/connection.py
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
from weakref import WeakSet
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from devlib.utils.misc import InitCheckpoint
|
||||||
|
|
||||||
|
_KILL_TIMEOUT = 3
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_pgid_cmd(pgid, sig):
|
||||||
|
return 'kill -{} -{}'.format(sig.name, 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()
|
||||||
|
|
||||||
|
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
|
||||||
|
popen.send_signal(signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
popen.wait(timeout=_KILL_TIMEOUT)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
popen.kill()
|
||||||
|
|
||||||
|
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)
|
@ -24,6 +24,7 @@ from pipes import quote
|
|||||||
|
|
||||||
from devlib.exception import TargetTransientError, TargetStableError
|
from devlib.exception import TargetTransientError, TargetStableError
|
||||||
from devlib.utils.misc import check_output
|
from devlib.utils.misc import check_output
|
||||||
|
from devlib.connection import ConnectionBase, PopenBackgroundCommand
|
||||||
|
|
||||||
|
|
||||||
PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
|
PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
|
||||||
@ -37,7 +38,7 @@ def kill_children(pid, signal=signal.SIGKILL):
|
|||||||
os.kill(cpid, signal)
|
os.kill(cpid, signal)
|
||||||
|
|
||||||
|
|
||||||
class LocalConnection(object):
|
class LocalConnection(ConnectionBase):
|
||||||
|
|
||||||
name = 'local'
|
name = 'local'
|
||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
@ -56,6 +57,7 @@ class LocalConnection(object):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def __init__(self, platform=None, keep_password=True, unrooted=False,
|
def __init__(self, platform=None, keep_password=True, unrooted=False,
|
||||||
password=None, timeout=None):
|
password=None, timeout=None):
|
||||||
|
super().__init__()
|
||||||
self._connected_as_root = None
|
self._connected_as_root = None
|
||||||
self.logger = logging.getLogger('local_connection')
|
self.logger = logging.getLogger('local_connection')
|
||||||
self.keep_password = keep_password
|
self.keep_password = keep_password
|
||||||
@ -105,9 +107,24 @@ class LocalConnection(object):
|
|||||||
raise TargetStableError('unrooted')
|
raise TargetStableError('unrooted')
|
||||||
password = self._get_password()
|
password = self._get_password()
|
||||||
command = 'echo {} | sudo -S '.format(quote(password)) + command
|
command = 'echo {} | sudo -S '.format(quote(password)) + command
|
||||||
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
|
|
||||||
|
|
||||||
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
|
pass
|
||||||
|
|
||||||
def cancel_running_command(self):
|
def cancel_running_command(self):
|
||||||
|
@ -30,6 +30,7 @@ from collections import defaultdict
|
|||||||
import pexpect
|
import pexpect
|
||||||
import xml.etree.ElementTree
|
import xml.etree.ElementTree
|
||||||
import zipfile
|
import zipfile
|
||||||
|
import uuid
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
@ -37,7 +38,8 @@ except ImportError:
|
|||||||
from pipes import quote
|
from pipes import quote
|
||||||
|
|
||||||
from devlib.exception import TargetTransientError, TargetStableError, HostError
|
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
|
||||||
|
from devlib.connection import ConnectionBase, AdbBackgroundCommand
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('android')
|
logger = logging.getLogger('android')
|
||||||
@ -233,7 +235,7 @@ class ApkInfo(object):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
class AdbConnection(object):
|
class AdbConnection(ConnectionBase):
|
||||||
|
|
||||||
# maintains the count of parallel active connections to a device, so that
|
# maintains the count of parallel active connections to a device, so that
|
||||||
# adb disconnect is not invoked untill all connections are closed
|
# adb disconnect is not invoked untill all connections are closed
|
||||||
@ -263,6 +265,7 @@ class AdbConnection(object):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def __init__(self, device=None, timeout=None, platform=None, adb_server=None,
|
def __init__(self, device=None, timeout=None, platform=None, adb_server=None,
|
||||||
adb_as_root=False):
|
adb_as_root=False):
|
||||||
|
super().__init__()
|
||||||
self.timeout = timeout if timeout is not None else self.default_timeout
|
self.timeout = timeout if timeout is not None else self.default_timeout
|
||||||
if device is None:
|
if device is None:
|
||||||
device = adb_get_device(timeout=timeout, adb_server=adb_server)
|
device = adb_get_device(timeout=timeout, adb_server=adb_server)
|
||||||
@ -312,13 +315,21 @@ class AdbConnection(object):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
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)
|
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
|
||||||
|
)
|
||||||
|
self._current_bg_cmds.add(bg_cmd)
|
||||||
|
return bg_cmd
|
||||||
|
|
||||||
def close(self):
|
def _close(self):
|
||||||
AdbConnection.active_connections[self.device] -= 1
|
AdbConnection.active_connections[self.device] -= 1
|
||||||
if AdbConnection.active_connections[self.device] <= 0:
|
if AdbConnection.active_connections[self.device] <= 0:
|
||||||
if self.adb_as_root:
|
if self.adb_as_root:
|
||||||
self.adb_root(self.device, enable=False)
|
self.adb_root(enable=False)
|
||||||
adb_disconnect(self.device, self.adb_server)
|
adb_disconnect(self.device, self.adb_server)
|
||||||
del AdbConnection.active_connections[self.device]
|
del AdbConnection.active_connections[self.device]
|
||||||
|
|
||||||
@ -536,20 +547,41 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def adb_background_shell(device, command,
|
def adb_background_shell(conn, command,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
as_root=False,
|
as_root=False):
|
||||||
adb_server=None):
|
|
||||||
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
|
"""Runs the sepcified command in a subprocess, returning the the Popen object."""
|
||||||
|
device = conn.device
|
||||||
|
adb_server = conn.adb_server
|
||||||
|
|
||||||
_check_env()
|
_check_env()
|
||||||
|
stdout, stderr, command = redirect_streams(stdout, stderr, command)
|
||||||
if as_root:
|
if as_root:
|
||||||
command = 'echo {} | su'.format(quote(command))
|
command = 'echo {} | su'.format(quote(command))
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
|
||||||
adb_cmd = get_adb_command(None, 'shell', adb_server)
|
adb_cmd = get_adb_command(None, 'shell', adb_server)
|
||||||
full_command = '{} {}'.format(adb_cmd, quote(command))
|
full_command = '{} {}'.format(adb_cmd, quote(command))
|
||||||
logger.debug(full_command)
|
logger.debug(full_command)
|
||||||
return subprocess.Popen(full_command, stdout=stdout, stderr=stderr, shell=True)
|
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):
|
def adb_kill_server(timeout=30, adb_server=None):
|
||||||
adb_command(None, 'kill-server', timeout, adb_server)
|
adb_command(None, 'kill-server', timeout, adb_server)
|
||||||
|
@ -20,7 +20,7 @@ Miscellaneous functions that don't fit anywhere else.
|
|||||||
"""
|
"""
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import partial, reduce
|
from functools import partial, reduce, wraps
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from weakref import WeakKeyDictionary, WeakSet
|
from weakref import WeakKeyDictionary, WeakSet
|
||||||
@ -854,3 +854,50 @@ class _BoundTLSProperty:
|
|||||||
instance=self.instance,
|
instance=self.instance,
|
||||||
owner=self.owner,
|
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
|
||||||
|
@ -26,9 +26,18 @@ import socket
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import atexit
|
import atexit
|
||||||
|
import contextlib
|
||||||
|
import weakref
|
||||||
|
import select
|
||||||
|
import copy
|
||||||
from pipes import quote
|
from pipes import quote
|
||||||
from future.utils import raise_from
|
from future.utils import raise_from
|
||||||
|
|
||||||
|
from paramiko.client import SSHClient, AutoAddPolicy, RejectPolicy
|
||||||
|
import paramiko.ssh_exception
|
||||||
|
# By default paramiko is very verbose, including at the INFO level
|
||||||
|
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
||||||
|
|
||||||
# pylint: disable=import-error,wrong-import-position,ungrouped-imports,wrong-import-order
|
# pylint: disable=import-error,wrong-import-position,ungrouped-imports,wrong-import-order
|
||||||
import pexpect
|
import pexpect
|
||||||
from distutils.version import StrictVersion as V
|
from distutils.version import StrictVersion as V
|
||||||
@ -42,8 +51,9 @@ from pexpect import EOF, TIMEOUT, spawn
|
|||||||
from devlib.exception import (HostError, TargetStableError, TargetNotRespondingError,
|
from devlib.exception import (HostError, TargetStableError, TargetNotRespondingError,
|
||||||
TimeoutError, TargetTransientError)
|
TimeoutError, TargetTransientError)
|
||||||
from devlib.utils.misc import (which, strip_bash_colors, check_output,
|
from devlib.utils.misc import (which, strip_bash_colors, check_output,
|
||||||
sanitize_cmd_template, memoized)
|
sanitize_cmd_template, memoized, redirect_streams)
|
||||||
from devlib.utils.types import boolean
|
from devlib.utils.types import boolean
|
||||||
|
from devlib.connection import ConnectionBase, ParamikoBackgroundCommand, PopenBackgroundCommand
|
||||||
|
|
||||||
|
|
||||||
ssh = None
|
ssh = None
|
||||||
@ -54,30 +64,112 @@ sshpass = None
|
|||||||
logger = logging.getLogger('ssh')
|
logger = logging.getLogger('ssh')
|
||||||
gem5_logger = logging.getLogger('gem5-connection')
|
gem5_logger = logging.getLogger('gem5-connection')
|
||||||
|
|
||||||
def ssh_get_shell(host,
|
@contextlib.contextmanager
|
||||||
|
def _handle_paramiko_exceptions(command=None):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except paramiko.ssh_exception.NoValidConnectionsError as e:
|
||||||
|
raise TargetNotRespondingError('Connection lost: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.AuthenticationException as e:
|
||||||
|
raise TargetStableError('Could not authenticate: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.BadAuthenticationType as e:
|
||||||
|
raise TargetStableError('Bad authentication type: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.BadHostKeyException as e:
|
||||||
|
raise TargetStableError('Bad host key: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.ChannelException as e:
|
||||||
|
raise TargetStableError('Could not open an SSH channel: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.PasswordRequiredException as e:
|
||||||
|
raise TargetStableError('Please unlock the private key file: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.ProxyCommandFailure as e:
|
||||||
|
raise TargetStableError('Proxy command failure: {}'.format(e))
|
||||||
|
except paramiko.ssh_exception.SSHException as e:
|
||||||
|
raise TargetTransientError('SSH logic error: {}'.format(e))
|
||||||
|
except socket.timeout:
|
||||||
|
raise TimeoutError(command, output=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_paramiko_streams(stdout, stderr, select_timeout, callback, init, chunk_size=int(1e42)):
|
||||||
|
try:
|
||||||
|
return _read_paramiko_streams_internal(stdout, stderr, select_timeout, callback, init, chunk_size)
|
||||||
|
finally:
|
||||||
|
# Close the channel to make sure the remove process will receive
|
||||||
|
# SIGPIPE when writing on its streams. That could happen if the
|
||||||
|
# user closed the out_streams but the remote process has not
|
||||||
|
# finished yet.
|
||||||
|
assert stdout.channel is stderr.channel
|
||||||
|
stdout.channel.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _read_paramiko_streams_internal(stdout, stderr, select_timeout, callback, init, chunk_size):
|
||||||
|
channel = stdout.channel
|
||||||
|
assert stdout.channel is stderr.channel
|
||||||
|
|
||||||
|
def read_channel(callback_state):
|
||||||
|
read_list, _, _ = select.select([channel], [], [], select_timeout)
|
||||||
|
for desc in read_list:
|
||||||
|
for ready, recv, name in (
|
||||||
|
(desc.recv_ready(), desc.recv, 'stdout'),
|
||||||
|
(desc.recv_stderr_ready(), desc.recv_stderr, 'stderr')
|
||||||
|
):
|
||||||
|
if ready:
|
||||||
|
chunk = recv(chunk_size)
|
||||||
|
if chunk:
|
||||||
|
try:
|
||||||
|
callback_state = callback(callback_state, name, chunk)
|
||||||
|
except Exception as e:
|
||||||
|
return (e, callback_state)
|
||||||
|
|
||||||
|
return (None, callback_state)
|
||||||
|
|
||||||
|
def read_all_channel(callback=None, callback_state=None):
|
||||||
|
for stream, name in ((stdout, 'stdout'), (stderr, 'stderr')):
|
||||||
|
try:
|
||||||
|
chunk = stream.read()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if callback is not None and chunk:
|
||||||
|
callback_state = callback(callback_state, name, chunk)
|
||||||
|
|
||||||
|
return callback_state
|
||||||
|
|
||||||
|
callback_excep = None
|
||||||
|
try:
|
||||||
|
callback_state = init
|
||||||
|
while not channel.exit_status_ready():
|
||||||
|
callback_excep, callback_state = read_channel(callback_state)
|
||||||
|
if callback_excep is not None:
|
||||||
|
raise callback_excep
|
||||||
|
# Make sure to always empty the streams to unblock the remote process on
|
||||||
|
# the way to exit, in case something bad happened. For example, the
|
||||||
|
# callback could raise an exception to signal it does not want to do
|
||||||
|
# anything anymore, or only reading from one of the stream might have
|
||||||
|
# raised an exception, leaving the other one non-empty.
|
||||||
|
except Exception as e:
|
||||||
|
if callback_excep is None:
|
||||||
|
# Only call the callback if there was no exception originally, as
|
||||||
|
# we don't want to reenter it if it raised an exception
|
||||||
|
read_all_channel(callback, callback_state)
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
# Finish emptying the buffers
|
||||||
|
callback_state = read_all_channel(callback, callback_state)
|
||||||
|
exit_code = channel.recv_exit_status()
|
||||||
|
return (callback_state, exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
def telnet_get_shell(host,
|
||||||
username,
|
username,
|
||||||
password=None,
|
password=None,
|
||||||
keyfile=None,
|
|
||||||
port=None,
|
port=None,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
telnet=False,
|
original_prompt=None):
|
||||||
original_prompt=None,
|
|
||||||
options=None):
|
|
||||||
_check_env()
|
_check_env()
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
while True:
|
while True:
|
||||||
if telnet:
|
|
||||||
if keyfile:
|
|
||||||
raise ValueError('keyfile may not be used with a telnet connection.')
|
|
||||||
conn = TelnetPxssh(original_prompt=original_prompt)
|
conn = TelnetPxssh(original_prompt=original_prompt)
|
||||||
else: # ssh
|
|
||||||
conn = pxssh.pxssh(options=options,
|
|
||||||
echo=False)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if keyfile:
|
|
||||||
conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout)
|
|
||||||
else:
|
|
||||||
conn.login(host, username, password, port=port, login_timeout=timeout)
|
conn.login(host, username, password, port=port, login_timeout=timeout)
|
||||||
break
|
break
|
||||||
except EOF:
|
except EOF:
|
||||||
@ -157,10 +249,11 @@ def check_keyfile(keyfile):
|
|||||||
return keyfile
|
return keyfile
|
||||||
|
|
||||||
|
|
||||||
class SshConnection(object):
|
class SshConnectionBase(ConnectionBase):
|
||||||
|
"""
|
||||||
|
Base class for SSH connections.
|
||||||
|
"""
|
||||||
|
|
||||||
default_password_prompt = '[sudo] password'
|
|
||||||
max_cancel_attempts = 5
|
|
||||||
default_timeout = 10
|
default_timeout = 10
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -170,51 +263,473 @@ class SshConnection(object):
|
|||||||
@property
|
@property
|
||||||
def connected_as_root(self):
|
def connected_as_root(self):
|
||||||
if self._connected_as_root is None:
|
if self._connected_as_root is None:
|
||||||
# Execute directly to prevent deadlocking of connection
|
try:
|
||||||
result = self._execute_and_wait_for_prompt('id', as_root=False)
|
result = self.execute('id', as_root=False)
|
||||||
self._connected_as_root = 'uid=0(' in result
|
except TargetStableError:
|
||||||
|
is_root = False
|
||||||
|
else:
|
||||||
|
is_root = 'uid=0(' in result
|
||||||
|
self._connected_as_root = is_root
|
||||||
return self._connected_as_root
|
return self._connected_as_root
|
||||||
|
|
||||||
@connected_as_root.setter
|
@connected_as_root.setter
|
||||||
def connected_as_root(self, state):
|
def connected_as_root(self, state):
|
||||||
self._connected_as_root = state
|
self._connected_as_root = state
|
||||||
|
|
||||||
# pylint: disable=unused-argument,super-init-not-called
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
host,
|
host,
|
||||||
username,
|
username,
|
||||||
password=None,
|
password=None,
|
||||||
keyfile=None,
|
keyfile=None,
|
||||||
port=None,
|
port=None,
|
||||||
timeout=None,
|
|
||||||
telnet=False,
|
|
||||||
password_prompt=None,
|
|
||||||
original_prompt=None,
|
|
||||||
platform=None,
|
platform=None,
|
||||||
sudo_cmd="sudo -- sh -c {}",
|
sudo_cmd="sudo -S -- sh -c {}",
|
||||||
options=None
|
strict_host_check=True,
|
||||||
):
|
):
|
||||||
|
super().__init__()
|
||||||
self._connected_as_root = None
|
self._connected_as_root = None
|
||||||
self.host = host
|
self.host = host
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
|
self.keyfile = check_keyfile(keyfile) if keyfile else keyfile
|
||||||
self.port = port
|
self.port = port
|
||||||
|
self.sudo_cmd = sanitize_cmd_template(sudo_cmd)
|
||||||
|
self.platform = platform
|
||||||
|
self.strict_host_check = strict_host_check
|
||||||
|
logger.debug('Logging in {}@{}'.format(username, host))
|
||||||
|
|
||||||
|
|
||||||
|
class SshConnection(SshConnectionBase):
|
||||||
|
# pylint: disable=unused-argument,super-init-not-called
|
||||||
|
def __init__(self,
|
||||||
|
host,
|
||||||
|
username,
|
||||||
|
password=None,
|
||||||
|
keyfile=None,
|
||||||
|
port=22,
|
||||||
|
timeout=None,
|
||||||
|
platform=None,
|
||||||
|
sudo_cmd="sudo -S -- sh -c {}",
|
||||||
|
strict_host_check=True,
|
||||||
|
):
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
host=host,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
keyfile=keyfile,
|
||||||
|
port=port,
|
||||||
|
platform=platform,
|
||||||
|
sudo_cmd=sudo_cmd,
|
||||||
|
strict_host_check=strict_host_check,
|
||||||
|
)
|
||||||
|
self.timeout = timeout if timeout is not None else self.default_timeout
|
||||||
|
|
||||||
|
self.client = self._make_client()
|
||||||
|
atexit.register(self.close)
|
||||||
|
|
||||||
|
# Use a marker in the output so that we will be able to differentiate
|
||||||
|
# target connection issues with "password needed".
|
||||||
|
# Also, sudo might not be installed at all on the target (but
|
||||||
|
# everything will work as long as we login as root). If sudo is still
|
||||||
|
# needed, it will explode when someone tries to use it. After all, the
|
||||||
|
# user might not be interested in being root at all.
|
||||||
|
self._sudo_needs_password = (
|
||||||
|
'NEED_PASSOWRD' in
|
||||||
|
self.execute(
|
||||||
|
# sudo -n is broken on some versions on MacOSX, revisit that if
|
||||||
|
# someone ever cares
|
||||||
|
'sudo -n true || echo NEED_PASSWORD',
|
||||||
|
as_root=False,
|
||||||
|
check_exit_code=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_client(self):
|
||||||
|
if self.strict_host_check:
|
||||||
|
policy = RejectPolicy
|
||||||
|
else:
|
||||||
|
policy = AutoAddPolicy
|
||||||
|
|
||||||
|
with _handle_paramiko_exceptions():
|
||||||
|
client = SSHClient()
|
||||||
|
client.load_system_host_keys()
|
||||||
|
client.set_missing_host_key_policy(policy)
|
||||||
|
client.connect(
|
||||||
|
hostname=self.host,
|
||||||
|
port=self.port,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
key_filename=self.keyfile,
|
||||||
|
timeout=self.timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
return client
|
||||||
|
|
||||||
|
def _make_channel(self):
|
||||||
|
with _handle_paramiko_exceptions():
|
||||||
|
transport = self.client.get_transport()
|
||||||
|
channel = transport.open_session()
|
||||||
|
return channel
|
||||||
|
|
||||||
|
def _get_sftp(self, timeout):
|
||||||
|
sftp = self.client.open_sftp()
|
||||||
|
sftp.get_channel().settimeout(timeout)
|
||||||
|
return sftp
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _push_file(cls, sftp, src, dst):
|
||||||
|
try:
|
||||||
|
sftp.put(src, dst)
|
||||||
|
# Maybe the dst was a folder
|
||||||
|
except OSError:
|
||||||
|
# This might fail if the folder already exists
|
||||||
|
with contextlib.suppress(IOError):
|
||||||
|
sftp.mkdir(dst)
|
||||||
|
|
||||||
|
new_dst = os.path.join(
|
||||||
|
dst,
|
||||||
|
os.path.basename(src),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls._push_file(sftp, src, new_dst)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _push_folder(cls, sftp, src, dst):
|
||||||
|
# Behave like the "mv" command or adb push: a new folder is created
|
||||||
|
# inside the destination folder, rather than merging the trees.
|
||||||
|
dst = os.path.join(
|
||||||
|
dst,
|
||||||
|
os.path.basename(src),
|
||||||
|
)
|
||||||
|
return cls._push_folder_internal(sftp, src, dst)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _push_folder_internal(cls, sftp, src, dst):
|
||||||
|
# This might fail if the folder already exists
|
||||||
|
with contextlib.suppress(IOError):
|
||||||
|
sftp.mkdir(dst)
|
||||||
|
|
||||||
|
for entry in os.scandir(src):
|
||||||
|
name = entry.name
|
||||||
|
src_path = os.path.join(src, name)
|
||||||
|
dst_path = os.path.join(dst, name)
|
||||||
|
if entry.is_dir():
|
||||||
|
push = cls._push_folder_internal
|
||||||
|
else:
|
||||||
|
push = cls._push_file
|
||||||
|
|
||||||
|
push(sftp, src_path, dst_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _push_path(cls, sftp, src, dst):
|
||||||
|
push = cls._push_folder if os.path.isdir(src) else cls._push_file
|
||||||
|
push(sftp, src, dst)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pull_file(cls, sftp, src, dst):
|
||||||
|
# Pulling a file into a folder will use the source basename
|
||||||
|
if os.path.isdir(dst):
|
||||||
|
dst = os.path.join(
|
||||||
|
dst,
|
||||||
|
os.path.basename(src),
|
||||||
|
)
|
||||||
|
|
||||||
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
os.remove(dst)
|
||||||
|
|
||||||
|
sftp.get(src, dst)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pull_folder(cls, sftp, src, dst):
|
||||||
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
except OSError:
|
||||||
|
os.remove(dst)
|
||||||
|
|
||||||
|
os.makedirs(dst)
|
||||||
|
for fileattr in sftp.listdir_attr(src):
|
||||||
|
filename = fileattr.filename
|
||||||
|
src_path = os.path.join(src, filename)
|
||||||
|
dst_path = os.path.join(dst, filename)
|
||||||
|
if stat.S_ISDIR(fileattr.st_mode):
|
||||||
|
pull = cls._pull_folder
|
||||||
|
else:
|
||||||
|
pull = cls._pull_file
|
||||||
|
|
||||||
|
pull(sftp, src_path, dst_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _pull_path(cls, sftp, src, dst):
|
||||||
|
try:
|
||||||
|
cls._pull_file(sftp, src, dst)
|
||||||
|
except IOError:
|
||||||
|
# Maybe that was a directory, so retry as such
|
||||||
|
cls._pull_folder(sftp, src, dst)
|
||||||
|
|
||||||
|
def push(self, source, dest, timeout=30):
|
||||||
|
with _handle_paramiko_exceptions(), self._get_sftp(timeout) as sftp:
|
||||||
|
self._push_path(sftp, source, dest)
|
||||||
|
|
||||||
|
def pull(self, source, dest, timeout=30):
|
||||||
|
with _handle_paramiko_exceptions(), self._get_sftp(timeout) as sftp:
|
||||||
|
self._pull_path(sftp, source, dest)
|
||||||
|
|
||||||
|
def execute(self, command, timeout=None, check_exit_code=True,
|
||||||
|
as_root=False, strip_colors=True, will_succeed=False): #pylint: disable=unused-argument
|
||||||
|
if command == '':
|
||||||
|
return ''
|
||||||
|
try:
|
||||||
|
with _handle_paramiko_exceptions(command):
|
||||||
|
exit_code, output = self._execute(command, timeout, as_root, strip_colors)
|
||||||
|
except TargetStableError as e:
|
||||||
|
if will_succeed:
|
||||||
|
raise TargetTransientError(e)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
if check_exit_code and exit_code:
|
||||||
|
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
|
||||||
|
raise TargetStableError(message.format(exit_code, command, output))
|
||||||
|
return output
|
||||||
|
|
||||||
|
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
|
||||||
|
with _handle_paramiko_exceptions(command):
|
||||||
|
bg_cmd = self._background(command, stdout, stderr, as_root)
|
||||||
|
|
||||||
|
self._current_bg_cmds.add(bg_cmd)
|
||||||
|
return bg_cmd
|
||||||
|
|
||||||
|
def _background(self, command, stdout, stderr, as_root):
|
||||||
|
stdout, stderr, command = redirect_streams(stdout, stderr, command)
|
||||||
|
|
||||||
|
command = "printf '%s\n' $$; exec sh -c {}".format(quote(command))
|
||||||
|
channel = self._make_channel()
|
||||||
|
|
||||||
|
def executor(cmd, timeout):
|
||||||
|
channel.exec_command(cmd)
|
||||||
|
# Read are not buffered so we will always get the data as soon as
|
||||||
|
# they arrive
|
||||||
|
return (
|
||||||
|
channel.makefile_stdin(),
|
||||||
|
channel.makefile(),
|
||||||
|
channel.makefile_stderr(),
|
||||||
|
)
|
||||||
|
|
||||||
|
stdin, stdout_in, stderr_in = self._execute_command(
|
||||||
|
command,
|
||||||
|
as_root=as_root,
|
||||||
|
log=False,
|
||||||
|
timeout=None,
|
||||||
|
executor=executor,
|
||||||
|
)
|
||||||
|
pid = int(stdout_in.readline())
|
||||||
|
|
||||||
|
def create_out_stream(stream_in, stream_out):
|
||||||
|
"""
|
||||||
|
Create a pair of file-like objects. The first one is used to read
|
||||||
|
data and the second one to write.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if stream_out == subprocess.DEVNULL:
|
||||||
|
r, w = None, None
|
||||||
|
# When asked for a pipe, we just give the file-like object as the
|
||||||
|
# reading end and no writing end, since paramiko already writes to
|
||||||
|
# it
|
||||||
|
elif stream_out == subprocess.PIPE:
|
||||||
|
r, w = os.pipe()
|
||||||
|
r = os.fdopen(r, 'rb')
|
||||||
|
w = os.fdopen(w, 'wb')
|
||||||
|
# Turn a file descriptor into a file-like object
|
||||||
|
elif isinstance(stream_out, int) and stream_out >= 0:
|
||||||
|
r = os.fdopen(stream_out, 'rb')
|
||||||
|
w = os.fdopen(stream_out, 'wb')
|
||||||
|
# file-like object
|
||||||
|
else:
|
||||||
|
r = stream_out
|
||||||
|
w = stream_out
|
||||||
|
|
||||||
|
return (r, w)
|
||||||
|
|
||||||
|
out_streams = {
|
||||||
|
name: create_out_stream(stream_in, stream_out)
|
||||||
|
for stream_in, stream_out, name in (
|
||||||
|
(stdout_in, stdout, 'stdout'),
|
||||||
|
(stderr_in, stderr, 'stderr'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def redirect_thread_f(stdout_in, stderr_in, out_streams, select_timeout):
|
||||||
|
def callback(out_streams, name, chunk):
|
||||||
|
try:
|
||||||
|
r, w = out_streams[name]
|
||||||
|
except KeyError:
|
||||||
|
return out_streams
|
||||||
|
|
||||||
|
try:
|
||||||
|
w.write(chunk)
|
||||||
|
# Write failed
|
||||||
|
except ValueError:
|
||||||
|
# Since that stream is now closed, stop trying to write to it
|
||||||
|
del out_streams[name]
|
||||||
|
# If that was the last open stream, we raise an
|
||||||
|
# exception so the thread can terminate.
|
||||||
|
if not out_streams:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return out_streams
|
||||||
|
|
||||||
|
try:
|
||||||
|
_read_paramiko_streams(stdout_in, stderr_in, select_timeout, callback, copy.copy(out_streams))
|
||||||
|
# The streams closed while we were writing to it, the job is done here
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Make sure the writing end are closed proper since we are not
|
||||||
|
# going to write anything anymore
|
||||||
|
for r, w in out_streams.values():
|
||||||
|
if r is not w and w is not None:
|
||||||
|
w.close()
|
||||||
|
|
||||||
|
# If there is anything we need to redirect to, spawn a thread taking
|
||||||
|
# care of that
|
||||||
|
select_timeout = 1
|
||||||
|
thread_out_streams = {
|
||||||
|
name: (r, w)
|
||||||
|
for name, (r, w) in out_streams.items()
|
||||||
|
if w is not None
|
||||||
|
}
|
||||||
|
redirect_thread = threading.Thread(
|
||||||
|
target=redirect_thread_f,
|
||||||
|
args=(stdout_in, stderr_in, thread_out_streams, select_timeout),
|
||||||
|
# The thread will die when the main thread dies
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
redirect_thread.start()
|
||||||
|
|
||||||
|
return ParamikoBackgroundCommand(
|
||||||
|
conn=self,
|
||||||
|
as_root=as_root,
|
||||||
|
chan=channel,
|
||||||
|
pid=pid,
|
||||||
|
stdin=stdin,
|
||||||
|
# We give the reading end to the consumer of the data
|
||||||
|
stdout=out_streams['stdout'][0],
|
||||||
|
stderr=out_streams['stderr'][0],
|
||||||
|
redirect_thread=redirect_thread,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _close(self):
|
||||||
|
logger.debug('Logging out {}@{}'.format(self.username, self.host))
|
||||||
|
with _handle_paramiko_exceptions():
|
||||||
|
bg_cmds = set(self._current_bg_cmds)
|
||||||
|
for bg_cmd in bg_cmds:
|
||||||
|
bg_cmd.close()
|
||||||
|
self.client.close()
|
||||||
|
|
||||||
|
def _execute_command(self, command, as_root, log, timeout, executor):
|
||||||
|
# As we're already root, there is no need to use sudo.
|
||||||
|
log_debug = logger.debug if log else lambda msg: None
|
||||||
|
use_sudo = as_root and not self.connected_as_root
|
||||||
|
|
||||||
|
if use_sudo:
|
||||||
|
if self._sudo_needs_password and not self.password:
|
||||||
|
raise TargetStableError('Attempt to use sudo but no password was specified')
|
||||||
|
|
||||||
|
command = self.sudo_cmd.format(quote(command))
|
||||||
|
|
||||||
|
log_debug(command)
|
||||||
|
streams = executor(command, timeout=timeout)
|
||||||
|
if self._sudo_needs_password:
|
||||||
|
stdin = streams[0]
|
||||||
|
stdin.write(self.password + '\n')
|
||||||
|
stdin.flush()
|
||||||
|
else:
|
||||||
|
log_debug(command)
|
||||||
|
streams = executor(command, timeout=timeout)
|
||||||
|
|
||||||
|
return streams
|
||||||
|
|
||||||
|
def _execute(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
|
||||||
|
# Merge stderr into stdout since we are going without a TTY
|
||||||
|
command = '({}) 2>&1'.format(command)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = self._execute_command(
|
||||||
|
command,
|
||||||
|
as_root=as_root,
|
||||||
|
log=log,
|
||||||
|
timeout=timeout,
|
||||||
|
executor=self.client.exec_command,
|
||||||
|
)
|
||||||
|
stdin.close()
|
||||||
|
|
||||||
|
# Empty the stdout buffer of the command, allowing it to carry on to
|
||||||
|
# completion
|
||||||
|
def callback(output_chunks, name, chunk):
|
||||||
|
output_chunks.append(chunk)
|
||||||
|
return output_chunks
|
||||||
|
|
||||||
|
select_timeout = 1
|
||||||
|
output_chunks, exit_code = _read_paramiko_streams(stdout, stderr, select_timeout, callback, [])
|
||||||
|
# Join in one go to avoid O(N^2) concatenation
|
||||||
|
output = b''.join(output_chunks)
|
||||||
|
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
output = output.decode(sys.stdout.encoding or 'utf-8', 'replace')
|
||||||
|
if strip_colors:
|
||||||
|
output = strip_bash_colors(output)
|
||||||
|
|
||||||
|
return (exit_code, output)
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetConnection(SshConnectionBase):
|
||||||
|
|
||||||
|
default_password_prompt = '[sudo] password'
|
||||||
|
max_cancel_attempts = 5
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument,super-init-not-called
|
||||||
|
def __init__(self,
|
||||||
|
host,
|
||||||
|
username,
|
||||||
|
password=None,
|
||||||
|
port=None,
|
||||||
|
timeout=None,
|
||||||
|
password_prompt=None,
|
||||||
|
original_prompt=None,
|
||||||
|
sudo_cmd="sudo -- sh -c {}",
|
||||||
|
strict_host_check=True,
|
||||||
|
platform=None):
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
host=host,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
keyfile=None,
|
||||||
|
port=port,
|
||||||
|
platform=platform,
|
||||||
|
sudo_cmd=sudo_cmd,
|
||||||
|
strict_host_check=strict_host_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.strict_host_check:
|
||||||
|
options = {
|
||||||
|
'StrictHostKeyChecking': 'yes',
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
options = {
|
||||||
|
'StrictHostKeyChecking': 'no',
|
||||||
|
'UserKnownHostsFile': '/dev/null',
|
||||||
|
}
|
||||||
|
self.options = options
|
||||||
|
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
|
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
|
||||||
self.sudo_cmd = sanitize_cmd_template(sudo_cmd)
|
|
||||||
logger.debug('Logging in {}@{}'.format(username, host))
|
logger.debug('Logging in {}@{}'.format(username, host))
|
||||||
timeout = timeout if timeout is not None else self.default_timeout
|
timeout = timeout if timeout is not None else self.default_timeout
|
||||||
self.options = options if options is not None else {}
|
|
||||||
self.conn = ssh_get_shell(host,
|
self.conn = telnet_get_shell(host, username, password, port, timeout, original_prompt)
|
||||||
username,
|
|
||||||
password,
|
|
||||||
self.keyfile,
|
|
||||||
port,
|
|
||||||
timeout,
|
|
||||||
False,
|
|
||||||
None,
|
|
||||||
self.options)
|
|
||||||
atexit.register(self.close)
|
atexit.register(self.close)
|
||||||
|
|
||||||
def push(self, source, dest, timeout=30):
|
def push(self, source, dest, timeout=30):
|
||||||
@ -282,7 +797,7 @@ class SshConnection(object):
|
|||||||
except EOF:
|
except EOF:
|
||||||
raise TargetNotRespondingError('Connection lost.')
|
raise TargetNotRespondingError('Connection lost.')
|
||||||
|
|
||||||
def close(self):
|
def _close(self):
|
||||||
logger.debug('Logging out {}@{}'.format(self.username, self.host))
|
logger.debug('Logging out {}@{}'.format(self.username, self.host))
|
||||||
try:
|
try:
|
||||||
self.conn.logout()
|
self.conn.logout()
|
||||||
@ -387,29 +902,6 @@ class SshConnection(object):
|
|||||||
def _get_window_size(self):
|
def _get_window_size(self):
|
||||||
return self.conn.getwinsize()
|
return self.conn.getwinsize()
|
||||||
|
|
||||||
class TelnetConnection(SshConnection):
|
|
||||||
|
|
||||||
# pylint: disable=super-init-not-called
|
|
||||||
def __init__(self,
|
|
||||||
host,
|
|
||||||
username,
|
|
||||||
password=None,
|
|
||||||
port=None,
|
|
||||||
timeout=None,
|
|
||||||
password_prompt=None,
|
|
||||||
original_prompt=None,
|
|
||||||
platform=None):
|
|
||||||
self.host = host
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.port = port
|
|
||||||
self.keyfile = None
|
|
||||||
self.lock = threading.Lock()
|
|
||||||
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
|
|
||||||
logger.debug('Logging in {}@{}'.format(username, host))
|
|
||||||
timeout = timeout if timeout is not None else self.default_timeout
|
|
||||||
self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt)
|
|
||||||
|
|
||||||
|
|
||||||
class Gem5Connection(TelnetConnection):
|
class Gem5Connection(TelnetConnection):
|
||||||
|
|
||||||
@ -616,7 +1108,7 @@ class Gem5Connection(TelnetConnection):
|
|||||||
'get this file'.format(redirection_file))
|
'get this file'.format(redirection_file))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def close(self):
|
def _close(self):
|
||||||
"""
|
"""
|
||||||
Close and disconnect from the gem5 simulation. Additionally, we remove
|
Close and disconnect from the gem5 simulation. Additionally, we remove
|
||||||
the temporary directory used to pass files into the simulation.
|
the temporary directory used to pass files into the simulation.
|
||||||
|
1
setup.py
1
setup.py
@ -82,6 +82,7 @@ params = dict(
|
|||||||
'python-dateutil', # converting between UTC and local time.
|
'python-dateutil', # converting between UTC and local time.
|
||||||
'pexpect>=3.3', # Send/recieve to/from device
|
'pexpect>=3.3', # Send/recieve to/from device
|
||||||
'pyserial', # Serial port interface
|
'pyserial', # Serial port interface
|
||||||
|
'paramiko', # SSH connection
|
||||||
'wrapt', # Basic for construction of decorator functions
|
'wrapt', # Basic for construction of decorator functions
|
||||||
'future', # Python 2-3 compatibility
|
'future', # Python 2-3 compatibility
|
||||||
'enum34;python_version<"3.4"', # Enums for Python < 3.4
|
'enum34;python_version<"3.4"', # Enums for Python < 3.4
|
||||||
|
Loading…
x
Reference in New Issue
Block a user