Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ the roswire library.
.. autoclass:: ROSWire
:members:


System
------

Expand All @@ -21,7 +22,18 @@ through a number of loosely-coupled proxies that are represented as
attributes. For example, :attr:`System.shell` exposes a proxy for interacting
with a :code:`bash` shell inside the application container.

.. autoclass:: System
.. autoclass:: System()
:members:


Shell
-----

.. py:module:: roswire.proxy.shell
.. autoclass:: ShellProxy()
:members:

.. autoclass:: Popen()
:members:


Expand Down
118 changes: 106 additions & 12 deletions src/roswire/proxy/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@


class Popen:
"""Provides access to a process that is running inside a given shell.
Inspired by the :class:`subprocess.Popen` interface within the Python
standard library. Unlike :class:`subprocess.Popen`, instances of this
class should be generated by :meth:`ShellProxy.popen` rather than via
the constructor.

Attributes
----------
stream: Iterator[str]
An output stream for this process.
args: str
The argument string that was used to generate this process.
pid: int, optional
The PID of this process, if known.
finished: bool
A dynamic flag (i.e., a property) that indicates whether this process
has terminated.
retcode: int, optional
The return code produced by this process, if known.
"""
def __init__(self,
args: str,
uid: str,
Expand Down Expand Up @@ -71,7 +91,6 @@ def pid(self) -> Optional[int]:

@property
def finished(self) -> bool:
"""True if the process has exited; False if not."""
return self.returncode is not None

@property
Expand All @@ -81,6 +100,13 @@ def returncode(self) -> Optional[int]:
return self.__returncode

def send_signal(self, sig: int) -> None:
"""Sends a given signal to the process.

Parameters
----------
sig: int
The signal number.
"""
pid = self.pid
if pid:
self.__shell.send_signal(pid, sig)
Expand All @@ -102,7 +128,7 @@ def wait(self, time_limit: Optional[float] = None) -> int:

Parameters
----------
time_limit: Optional[float] = None
time_limit: float, optional
An optional time limit.

Raises
Expand Down Expand Up @@ -136,7 +162,18 @@ def exec_id_to_host_pid(self, exec_id: str) -> int:
return self.__api_docker.exec_inspect(exec_id)['Pid']

def local_to_host_pid(self, pid_local: int) -> Optional[int]:
"""Finds the host PID for a process inside this shell."""
"""Finds the host PID for a process inside this shell.

Parameters
----------
pid_local: int
The PID of the process inside the container.

Returns
-------
int
The PID of the same process on the host machine.
"""
ctr_pids = [self.__container_pid]
info = self.__api_docker.inspect_container(self.__container_docker.id)
ctr_pids += [self.exec_id_to_host_pid(i) for i in info['ExecIDs']]
Expand All @@ -163,6 +200,15 @@ def local_to_host_pid(self, pid_local: int) -> Optional[int]:
return None

def send_signal(self, pid: int, sig: int) -> None:
"""Sends a given signal to a specified process.

Parameters
----------
pid: int
The PID of the process.
sig: int
The signal number.
"""
self.execute(f'kill -{sig} {pid}', user='root')

def __generate_popen_uid(self, command: str) -> str:
Expand All @@ -184,12 +230,12 @@ def environ(self, var: str) -> str:
raise exceptions.EnvNotFoundError(var)
return val

def instrument(self,
command: str,
time_limit: Optional[int] = None,
kill_after: int = 1,
identifier: Optional[str] = None
) -> str:
def _instrument(self,
command: str,
time_limit: Optional[int] = None,
kill_after: int = 1,
identifier: Optional[str] = None
) -> str:
logger.debug("instrumenting command: %s", command)
q = shlex.quote
command = f'source /.environment && {command}'
Expand All @@ -212,12 +258,48 @@ def popen(self,
time_limit: Optional[int] = None,
kill_after: int = 1
) -> Popen:
"""Creates a process without blocking, and returns an interface to it.
Inspired by :meth:`subprocess.Popen` in the Python standard library.
This method can be used, for example, to stream the output of a
non-blocking process, or to send a signal (e.g., SIGTERM) to a process
at run-time.

Parameters
----------
command: str
The command that should be executed.
stdout: bool
If :code:`True`, includes stdout as part of output.
stderr: bool
If :code:`True`, includes stderr as part of output.
user: str, optional
The name or UID of the user, inside the container, that should
execute the command. If left unspecified, the default user for
the container will be used.
context: str, optional
The absolute path to the working directory that should be used
when executing the command. If unspecified, the default working
directory for the container will be used.
time_limit: int, optional
The maximum number of seconds that the command is allowed to run
before being terminated via SIGTERM. If unspecified, no time limit
will be imposed on command execution.
kill_after: int
The maximum number of seconds to wait before sending SIGKILL to
the process after attempting termination via SIGTERM. Only applies
when :code:`time_limit` is specified.

Returns
-------
Popen
An interface for interacting with and inspecting the process.
"""
uid_popen = self.__generate_popen_uid(command)
id_container = self.__container_docker.id
api_docker = self.__api_docker
command_orig = command
command = self.instrument(command, time_limit, kill_after,
identifier=uid_popen)
command = self._instrument(command, time_limit, kill_after,
identifier=uid_popen)
exec_resp = api_docker.exec_create(id_container, command,
tty=True,
stdout=stdout,
Expand All @@ -241,9 +323,21 @@ def execute(self,
time_limit: Optional[int] = None,
kill_after: int = 1
) -> Tuple[int, str, float]:
"""Executes a given command and blocks until its completion.

Note
----
Accepts the same arguments as :meth:`popen`.

Returns
-------
Tuple[int, str, float]
The return code, output, and wall-clock running time of the
execution.
"""
logger.debug("executing command: %s", command)
dockerc = self.__container_docker
command = self.instrument(command, time_limit, kill_after)
command = self._instrument(command, time_limit, kill_after)

timer = Stopwatch()
timer.start()
Expand Down