Skip to content

Commit 38c1dfe

Browse files
Add API reference docs for ShellProxy (#295)
* added shell section * describe members * added docstring to send_signal * added docstring to execute * changed visibility of instrument * added docstring to send_signal * added docs to Popen * described popen * tidied docs for shell * fixed bad style * hide system constructor
1 parent cfc97b1 commit 38c1dfe

File tree

2 files changed

+119
-13
lines changed

2 files changed

+119
-13
lines changed

docs/api.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ the roswire library.
1010
.. autoclass:: ROSWire
1111
:members:
1212

13+
1314
System
1415
------
1516

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

24-
.. autoclass:: System
25+
.. autoclass:: System()
26+
:members:
27+
28+
29+
Shell
30+
-----
31+
32+
.. py:module:: roswire.proxy.shell
33+
.. autoclass:: ShellProxy()
34+
:members:
35+
36+
.. autoclass:: Popen()
2537
:members:
2638

2739

src/roswire/proxy/shell.py

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,26 @@
2323

2424

2525
class Popen:
26+
"""Provides access to a process that is running inside a given shell.
27+
Inspired by the :class:`subprocess.Popen` interface within the Python
28+
standard library. Unlike :class:`subprocess.Popen`, instances of this
29+
class should be generated by :meth:`ShellProxy.popen` rather than via
30+
the constructor.
31+
32+
Attributes
33+
----------
34+
stream: Iterator[str]
35+
An output stream for this process.
36+
args: str
37+
The argument string that was used to generate this process.
38+
pid: int, optional
39+
The PID of this process, if known.
40+
finished: bool
41+
A dynamic flag (i.e., a property) that indicates whether this process
42+
has terminated.
43+
retcode: int, optional
44+
The return code produced by this process, if known.
45+
"""
2646
def __init__(self,
2747
args: str,
2848
uid: str,
@@ -71,7 +91,6 @@ def pid(self) -> Optional[int]:
7191

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

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

83102
def send_signal(self, sig: int) -> None:
103+
"""Sends a given signal to the process.
104+
105+
Parameters
106+
----------
107+
sig: int
108+
The signal number.
109+
"""
84110
pid = self.pid
85111
if pid:
86112
self.__shell.send_signal(pid, sig)
@@ -102,7 +128,7 @@ def wait(self, time_limit: Optional[float] = None) -> int:
102128
103129
Parameters
104130
----------
105-
time_limit: Optional[float] = None
131+
time_limit: float, optional
106132
An optional time limit.
107133
108134
Raises
@@ -136,7 +162,18 @@ def exec_id_to_host_pid(self, exec_id: str) -> int:
136162
return self.__api_docker.exec_inspect(exec_id)['Pid']
137163

138164
def local_to_host_pid(self, pid_local: int) -> Optional[int]:
139-
"""Finds the host PID for a process inside this shell."""
165+
"""Finds the host PID for a process inside this shell.
166+
167+
Parameters
168+
----------
169+
pid_local: int
170+
The PID of the process inside the container.
171+
172+
Returns
173+
-------
174+
int
175+
The PID of the same process on the host machine.
176+
"""
140177
ctr_pids = [self.__container_pid]
141178
info = self.__api_docker.inspect_container(self.__container_docker.id)
142179
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]:
163200
return None
164201

165202
def send_signal(self, pid: int, sig: int) -> None:
203+
"""Sends a given signal to a specified process.
204+
205+
Parameters
206+
----------
207+
pid: int
208+
The PID of the process.
209+
sig: int
210+
The signal number.
211+
"""
166212
self.execute(f'kill -{sig} {pid}', user='root')
167213

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

187-
def instrument(self,
188-
command: str,
189-
time_limit: Optional[int] = None,
190-
kill_after: int = 1,
191-
identifier: Optional[str] = None
192-
) -> str:
233+
def _instrument(self,
234+
command: str,
235+
time_limit: Optional[int] = None,
236+
kill_after: int = 1,
237+
identifier: Optional[str] = None
238+
) -> str:
193239
logger.debug("instrumenting command: %s", command)
194240
q = shlex.quote
195241
command = f'source /.environment && {command}'
@@ -212,12 +258,48 @@ def popen(self,
212258
time_limit: Optional[int] = None,
213259
kill_after: int = 1
214260
) -> Popen:
261+
"""Creates a process without blocking, and returns an interface to it.
262+
Inspired by :meth:`subprocess.Popen` in the Python standard library.
263+
This method can be used, for example, to stream the output of a
264+
non-blocking process, or to send a signal (e.g., SIGTERM) to a process
265+
at run-time.
266+
267+
Parameters
268+
----------
269+
command: str
270+
The command that should be executed.
271+
stdout: bool
272+
If :code:`True`, includes stdout as part of output.
273+
stderr: bool
274+
If :code:`True`, includes stderr as part of output.
275+
user: str, optional
276+
The name or UID of the user, inside the container, that should
277+
execute the command. If left unspecified, the default user for
278+
the container will be used.
279+
context: str, optional
280+
The absolute path to the working directory that should be used
281+
when executing the command. If unspecified, the default working
282+
directory for the container will be used.
283+
time_limit: int, optional
284+
The maximum number of seconds that the command is allowed to run
285+
before being terminated via SIGTERM. If unspecified, no time limit
286+
will be imposed on command execution.
287+
kill_after: int
288+
The maximum number of seconds to wait before sending SIGKILL to
289+
the process after attempting termination via SIGTERM. Only applies
290+
when :code:`time_limit` is specified.
291+
292+
Returns
293+
-------
294+
Popen
295+
An interface for interacting with and inspecting the process.
296+
"""
215297
uid_popen = self.__generate_popen_uid(command)
216298
id_container = self.__container_docker.id
217299
api_docker = self.__api_docker
218300
command_orig = command
219-
command = self.instrument(command, time_limit, kill_after,
220-
identifier=uid_popen)
301+
command = self._instrument(command, time_limit, kill_after,
302+
identifier=uid_popen)
221303
exec_resp = api_docker.exec_create(id_container, command,
222304
tty=True,
223305
stdout=stdout,
@@ -241,9 +323,21 @@ def execute(self,
241323
time_limit: Optional[int] = None,
242324
kill_after: int = 1
243325
) -> Tuple[int, str, float]:
326+
"""Executes a given command and blocks until its completion.
327+
328+
Note
329+
----
330+
Accepts the same arguments as :meth:`popen`.
331+
332+
Returns
333+
-------
334+
Tuple[int, str, float]
335+
The return code, output, and wall-clock running time of the
336+
execution.
337+
"""
244338
logger.debug("executing command: %s", command)
245339
dockerc = self.__container_docker
246-
command = self.instrument(command, time_limit, kill_after)
340+
command = self._instrument(command, time_limit, kill_after)
247341

248342
timer = Stopwatch()
249343
timer.start()

0 commit comments

Comments
 (0)