bounded_subprocess 2.4.0__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.
Files changed (38) hide show
  1. bounded_subprocess-2.4.0/.git +1 -0
  2. bounded_subprocess-2.4.0/.github/workflows/docs.yaml +29 -0
  3. bounded_subprocess-2.4.0/.github/workflows/test.yml +19 -0
  4. bounded_subprocess-2.4.0/.gitignore +5 -0
  5. bounded_subprocess-2.4.0/AGENTS.md +5 -0
  6. bounded_subprocess-2.4.0/LICENSE.txt +9 -0
  7. bounded_subprocess-2.4.0/Makefile +13 -0
  8. bounded_subprocess-2.4.0/PKG-INFO +51 -0
  9. bounded_subprocess-2.4.0/README.md +27 -0
  10. bounded_subprocess-2.4.0/cspell.config.yaml +23 -0
  11. bounded_subprocess-2.4.0/docs/index.md +11 -0
  12. bounded_subprocess-2.4.0/mkdocs.yaml +67 -0
  13. bounded_subprocess-2.4.0/pyproject.toml +45 -0
  14. bounded_subprocess-2.4.0/src/bounded_subprocess/__init__.py +31 -0
  15. bounded_subprocess-2.4.0/src/bounded_subprocess/bounded_subprocess.py +122 -0
  16. bounded_subprocess-2.4.0/src/bounded_subprocess/bounded_subprocess_async.py +274 -0
  17. bounded_subprocess-2.4.0/src/bounded_subprocess/interactive.py +170 -0
  18. bounded_subprocess-2.4.0/src/bounded_subprocess/interactive_async.py +91 -0
  19. bounded_subprocess-2.4.0/src/bounded_subprocess/util.py +296 -0
  20. bounded_subprocess-2.4.0/test/__init__.py +3 -0
  21. bounded_subprocess-2.4.0/test/evil_programs/block_on_inputs.py +2 -0
  22. bounded_subprocess-2.4.0/test/evil_programs/close_outputs.py +7 -0
  23. bounded_subprocess-2.4.0/test/evil_programs/dies_shortly_after_launch.py +5 -0
  24. bounded_subprocess-2.4.0/test/evil_programs/dies_while_writing.py +7 -0
  25. bounded_subprocess-2.4.0/test/evil_programs/does_not_read.py +4 -0
  26. bounded_subprocess-2.4.0/test/evil_programs/echo_stdin.py +4 -0
  27. bounded_subprocess-2.4.0/test/evil_programs/fork_bomb.py +4 -0
  28. bounded_subprocess-2.4.0/test/evil_programs/fork_once.py +6 -0
  29. bounded_subprocess-2.4.0/test/evil_programs/long_stdout.py +1 -0
  30. bounded_subprocess-2.4.0/test/evil_programs/read_one_line.py +6 -0
  31. bounded_subprocess-2.4.0/test/evil_programs/sleep_forever.py +4 -0
  32. bounded_subprocess-2.4.0/test/evil_programs/unbounded_output.py +4 -0
  33. bounded_subprocess-2.4.0/test/evil_programs/write_forever_but_no_newline.py +2 -0
  34. bounded_subprocess-2.4.0/test/module_test.py +180 -0
  35. bounded_subprocess-2.4.0/test/test_async.py +173 -0
  36. bounded_subprocess-2.4.0/test/test_interactive.py +123 -0
  37. bounded_subprocess-2.4.0/test/test_interactive_async.py +136 -0
  38. bounded_subprocess-2.4.0/uv.lock +982 -0
@@ -0,0 +1 @@
1
+ gitdir: /media/external0/arjun-nosudo/repos/arjunguha/bounded_subprocess/bare/worktrees/main
@@ -0,0 +1,29 @@
1
+ name: Documentation
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ jobs:
7
+ docs:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v5
18
+ - name: Install project
19
+ run: uv sync --locked --all-extras --dev
20
+ - name: Build MkDocs site
21
+ run: make docs
22
+ - name: Upload GitHub Pages artifact
23
+ uses: actions/upload-pages-artifact@v3
24
+ with:
25
+ path: ./site
26
+ - name: Deploy to GitHub Pages
27
+ uses: actions/deploy-pages@v4
28
+ with:
29
+ token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,19 @@
1
+ name: Test
2
+ on:
3
+ push:
4
+ branches: [ "main" ]
5
+ pull_request:
6
+ branches: [ "main" ]
7
+ permissions:
8
+ contents: read
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v5
16
+ - name: Install the project
17
+ run: uv sync --locked --all-extras --dev
18
+ - name: Run tests
19
+ run: uv run python -m pytest -m "not unsafe"
@@ -0,0 +1,5 @@
1
+ *.pyc
2
+ __pycache__
3
+ /.venv
4
+ /dist
5
+ /bounded_subprocess.egg-info
@@ -0,0 +1,5 @@
1
+ ## Publishing Process
2
+
3
+ Increment the version number as appropriate in pyproject.toml. Delete old
4
+ builds from dist/. Run uv sync to update the lock file. Commit the changes,
5
+ which should have just changes to pyproject.taml and uv.lock.
@@ -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.
@@ -0,0 +1,13 @@
1
+ .PHONY: test build publish docs
2
+
3
+ build:
4
+ uv build
5
+
6
+ publish:
7
+ uv publish
8
+
9
+ test:
10
+ uv run python -m pytest -m "not unsafe"
11
+
12
+ docs:
13
+ uv run mkdocs build
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: bounded_subprocess
3
+ Version: 2.4.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
+ [![PyPI - Version](https://img.shields.io/pypi/v/bounded-subprocess.svg)](https://pypi.org/project/bounded-subprocess)
28
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bounded-subprocess.svg)](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
+ - Documentation: https://arjunguha.github.io/bounded_subprocess/
42
+
43
+ ## Installation
44
+
45
+ ```console
46
+ python3 -m pip install bounded-subprocess
47
+ ```
48
+
49
+ ## License
50
+
51
+ `bounded-subprocess` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,27 @@
1
+ # bounded_subprocess
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/bounded-subprocess.svg)](https://pypi.org/project/bounded-subprocess)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bounded-subprocess.svg)](https://pypi.org/project/bounded-subprocess)
5
+
6
+ The `bounded-subprocess` module runs a subprocess with several bounds:
7
+
8
+ 1. The subprocess runs in a Linux session, so the process and all its children
9
+ can be killed;
10
+ 2. The subprocess runs with a given timeout; and
11
+ 3. The parent captures a bounded amount of output from the subprocess and
12
+ discards the rest.
13
+
14
+ Note that the subprocess is not isolated: it can use the network, the filesystem,
15
+ or create new sessions.
16
+
17
+ - Documentation: https://arjunguha.github.io/bounded_subprocess/
18
+
19
+ ## Installation
20
+
21
+ ```console
22
+ python3 -m pip install bounded-subprocess
23
+ ```
24
+
25
+ ## License
26
+
27
+ `bounded-subprocess` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,23 @@
1
+ version: "0.2"
2
+ ignorePaths: []
3
+ dictionaryDefinitions: []
4
+ dictionaries: []
5
+ words:
6
+ - killpg
7
+ - nonblocking
8
+ - pgrep
9
+ - GETFL
10
+ - SETFL
11
+ - NONBLOCK
12
+ - getpgid
13
+ - typeguard
14
+ - tobytes
15
+ - Arjun
16
+ - Guha
17
+ - pytest
18
+ - Lucchetti
19
+ - pythonpath
20
+ - asyncio
21
+ - bufs
22
+ ignoreWords: []
23
+ import: []
@@ -0,0 +1,11 @@
1
+ # bounded_subprocess
2
+
3
+ ::: bounded_subprocess
4
+
5
+ ::: bounded_subprocess.bounded_subprocess
6
+
7
+ ::: bounded_subprocess.bounded_subprocess_async
8
+
9
+ ::: bounded_subprocess.interactive
10
+
11
+ ::: bounded_subprocess.util
@@ -0,0 +1,67 @@
1
+ site_name: bounded_subprocess
2
+ site_description: Bounded subprocess execution with timeout and output limits
3
+
4
+ theme:
5
+ name: material
6
+ locale: en
7
+ palette:
8
+ - media: "(prefers-color-scheme: light)"
9
+ scheme: default
10
+ primary: indigo
11
+ accent: indigo
12
+ toggle:
13
+ icon: material/brightness-7
14
+ name: Switch to dark mode
15
+ - media: "(prefers-color-scheme: dark)"
16
+ scheme: slate
17
+ primary: indigo
18
+ accent: indigo
19
+ toggle:
20
+ icon: material/brightness-4
21
+ name: Switch to light mode
22
+ features:
23
+ - toc.integrate
24
+ - navigation.tabs
25
+ - navigation.sections
26
+ - navigation.expand
27
+ - navigation.top
28
+ - search.highlight
29
+ - search.share
30
+
31
+ markdown_extensions:
32
+ - pymdownx.highlight:
33
+ anchor_linenums: true
34
+ - pymdownx.superfences
35
+ - pymdownx.tabbed:
36
+ alternate_style: true
37
+ - admonition
38
+ - pymdownx.details
39
+ - attr_list
40
+ - md_in_html
41
+
42
+ plugins:
43
+ - search
44
+ - mkdocstrings:
45
+ handlers:
46
+ python:
47
+ paths: ["src"]
48
+ options:
49
+ show_signature_annotations: true
50
+ show_root_heading: true
51
+ heading_level: 2
52
+ docstring_style: google
53
+ merge_init_into_class: true
54
+
55
+ nav:
56
+ - Home: index.md
57
+
58
+ repo_url: https://github.com/arjunguha/bounded_subprocess
59
+ repo_name: arjunguha/bounded_subprocess
60
+ edit_uri: edit/main/docs/
61
+
62
+ extra:
63
+ social:
64
+ - icon: fontawesome/brands/github
65
+ link: https://github.com/arjunguha/bounded_subprocess
66
+ - icon: fontawesome/brands/python
67
+ link: https://pypi.org/project/bounded-subprocess/
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "bounded_subprocess"
3
+ version = "2.4.0"
4
+ authors = [
5
+ { name="Arjun Guha" },
6
+ { name="Ming-Ho Yee" },
7
+ { name="Francesca Lucchetti" }
8
+ ]
9
+ description = "A library to facilitate running subprocesses that may misbehave."
10
+ readme = "README.md"
11
+ license = { file="LICENSE.txt" }
12
+ requires-python = ">=3.9"
13
+ dependencies =[
14
+ "typeguard>=4.4.2,<5.0.0"
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX :: Linux",
20
+ ]
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [tool.pytest.ini_options]
27
+ pythonpath = ["src"]
28
+ markers = [
29
+ "unsafe: Tests that may crash the machine on failure",
30
+ ]
31
+ asyncio_default_fixture_loop_scope = "function"
32
+
33
+ [project.urls]
34
+ "Homepage" = "https://github.com/arjunguha/bounded_subprocess"
35
+ "Bug Tracker" = "https://github.com/arjunguha/bounded_subprocess"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest>=8.3.5",
40
+ "pytest-asyncio>=1.0.0",
41
+ "pytest-timeout>=2.4.0",
42
+ "mkdocs>=1.6.1",
43
+ "mkdocs-material>=9.6.15",
44
+ "mkdocstrings[python]>=0.29.1",
45
+ ]
@@ -0,0 +1,31 @@
1
+ """
2
+ Bounded subprocess execution with timeout and output limits.
3
+
4
+ This package provides convenient functions for running subprocesses with bounded
5
+ execution time and output size, with support for both synchronous and asynchronous
6
+ execution patterns.
7
+ """
8
+
9
+ __version__ = "1.0.0"
10
+
11
+
12
+ # Lazy imports for better startup performance
13
+ def __getattr__(name):
14
+ if name == "run":
15
+ from .bounded_subprocess import run
16
+
17
+ return run
18
+ elif name == "Result":
19
+ from .util import Result
20
+
21
+ return Result
22
+ elif name == "SLEEP_BETWEEN_READS":
23
+ from .util import SLEEP_BETWEEN_READS
24
+
25
+ return SLEEP_BETWEEN_READS
26
+ else:
27
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
28
+
29
+
30
+ # Expose key classes and constants for convenience
31
+ __all__ = ["run", "Result", "SLEEP_BETWEEN_READS"]
@@ -0,0 +1,122 @@
1
+ """
2
+ Synchronous subprocess execution with bounds on runtime and output size.
3
+ """
4
+
5
+ import subprocess
6
+ import os
7
+ import signal
8
+ from typing import List, Optional
9
+ import time
10
+
11
+ from .util import (
12
+ Result,
13
+ set_nonblocking,
14
+ MAX_BYTES_PER_READ,
15
+ write_nonblocking_sync,
16
+ read_to_eof_sync,
17
+ )
18
+
19
+
20
+ def run(
21
+ args: List[str],
22
+ timeout_seconds: int = 15,
23
+ max_output_size: int = 2048,
24
+ env=None,
25
+ stdin_data: Optional[str] = None,
26
+ stdin_write_timeout: Optional[int] = None,
27
+ ) -> Result:
28
+ """
29
+ Run a subprocess with a timeout and bounded stdout/stderr capture.
30
+
31
+ This helper starts the child in a new session so timeout cleanup can kill
32
+ the entire process group. Stdout and stderr are read in nonblocking mode and
33
+ truncated to `max_output_size` bytes each. If the timeout elapses, the
34
+ returned `Result.timeout` is True and `Result.exit_code` is -1. If
35
+ `stdin_data` cannot be fully written before `stdin_write_timeout`,
36
+ `Result.exit_code` is set to -1 even if the process exits normally.
37
+
38
+ Example:
39
+
40
+ ```python
41
+ from bounded_subprocess import run
42
+
43
+ result = run(
44
+ ["bash", "-lc", "echo ok; echo err 1>&2"],
45
+ timeout_seconds=5,
46
+ max_output_size=1024,
47
+ )
48
+ print(result.exit_code)
49
+ print(result.stdout.strip())
50
+ print(result.stderr.strip())
51
+ ```
52
+ """
53
+ deadline = time.time() + timeout_seconds
54
+
55
+ p = subprocess.Popen(
56
+ args,
57
+ env=env,
58
+ stdin=subprocess.PIPE if stdin_data is not None else subprocess.DEVNULL,
59
+ stdout=subprocess.PIPE,
60
+ stderr=subprocess.PIPE,
61
+ start_new_session=True,
62
+ bufsize=MAX_BYTES_PER_READ,
63
+ )
64
+ process_group_id = os.getpgid(p.pid)
65
+
66
+ set_nonblocking(p.stdout)
67
+ set_nonblocking(p.stderr)
68
+
69
+ if stdin_data is not None:
70
+ set_nonblocking(p.stdin)
71
+ write_ok = write_nonblocking_sync(
72
+ fd=p.stdin,
73
+ data=stdin_data.encode(),
74
+ timeout_seconds=stdin_write_timeout
75
+ if stdin_write_timeout is not None
76
+ else 15,
77
+ )
78
+ # From what I recall, closing stdin is not necessary, but is customary.
79
+ try:
80
+ p.stdin.close()
81
+ except (BrokenPipeError, BlockingIOError):
82
+ pass
83
+
84
+ bufs = read_to_eof_sync(
85
+ [p.stdout, p.stderr],
86
+ timeout_seconds=timeout_seconds,
87
+ max_len=max_output_size,
88
+ )
89
+
90
+ # Without this, even the trivial test fails on Linux but not on macOS. It
91
+ # seems possible for (1) both stdout and stderr to close (2) before the child
92
+ # process exits, and we can observe the instant between (1) and (2). So, we
93
+ # need to p.wait and not p.poll.
94
+ #
95
+ # Reading the above, we should be able to write a test case that just closes
96
+ # both stdout and stderr explicitly, and then sleeps for an instant before
97
+ # terminating normally. That program should not timeout.
98
+ try:
99
+ exit_code = p.wait(timeout=max(0, deadline - time.time()))
100
+ is_timeout = False
101
+ except subprocess.TimeoutExpired:
102
+ exit_code = None
103
+ is_timeout = True
104
+
105
+ try:
106
+ # Kills the process group. Without this line, test_fork_once fails.
107
+ os.killpg(process_group_id, signal.SIGKILL)
108
+ except ProcessLookupError:
109
+ pass
110
+
111
+ # Even if the process terminates normally, if we failed to write everything to
112
+ # stdin, we return -1 as the exit code.
113
+ exit_code = (
114
+ -1 if is_timeout or (stdin_data is not None and not write_ok) else exit_code
115
+ )
116
+
117
+ return Result(
118
+ timeout=is_timeout,
119
+ exit_code=exit_code,
120
+ stdout=bufs[0].decode(errors="ignore"),
121
+ stderr=bufs[1].decode(errors="ignore"),
122
+ )