diff --git a/docs/api.rst b/docs/api.rst index 54ff8395..6001e450 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -10,6 +10,7 @@ the roswire library. .. autoclass:: ROSWire :members: + System ------ @@ -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: diff --git a/src/roswire/proxy/shell.py b/src/roswire/proxy/shell.py index 8a2bc901..d43cc449 100644 --- a/src/roswire/proxy/shell.py +++ b/src/roswire/proxy/shell.py @@ -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, @@ -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 @@ -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) @@ -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 @@ -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']] @@ -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: @@ -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}' @@ -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, @@ -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()