profiler-runit 0.1.4.post1.dev9__tar.gz → 0.2.0.post1.dev5__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.
- profiler_runit-0.2.0.post1.dev5/CHANGES.md +10 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/PKG-INFO +28 -7
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/README.md +23 -5
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/PKG-INFO +28 -7
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/SOURCES.txt +3 -0
- profiler_runit-0.2.0.post1.dev5/profiler_runit.egg-info/requires.txt +2 -0
- profiler_runit-0.2.0.post1.dev5/profiler_runit.egg-info/scm_file_list.json +23 -0
- profiler_runit-0.2.0.post1.dev5/profiler_runit.egg-info/scm_version.json +8 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/pyproject.toml +6 -1
- profiler_runit-0.2.0.post1.dev5/requirements.txt +2 -0
- profiler_runit-0.2.0.post1.dev5/runit/cli.py +132 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/monitor.py +44 -3
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/plotting.py +7 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/report.py +2 -1
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/utils.py +24 -0
- profiler_runit-0.2.0.post1.dev5/test.py +446 -0
- profiler_runit-0.1.4.post1.dev9/profiler_runit.egg-info/requires.txt +0 -2
- profiler_runit-0.1.4.post1.dev9/requirements.txt +0 -2
- profiler_runit-0.1.4.post1.dev9/runit/cli.py +0 -89
- profiler_runit-0.1.4.post1.dev9/test.py +0 -230
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/.github/workflows/ci.yml +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/.github/workflows/release.yml +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/.github/workflows/testpypi.yml +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/.gitignore +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/LICENSE +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/assets/runit-demo.gif +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/benchmark.py +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/dependency_links.txt +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/entry_points.txt +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/top_level.txt +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/__init__.py +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/runit/logging_config.py +0 -0
- {profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/setup.cfg +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changes
|
|
2
|
+
|
|
3
|
+
## 0.2.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
- Fixed: `--out-file` no longer crashes the output thread or kills the command with a broken pipe. All output of the command is shown and captured.
|
|
6
|
+
- Fixed: output larger than the pipe buffer (about 64 KB) no longer makes runit hang. This was fixed on main by streaming stdout and stderr (PR #1). The 0.1.4 release still has the hang.
|
|
7
|
+
- Changed (new in 0.2.0): runit exits with the exit code of the command (128 + N for signal N, 127 when the command is not found, 126 when it cannot be executed, also for a script without a `#!` line). Before, it exited with 0; use `runit ... || true` if a script relies on that. The report's `Exit Code` shows the same value, with the signal name for a signal (for example `143 (signal 15, SIGTERM)`).
|
|
8
|
+
- Fixed: an `--out-file` that cannot be written, or a full stdout, now gives one error line instead of a traceback. For a full stdout, runit keeps the command's exit code. For an unwritable `--out-file`, a non-zero exit code of the command wins; otherwise runit exits 1. `--out-file` is written before the report goes to the terminal. When the stdout reader goes away (`runit yes | head`), runit stops quietly.
|
|
9
|
+
- Changed: the plotext dependency is pinned to `plotext>=5.3,<6`. plotext 6 replaced the API that `--plot` uses, so charts were silently skipped, and plotext 5.2 draws empty charts. runit now logs a clear warning if an unsupported plotext is installed.
|
|
10
|
+
- Tests: added regression tests that run the installed `runit` command for these cases.
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: profiler-runit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0.post1.dev5
|
|
4
4
|
Summary: Execute and profile cli commands
|
|
5
5
|
Author-email: NotesOfCliff <notesofcliff@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/McIndi/runit
|
|
8
|
+
Project-URL: Repository, https://github.com/McIndi/runit
|
|
9
|
+
Project-URL: Issues, https://github.com/McIndi/runit/issues
|
|
7
10
|
Requires-Python: >=3.9
|
|
8
11
|
Description-Content-Type: text/markdown
|
|
9
12
|
License-File: LICENSE
|
|
10
13
|
Requires-Dist: psutil
|
|
11
|
-
Requires-Dist: plotext
|
|
14
|
+
Requires-Dist: plotext<6,>=5.3
|
|
12
15
|
Dynamic: license-file
|
|
13
16
|
|
|
14
17
|
# runit
|
|
@@ -39,14 +42,14 @@ See `runit` in action:
|
|
|
39
42
|
## Requirements
|
|
40
43
|
* Python 3.9+
|
|
41
44
|
* psutil
|
|
42
|
-
* plotext
|
|
45
|
+
* plotext 5.x (`plotext>=5.3,<6`). plotext 6 replaced the plotting API that runit uses, so it is not supported yet.
|
|
43
46
|
|
|
44
47
|
## Installation
|
|
45
48
|
|
|
46
49
|
Clone this repository and install with pip:
|
|
47
50
|
|
|
48
51
|
```sh
|
|
49
|
-
git clone https://github.com/
|
|
52
|
+
git clone https://github.com/McIndi/runit.git
|
|
50
53
|
cd runit
|
|
51
54
|
python -m venv .venv
|
|
52
55
|
source .venv/bin/activate # Or (for Windows) .venv/Scripts/activate
|
|
@@ -59,7 +62,7 @@ python test.py
|
|
|
59
62
|
Or, install directly from GitHub:
|
|
60
63
|
|
|
61
64
|
```sh
|
|
62
|
-
pip install git+https://github.com/
|
|
65
|
+
pip install git+https://github.com/McIndi/runit.git
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
After installation, use `runit` to run and analyze any command:
|
|
@@ -91,6 +94,7 @@ PID: 12345
|
|
|
91
94
|
Start Time: 2025-07-31 12:00:00.000000
|
|
92
95
|
End Time: 2025-07-31 12:00:01.000000
|
|
93
96
|
Duration: 0:00:01
|
|
97
|
+
Exit Code: 0
|
|
94
98
|
Max RSS (bytes): 12345678
|
|
95
99
|
Max Threads: 4
|
|
96
100
|
Max Children: 0
|
|
@@ -110,12 +114,15 @@ runit python benchmark.py
|
|
|
110
114
|
## CLI Options
|
|
111
115
|
|
|
112
116
|
- `--log-level LEVEL`: Set the logging level (default: INFO)
|
|
113
|
-
- `--out-file FILE`:
|
|
117
|
+
- `--out-file FILE`: Also write the report and charts to a file (they are still shown in the terminal)
|
|
114
118
|
- `--strip-ansi`: Remove ANSI escape codes from output file (for plain text files)
|
|
115
119
|
- `--plot`: If specified, attempt to plot CPU and memory usage
|
|
120
|
+
- `--plot-width N`, `--plot-height N`: Set the chart size (default: 80 x 20)
|
|
116
121
|
- `command`: The command to run and monitor
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
The output of the command goes to the terminal while it runs (stdout to stdout, stderr to stderr), with or without `--out-file`. When the command ends, runit shows the report, which also contains the captured stdout and stderr.
|
|
124
|
+
|
|
125
|
+
When using `--out-file`, the report and charts are both displayed in the terminal and written to the file. For plain text (no colors or terminal formatting), simply add `--strip-ansi`.
|
|
119
126
|
|
|
120
127
|
```sh
|
|
121
128
|
runit --out-file out.txt --strip-ansi python benchmark.py
|
|
@@ -123,6 +130,20 @@ runit --out-file out.txt --strip-ansi python benchmark.py
|
|
|
123
130
|
|
|
124
131
|
You'll see the report and charts in your terminal, and a plain text version in `out.txt`.
|
|
125
132
|
|
|
133
|
+
## Exit Status
|
|
134
|
+
|
|
135
|
+
runit exits with the exit code of the command, so you can use it in scripts and CI:
|
|
136
|
+
|
|
137
|
+
- The exit code of the command (for example, `runit false` exits with 1).
|
|
138
|
+
- 128 + N when the command is stopped by signal N (for example, 143 for SIGTERM).
|
|
139
|
+
- 127 when the command is not found, and 126 when it cannot be executed (for example, no permission, or a script without a `#!` line).
|
|
140
|
+
|
|
141
|
+
The `Exit Code:` line of the report shows the same value that runit exits with. For a signal, it also shows the signal, for example `Exit Code: 143 (signal 15, SIGTERM)`.
|
|
142
|
+
|
|
143
|
+
If runit cannot write the `--out-file` file, it logs one error line. If the command failed, runit exits with the exit code of the command; if the command exited with 0, runit exits with 1. If runit cannot write the report to stdout (for example, a full disk), it logs one error line and exits with the exit code of the command. If the reader of stdout goes away (for example, `runit yes | head`), runit stops quietly. The `--out-file` file is written before the report goes to the terminal.
|
|
144
|
+
|
|
145
|
+
This behavior is new in 0.2.0. Releases up to 0.1.4 always exited with 0. If a script relies on that, use `runit ... || true`.
|
|
146
|
+
|
|
126
147
|
## Development & Extending
|
|
127
148
|
|
|
128
149
|
### Project Structure
|
|
@@ -26,14 +26,14 @@ See `runit` in action:
|
|
|
26
26
|
## Requirements
|
|
27
27
|
* Python 3.9+
|
|
28
28
|
* psutil
|
|
29
|
-
* plotext
|
|
29
|
+
* plotext 5.x (`plotext>=5.3,<6`). plotext 6 replaced the plotting API that runit uses, so it is not supported yet.
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
32
32
|
|
|
33
33
|
Clone this repository and install with pip:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
git clone https://github.com/
|
|
36
|
+
git clone https://github.com/McIndi/runit.git
|
|
37
37
|
cd runit
|
|
38
38
|
python -m venv .venv
|
|
39
39
|
source .venv/bin/activate # Or (for Windows) .venv/Scripts/activate
|
|
@@ -46,7 +46,7 @@ python test.py
|
|
|
46
46
|
Or, install directly from GitHub:
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
pip install git+https://github.com/
|
|
49
|
+
pip install git+https://github.com/McIndi/runit.git
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
After installation, use `runit` to run and analyze any command:
|
|
@@ -78,6 +78,7 @@ PID: 12345
|
|
|
78
78
|
Start Time: 2025-07-31 12:00:00.000000
|
|
79
79
|
End Time: 2025-07-31 12:00:01.000000
|
|
80
80
|
Duration: 0:00:01
|
|
81
|
+
Exit Code: 0
|
|
81
82
|
Max RSS (bytes): 12345678
|
|
82
83
|
Max Threads: 4
|
|
83
84
|
Max Children: 0
|
|
@@ -97,12 +98,15 @@ runit python benchmark.py
|
|
|
97
98
|
## CLI Options
|
|
98
99
|
|
|
99
100
|
- `--log-level LEVEL`: Set the logging level (default: INFO)
|
|
100
|
-
- `--out-file FILE`:
|
|
101
|
+
- `--out-file FILE`: Also write the report and charts to a file (they are still shown in the terminal)
|
|
101
102
|
- `--strip-ansi`: Remove ANSI escape codes from output file (for plain text files)
|
|
102
103
|
- `--plot`: If specified, attempt to plot CPU and memory usage
|
|
104
|
+
- `--plot-width N`, `--plot-height N`: Set the chart size (default: 80 x 20)
|
|
103
105
|
- `command`: The command to run and monitor
|
|
104
106
|
|
|
105
|
-
|
|
107
|
+
The output of the command goes to the terminal while it runs (stdout to stdout, stderr to stderr), with or without `--out-file`. When the command ends, runit shows the report, which also contains the captured stdout and stderr.
|
|
108
|
+
|
|
109
|
+
When using `--out-file`, the report and charts are both displayed in the terminal and written to the file. For plain text (no colors or terminal formatting), simply add `--strip-ansi`.
|
|
106
110
|
|
|
107
111
|
```sh
|
|
108
112
|
runit --out-file out.txt --strip-ansi python benchmark.py
|
|
@@ -110,6 +114,20 @@ runit --out-file out.txt --strip-ansi python benchmark.py
|
|
|
110
114
|
|
|
111
115
|
You'll see the report and charts in your terminal, and a plain text version in `out.txt`.
|
|
112
116
|
|
|
117
|
+
## Exit Status
|
|
118
|
+
|
|
119
|
+
runit exits with the exit code of the command, so you can use it in scripts and CI:
|
|
120
|
+
|
|
121
|
+
- The exit code of the command (for example, `runit false` exits with 1).
|
|
122
|
+
- 128 + N when the command is stopped by signal N (for example, 143 for SIGTERM).
|
|
123
|
+
- 127 when the command is not found, and 126 when it cannot be executed (for example, no permission, or a script without a `#!` line).
|
|
124
|
+
|
|
125
|
+
The `Exit Code:` line of the report shows the same value that runit exits with. For a signal, it also shows the signal, for example `Exit Code: 143 (signal 15, SIGTERM)`.
|
|
126
|
+
|
|
127
|
+
If runit cannot write the `--out-file` file, it logs one error line. If the command failed, runit exits with the exit code of the command; if the command exited with 0, runit exits with 1. If runit cannot write the report to stdout (for example, a full disk), it logs one error line and exits with the exit code of the command. If the reader of stdout goes away (for example, `runit yes | head`), runit stops quietly. The `--out-file` file is written before the report goes to the terminal.
|
|
128
|
+
|
|
129
|
+
This behavior is new in 0.2.0. Releases up to 0.1.4 always exited with 0. If a script relies on that, use `runit ... || true`.
|
|
130
|
+
|
|
113
131
|
## Development & Extending
|
|
114
132
|
|
|
115
133
|
### Project Structure
|
{profiler_runit-0.1.4.post1.dev9 → profiler_runit-0.2.0.post1.dev5}/profiler_runit.egg-info/PKG-INFO
RENAMED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: profiler-runit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0.post1.dev5
|
|
4
4
|
Summary: Execute and profile cli commands
|
|
5
5
|
Author-email: NotesOfCliff <notesofcliff@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/McIndi/runit
|
|
8
|
+
Project-URL: Repository, https://github.com/McIndi/runit
|
|
9
|
+
Project-URL: Issues, https://github.com/McIndi/runit/issues
|
|
7
10
|
Requires-Python: >=3.9
|
|
8
11
|
Description-Content-Type: text/markdown
|
|
9
12
|
License-File: LICENSE
|
|
10
13
|
Requires-Dist: psutil
|
|
11
|
-
Requires-Dist: plotext
|
|
14
|
+
Requires-Dist: plotext<6,>=5.3
|
|
12
15
|
Dynamic: license-file
|
|
13
16
|
|
|
14
17
|
# runit
|
|
@@ -39,14 +42,14 @@ See `runit` in action:
|
|
|
39
42
|
## Requirements
|
|
40
43
|
* Python 3.9+
|
|
41
44
|
* psutil
|
|
42
|
-
* plotext
|
|
45
|
+
* plotext 5.x (`plotext>=5.3,<6`). plotext 6 replaced the plotting API that runit uses, so it is not supported yet.
|
|
43
46
|
|
|
44
47
|
## Installation
|
|
45
48
|
|
|
46
49
|
Clone this repository and install with pip:
|
|
47
50
|
|
|
48
51
|
```sh
|
|
49
|
-
git clone https://github.com/
|
|
52
|
+
git clone https://github.com/McIndi/runit.git
|
|
50
53
|
cd runit
|
|
51
54
|
python -m venv .venv
|
|
52
55
|
source .venv/bin/activate # Or (for Windows) .venv/Scripts/activate
|
|
@@ -59,7 +62,7 @@ python test.py
|
|
|
59
62
|
Or, install directly from GitHub:
|
|
60
63
|
|
|
61
64
|
```sh
|
|
62
|
-
pip install git+https://github.com/
|
|
65
|
+
pip install git+https://github.com/McIndi/runit.git
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
After installation, use `runit` to run and analyze any command:
|
|
@@ -91,6 +94,7 @@ PID: 12345
|
|
|
91
94
|
Start Time: 2025-07-31 12:00:00.000000
|
|
92
95
|
End Time: 2025-07-31 12:00:01.000000
|
|
93
96
|
Duration: 0:00:01
|
|
97
|
+
Exit Code: 0
|
|
94
98
|
Max RSS (bytes): 12345678
|
|
95
99
|
Max Threads: 4
|
|
96
100
|
Max Children: 0
|
|
@@ -110,12 +114,15 @@ runit python benchmark.py
|
|
|
110
114
|
## CLI Options
|
|
111
115
|
|
|
112
116
|
- `--log-level LEVEL`: Set the logging level (default: INFO)
|
|
113
|
-
- `--out-file FILE`:
|
|
117
|
+
- `--out-file FILE`: Also write the report and charts to a file (they are still shown in the terminal)
|
|
114
118
|
- `--strip-ansi`: Remove ANSI escape codes from output file (for plain text files)
|
|
115
119
|
- `--plot`: If specified, attempt to plot CPU and memory usage
|
|
120
|
+
- `--plot-width N`, `--plot-height N`: Set the chart size (default: 80 x 20)
|
|
116
121
|
- `command`: The command to run and monitor
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
The output of the command goes to the terminal while it runs (stdout to stdout, stderr to stderr), with or without `--out-file`. When the command ends, runit shows the report, which also contains the captured stdout and stderr.
|
|
124
|
+
|
|
125
|
+
When using `--out-file`, the report and charts are both displayed in the terminal and written to the file. For plain text (no colors or terminal formatting), simply add `--strip-ansi`.
|
|
119
126
|
|
|
120
127
|
```sh
|
|
121
128
|
runit --out-file out.txt --strip-ansi python benchmark.py
|
|
@@ -123,6 +130,20 @@ runit --out-file out.txt --strip-ansi python benchmark.py
|
|
|
123
130
|
|
|
124
131
|
You'll see the report and charts in your terminal, and a plain text version in `out.txt`.
|
|
125
132
|
|
|
133
|
+
## Exit Status
|
|
134
|
+
|
|
135
|
+
runit exits with the exit code of the command, so you can use it in scripts and CI:
|
|
136
|
+
|
|
137
|
+
- The exit code of the command (for example, `runit false` exits with 1).
|
|
138
|
+
- 128 + N when the command is stopped by signal N (for example, 143 for SIGTERM).
|
|
139
|
+
- 127 when the command is not found, and 126 when it cannot be executed (for example, no permission, or a script without a `#!` line).
|
|
140
|
+
|
|
141
|
+
The `Exit Code:` line of the report shows the same value that runit exits with. For a signal, it also shows the signal, for example `Exit Code: 143 (signal 15, SIGTERM)`.
|
|
142
|
+
|
|
143
|
+
If runit cannot write the `--out-file` file, it logs one error line. If the command failed, runit exits with the exit code of the command; if the command exited with 0, runit exits with 1. If runit cannot write the report to stdout (for example, a full disk), it logs one error line and exits with the exit code of the command. If the reader of stdout goes away (for example, `runit yes | head`), runit stops quietly. The `--out-file` file is written before the report goes to the terminal.
|
|
144
|
+
|
|
145
|
+
This behavior is new in 0.2.0. Releases up to 0.1.4 always exited with 0. If a script relies on that, use `runit ... || true`.
|
|
146
|
+
|
|
126
147
|
## Development & Extending
|
|
127
148
|
|
|
128
149
|
### Project Structure
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
.gitignore
|
|
2
|
+
CHANGES.md
|
|
2
3
|
LICENSE
|
|
3
4
|
README.md
|
|
4
5
|
benchmark.py
|
|
@@ -14,6 +15,8 @@ profiler_runit.egg-info/SOURCES.txt
|
|
|
14
15
|
profiler_runit.egg-info/dependency_links.txt
|
|
15
16
|
profiler_runit.egg-info/entry_points.txt
|
|
16
17
|
profiler_runit.egg-info/requires.txt
|
|
18
|
+
profiler_runit.egg-info/scm_file_list.json
|
|
19
|
+
profiler_runit.egg-info/scm_version.json
|
|
17
20
|
profiler_runit.egg-info/top_level.txt
|
|
18
21
|
runit/__init__.py
|
|
19
22
|
runit/cli.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".github/workflows/ci.yml",
|
|
4
|
+
".github/workflows/release.yml",
|
|
5
|
+
".github/workflows/testpypi.yml",
|
|
6
|
+
".gitignore",
|
|
7
|
+
"CHANGES.md",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md",
|
|
10
|
+
"assets/runit-demo.gif",
|
|
11
|
+
"benchmark.py",
|
|
12
|
+
"pyproject.toml",
|
|
13
|
+
"requirements.txt",
|
|
14
|
+
"runit/__init__.py",
|
|
15
|
+
"runit/cli.py",
|
|
16
|
+
"runit/logging_config.py",
|
|
17
|
+
"runit/monitor.py",
|
|
18
|
+
"runit/plotting.py",
|
|
19
|
+
"runit/report.py",
|
|
20
|
+
"runit/utils.py",
|
|
21
|
+
"test.py"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -12,9 +12,14 @@ authors = [{ name = "NotesOfCliff", email = "notesofcliff@gmail.com" }]
|
|
|
12
12
|
license = "GPL-3.0-only"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"psutil",
|
|
15
|
-
"plotext",
|
|
15
|
+
"plotext>=5.3,<6",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/McIndi/runit"
|
|
20
|
+
Repository = "https://github.com/McIndi/runit"
|
|
21
|
+
Issues = "https://github.com/McIndi/runit/issues"
|
|
22
|
+
|
|
18
23
|
[tool.setuptools_scm]
|
|
19
24
|
version_scheme = "no-guess-dev"
|
|
20
25
|
local_scheme = "no-local-version"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .monitor import monitor_process
|
|
3
|
+
from .report import format_report
|
|
4
|
+
from .plotting import plot_charts
|
|
5
|
+
from .utils import strip_ansi, exit_status
|
|
6
|
+
from .logging_config import setup_logging
|
|
7
|
+
import argparse
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from contextlib import redirect_stdout
|
|
11
|
+
from io import StringIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _write_stdout(text, log):
|
|
15
|
+
"""Write the report to stdout. Return False if stdout is closed or broken.
|
|
16
|
+
|
|
17
|
+
When the reader has gone away (for example `runit cmd | head`), stay quiet
|
|
18
|
+
like other command line tools. For other errors (for example a full disk),
|
|
19
|
+
log one line. In both cases stdout is pointed at os.devnull, so that the
|
|
20
|
+
flush at interpreter exit does not raise again.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
sys.stdout.write(text)
|
|
24
|
+
sys.stdout.flush()
|
|
25
|
+
return True
|
|
26
|
+
except (OSError, ValueError) as e:
|
|
27
|
+
if isinstance(e, BrokenPipeError):
|
|
28
|
+
log.info("Output reader closed the pipe; report not shown.")
|
|
29
|
+
else:
|
|
30
|
+
log.error("Cannot write the report to stdout: %s", e)
|
|
31
|
+
try:
|
|
32
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
33
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
34
|
+
os.close(devnull)
|
|
35
|
+
except (OSError, ValueError):
|
|
36
|
+
pass
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_cli():
|
|
41
|
+
parser = create_parser()
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
setup_logging(args.log_level)
|
|
44
|
+
log = logging.getLogger(__name__)
|
|
45
|
+
log.info("Starting runit CLI")
|
|
46
|
+
if not args.command:
|
|
47
|
+
parser.print_help()
|
|
48
|
+
log.info("No command provided. Exiting.")
|
|
49
|
+
return 0
|
|
50
|
+
try:
|
|
51
|
+
# The program's stdout and stderr go to the terminal as it runs.
|
|
52
|
+
stats = monitor_process(args.command)
|
|
53
|
+
except FileNotFoundError as e:
|
|
54
|
+
log.error("Command not found: %s (%s)", args.command[0], e)
|
|
55
|
+
return 127
|
|
56
|
+
except OSError as e:
|
|
57
|
+
# For example EACCES, or ENOEXEC for a script without a shebang.
|
|
58
|
+
log.error("Command cannot be executed: %s (%s)", args.command[0], e)
|
|
59
|
+
return 126
|
|
60
|
+
status = exit_status(stats.get('returncode'))
|
|
61
|
+
report = format_report(stats)
|
|
62
|
+
# Capture the report and charts, write --out-file first, then the terminal.
|
|
63
|
+
buf = StringIO()
|
|
64
|
+
with redirect_stdout(buf):
|
|
65
|
+
if args.plot:
|
|
66
|
+
plot_charts(stats, args.plot_width, args.plot_height)
|
|
67
|
+
print(report)
|
|
68
|
+
output = buf.getvalue()
|
|
69
|
+
if args.out_file:
|
|
70
|
+
file_output = output
|
|
71
|
+
if getattr(args, 'strip_ansi', False):
|
|
72
|
+
log.info("Stripping ANSI codes for output file.")
|
|
73
|
+
file_output = strip_ansi(output)
|
|
74
|
+
try:
|
|
75
|
+
with open(args.out_file, 'w', encoding='utf-8') as f:
|
|
76
|
+
f.write(file_output)
|
|
77
|
+
log.info("Wrote output to file: %s", args.out_file)
|
|
78
|
+
except OSError as e:
|
|
79
|
+
# runit's own job failed: report it in one line. A non-zero
|
|
80
|
+
# exit code of the command wins; otherwise runit exits 1.
|
|
81
|
+
log.error("Cannot write --out-file %s: %s", args.out_file, e)
|
|
82
|
+
if status == 0:
|
|
83
|
+
status = 1
|
|
84
|
+
_write_stdout(output, log)
|
|
85
|
+
log.info("runit CLI finished.")
|
|
86
|
+
return status
|
|
87
|
+
|
|
88
|
+
def create_parser():
|
|
89
|
+
parser = argparse.ArgumentParser(
|
|
90
|
+
description="Run a command and report on its execution (time, resources, etc)."
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
'--log-level',
|
|
94
|
+
default='INFO',
|
|
95
|
+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
|
96
|
+
help='Set the logging level (default: INFO)'
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
'--out-file',
|
|
100
|
+
type=str,
|
|
101
|
+
default=None,
|
|
102
|
+
help='Also write the report (and charts) to this file. '
|
|
103
|
+
'The program output and the report are still shown in the terminal.'
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
'--strip-ansi',
|
|
107
|
+
action='store_true',
|
|
108
|
+
help='Remove ANSI escape codes from output (for plain text files).'
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
'--plot',
|
|
112
|
+
action='store_true',
|
|
113
|
+
help='Show resource usage charts (requires plotext).'
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
'--plot-width',
|
|
117
|
+
type=int,
|
|
118
|
+
default=80,
|
|
119
|
+
help='Set the plot width (default: 80)'
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
'--plot-height',
|
|
123
|
+
type=int,
|
|
124
|
+
default=20,
|
|
125
|
+
help='Set the plot height (default: 20)'
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
'command',
|
|
129
|
+
nargs=argparse.REMAINDER,
|
|
130
|
+
help='The command and arguments to run.'
|
|
131
|
+
)
|
|
132
|
+
return parser
|
|
@@ -9,6 +9,24 @@ import sys
|
|
|
9
9
|
log = logging.getLogger(__name__)
|
|
10
10
|
_STREAM_CHUNK_SIZE = 4096
|
|
11
11
|
_THREAD_JOIN_TIMEOUT_SECONDS = 5
|
|
12
|
+
|
|
13
|
+
def _binary_writer(target):
|
|
14
|
+
"""Return a function that writes bytes to ``target`` and flushes it.
|
|
15
|
+
|
|
16
|
+
``target`` is normally ``sys.stdout`` or ``sys.stderr``. A text stream
|
|
17
|
+
without a binary ``buffer`` (for example ``io.StringIO``) gets the bytes
|
|
18
|
+
decoded, so that forwarding never fails because of the stream type.
|
|
19
|
+
"""
|
|
20
|
+
buffer = getattr(target, 'buffer', None)
|
|
21
|
+
if buffer is not None:
|
|
22
|
+
def write(chunk):
|
|
23
|
+
buffer.write(chunk)
|
|
24
|
+
buffer.flush()
|
|
25
|
+
else:
|
|
26
|
+
def write(chunk):
|
|
27
|
+
target.write(chunk.decode(errors='backslashreplace'))
|
|
28
|
+
target.flush()
|
|
29
|
+
return write
|
|
12
30
|
|
|
13
31
|
def _start_proc(command: list):
|
|
14
32
|
log.info("Starting process: %s", command)
|
|
@@ -29,6 +47,9 @@ def monitor_process(command):
|
|
|
29
47
|
'memory_info': [],
|
|
30
48
|
'children': [],
|
|
31
49
|
'threads': [],
|
|
50
|
+
'stdout': '',
|
|
51
|
+
'stderr': '',
|
|
52
|
+
'returncode': None,
|
|
32
53
|
}
|
|
33
54
|
proc = _start_proc(command)
|
|
34
55
|
ps_proc = psutil.Process(proc.pid)
|
|
@@ -48,14 +69,32 @@ def monitor_process(command):
|
|
|
48
69
|
if reader is None:
|
|
49
70
|
# Fallback for file-like streams that do not implement read1().
|
|
50
71
|
reader = stream.read
|
|
72
|
+
write = _binary_writer(target)
|
|
51
73
|
try:
|
|
52
74
|
while True:
|
|
53
75
|
chunk = reader(_STREAM_CHUNK_SIZE)
|
|
54
76
|
if not chunk:
|
|
55
77
|
break
|
|
56
78
|
chunks.append(chunk)
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
if write is None:
|
|
80
|
+
continue
|
|
81
|
+
try:
|
|
82
|
+
write(chunk)
|
|
83
|
+
except (OSError, ValueError) as e:
|
|
84
|
+
# The destination is closed or full (for example EPIPE
|
|
85
|
+
# from `runit cmd | head`). Stop and close the pipe so
|
|
86
|
+
# the program sees a closed pipe, as without runit.
|
|
87
|
+
if isinstance(e, BrokenPipeError):
|
|
88
|
+
log.info("Output reader closed the pipe; stopped forwarding.")
|
|
89
|
+
else:
|
|
90
|
+
log.warning("Stream forwarding stopped due to write error: %s", e)
|
|
91
|
+
break
|
|
92
|
+
except Exception as e:
|
|
93
|
+
# Any other failure must not stop the reader: a program
|
|
94
|
+
# that cannot write blocks on a full pipe or is killed by
|
|
95
|
+
# a closed one. Keep capturing and stop forwarding.
|
|
96
|
+
log.warning("Stream forwarding stopped due to write error: %s", e)
|
|
97
|
+
write = None
|
|
59
98
|
except (OSError, ValueError) as e:
|
|
60
99
|
log.warning("Stream forwarding stopped due to read error: %s", e)
|
|
61
100
|
finally:
|
|
@@ -92,7 +131,7 @@ def monitor_process(command):
|
|
|
92
131
|
log.warning("Process ended or became inaccessible during monitoring.")
|
|
93
132
|
break
|
|
94
133
|
time.sleep(0.1)
|
|
95
|
-
proc.wait()
|
|
134
|
+
stats['returncode'] = proc.wait()
|
|
96
135
|
stdout_thread.join(timeout=_THREAD_JOIN_TIMEOUT_SECONDS)
|
|
97
136
|
stderr_thread.join(timeout=_THREAD_JOIN_TIMEOUT_SECONDS)
|
|
98
137
|
if stdout_thread.is_alive() or stderr_thread.is_alive():
|
|
@@ -107,6 +146,8 @@ def monitor_process(command):
|
|
|
107
146
|
|
|
108
147
|
except Exception as e:
|
|
109
148
|
log.warning("Exception during stat collection: %s", e)
|
|
149
|
+
if stats['returncode'] is None:
|
|
150
|
+
stats['returncode'] = proc.wait()
|
|
110
151
|
stats['end_time'] = datetime.now()
|
|
111
152
|
stats['duration'] = stats['end_time'] - stats['start_time']
|
|
112
153
|
log.info("Process monitoring complete. Duration: %s", stats['duration'])
|
|
@@ -10,6 +10,13 @@ def plot_charts(stats, width=80, height=20):
|
|
|
10
10
|
if plt is None:
|
|
11
11
|
log.warning("plotext is not installed. Skipping plotting.")
|
|
12
12
|
return
|
|
13
|
+
if not hasattr(plt, 'plot_size'):
|
|
14
|
+
# plotext 6 replaced the module-level API that runit uses.
|
|
15
|
+
log.warning(
|
|
16
|
+
"plotext %s is not supported (runit needs plotext>=5.3,<6). Skipping plotting.",
|
|
17
|
+
getattr(plt, '__version__', 'unknown'),
|
|
18
|
+
)
|
|
19
|
+
return
|
|
13
20
|
if stats['check_times'] and stats['cpu_percent']:
|
|
14
21
|
try:
|
|
15
22
|
log.info("Plotting charts.")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .utils import extract_memory_rss, extract_num_threads, extract_num_children
|
|
1
|
+
from .utils import extract_memory_rss, extract_num_threads, extract_num_children, describe_exit
|
|
2
2
|
import logging
|
|
3
3
|
|
|
4
4
|
log = logging.getLogger(__name__)
|
|
@@ -14,6 +14,7 @@ def format_report(stats):
|
|
|
14
14
|
"Start Time: {}".format(stats['start_time']),
|
|
15
15
|
"End Time: {}".format(stats['end_time']),
|
|
16
16
|
"Duration: {}".format(stats['duration']),
|
|
17
|
+
"Exit Code: {}".format(describe_exit(stats.get('returncode'))),
|
|
17
18
|
"Max RSS (bytes): {}".format(max(rss_values) if rss_values else 'N/A'),
|
|
18
19
|
"Max Threads: {}".format(max(num_threads) if num_threads else 'N/A'),
|
|
19
20
|
"Max Children: {}".format(max(num_children) if num_children else 'N/A'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import logging
|
|
3
|
+
import signal
|
|
3
4
|
from ast import literal_eval
|
|
4
5
|
|
|
5
6
|
log = logging.getLogger(__name__)
|
|
@@ -9,6 +10,29 @@ def strip_ansi(text):
|
|
|
9
10
|
ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]')
|
|
10
11
|
return ansi_escape.sub('', text)
|
|
11
12
|
|
|
13
|
+
def exit_status(returncode):
|
|
14
|
+
"""Map a subprocess return code to the exit status runit uses.
|
|
15
|
+
|
|
16
|
+
A program killed by signal N has a negative return code; shells report
|
|
17
|
+
that as 128 + N, so runit does the same.
|
|
18
|
+
"""
|
|
19
|
+
if returncode is None:
|
|
20
|
+
return 1
|
|
21
|
+
if returncode < 0:
|
|
22
|
+
return 128 - returncode
|
|
23
|
+
return returncode
|
|
24
|
+
|
|
25
|
+
def describe_exit(returncode):
|
|
26
|
+
"""Text for the report: runit's exit status, plus the signal if there was one."""
|
|
27
|
+
status = exit_status(returncode)
|
|
28
|
+
if returncode is None or returncode >= 0:
|
|
29
|
+
return str(status)
|
|
30
|
+
try:
|
|
31
|
+
name = signal.Signals(-returncode).name
|
|
32
|
+
except ValueError:
|
|
33
|
+
return "{} (signal {})".format(status, -returncode)
|
|
34
|
+
return "{} (signal {}, {})".format(status, -returncode, name)
|
|
35
|
+
|
|
12
36
|
def extract_memory_rss(memory_info_list):
|
|
13
37
|
log.debug("Extracting memory RSS values.")
|
|
14
38
|
rss_values = []
|