bounded_subprocess 2.2.0__tar.gz → 2.3.1__tar.gz
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-2.2.0 → bounded_subprocess-2.3.1}/PKG-INFO +1 -1
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/pyproject.toml +1 -1
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/bounded_subprocess.py +9 -3
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/bounded_subprocess_async.py +14 -7
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/util.py +55 -1
- bounded_subprocess-2.3.1/test/evil_programs/read_one_line.py +5 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/test_async.py +20 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/uv.lock +1 -1
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/.github/workflows/test.yml +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/.gitignore +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/LICENSE.txt +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/Makefile +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/README.md +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/cspell.config.yaml +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/__init__.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/interactive.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/interactive_async.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/__init__.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/block_on_inputs.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/close_outputs.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/dies_shortly_after_launch.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/dies_while_writing.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/does_not_read.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/echo_stdin.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/fork_bomb.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/fork_once.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/long_stdout.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/sleep_forever.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/unbounded_output.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/write_forever_but_no_newline.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/module_test.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/test_interactive.py +0 -0
- {bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/test_interactive_async.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bounded_subprocess
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: A library to facilitate running subprocesses that may misbehave.
|
|
5
5
|
Project-URL: Homepage, https://github.com/arjunguha/bounded_subprocess
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/arjunguha/bounded_subprocess
|
{bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/bounded_subprocess.py
RENAMED
|
@@ -7,6 +7,7 @@ from .util import (
|
|
|
7
7
|
SLEEP_BETWEEN_READS,
|
|
8
8
|
write_loop_sync,
|
|
9
9
|
_STDIN_WRITE_TIMEOUT,
|
|
10
|
+
SLEEP_BETWEEN_WRITES,
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
|
|
@@ -16,6 +17,7 @@ def run(
|
|
|
16
17
|
max_output_size: int = 2048,
|
|
17
18
|
env=None,
|
|
18
19
|
stdin_data: Optional[str] = None,
|
|
20
|
+
stdin_write_timeout: Optional[int] = None,
|
|
19
21
|
) -> Result:
|
|
20
22
|
"""
|
|
21
23
|
Runs the given program with arguments. After the timeout elapses, kills the process
|
|
@@ -24,12 +26,16 @@ def run(
|
|
|
24
26
|
"""
|
|
25
27
|
state = BoundedSubprocessState(args, env, max_output_size, stdin_data is not None)
|
|
26
28
|
if stdin_data is not None:
|
|
27
|
-
write_loop_sync(
|
|
29
|
+
ok = write_loop_sync(
|
|
28
30
|
state.write_chunk,
|
|
29
31
|
stdin_data.encode(),
|
|
30
|
-
|
|
31
|
-
sleep_interval=
|
|
32
|
+
stdin_write_timeout if stdin_write_timeout is not None else 15,
|
|
33
|
+
sleep_interval=SLEEP_BETWEEN_WRITES,
|
|
32
34
|
)
|
|
35
|
+
if not ok:
|
|
36
|
+
state.terminate()
|
|
37
|
+
return Result(True, -1, "", "failed to write to stdin")
|
|
38
|
+
|
|
33
39
|
state.close_stdin()
|
|
34
40
|
|
|
35
41
|
# We sleep for 0.1 seconds in each iteration.
|
|
@@ -4,7 +4,7 @@ from .util import (
|
|
|
4
4
|
Result,
|
|
5
5
|
BoundedSubprocessState,
|
|
6
6
|
SLEEP_BETWEEN_READS,
|
|
7
|
-
|
|
7
|
+
write_nonblocking_async,
|
|
8
8
|
_STDIN_WRITE_TIMEOUT,
|
|
9
9
|
)
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ async def run(
|
|
|
15
15
|
max_output_size: int = 2048,
|
|
16
16
|
env=None,
|
|
17
17
|
stdin_data: Optional[str] = None,
|
|
18
|
+
stdin_write_timeout: Optional[int] = None,
|
|
18
19
|
) -> Result:
|
|
19
20
|
"""
|
|
20
21
|
Runs the given program with arguments. After the timeout elapses, kills the process
|
|
@@ -27,14 +28,16 @@ async def run(
|
|
|
27
28
|
# async here? It's just the sleep between reads.
|
|
28
29
|
state = BoundedSubprocessState(args, env, max_output_size, stdin_data is not None)
|
|
29
30
|
if stdin_data is not None:
|
|
30
|
-
await
|
|
31
|
-
state.
|
|
32
|
-
stdin_data.encode(),
|
|
33
|
-
|
|
34
|
-
sleep_interval=SLEEP_BETWEEN_READS,
|
|
31
|
+
write_ok = await write_nonblocking_async(
|
|
32
|
+
fd=state.p.stdin,
|
|
33
|
+
data=stdin_data.encode(),
|
|
34
|
+
timeout_seconds=stdin_write_timeout if stdin_write_timeout is not None else 15,
|
|
35
35
|
)
|
|
36
36
|
await state.close_stdin_async(_STDIN_WRITE_TIMEOUT)
|
|
37
37
|
|
|
38
|
+
# Notice that we do not immediately terminate if the write fails. This allows
|
|
39
|
+
# us to read an error message from the process.
|
|
40
|
+
|
|
38
41
|
# We sleep for 0.1 seconds in each iteration.
|
|
39
42
|
max_iterations = timeout_seconds * 10
|
|
40
43
|
|
|
@@ -45,5 +48,9 @@ async def run(
|
|
|
45
48
|
else:
|
|
46
49
|
break
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
result = state.terminate()
|
|
52
|
+
if stdin_data is not None and not write_ok:
|
|
53
|
+
result.exit_code = -1
|
|
54
|
+
result.stderr = result.stderr + "\nFailed to write all data to subprocess."
|
|
55
|
+
return result
|
|
49
56
|
|
|
@@ -9,7 +9,8 @@ import asyncio
|
|
|
9
9
|
|
|
10
10
|
MAX_BYTES_PER_READ = 1024
|
|
11
11
|
SLEEP_BETWEEN_READS = 0.1
|
|
12
|
-
_STDIN_WRITE_TIMEOUT =
|
|
12
|
+
_STDIN_WRITE_TIMEOUT = 15
|
|
13
|
+
SLEEP_BETWEEN_WRITES = 0.01
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Result:
|
|
@@ -93,6 +94,59 @@ async def write_loop_async(
|
|
|
93
94
|
return True
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
async def can_write(fd):
|
|
98
|
+
"""
|
|
99
|
+
Waits for the file descriptor to be writable.
|
|
100
|
+
"""
|
|
101
|
+
future = asyncio.Future()
|
|
102
|
+
loop = asyncio.get_running_loop()
|
|
103
|
+
loop.add_writer(fd, future.set_result, None)
|
|
104
|
+
future.add_done_callback(lambda f: loop.remove_writer(fd))
|
|
105
|
+
await future
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def write_nonblocking_async(*, fd, data: bytes, timeout_seconds: int) -> bool:
|
|
109
|
+
"""
|
|
110
|
+
Writes to a nonblocking file descriptor with the timeout.
|
|
111
|
+
|
|
112
|
+
Returns True if all the data was written. False indicates that there was
|
|
113
|
+
either a timeout or a broken pipe.
|
|
114
|
+
"""
|
|
115
|
+
start_time_seconds = time.time()
|
|
116
|
+
|
|
117
|
+
# A slice, data[..], would create a copy. A memoryview does not.
|
|
118
|
+
mv = memoryview(data)
|
|
119
|
+
start = 0
|
|
120
|
+
while start < len(mv):
|
|
121
|
+
try:
|
|
122
|
+
# Write as much as possible without blocking.
|
|
123
|
+
written = fd.write(mv[start:])
|
|
124
|
+
if written is None:
|
|
125
|
+
written = 0
|
|
126
|
+
start = start + written
|
|
127
|
+
except BrokenPipeError:
|
|
128
|
+
return False
|
|
129
|
+
except BlockingIOError as exn:
|
|
130
|
+
if exn.errno != errno.EAGAIN:
|
|
131
|
+
# NOTE(arjun): I am not certain why this would happen. However,
|
|
132
|
+
# you are only supposed to retry on EAGAIN.
|
|
133
|
+
return False
|
|
134
|
+
# Some, but not all the bytes were written.
|
|
135
|
+
start = start + exn.characters_written
|
|
136
|
+
|
|
137
|
+
# Compute how much more time we have left.
|
|
138
|
+
wait_timeout = timeout_seconds - (time.time() - start_time_seconds)
|
|
139
|
+
# We are already past the deadline, so abort.
|
|
140
|
+
if wait_timeout <= 0:
|
|
141
|
+
return False
|
|
142
|
+
try:
|
|
143
|
+
await asyncio.wait_for(can_write(fd), wait_timeout)
|
|
144
|
+
except asyncio.TimeoutError:
|
|
145
|
+
# Deadline elapsed, so abort.
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
return True
|
|
149
|
+
|
|
96
150
|
class BoundedSubprocessState:
|
|
97
151
|
"""State shared between synchronous and asynchronous subprocess helpers."""
|
|
98
152
|
|
|
@@ -103,3 +103,23 @@ async def test_stdin_data_async_echo():
|
|
|
103
103
|
assert result.timeout is False
|
|
104
104
|
assert result.stdout == data
|
|
105
105
|
await assert_no_running_evil()
|
|
106
|
+
|
|
107
|
+
@pytest.mark.asyncio
|
|
108
|
+
async def test_read_one_line():
|
|
109
|
+
"""
|
|
110
|
+
The test program reads just one line of input, but we are trying to send
|
|
111
|
+
two. The program still runs and prints. It runs for longer than the
|
|
112
|
+
stdin_write_timeout, but shorter than timeout_seconds. However, we still
|
|
113
|
+
get -1 as the exit_code because it did not receive the entire input.
|
|
114
|
+
"""
|
|
115
|
+
result = await run(
|
|
116
|
+
["python3", ROOT / "read_one_line.py"],
|
|
117
|
+
timeout_seconds=30,
|
|
118
|
+
max_output_size=1024,
|
|
119
|
+
stdin_data="Line 1\n" + ("x" * 128 * 1024),
|
|
120
|
+
stdin_write_timeout=3,
|
|
121
|
+
)
|
|
122
|
+
assert result.exit_code == -1
|
|
123
|
+
assert result.timeout is False
|
|
124
|
+
assert result.stdout == "I read one line\n"
|
|
125
|
+
await assert_no_running_evil()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/src/bounded_subprocess/interactive_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/dies_while_writing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bounded_subprocess-2.2.0 → bounded_subprocess-2.3.1}/test/evil_programs/unbounded_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|