multidog 0.1.0a1__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.
- multidog-0.1.0a1/.editorconfig +26 -0
- multidog-0.1.0a1/.gitignore +13 -0
- multidog-0.1.0a1/LICENSES/MIT.txt +18 -0
- multidog-0.1.0a1/Makefile +29 -0
- multidog-0.1.0a1/PKG-INFO +164 -0
- multidog-0.1.0a1/README.md +141 -0
- multidog-0.1.0a1/REUSE.toml +9 -0
- multidog-0.1.0a1/multidog/__init__.py +121 -0
- multidog-0.1.0a1/multidog/py.typed +0 -0
- multidog-0.1.0a1/pyproject.toml +144 -0
- multidog-0.1.0a1/tests/test_state.py +26 -0
- multidog-0.1.0a1/uv.lock +567 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 scy
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
[*]
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
charset = utf-8
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.{bash,sh}]
|
|
12
|
+
indent_style = tab
|
|
13
|
+
|
|
14
|
+
[*.{css,html,js,json,md,scss,ts,yaml}]
|
|
15
|
+
indent_size = 2
|
|
16
|
+
indent_style = space
|
|
17
|
+
|
|
18
|
+
[*.{py,toml}]
|
|
19
|
+
indent_size = 4
|
|
20
|
+
indent_style = space
|
|
21
|
+
|
|
22
|
+
[*.py]
|
|
23
|
+
max_line_length = 79
|
|
24
|
+
|
|
25
|
+
[Makefile]
|
|
26
|
+
indent_style = tab
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <year> <copyright holders>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 scy
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
.PHONY: all fmt noqa qa test reuse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
all: fmt qa reuse test
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
fmt:
|
|
12
|
+
ruff format
|
|
13
|
+
ruff check --fix
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
noqa:
|
|
17
|
+
ruff check --add-noqa
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
qa:
|
|
21
|
+
mypy
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
reuse:
|
|
25
|
+
reuse lint --lines
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
test:
|
|
29
|
+
pytest
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: multidog
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: A SIGALRM-based watchdog that can handle multiple timeouts.
|
|
5
|
+
Project-URL: Source, https://codeberg.org/scy/multidog
|
|
6
|
+
Project-URL: Documentation, https://codeberg.org/scy/multidog
|
|
7
|
+
Project-URL: Issues, https://codeberg.org/scy/multidog/issues
|
|
8
|
+
Author: scy
|
|
9
|
+
Maintainer: scy
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSES/MIT.txt
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: <4,>=3.12
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<!--
|
|
25
|
+
SPDX-FileCopyrightText: © 2026 scy
|
|
26
|
+
|
|
27
|
+
SPDX-License-Identifier: MIT
|
|
28
|
+
-->
|
|
29
|
+
|
|
30
|
+
# Multidog
|
|
31
|
+
|
|
32
|
+
_A SIGALRM-based watchdog that can handle multiple timeouts._
|
|
33
|
+
|
|
34
|
+
➡️ **Quick Links:** [Repository](https://codeberg.org/scy/multidog) · [Issues](https://codeberg.org/scy/multidog/issues)
|
|
35
|
+
|
|
36
|
+
Multidog works by keeping track of one or more timeouts and setting up an [`alarm`](https://docs.python.org/3/library/signal.html#signal.alarm) signal that will cause the OS to terminate your process when the timeout occurs.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## 🌱 Status
|
|
40
|
+
|
|
41
|
+
Multidog has recently been extracted from another project of mine.
|
|
42
|
+
It kind of works, but I still have to fix some issues.
|
|
43
|
+
|
|
44
|
+
Things that are not yet implemented:
|
|
45
|
+
|
|
46
|
+
- [ ] Timeouts are only checked with the granularity of the shortest timeout. See the example below for what this means.
|
|
47
|
+
- [ ] You may only use one running instance of Multidog per process, because a process can only have one handler per signal. There is no safeguard against this at the moment.
|
|
48
|
+
- [ ] Adding more timeouts after creating the `Multidog` instance is not possible yet.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## 📚 Usage
|
|
52
|
+
|
|
53
|
+
Example usage:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from time import sleep
|
|
57
|
+
from multidog import Multidog
|
|
58
|
+
|
|
59
|
+
dog = Multidog({"a": 5, "b": 10})
|
|
60
|
+
|
|
61
|
+
dog.start()
|
|
62
|
+
# This will call `sys.exit()` in 5 seconds unless you reset the timeouts regularly.
|
|
63
|
+
|
|
64
|
+
sleep(4)
|
|
65
|
+
dog.reset("a")
|
|
66
|
+
# `sys.exit()` will be deferred for another 5 seconds.
|
|
67
|
+
|
|
68
|
+
sleep(4)
|
|
69
|
+
dog.reset("a")
|
|
70
|
+
# `sys.exit()` will be deferred for another 5 seconds.
|
|
71
|
+
# This is a bug actually: The "b" timeout has never been reset and only has two
|
|
72
|
+
# seconds left. Multidog should defer for only 2 seconds, but right now it doesn't.
|
|
73
|
+
|
|
74
|
+
sleep(4)
|
|
75
|
+
dog.reset("a")
|
|
76
|
+
# Multidog finally notices that "b" is overdue and will refuse to reset the alarm,
|
|
77
|
+
# causing your application to exit in one second.
|
|
78
|
+
|
|
79
|
+
dog.stop()
|
|
80
|
+
# Stops the watchdog before it kills your app.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Anonymous timeout
|
|
84
|
+
|
|
85
|
+
If you only have a single timeout to track, you can simplify your usage:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
dog = Multidog(5) # equivalent to Multidog({"": 5})
|
|
89
|
+
dog.start()
|
|
90
|
+
dog.reset() # equivalent to dog.reset("")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Exit timeout
|
|
94
|
+
|
|
95
|
+
Your Python application might not shut down fast enough (or at all) on [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) (because yeah, that's something that can be suppressed).
|
|
96
|
+
Therefore, Multidog will set up a second `SIGALRM` after calling `sys.exit()`, with the signal handler reset to the default, which _should_ kill your process when the timeout expires.
|
|
97
|
+
|
|
98
|
+
The default timeout for this is 5 seconds, you can choose a different one like so:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
dog = Multidog({"a": 5, "b": 10}, exit_timeout=120)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## 🗃️ Installation
|
|
106
|
+
|
|
107
|
+
Simply install the `multidog` package from PyPI via your preferred package manager.
|
|
108
|
+
|
|
109
|
+
For example, to add it as a dependency to your [uv](https://docs.astral.sh/uv/)-managed project, use this:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv add multidog
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## 🧑💻 Development
|
|
117
|
+
|
|
118
|
+
### Preparation
|
|
119
|
+
|
|
120
|
+
We're using [uv](https://docs.astral.sh/uv/) to manage this project and its dependencies.
|
|
121
|
+
After cloning the repository using Git, a simple `uv sync` should get everything you need.
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
git clone https://codeberg.org/scy/multidog.git multidog
|
|
125
|
+
cd multidog
|
|
126
|
+
uv sync
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### direnv
|
|
130
|
+
|
|
131
|
+
We recommend using [direnv](https://direnv.net/) to add installed dependencies to your `$PATH`, so that you don't need to prepend `uv run` to every command.
|
|
132
|
+
For example, after installing direnv, you can use `direnv edit` in this repository's directory and add the following line:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
PATH_add .venv/bin
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**The rest of this readme assumes that you have `.venv/bin` in your `$PATH`.**
|
|
139
|
+
|
|
140
|
+
### EditorConfig
|
|
141
|
+
|
|
142
|
+
Make sure your editor or IDE supports the [EditorConfig](https://editorconfig.org/) standard, so that your code adheres to the project's [preferred](.editorconfig) indentation, line lengths, etc.
|
|
143
|
+
|
|
144
|
+
### Makefile
|
|
145
|
+
|
|
146
|
+
There is a [`Makefile`](Makefile) that contains frequently used commands to speed up development, but it's optional to use.
|
|
147
|
+
The following targets exist:
|
|
148
|
+
|
|
149
|
+
- `fmt`: Use [Ruff](https://docs.astral.sh/ruff/) to format and lint the code.
|
|
150
|
+
- `qa`: Use [mypy](https://mypy.readthedocs.io/) for static type analysis.
|
|
151
|
+
- `reuse`: Use [`reuse lint`](https://codeberg.org/fsfe/reuse-tool) to make sure every file contains licensing information.
|
|
152
|
+
- `test`: Use [pytest](https://pytest.org/) for automated testing and code coverage.
|
|
153
|
+
- `noqa`: Add `noqa` statements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.
|
|
154
|
+
|
|
155
|
+
`make all`, or simply `make`, will run `fmt`, `qa`, `reuse`, and `test`.
|
|
156
|
+
You should do this before every commit and fix all issues that are reported.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## 📃 License
|
|
160
|
+
|
|
161
|
+
This project is licensed under the terms of the [MIT License](https://spdx.org/licenses/MIT.html).
|
|
162
|
+
|
|
163
|
+
The project also conforms to the [REUSE Specification, version 3.3](https://reuse.software/spec-3.3/).
|
|
164
|
+
You can use [the `reuse` tool](https://codeberg.org/fsfe/reuse-tool) to interpret the machine-readable licensing information.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: © 2026 scy
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# Multidog
|
|
8
|
+
|
|
9
|
+
_A SIGALRM-based watchdog that can handle multiple timeouts._
|
|
10
|
+
|
|
11
|
+
➡️ **Quick Links:** [Repository](https://codeberg.org/scy/multidog) · [Issues](https://codeberg.org/scy/multidog/issues)
|
|
12
|
+
|
|
13
|
+
Multidog works by keeping track of one or more timeouts and setting up an [`alarm`](https://docs.python.org/3/library/signal.html#signal.alarm) signal that will cause the OS to terminate your process when the timeout occurs.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 🌱 Status
|
|
17
|
+
|
|
18
|
+
Multidog has recently been extracted from another project of mine.
|
|
19
|
+
It kind of works, but I still have to fix some issues.
|
|
20
|
+
|
|
21
|
+
Things that are not yet implemented:
|
|
22
|
+
|
|
23
|
+
- [ ] Timeouts are only checked with the granularity of the shortest timeout. See the example below for what this means.
|
|
24
|
+
- [ ] You may only use one running instance of Multidog per process, because a process can only have one handler per signal. There is no safeguard against this at the moment.
|
|
25
|
+
- [ ] Adding more timeouts after creating the `Multidog` instance is not possible yet.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## 📚 Usage
|
|
29
|
+
|
|
30
|
+
Example usage:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from time import sleep
|
|
34
|
+
from multidog import Multidog
|
|
35
|
+
|
|
36
|
+
dog = Multidog({"a": 5, "b": 10})
|
|
37
|
+
|
|
38
|
+
dog.start()
|
|
39
|
+
# This will call `sys.exit()` in 5 seconds unless you reset the timeouts regularly.
|
|
40
|
+
|
|
41
|
+
sleep(4)
|
|
42
|
+
dog.reset("a")
|
|
43
|
+
# `sys.exit()` will be deferred for another 5 seconds.
|
|
44
|
+
|
|
45
|
+
sleep(4)
|
|
46
|
+
dog.reset("a")
|
|
47
|
+
# `sys.exit()` will be deferred for another 5 seconds.
|
|
48
|
+
# This is a bug actually: The "b" timeout has never been reset and only has two
|
|
49
|
+
# seconds left. Multidog should defer for only 2 seconds, but right now it doesn't.
|
|
50
|
+
|
|
51
|
+
sleep(4)
|
|
52
|
+
dog.reset("a")
|
|
53
|
+
# Multidog finally notices that "b" is overdue and will refuse to reset the alarm,
|
|
54
|
+
# causing your application to exit in one second.
|
|
55
|
+
|
|
56
|
+
dog.stop()
|
|
57
|
+
# Stops the watchdog before it kills your app.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Anonymous timeout
|
|
61
|
+
|
|
62
|
+
If you only have a single timeout to track, you can simplify your usage:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
dog = Multidog(5) # equivalent to Multidog({"": 5})
|
|
66
|
+
dog.start()
|
|
67
|
+
dog.reset() # equivalent to dog.reset("")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Exit timeout
|
|
71
|
+
|
|
72
|
+
Your Python application might not shut down fast enough (or at all) on [`sys.exit()`](https://docs.python.org/3/library/sys.html#sys.exit) (because yeah, that's something that can be suppressed).
|
|
73
|
+
Therefore, Multidog will set up a second `SIGALRM` after calling `sys.exit()`, with the signal handler reset to the default, which _should_ kill your process when the timeout expires.
|
|
74
|
+
|
|
75
|
+
The default timeout for this is 5 seconds, you can choose a different one like so:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
dog = Multidog({"a": 5, "b": 10}, exit_timeout=120)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## 🗃️ Installation
|
|
83
|
+
|
|
84
|
+
Simply install the `multidog` package from PyPI via your preferred package manager.
|
|
85
|
+
|
|
86
|
+
For example, to add it as a dependency to your [uv](https://docs.astral.sh/uv/)-managed project, use this:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
uv add multidog
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## 🧑💻 Development
|
|
94
|
+
|
|
95
|
+
### Preparation
|
|
96
|
+
|
|
97
|
+
We're using [uv](https://docs.astral.sh/uv/) to manage this project and its dependencies.
|
|
98
|
+
After cloning the repository using Git, a simple `uv sync` should get everything you need.
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
git clone https://codeberg.org/scy/multidog.git multidog
|
|
102
|
+
cd multidog
|
|
103
|
+
uv sync
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### direnv
|
|
107
|
+
|
|
108
|
+
We recommend using [direnv](https://direnv.net/) to add installed dependencies to your `$PATH`, so that you don't need to prepend `uv run` to every command.
|
|
109
|
+
For example, after installing direnv, you can use `direnv edit` in this repository's directory and add the following line:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
PATH_add .venv/bin
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**The rest of this readme assumes that you have `.venv/bin` in your `$PATH`.**
|
|
116
|
+
|
|
117
|
+
### EditorConfig
|
|
118
|
+
|
|
119
|
+
Make sure your editor or IDE supports the [EditorConfig](https://editorconfig.org/) standard, so that your code adheres to the project's [preferred](.editorconfig) indentation, line lengths, etc.
|
|
120
|
+
|
|
121
|
+
### Makefile
|
|
122
|
+
|
|
123
|
+
There is a [`Makefile`](Makefile) that contains frequently used commands to speed up development, but it's optional to use.
|
|
124
|
+
The following targets exist:
|
|
125
|
+
|
|
126
|
+
- `fmt`: Use [Ruff](https://docs.astral.sh/ruff/) to format and lint the code.
|
|
127
|
+
- `qa`: Use [mypy](https://mypy.readthedocs.io/) for static type analysis.
|
|
128
|
+
- `reuse`: Use [`reuse lint`](https://codeberg.org/fsfe/reuse-tool) to make sure every file contains licensing information.
|
|
129
|
+
- `test`: Use [pytest](https://pytest.org/) for automated testing and code coverage.
|
|
130
|
+
- `noqa`: Add `noqa` statements to ignore everything Ruff complains about. Only use this after fixing everything that needs fixing.
|
|
131
|
+
|
|
132
|
+
`make all`, or simply `make`, will run `fmt`, `qa`, `reuse`, and `test`.
|
|
133
|
+
You should do this before every commit and fix all issues that are reported.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## 📃 License
|
|
137
|
+
|
|
138
|
+
This project is licensed under the terms of the [MIT License](https://spdx.org/licenses/MIT.html).
|
|
139
|
+
|
|
140
|
+
The project also conforms to the [REUSE Specification, version 3.3](https://reuse.software/spec-3.3/).
|
|
141
|
+
You can use [the `reuse` tool](https://codeberg.org/fsfe/reuse-tool) to interpret the machine-readable licensing information.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 scy
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import signal
|
|
7
|
+
import sys
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from datetime import UTC, datetime, timedelta
|
|
10
|
+
from importlib import metadata
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PKG_NAME = "multidog"
|
|
14
|
+
PROJECT_URL = "https://codeberg.org/scy/multidog"
|
|
15
|
+
|
|
16
|
+
__version__ = metadata.version(PKG_NAME)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Timeout = timedelta | int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Multidog:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
timeout: Mapping[str, Timeout] | Timeout,
|
|
29
|
+
*,
|
|
30
|
+
exit_timeout: Timeout = 5,
|
|
31
|
+
) -> None:
|
|
32
|
+
# Create our dict of timeouts. If we just got a single timeout value,
|
|
33
|
+
# use the empty string as its name.
|
|
34
|
+
self._timeouts = (
|
|
35
|
+
{name: self.to_timedelta(v) for name, v in timeout.items()}
|
|
36
|
+
if isinstance(timeout, Mapping)
|
|
37
|
+
else {"": self.to_timedelta(timeout)}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
self._exit_timeout = self.to_timedelta(exit_timeout)
|
|
41
|
+
self._reset_all()
|
|
42
|
+
|
|
43
|
+
def _alarm(self, _signal, _stack) -> None:
|
|
44
|
+
_log.critical("watchdog timeout, shutting down")
|
|
45
|
+
|
|
46
|
+
# Remove existing signal handler and schedule another alarm that will
|
|
47
|
+
# kill us if for some reason the sys.exit() doesn't work.
|
|
48
|
+
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
|
49
|
+
signal.alarm(int(self._exit_timeout.total_seconds()))
|
|
50
|
+
|
|
51
|
+
sys.exit(128 + signal.SIGALRM)
|
|
52
|
+
|
|
53
|
+
def _reset_all(self) -> None:
|
|
54
|
+
"""Reset all timers to now, i.e. not yet expired."""
|
|
55
|
+
now = datetime.now(UTC)
|
|
56
|
+
self._resets = dict.fromkeys(self._timeouts, now)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def to_timedelta(timeout: Timeout) -> timedelta:
|
|
60
|
+
if isinstance(timeout, int):
|
|
61
|
+
return timedelta(seconds=timeout)
|
|
62
|
+
if isinstance(timeout, timedelta):
|
|
63
|
+
return timeout
|
|
64
|
+
raise TypeError(f"invalid timeout type: {type(timeout)}")
|
|
65
|
+
|
|
66
|
+
def alarm_interval(self) -> int:
|
|
67
|
+
"""Return the number of seconds of the shortest configured timeout."""
|
|
68
|
+
return min(
|
|
69
|
+
int(timeout.total_seconds()) for timeout in self._timeouts.values()
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def reset(self, key: str = "") -> bool:
|
|
73
|
+
"""Reset the given timeout."""
|
|
74
|
+
now = datetime.now(UTC)
|
|
75
|
+
if key not in self._timeouts:
|
|
76
|
+
raise KeyError(f"unknown watchdog key: {key}")
|
|
77
|
+
self._resets[key] = now
|
|
78
|
+
|
|
79
|
+
# If one of the configured timers has not been reset in its expected
|
|
80
|
+
# time frame, we don't reset the alarm just yet. In other words, all
|
|
81
|
+
# expected timers need to have been reset before their timeout for us
|
|
82
|
+
# to reset the alarm signal and thus not get killed.
|
|
83
|
+
if any(
|
|
84
|
+
name
|
|
85
|
+
for name, timeout in self._timeouts.items()
|
|
86
|
+
if now > self._resets[name] + timeout
|
|
87
|
+
):
|
|
88
|
+
_log.debug(
|
|
89
|
+
"not resetting watchdog, some expected resets are missing"
|
|
90
|
+
)
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
_log.debug("resetting watchdog")
|
|
94
|
+
signal.alarm(self.alarm_interval())
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def resetfunc(self, key: str = "") -> Callable[[], bool]:
|
|
98
|
+
"""Get a function to reset the given timeout."""
|
|
99
|
+
|
|
100
|
+
def reset_func() -> bool:
|
|
101
|
+
return self.reset(key)
|
|
102
|
+
|
|
103
|
+
return reset_func
|
|
104
|
+
|
|
105
|
+
def start(self) -> None:
|
|
106
|
+
"""Start watchdog operation."""
|
|
107
|
+
# Reset all timers.
|
|
108
|
+
self._reset_all()
|
|
109
|
+
# Set our signal handler.
|
|
110
|
+
signal.signal(signal.SIGALRM, self._alarm)
|
|
111
|
+
# Have the signal fire when the shortest timeout has been reached.
|
|
112
|
+
signal.alarm(self.alarm_interval())
|
|
113
|
+
|
|
114
|
+
def stop(self) -> None:
|
|
115
|
+
"""Stop watchdog operation."""
|
|
116
|
+
# Disable the signal.
|
|
117
|
+
signal.alarm(0)
|
|
118
|
+
# Reset all timers.
|
|
119
|
+
self._reset_all()
|
|
120
|
+
# Restore the default signal handler.
|
|
121
|
+
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 scy
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "multidog"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "A SIGALRM-based watchdog that can handle multiple timeouts."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSES/*.txt"]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "scy"},
|
|
15
|
+
]
|
|
16
|
+
maintainers = [
|
|
17
|
+
{name = "scy"},
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Operating System :: POSIX",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Source = "https://codeberg.org/scy/multidog"
|
|
35
|
+
Documentation = "https://codeberg.org/scy/multidog"
|
|
36
|
+
Issues = "https://codeberg.org/scy/multidog/issues"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"mypy>=1.19",
|
|
41
|
+
"pytest>=9",
|
|
42
|
+
"pytest-cov>=7",
|
|
43
|
+
"reuse>=6.2",
|
|
44
|
+
"ruff<=0.15.6", # last version before Astral joined OpenAI
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.uv]
|
|
52
|
+
package = true
|
|
53
|
+
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
packages = [
|
|
56
|
+
"multidog",
|
|
57
|
+
"tests",
|
|
58
|
+
]
|
|
59
|
+
python_version = "3.12"
|
|
60
|
+
warn_return_any = true
|
|
61
|
+
|
|
62
|
+
[tool.pytest]
|
|
63
|
+
addopts = [
|
|
64
|
+
"--strict-markers",
|
|
65
|
+
"--junit-xml=report.xml",
|
|
66
|
+
"--cov-report=term",
|
|
67
|
+
"--cov-report=lcov",
|
|
68
|
+
"--cov-report=xml",
|
|
69
|
+
"--cov=multidog",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 79
|
|
74
|
+
|
|
75
|
+
[tool.ruff.format]
|
|
76
|
+
quote-style = "double"
|
|
77
|
+
indent-style = "space"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
preview = true
|
|
81
|
+
select = [
|
|
82
|
+
"ANN", # flake8-annotations
|
|
83
|
+
"ARG", # flake8-unused-arguments
|
|
84
|
+
"ASYNC", # flake8-async
|
|
85
|
+
"B", # flake8-bugbear
|
|
86
|
+
"C4", # flake8-comprehensions
|
|
87
|
+
"C90", # mccabe complexity
|
|
88
|
+
"DTZ", # flake8-datetimez (naive datetimes)
|
|
89
|
+
"E", # pycodestyle errors
|
|
90
|
+
"ERA", # eradicate (commented-out code)
|
|
91
|
+
"EXE", # flake8-executable (shebang lines)
|
|
92
|
+
"F", # pyflakes
|
|
93
|
+
"FA", # flake8-future-annotations
|
|
94
|
+
"FURB", # refurb
|
|
95
|
+
"I", # isort
|
|
96
|
+
"INP", # flake8-no-pep420 (implicit namespace package)
|
|
97
|
+
"ISC", # flake8-implicit-str-concat
|
|
98
|
+
"LOG", # flake8-logging
|
|
99
|
+
"N", # pep8-naming
|
|
100
|
+
"PL", # pylint
|
|
101
|
+
"PT", # flake8-pytest-style
|
|
102
|
+
"PTH", # flake8-use-pathlib
|
|
103
|
+
"Q", # flake8-quotes
|
|
104
|
+
"RET", # flake8-return
|
|
105
|
+
"RSE", # flake8-raise
|
|
106
|
+
"RUF", # Ruff-specific rules
|
|
107
|
+
"S", # flake8-bandit (security)
|
|
108
|
+
"SIM", # flake8-simplify
|
|
109
|
+
"SLF", # flake8-self (private member access)
|
|
110
|
+
"SLOT", # flake8-slots
|
|
111
|
+
"TCH", # flake8-type-checking
|
|
112
|
+
"TRY", # tryceratops
|
|
113
|
+
"T20", # flake8-print
|
|
114
|
+
"UP", # pyupgrade
|
|
115
|
+
"W", # pycodestyle warnings
|
|
116
|
+
]
|
|
117
|
+
ignore = [
|
|
118
|
+
"C408", # micro-optimization; I'd rather get rid of too many quotes and use dict(x=…)
|
|
119
|
+
"ISC001", # conflict w/ formatter, see https://github.com/astral-sh/ruff/issues/8272
|
|
120
|
+
"PLC0415", # forbids dynamic imports
|
|
121
|
+
"RUF067", # disallows constants in __init__.py
|
|
122
|
+
"RUF200", # requires a name for projects
|
|
123
|
+
"TRY003", # complains about simple ValueError messages
|
|
124
|
+
]
|
|
125
|
+
allowed-confusables = [ # exceptions to RUF002, characters in docstrings
|
|
126
|
+
" ", # non-breaking space
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
[tool.ruff.lint.flake8-annotations]
|
|
130
|
+
mypy-init-return = true # no return annotation required for __init__
|
|
131
|
+
suppress-dummy-args = true # don't require annotations for _foo variables
|
|
132
|
+
|
|
133
|
+
[tool.ruff.lint.per-file-ignores]
|
|
134
|
+
"tests/*" = [
|
|
135
|
+
"ANN001", # missing function argument annotation
|
|
136
|
+
"ANN201", # missing return type annotation
|
|
137
|
+
"ARG001", # unused function argument (e.g. require-only fixtures)
|
|
138
|
+
"INP001", # implicit namespace package
|
|
139
|
+
"S101", # use of assert
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
[tool.ruff.lint.isort]
|
|
143
|
+
# Two blank lines after the imports.
|
|
144
|
+
lines-after-imports = 2
|