bounded_subprocess 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of bounded_subprocess might be problematic. Click here for more details.
- bounded_subprocess/__init__.py +30 -0
- bounded_subprocess/bounded_subprocess_async.py +33 -0
- bounded_subprocess/interactive.py +131 -0
- bounded_subprocess/interactive_async.py +131 -0
- bounded_subprocess/util.py +110 -0
- bounded_subprocess-1.5.0.dist-info/METADATA +49 -0
- bounded_subprocess-1.5.0.dist-info/RECORD +9 -0
- bounded_subprocess-1.5.0.dist-info/WHEEL +4 -0
- bounded_subprocess-1.5.0.dist-info/licenses/LICENSE.txt +9 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from .util import Result, BoundedSubprocessState, SLEEP_BETWEEN_READS
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run(
|
|
8
|
+
args: List[str],
|
|
9
|
+
timeout_seconds: int = 15,
|
|
10
|
+
max_output_size: int = 2048,
|
|
11
|
+
env=None,
|
|
12
|
+
) -> Result:
|
|
13
|
+
"""
|
|
14
|
+
Runs the given program with arguments. After the timeout elapses, kills the process
|
|
15
|
+
and all other processes in the process group. Captures at most max_output_size bytes
|
|
16
|
+
of stdout and stderr each, and discards any output beyond that.
|
|
17
|
+
"""
|
|
18
|
+
state = BoundedSubprocessState(args, env, max_output_size)
|
|
19
|
+
|
|
20
|
+
# We sleep for 0.1 seconds in each iteration.
|
|
21
|
+
max_iterations = timeout_seconds * 10
|
|
22
|
+
|
|
23
|
+
for _ in range(max_iterations):
|
|
24
|
+
keep_reading = state.try_read()
|
|
25
|
+
if keep_reading:
|
|
26
|
+
time.sleep(SLEEP_BETWEEN_READS)
|
|
27
|
+
else:
|
|
28
|
+
break
|
|
29
|
+
|
|
30
|
+
return state.terminate()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import List
|
|
3
|
+
from .util import Result, BoundedSubprocessState, SLEEP_BETWEEN_READS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def run(
|
|
7
|
+
args: List[str],
|
|
8
|
+
timeout_seconds: int = 15,
|
|
9
|
+
max_output_size: int = 2048,
|
|
10
|
+
env=None,
|
|
11
|
+
) -> Result:
|
|
12
|
+
"""
|
|
13
|
+
Runs the given program with arguments. After the timeout elapses, kills the process
|
|
14
|
+
and all other processes in the process group. Captures at most max_output_size bytes
|
|
15
|
+
of stdout and stderr each, and discards any output beyond that.
|
|
16
|
+
"""
|
|
17
|
+
# If you read the code for BoundedSubprocessState, you probably thought we
|
|
18
|
+
# were going to use asyncio.create_subprocess_exec. But, we're not. We're
|
|
19
|
+
# using subprocess.Popen because it supports non-blocking reads. What's
|
|
20
|
+
# async here? It's just the sleep between reads.
|
|
21
|
+
state = BoundedSubprocessState(args, env, max_output_size)
|
|
22
|
+
|
|
23
|
+
# We sleep for 0.1 seconds in each iteration.
|
|
24
|
+
max_iterations = timeout_seconds * 10
|
|
25
|
+
|
|
26
|
+
for _ in range(max_iterations):
|
|
27
|
+
keep_reading = state.try_read()
|
|
28
|
+
if keep_reading:
|
|
29
|
+
await asyncio.sleep(SLEEP_BETWEEN_READS)
|
|
30
|
+
else:
|
|
31
|
+
break
|
|
32
|
+
|
|
33
|
+
return state.terminate()
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from typeguard import typechecked
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
import time
|
|
4
|
+
import errno
|
|
5
|
+
import subprocess
|
|
6
|
+
from .util import set_nonblocking
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_MAX_BYTES_PER_READ = 1024
|
|
10
|
+
_SLEEP_AFTER_WOUND_BLOCK = 0.5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
class Interactive:
|
|
15
|
+
"""
|
|
16
|
+
A class for interacting with a subprocess that is careful to use non-blocking
|
|
17
|
+
I/O so that we can timeout reads and writes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, args: List[str], read_buffer_size: int):
|
|
21
|
+
"""
|
|
22
|
+
read_buffer_size is the maximum number of bytes to read from stdout
|
|
23
|
+
and stdout each. If the process writes more than this, the extra bytes
|
|
24
|
+
will be discarded.
|
|
25
|
+
"""
|
|
26
|
+
popen = subprocess.Popen(
|
|
27
|
+
args,
|
|
28
|
+
stdin=subprocess.PIPE,
|
|
29
|
+
stdout=subprocess.PIPE,
|
|
30
|
+
# stderr=subprocess.PIPE,
|
|
31
|
+
bufsize=_MAX_BYTES_PER_READ,
|
|
32
|
+
)
|
|
33
|
+
set_nonblocking(popen.stdin)
|
|
34
|
+
set_nonblocking(popen.stdout)
|
|
35
|
+
self._read_buffer_size = read_buffer_size
|
|
36
|
+
self._stdout_saved_bytes = bytearray()
|
|
37
|
+
self._stderr_saved_bytes = bytearray()
|
|
38
|
+
self._popen = popen
|
|
39
|
+
|
|
40
|
+
def close(self, nice_timeout_seconds: int) -> int:
|
|
41
|
+
"""
|
|
42
|
+
Close the process and wait for it to exit.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
self._popen.stdin.close()
|
|
46
|
+
except BlockingIOError:
|
|
47
|
+
# .close() will attempt to flush any buffered writes to stdout
|
|
48
|
+
# before the close returns. This may block, but since the file
|
|
49
|
+
# descriptor is non-blocking, we get a BlockingIOError.
|
|
50
|
+
pass
|
|
51
|
+
self._popen.stdout.close()
|
|
52
|
+
for _ in range(nice_timeout_seconds):
|
|
53
|
+
if self._popen.poll() is not None:
|
|
54
|
+
break
|
|
55
|
+
time.sleep(1)
|
|
56
|
+
self._popen.kill()
|
|
57
|
+
return_code = self._popen.returncode
|
|
58
|
+
return return_code if return_code is not None else -9
|
|
59
|
+
|
|
60
|
+
def write(self, stdin_data: bytes, timeout_seconds: int):
|
|
61
|
+
"""
|
|
62
|
+
Write data to the process's stdin.
|
|
63
|
+
"""
|
|
64
|
+
if self._popen.poll() is not None:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
write_start_index = 0
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
while write_start_index < len(stdin_data):
|
|
70
|
+
try:
|
|
71
|
+
bytes_written = self._popen.stdin.write(stdin_data[write_start_index:])
|
|
72
|
+
self._popen.stdin.flush()
|
|
73
|
+
except BlockingIOError as exn:
|
|
74
|
+
if exn.errno != errno.EAGAIN:
|
|
75
|
+
return False
|
|
76
|
+
bytes_written = exn.characters_written
|
|
77
|
+
time.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
78
|
+
except BrokenPipeError:
|
|
79
|
+
# The child has closed stdin. It is likely dead.
|
|
80
|
+
return False
|
|
81
|
+
write_start_index += bytes_written
|
|
82
|
+
if time.time() - start_time > timeout_seconds:
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def _read_line_from_saved_bytes(self, newline_search_index: int) -> Optional[bytes]:
|
|
87
|
+
"""
|
|
88
|
+
Try to read a line of output from the buffer of stdout that this object
|
|
89
|
+
manages itself. The newline_search_index to indicate where to start
|
|
90
|
+
looking for b"\n". Typically will be 0, but there are cases where we
|
|
91
|
+
are certain that the the newline is not in a prefix.
|
|
92
|
+
"""
|
|
93
|
+
newline_index = self._stdout_saved_bytes.find(b"\n", newline_search_index)
|
|
94
|
+
if newline_index == -1:
|
|
95
|
+
return None
|
|
96
|
+
# memoryview helps avoid a pointless copy
|
|
97
|
+
line = memoryview(self._stdout_saved_bytes)[:newline_index].tobytes()
|
|
98
|
+
del self._stdout_saved_bytes[: newline_index + 1]
|
|
99
|
+
return line
|
|
100
|
+
|
|
101
|
+
def read_line(self, timeout_seconds: int) -> Optional[bytes]:
|
|
102
|
+
"""
|
|
103
|
+
Read a line from the process's stdout.
|
|
104
|
+
"""
|
|
105
|
+
from_saved_bytes = self._read_line_from_saved_bytes(0)
|
|
106
|
+
if from_saved_bytes is not None:
|
|
107
|
+
return from_saved_bytes
|
|
108
|
+
if self._popen.poll() is not None:
|
|
109
|
+
return None
|
|
110
|
+
deadline = time.time() + timeout_seconds
|
|
111
|
+
while time.time() < deadline:
|
|
112
|
+
new_bytes = self._popen.stdout.read(_MAX_BYTES_PER_READ)
|
|
113
|
+
# If the read would block, we get None and not zero bytes on MacOS.
|
|
114
|
+
if new_bytes is None:
|
|
115
|
+
time.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
116
|
+
continue
|
|
117
|
+
# If we read 0 bytes, the child has closed stdout and is likely dead.
|
|
118
|
+
if len(new_bytes) == 0:
|
|
119
|
+
return None
|
|
120
|
+
prev_saved_bytes_len = len(self._stdout_saved_bytes)
|
|
121
|
+
self._stdout_saved_bytes.extend(new_bytes)
|
|
122
|
+
from_saved_bytes = self._read_line_from_saved_bytes(prev_saved_bytes_len)
|
|
123
|
+
if from_saved_bytes is not None:
|
|
124
|
+
return from_saved_bytes
|
|
125
|
+
if len(self._stdout_saved_bytes) > self._read_buffer_size:
|
|
126
|
+
del self._stdout_saved_bytes[
|
|
127
|
+
: len(self._stdout_saved_bytes) - self._read_buffer_size
|
|
128
|
+
]
|
|
129
|
+
time.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
130
|
+
|
|
131
|
+
return None
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from typeguard import typechecked
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
import time
|
|
4
|
+
import errno
|
|
5
|
+
import subprocess
|
|
6
|
+
from .util import set_nonblocking
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
_MAX_BYTES_PER_READ = 1024
|
|
10
|
+
_SLEEP_AFTER_WOUND_BLOCK = 0.5
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@typechecked
|
|
14
|
+
class Interactive:
|
|
15
|
+
"""
|
|
16
|
+
A class for interacting with a subprocess that is careful to use non-blocking
|
|
17
|
+
I/O so that we can timeout reads and writes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, args: List[str], read_buffer_size: int):
|
|
21
|
+
"""
|
|
22
|
+
read_buffer_size is the maximum number of bytes to read from stdout
|
|
23
|
+
and stdout each. If the process writes more than this, the extra bytes
|
|
24
|
+
will be discarded.
|
|
25
|
+
"""
|
|
26
|
+
popen = subprocess.Popen(
|
|
27
|
+
args,
|
|
28
|
+
stdin=subprocess.PIPE,
|
|
29
|
+
stdout=subprocess.PIPE,
|
|
30
|
+
# stderr=subprocess.PIPE,
|
|
31
|
+
bufsize=_MAX_BYTES_PER_READ,
|
|
32
|
+
)
|
|
33
|
+
set_nonblocking(popen.stdin)
|
|
34
|
+
set_nonblocking(popen.stdout)
|
|
35
|
+
self._read_buffer_size = read_buffer_size
|
|
36
|
+
self._stdout_saved_bytes = bytearray()
|
|
37
|
+
self._stderr_saved_bytes = bytearray()
|
|
38
|
+
self._popen = popen
|
|
39
|
+
|
|
40
|
+
async def close(self, nice_timeout_seconds: int) -> int:
|
|
41
|
+
"""
|
|
42
|
+
Close the process and wait for it to exit.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
self._popen.stdin.close()
|
|
46
|
+
except BlockingIOError:
|
|
47
|
+
# .close() will attempt to flush any buffered writes to stdout
|
|
48
|
+
# before the close returns. This may block, but since the file
|
|
49
|
+
# descriptor is non-blocking, we get a BlockingIOError.
|
|
50
|
+
pass
|
|
51
|
+
self._popen.stdout.close()
|
|
52
|
+
for _ in range(nice_timeout_seconds):
|
|
53
|
+
if self._popen.poll() is not None:
|
|
54
|
+
break
|
|
55
|
+
await asyncio.sleep(1)
|
|
56
|
+
self._popen.kill()
|
|
57
|
+
return_code = self._popen.returncode
|
|
58
|
+
return return_code if return_code is not None else -9
|
|
59
|
+
|
|
60
|
+
async def write(self, stdin_data: bytes, timeout_seconds: int):
|
|
61
|
+
"""
|
|
62
|
+
Write data to the process's stdin.
|
|
63
|
+
"""
|
|
64
|
+
if self._popen.poll() is not None:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
write_start_index = 0
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
while write_start_index < len(stdin_data):
|
|
70
|
+
try:
|
|
71
|
+
bytes_written = self._popen.stdin.write(stdin_data[write_start_index:])
|
|
72
|
+
self._popen.stdin.flush()
|
|
73
|
+
except BlockingIOError as exn:
|
|
74
|
+
if exn.errno != errno.EAGAIN:
|
|
75
|
+
return False
|
|
76
|
+
bytes_written = exn.characters_written
|
|
77
|
+
await asyncio.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
78
|
+
except BrokenPipeError:
|
|
79
|
+
# The child has closed stdin. It is likely dead.
|
|
80
|
+
return False
|
|
81
|
+
write_start_index += bytes_written
|
|
82
|
+
if time.time() - start_time > timeout_seconds:
|
|
83
|
+
return False
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
def _read_line_from_saved_bytes(self, newline_search_index: int) -> Optional[bytes]:
|
|
87
|
+
"""
|
|
88
|
+
Try to read a line of output from the buffer of stdout that this object
|
|
89
|
+
manages itself. The newline_search_index to indicate where to start
|
|
90
|
+
looking for b"\n". Typically will be 0, but there are cases where we
|
|
91
|
+
are certain that the the newline is not in a prefix.
|
|
92
|
+
"""
|
|
93
|
+
newline_index = self._stdout_saved_bytes.find(b"\n", newline_search_index)
|
|
94
|
+
if newline_index == -1:
|
|
95
|
+
return None
|
|
96
|
+
# memoryview helps avoid a pointless copy
|
|
97
|
+
line = memoryview(self._stdout_saved_bytes)[:newline_index].tobytes()
|
|
98
|
+
del self._stdout_saved_bytes[: newline_index + 1]
|
|
99
|
+
return line
|
|
100
|
+
|
|
101
|
+
async def read_line(self, timeout_seconds: int) -> Optional[bytes]:
|
|
102
|
+
"""
|
|
103
|
+
Read a line from the process's stdout.
|
|
104
|
+
"""
|
|
105
|
+
from_saved_bytes = self._read_line_from_saved_bytes(0)
|
|
106
|
+
if from_saved_bytes is not None:
|
|
107
|
+
return from_saved_bytes
|
|
108
|
+
if self._popen.poll() is not None:
|
|
109
|
+
return None
|
|
110
|
+
deadline = time.time() + timeout_seconds
|
|
111
|
+
while time.time() < deadline:
|
|
112
|
+
new_bytes = self._popen.stdout.read(_MAX_BYTES_PER_READ)
|
|
113
|
+
# If the read would block, we get None and not zero bytes on MacOS.
|
|
114
|
+
if new_bytes is None:
|
|
115
|
+
await asyncio.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
116
|
+
continue
|
|
117
|
+
# If we read 0 bytes, the child has closed stdout and is likely dead.
|
|
118
|
+
if len(new_bytes) == 0:
|
|
119
|
+
return None
|
|
120
|
+
prev_saved_bytes_len = len(self._stdout_saved_bytes)
|
|
121
|
+
self._stdout_saved_bytes.extend(new_bytes)
|
|
122
|
+
from_saved_bytes = self._read_line_from_saved_bytes(prev_saved_bytes_len)
|
|
123
|
+
if from_saved_bytes is not None:
|
|
124
|
+
return from_saved_bytes
|
|
125
|
+
if len(self._stdout_saved_bytes) > self._read_buffer_size:
|
|
126
|
+
del self._stdout_saved_bytes[
|
|
127
|
+
: len(self._stdout_saved_bytes) - self._read_buffer_size
|
|
128
|
+
]
|
|
129
|
+
await asyncio.sleep(_SLEEP_AFTER_WOUND_BLOCK)
|
|
130
|
+
|
|
131
|
+
return None
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import fcntl
|
|
4
|
+
import signal
|
|
5
|
+
|
|
6
|
+
MAX_BYTES_PER_READ = 1024
|
|
7
|
+
SLEEP_BETWEEN_READS = 0.1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Result:
|
|
11
|
+
timeout: int
|
|
12
|
+
exit_code: int
|
|
13
|
+
stdout: str
|
|
14
|
+
stderr: str
|
|
15
|
+
|
|
16
|
+
def __init__(self, timeout, exit_code, stdout, stderr):
|
|
17
|
+
self.timeout = timeout
|
|
18
|
+
self.exit_code = exit_code
|
|
19
|
+
self.stdout = stdout
|
|
20
|
+
self.stderr = stderr
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_nonblocking(reader):
|
|
24
|
+
fd = reader.fileno()
|
|
25
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
26
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BoundedSubprocessState:
|
|
30
|
+
"""
|
|
31
|
+
This class lets us share code between the synchronous and asynchronous
|
|
32
|
+
implementations.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, args, env, max_output_size):
|
|
36
|
+
"""
|
|
37
|
+
Start the process in a new session.
|
|
38
|
+
"""
|
|
39
|
+
p = subprocess.Popen(
|
|
40
|
+
args,
|
|
41
|
+
env=env,
|
|
42
|
+
stdin=subprocess.DEVNULL,
|
|
43
|
+
stdout=subprocess.PIPE,
|
|
44
|
+
stderr=subprocess.PIPE,
|
|
45
|
+
start_new_session=True,
|
|
46
|
+
bufsize=MAX_BYTES_PER_READ,
|
|
47
|
+
)
|
|
48
|
+
set_nonblocking(p.stdout)
|
|
49
|
+
set_nonblocking(p.stderr)
|
|
50
|
+
|
|
51
|
+
self.process_group_id = os.getpgid(p.pid)
|
|
52
|
+
self.p = p
|
|
53
|
+
self.exit_code = None
|
|
54
|
+
self.stdout_saved_bytes = []
|
|
55
|
+
self.stderr_saved_bytes = []
|
|
56
|
+
self.stdout_bytes_read = 0
|
|
57
|
+
self.stderr_bytes_read = 0
|
|
58
|
+
self.max_output_size = max_output_size
|
|
59
|
+
|
|
60
|
+
def try_read(self) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Reads from the process. Returning False indicates that we should stop
|
|
63
|
+
reading.
|
|
64
|
+
"""
|
|
65
|
+
this_stdout_read = self.p.stdout.read(MAX_BYTES_PER_READ)
|
|
66
|
+
this_stderr_read = self.p.stderr.read(MAX_BYTES_PER_READ)
|
|
67
|
+
# this_stdout_read and this_stderr_read may be None if stdout or stderr
|
|
68
|
+
# are closed. Without these checks, test_close_output fails.
|
|
69
|
+
if (
|
|
70
|
+
this_stdout_read is not None
|
|
71
|
+
and self.stdout_bytes_read < self.max_output_size
|
|
72
|
+
):
|
|
73
|
+
self.stdout_saved_bytes.append(this_stdout_read)
|
|
74
|
+
self.stdout_bytes_read += len(this_stdout_read)
|
|
75
|
+
if (
|
|
76
|
+
this_stderr_read is not None
|
|
77
|
+
and self.stderr_bytes_read < self.max_output_size
|
|
78
|
+
):
|
|
79
|
+
self.stderr_saved_bytes.append(this_stderr_read)
|
|
80
|
+
self.stderr_bytes_read += len(this_stderr_read)
|
|
81
|
+
|
|
82
|
+
self.exit_code = self.p.poll()
|
|
83
|
+
if self.exit_code is not None:
|
|
84
|
+
# Finish reading output if needed.
|
|
85
|
+
left_to_read = self.max_output_size - self.stdout_bytes_read
|
|
86
|
+
if left_to_read <= 0:
|
|
87
|
+
return False
|
|
88
|
+
this_stdout_read = self.p.stdout.read(left_to_read)
|
|
89
|
+
this_stderr_read = self.p.stderr.read(left_to_read)
|
|
90
|
+
if this_stdout_read is not None:
|
|
91
|
+
self.stdout_saved_bytes.append(this_stdout_read)
|
|
92
|
+
if this_stderr_read is not None:
|
|
93
|
+
self.stderr_saved_bytes.append(this_stderr_read)
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def terminate(self) -> Result:
|
|
98
|
+
try:
|
|
99
|
+
# Kills the process group. Without this line, test_fork_once fails.
|
|
100
|
+
os.killpg(self.process_group_id, signal.SIGKILL)
|
|
101
|
+
except ProcessLookupError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
timeout = self.exit_code is None
|
|
105
|
+
exit_code = self.exit_code if self.exit_code is not None else -1
|
|
106
|
+
stdout = b"".join(self.stdout_saved_bytes).decode("utf-8", errors="ignore")
|
|
107
|
+
stderr = b"".join(self.stderr_saved_bytes).decode("utf-8", errors="ignore")
|
|
108
|
+
return Result(
|
|
109
|
+
timeout=timeout, exit_code=exit_code, stdout=stdout, stderr=stderr
|
|
110
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bounded_subprocess
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: A library to facilitate running subprocesses that may misbehave.
|
|
5
|
+
Project-URL: Homepage, https://github.com/arjunguha/bounded_subprocess
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/arjunguha/bounded_subprocess
|
|
7
|
+
Author: Arjun Guha, Ming-Ho Yee, Francesca Lucchetti
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2022--2024 Northeastern University
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
17
|
+
License-File: LICENSE.txt
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: typeguard<5.0.0,>=4.4.2
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# bounded_subprocess
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/bounded-subprocess)
|
|
28
|
+
[](https://pypi.org/project/bounded-subprocess)
|
|
29
|
+
|
|
30
|
+
The `bounded-subprocess` module runs a subprocess with several bounds:
|
|
31
|
+
|
|
32
|
+
1. The subprocess runs in a Linux session, so the process and all its children
|
|
33
|
+
can be killed;
|
|
34
|
+
2. The subprocess runs with a given timeout; and
|
|
35
|
+
3. The parent captures a bounded amount of output from the subprocess and
|
|
36
|
+
discards the rest.
|
|
37
|
+
|
|
38
|
+
Note that the subprocess is not isolated: it can use the network, the filesystem,
|
|
39
|
+
or create new sessions.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
python3 -m pip install bounded-subprocess
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
`bounded-subprocess` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
bounded_subprocess/__init__.py,sha256=PHsdbPdGYopVQH_KG8vdpifyt8ZuCLd0h_JeMRCPNdI,855
|
|
2
|
+
bounded_subprocess/bounded_subprocess_async.py,sha256=uvwMhB0OSJ91qJXVFlIzWfNQXQzw9I8WV7RdNzNWVDc,1158
|
|
3
|
+
bounded_subprocess/interactive.py,sha256=Vn-002fH6rAekOwugGqdZUZ3O3x1wY-mLxApBtyC-DA,5016
|
|
4
|
+
bounded_subprocess/interactive_async.py,sha256=igxFfeC7zdQxX34-pQRlTNgYIpSbVFgmGYCCvpwsMPw,5085
|
|
5
|
+
bounded_subprocess/util.py,sha256=MBP34Juj-3JGJj6-XFzTaLhPZztQGk2tpONH8AvUX48,3638
|
|
6
|
+
bounded_subprocess-1.5.0.dist-info/METADATA,sha256=FA0SjIq_lciG1NrdOXjF29tCXIKAuCFO2WyiAlkwkdI,2664
|
|
7
|
+
bounded_subprocess-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
bounded_subprocess-1.5.0.dist-info/licenses/LICENSE.txt,sha256=UVerBV0_1vMFt8QkaXuVnZVSlOiKDiBSieK5MNLy4Ls,1086
|
|
9
|
+
bounded_subprocess-1.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022--2024 Northeastern University
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|