python-dmon 0.2.4__tar.gz → 0.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.
- {python_dmon-0.2.4 → python_dmon-0.3.1}/PKG-INFO +46 -4
- {python_dmon-0.2.4 → python_dmon-0.3.1}/README.md +45 -3
- {python_dmon-0.2.4 → python_dmon-0.3.1}/pyproject.toml +1 -1
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/cli.py +116 -44
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/config.py +62 -19
- python_dmon-0.3.1/src/dmon/control.py +775 -0
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/runner.py +74 -27
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/types.py +40 -3
- python_dmon-0.2.4/src/dmon/control.py +0 -584
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/__init__.py +0 -0
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/__main__.py +0 -0
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/constants.py +0 -0
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/py.typed +0 -0
- {python_dmon-0.2.4 → python_dmon-0.3.1}/src/dmon/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-dmon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A lightweight, cross-platform daemon manager that runs any command as a background process.
|
|
5
5
|
Keywords: python-dmon,dmon,daemon,background,detach,process management
|
|
6
6
|
Author: Atomie CHEN
|
|
@@ -21,6 +21,7 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
[](https://github.com/atomiechen/python-dmon)
|
|
23
23
|
[](https://pypi.org/project/python-dmon/)
|
|
24
|
+
[](https://deepwiki.com/atomiechen/python-dmon)
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
|
|
@@ -36,7 +37,7 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
36
37
|
- 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
|
|
37
38
|
- ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
|
|
38
39
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
39
|
-
- 🪵 **Logging & log rotation:**
|
|
40
|
+
- 🪵 **Logging & log rotation:** Keep active log files manageable, with optional archive retention limits.
|
|
40
41
|
|
|
41
42
|

|
|
42
43
|
|
|
@@ -113,6 +114,9 @@ dmon start app
|
|
|
113
114
|
# Stop a running task
|
|
114
115
|
dmon stop app
|
|
115
116
|
|
|
117
|
+
# Restart a task
|
|
118
|
+
dmon restart app
|
|
119
|
+
|
|
116
120
|
# Check task status
|
|
117
121
|
dmon status app
|
|
118
122
|
|
|
@@ -120,21 +124,38 @@ dmon status app
|
|
|
120
124
|
dmon exec app
|
|
121
125
|
```
|
|
122
126
|
|
|
127
|
+
You can specify multiple tasks at once, e.g.: `dmon start app1 app2 app3`, except for `dmon exec` which only accepts one task.
|
|
128
|
+
|
|
129
|
+
Multi-task `start` is best-effort: dmon attempts every requested task and leaves
|
|
130
|
+
successful tasks running if another task cannot start. The command returns a
|
|
131
|
+
non-zero status and prints a summary naming the failed tasks. This is useful for
|
|
132
|
+
independent background services and does not provide atomic stack semantics.
|
|
133
|
+
|
|
134
|
+
Or use `--all` to operate on all tasks:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
# all configured tasks
|
|
138
|
+
dmon start/stop/restart --all
|
|
139
|
+
# all running tasks
|
|
140
|
+
dmon stop/status --all
|
|
141
|
+
```
|
|
142
|
+
|
|
123
143
|
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
124
144
|
|
|
125
145
|
```sh
|
|
126
146
|
dmon start
|
|
127
147
|
dmon stop
|
|
148
|
+
dmon restart
|
|
128
149
|
dmon status
|
|
129
150
|
dmon exec
|
|
130
151
|
```
|
|
131
152
|
|
|
132
|
-
You can use `--config` to specify a custom config file or the directory containing it:
|
|
153
|
+
You can use `-c` / `--config` to specify a custom config file or the directory containing it:
|
|
133
154
|
|
|
134
155
|
```sh
|
|
135
156
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
136
157
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
137
|
-
dmon start
|
|
158
|
+
dmon start -c /path/to/dir app # shorter, dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
138
159
|
```
|
|
139
160
|
|
|
140
161
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -156,6 +177,9 @@ tasks:
|
|
|
156
177
|
# Run a command with arguments in the background
|
|
157
178
|
dmon run --name myserver python -u server.py
|
|
158
179
|
|
|
180
|
+
# Optionally use -- to make the child-command boundary explicit
|
|
181
|
+
dmon run --name timer -- python -c 'import time; time.sleep(30)'
|
|
182
|
+
|
|
159
183
|
# Run a shell command in the background
|
|
160
184
|
dmon run --shell echo "Hello World"
|
|
161
185
|
|
|
@@ -178,6 +202,16 @@ dmon list
|
|
|
178
202
|
|
|
179
203
|
A task can be a **string**, **list**, or **dictionary**.
|
|
180
204
|
|
|
205
|
+
When rotation is enabled, dmon keeps timestamped archives such as
|
|
206
|
+
`app.log.20260807-142106`. Both task and runner logs use this cross-platform
|
|
207
|
+
format; a same-second collision adds `.1`, `.2`, and so on. Archives are never
|
|
208
|
+
deleted by default. Set a backup count explicitly to enable retention cleanup.
|
|
209
|
+
`log_path` contains task output; `rotate_log_path` contains diagnostics from the
|
|
210
|
+
dmon process that captures and rotates that output. They are independent log
|
|
211
|
+
streams and use independent retention settings.
|
|
212
|
+
The size limit is checked at line boundaries, so a single long line may exceed
|
|
213
|
+
the configured limit.
|
|
214
|
+
|
|
181
215
|
Here is a more complete example with default values:
|
|
182
216
|
|
|
183
217
|
```yaml
|
|
@@ -192,8 +226,10 @@ tasks:
|
|
|
192
226
|
log_path: "logs/<task>.log" # path to log file
|
|
193
227
|
log_rotate: false # enable log rotation
|
|
194
228
|
log_max_size: 5 # max log file size before rotation in MB
|
|
229
|
+
# log_backup_count: 10 # optional; omit to retain all task log archives
|
|
195
230
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
196
231
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
232
|
+
# rotate_log_backup_count: 10 # optional; omit to retain all runner log archives
|
|
197
233
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
198
234
|
default_task: your_task_name # the default task name
|
|
199
235
|
```
|
|
@@ -211,6 +247,8 @@ another_task = "cd subdir && ls && bash start.sh"
|
|
|
211
247
|
default_task = "your_task_name"
|
|
212
248
|
```
|
|
213
249
|
|
|
250
|
+
All paths can be absolute or relative to the **config file location**.
|
|
251
|
+
|
|
214
252
|
|
|
215
253
|
## Under the Hood
|
|
216
254
|
|
|
@@ -218,6 +256,10 @@ Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored
|
|
|
218
256
|
The file contains details such as the command, PID, log path, and more.
|
|
219
257
|
**Do not** modify or delete these files manually.
|
|
220
258
|
|
|
259
|
+
`dmon status` returns a non-zero status if a recorded task has exited. Starting
|
|
260
|
+
that task again removes its stale metadata automatically. `dmon stop` terminates
|
|
261
|
+
the complete process tree and also cleans stale metadata left by an exited task.
|
|
262
|
+
|
|
221
263
|
|
|
222
264
|
## License
|
|
223
265
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
[](https://github.com/atomiechen/python-dmon)
|
|
5
5
|
[](https://pypi.org/project/python-dmon/)
|
|
6
|
+
[](https://deepwiki.com/atomiechen/python-dmon)
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
|
|
@@ -18,7 +19,7 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
18
19
|
- 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
|
|
19
20
|
- ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
|
|
20
21
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
21
|
-
- 🪵 **Logging & log rotation:**
|
|
22
|
+
- 🪵 **Logging & log rotation:** Keep active log files manageable, with optional archive retention limits.
|
|
22
23
|
|
|
23
24
|

|
|
24
25
|
|
|
@@ -95,6 +96,9 @@ dmon start app
|
|
|
95
96
|
# Stop a running task
|
|
96
97
|
dmon stop app
|
|
97
98
|
|
|
99
|
+
# Restart a task
|
|
100
|
+
dmon restart app
|
|
101
|
+
|
|
98
102
|
# Check task status
|
|
99
103
|
dmon status app
|
|
100
104
|
|
|
@@ -102,21 +106,38 @@ dmon status app
|
|
|
102
106
|
dmon exec app
|
|
103
107
|
```
|
|
104
108
|
|
|
109
|
+
You can specify multiple tasks at once, e.g.: `dmon start app1 app2 app3`, except for `dmon exec` which only accepts one task.
|
|
110
|
+
|
|
111
|
+
Multi-task `start` is best-effort: dmon attempts every requested task and leaves
|
|
112
|
+
successful tasks running if another task cannot start. The command returns a
|
|
113
|
+
non-zero status and prints a summary naming the failed tasks. This is useful for
|
|
114
|
+
independent background services and does not provide atomic stack semantics.
|
|
115
|
+
|
|
116
|
+
Or use `--all` to operate on all tasks:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# all configured tasks
|
|
120
|
+
dmon start/stop/restart --all
|
|
121
|
+
# all running tasks
|
|
122
|
+
dmon stop/status --all
|
|
123
|
+
```
|
|
124
|
+
|
|
105
125
|
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
106
126
|
|
|
107
127
|
```sh
|
|
108
128
|
dmon start
|
|
109
129
|
dmon stop
|
|
130
|
+
dmon restart
|
|
110
131
|
dmon status
|
|
111
132
|
dmon exec
|
|
112
133
|
```
|
|
113
134
|
|
|
114
|
-
You can use `--config` to specify a custom config file or the directory containing it:
|
|
135
|
+
You can use `-c` / `--config` to specify a custom config file or the directory containing it:
|
|
115
136
|
|
|
116
137
|
```sh
|
|
117
138
|
dmon start --config /path/to/dmon.yaml app # YAML
|
|
118
139
|
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
119
|
-
dmon start
|
|
140
|
+
dmon start -c /path/to/dir app # shorter, dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
120
141
|
```
|
|
121
142
|
|
|
122
143
|
And yes, you can use `dmon` to run in a nested manner:
|
|
@@ -138,6 +159,9 @@ tasks:
|
|
|
138
159
|
# Run a command with arguments in the background
|
|
139
160
|
dmon run --name myserver python -u server.py
|
|
140
161
|
|
|
162
|
+
# Optionally use -- to make the child-command boundary explicit
|
|
163
|
+
dmon run --name timer -- python -c 'import time; time.sleep(30)'
|
|
164
|
+
|
|
141
165
|
# Run a shell command in the background
|
|
142
166
|
dmon run --shell echo "Hello World"
|
|
143
167
|
|
|
@@ -160,6 +184,16 @@ dmon list
|
|
|
160
184
|
|
|
161
185
|
A task can be a **string**, **list**, or **dictionary**.
|
|
162
186
|
|
|
187
|
+
When rotation is enabled, dmon keeps timestamped archives such as
|
|
188
|
+
`app.log.20260807-142106`. Both task and runner logs use this cross-platform
|
|
189
|
+
format; a same-second collision adds `.1`, `.2`, and so on. Archives are never
|
|
190
|
+
deleted by default. Set a backup count explicitly to enable retention cleanup.
|
|
191
|
+
`log_path` contains task output; `rotate_log_path` contains diagnostics from the
|
|
192
|
+
dmon process that captures and rotates that output. They are independent log
|
|
193
|
+
streams and use independent retention settings.
|
|
194
|
+
The size limit is checked at line boundaries, so a single long line may exceed
|
|
195
|
+
the configured limit.
|
|
196
|
+
|
|
163
197
|
Here is a more complete example with default values:
|
|
164
198
|
|
|
165
199
|
```yaml
|
|
@@ -174,8 +208,10 @@ tasks:
|
|
|
174
208
|
log_path: "logs/<task>.log" # path to log file
|
|
175
209
|
log_rotate: false # enable log rotation
|
|
176
210
|
log_max_size: 5 # max log file size before rotation in MB
|
|
211
|
+
# log_backup_count: 10 # optional; omit to retain all task log archives
|
|
177
212
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
178
213
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
214
|
+
# rotate_log_backup_count: 10 # optional; omit to retain all runner log archives
|
|
179
215
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
180
216
|
default_task: your_task_name # the default task name
|
|
181
217
|
```
|
|
@@ -193,6 +229,8 @@ another_task = "cd subdir && ls && bash start.sh"
|
|
|
193
229
|
default_task = "your_task_name"
|
|
194
230
|
```
|
|
195
231
|
|
|
232
|
+
All paths can be absolute or relative to the **config file location**.
|
|
233
|
+
|
|
196
234
|
|
|
197
235
|
## Under the Hood
|
|
198
236
|
|
|
@@ -200,6 +238,10 @@ Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored
|
|
|
200
238
|
The file contains details such as the command, PID, log path, and more.
|
|
201
239
|
**Do not** modify or delete these files manually.
|
|
202
240
|
|
|
241
|
+
`dmon status` returns a non-zero status if a recorded task has exited. Starting
|
|
242
|
+
that task again removes its stale metadata automatically. `dmon stop` terminates
|
|
243
|
+
the complete process tree and also cleans stale metadata left by an exited task.
|
|
244
|
+
|
|
203
245
|
|
|
204
246
|
## License
|
|
205
247
|
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
2
4
|
import shlex
|
|
3
|
-
import sys
|
|
4
5
|
|
|
5
6
|
from colorama import just_fix_windows_console
|
|
6
7
|
|
|
7
|
-
from .config import check_name_in_config, get_task_config
|
|
8
|
-
from .control import
|
|
8
|
+
from .config import check_name_in_config, get_task_config, load_config
|
|
9
|
+
from .control import (
|
|
10
|
+
execute,
|
|
11
|
+
get_meta_paths,
|
|
12
|
+
list_processes,
|
|
13
|
+
restart,
|
|
14
|
+
start,
|
|
15
|
+
stop,
|
|
16
|
+
status,
|
|
17
|
+
)
|
|
9
18
|
from .constants import (
|
|
10
19
|
DEFAULT_META_DIR,
|
|
11
20
|
DEFAULT_RUN_NAME,
|
|
@@ -50,7 +59,7 @@ def main():
|
|
|
50
59
|
sp_start.add_argument(
|
|
51
60
|
"task",
|
|
52
61
|
help="Configured task name (default: the only task if there's just one)",
|
|
53
|
-
nargs="
|
|
62
|
+
nargs="*",
|
|
54
63
|
)
|
|
55
64
|
sp_start.add_argument(
|
|
56
65
|
"--meta-file",
|
|
@@ -60,20 +69,26 @@ def main():
|
|
|
60
69
|
"--log-file",
|
|
61
70
|
help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
|
|
62
71
|
)
|
|
72
|
+
sp_start.add_argument("--all", action="store_true", help="Start all processes")
|
|
63
73
|
|
|
64
74
|
# stop subcommand
|
|
65
75
|
sp_stop = subparsers.add_parser(
|
|
66
76
|
"stop",
|
|
67
|
-
help="Stop
|
|
68
|
-
description="Stop
|
|
77
|
+
help="Stop background process(es)",
|
|
78
|
+
description="Stop background process(es) given name or meta file",
|
|
69
79
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
70
80
|
)
|
|
71
81
|
sp_stop.add_argument(
|
|
72
82
|
"task",
|
|
73
83
|
help="Configured task name (default: the only task if there's just one)",
|
|
74
|
-
nargs="
|
|
84
|
+
nargs="*",
|
|
75
85
|
)
|
|
76
86
|
sp_stop.add_argument("--meta-file", help="Path to meta file")
|
|
87
|
+
sp_stop.add_argument(
|
|
88
|
+
"--all",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help=f"Stop all processes in meta dir ({DEFAULT_META_DIR})",
|
|
91
|
+
)
|
|
77
92
|
|
|
78
93
|
# restart subcommand
|
|
79
94
|
sp_restart = subparsers.add_parser(
|
|
@@ -85,7 +100,7 @@ def main():
|
|
|
85
100
|
sp_restart.add_argument(
|
|
86
101
|
"task",
|
|
87
102
|
help="Configured task name (default: the only task if there's just one)",
|
|
88
|
-
nargs="
|
|
103
|
+
nargs="*",
|
|
89
104
|
)
|
|
90
105
|
sp_restart.add_argument(
|
|
91
106
|
"--meta-file",
|
|
@@ -95,23 +110,30 @@ def main():
|
|
|
95
110
|
"--log-file",
|
|
96
111
|
help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
|
|
97
112
|
)
|
|
113
|
+
sp_restart.add_argument("--all", action="store_true", help="Restart all processes")
|
|
98
114
|
|
|
99
115
|
# status subcommand
|
|
100
116
|
sp_status = subparsers.add_parser(
|
|
101
117
|
"status",
|
|
102
|
-
help="Check process
|
|
103
|
-
description="Check status of
|
|
118
|
+
help="Check status of background process(es)",
|
|
119
|
+
description="Check status of background process(es) given name or meta file",
|
|
104
120
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
105
121
|
)
|
|
106
122
|
sp_status.add_argument(
|
|
107
123
|
"task",
|
|
108
124
|
help="Configured task name (default: the only task if there's just one)",
|
|
109
|
-
nargs="
|
|
125
|
+
nargs="*",
|
|
110
126
|
)
|
|
111
127
|
sp_status.add_argument(
|
|
112
128
|
"--meta-file",
|
|
113
129
|
help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
|
|
114
130
|
)
|
|
131
|
+
sp_status.add_argument(
|
|
132
|
+
"-a",
|
|
133
|
+
"--all",
|
|
134
|
+
action="store_true",
|
|
135
|
+
help=f"Check status of all processes in meta dir ({DEFAULT_META_DIR})",
|
|
136
|
+
)
|
|
115
137
|
|
|
116
138
|
# list subcommand
|
|
117
139
|
sp_list = subparsers.add_parser(
|
|
@@ -171,9 +193,9 @@ def main():
|
|
|
171
193
|
)
|
|
172
194
|
sp_run.add_argument(
|
|
173
195
|
"command_list",
|
|
174
|
-
metavar="
|
|
175
|
-
nargs=argparse.
|
|
176
|
-
help="Command
|
|
196
|
+
metavar="COMMAND",
|
|
197
|
+
nargs=argparse.REMAINDER,
|
|
198
|
+
help="Command and arguments to run; '--' is an optional separator",
|
|
177
199
|
)
|
|
178
200
|
|
|
179
201
|
sp_exec = subparsers.add_parser(
|
|
@@ -190,7 +212,9 @@ def main():
|
|
|
190
212
|
# add custom config file option
|
|
191
213
|
for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
|
|
192
214
|
sp.add_argument(
|
|
215
|
+
"-c",
|
|
193
216
|
"--config",
|
|
217
|
+
metavar="PATH",
|
|
194
218
|
help="Path to config file or the directory containing it (default: search from current directory upwards)",
|
|
195
219
|
)
|
|
196
220
|
|
|
@@ -199,50 +223,98 @@ def main():
|
|
|
199
223
|
if args.command in ["start", "restart"]:
|
|
200
224
|
sp = sp_start if args.command == "start" else sp_restart
|
|
201
225
|
try:
|
|
202
|
-
|
|
226
|
+
tasks, task_cfgs, cfg_path = get_task_config(
|
|
227
|
+
args.task, args.config, args.all
|
|
228
|
+
)
|
|
229
|
+
os.chdir(cfg_path.parent)
|
|
203
230
|
except Exception as e:
|
|
204
231
|
sp.error(str(e))
|
|
205
232
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
233
|
+
# check if meta_file or log_file path is provided;
|
|
234
|
+
# if so, only one task should be specified
|
|
235
|
+
if args.meta_file or args.log_file:
|
|
236
|
+
if len(tasks) == 1:
|
|
237
|
+
task_cfgs[0].meta_path = args.meta_file or task_cfgs[0].meta_path
|
|
238
|
+
task_cfgs[0].log_path = args.log_file or task_cfgs[0].log_path
|
|
239
|
+
else:
|
|
240
|
+
sp.error(
|
|
241
|
+
f"'--meta-file' and '--log-file' can only be specified when {args.command}ing a single task"
|
|
242
|
+
)
|
|
243
|
+
# fill in default values if not provided
|
|
244
|
+
for task, task_cfg in zip(tasks, task_cfgs):
|
|
245
|
+
task_cfg.meta_path = task_cfg.meta_path or META_PATH_TEMPLATE.format(
|
|
246
|
+
task=task
|
|
247
|
+
)
|
|
248
|
+
task_cfg.log_path = task_cfg.log_path or LOG_PATH_TEMPLATE.format(task=task)
|
|
249
|
+
task_cfg.rotate_log_path = (
|
|
250
|
+
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
|
|
251
|
+
)
|
|
215
252
|
if args.command == "start":
|
|
216
|
-
|
|
253
|
+
sp.exit(start(task_cfgs))
|
|
217
254
|
else:
|
|
218
|
-
|
|
255
|
+
sp.exit(restart(task_cfgs))
|
|
219
256
|
elif args.command == "exec":
|
|
220
257
|
try:
|
|
221
|
-
|
|
258
|
+
_, task_cfgs, cfg_path = get_task_config(args.task, args.config)
|
|
259
|
+
os.chdir(cfg_path.parent)
|
|
222
260
|
except Exception as e:
|
|
223
261
|
sp_exec.error(str(e))
|
|
224
|
-
|
|
262
|
+
sp_exec.exit(execute(task_cfgs[0]))
|
|
225
263
|
elif args.command in ["stop", "status"]:
|
|
226
264
|
sp = sp_stop if args.command == "stop" else sp_status
|
|
265
|
+
meta_paths = []
|
|
266
|
+
|
|
267
|
+
tasks = args.task
|
|
268
|
+
if args.task:
|
|
269
|
+
try:
|
|
270
|
+
tasks, _, cfg_path = get_task_config(args.task, args.config)
|
|
271
|
+
os.chdir(cfg_path.parent)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
sp.error(str(e))
|
|
274
|
+
elif args.config:
|
|
275
|
+
try:
|
|
276
|
+
_, cfg_path = load_config(args.config)
|
|
277
|
+
os.chdir(cfg_path.parent)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
sp.error(str(e))
|
|
280
|
+
|
|
281
|
+
# Collect meta paths from --all
|
|
282
|
+
if args.all:
|
|
283
|
+
meta_paths.extend(get_meta_paths(DEFAULT_META_DIR))
|
|
284
|
+
|
|
285
|
+
# Collect meta paths from --meta-file
|
|
227
286
|
if args.meta_file:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
287
|
+
meta_paths.append(args.meta_file)
|
|
288
|
+
|
|
289
|
+
# Collect meta paths from task names
|
|
290
|
+
if len(tasks) > 0:
|
|
291
|
+
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
292
|
+
|
|
293
|
+
# If no meta paths collected, use default task
|
|
294
|
+
if len(meta_paths) == 0:
|
|
295
|
+
try:
|
|
296
|
+
tasks, _, cfg_path = get_task_config(args.task, args.config)
|
|
297
|
+
os.chdir(cfg_path.parent)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
sp.error(str(e))
|
|
300
|
+
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
301
|
+
|
|
302
|
+
# Remove duplicates
|
|
303
|
+
unique_meta_paths = sorted(set(Path(p).resolve() for p in meta_paths))
|
|
304
|
+
|
|
238
305
|
if args.command == "stop":
|
|
239
|
-
|
|
306
|
+
sp.exit(stop(unique_meta_paths))
|
|
240
307
|
else:
|
|
241
|
-
|
|
308
|
+
sp.exit(status(unique_meta_paths))
|
|
242
309
|
elif args.command == "list":
|
|
243
310
|
dir = args.dir or DEFAULT_META_DIR
|
|
244
|
-
|
|
311
|
+
sp_list.exit(list_processes(dir, args.full))
|
|
245
312
|
elif args.command == "run":
|
|
313
|
+
command_list = args.command_list
|
|
314
|
+
if command_list and command_list[0] == "--":
|
|
315
|
+
command_list = command_list[1:]
|
|
316
|
+
if not command_list:
|
|
317
|
+
sp_run.error("Please provide a command to run.")
|
|
246
318
|
if not args.name:
|
|
247
319
|
sp_run.error("Please provide a non-empty name for the task.")
|
|
248
320
|
elif check_name_in_config(args.name):
|
|
@@ -252,7 +324,7 @@ def main():
|
|
|
252
324
|
|
|
253
325
|
task_cfg = DmonTaskConfig(
|
|
254
326
|
task=args.name,
|
|
255
|
-
cmd=shlex.join(
|
|
327
|
+
cmd=shlex.join(command_list) if args.shell else command_list,
|
|
256
328
|
cwd=args.cwd,
|
|
257
329
|
meta_path=args.meta_file or META_PATH_TEMPLATE.format(task=args.name),
|
|
258
330
|
log_path=args.log_file or LOG_PATH_TEMPLATE.format(task=args.name),
|
|
@@ -260,10 +332,10 @@ def main():
|
|
|
260
332
|
rotate_log_path=args.rotate_log_path
|
|
261
333
|
or ROTATE_LOG_PATH_TEMPLATE.format(task=args.name),
|
|
262
334
|
)
|
|
263
|
-
|
|
335
|
+
sp_run.exit(start([task_cfg]))
|
|
264
336
|
else:
|
|
265
337
|
parser.print_help()
|
|
266
|
-
|
|
338
|
+
parser.exit(1)
|
|
267
339
|
|
|
268
340
|
|
|
269
341
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict, Optional, cast
|
|
3
|
+
from typing import Dict, List, Optional, Sequence, Tuple, Union, cast
|
|
4
4
|
|
|
5
5
|
if sys.version_info >= (3, 11):
|
|
6
6
|
import tomllib
|
|
@@ -64,6 +64,10 @@ def load_config(cfg_path: Optional[str] = None):
|
|
|
64
64
|
cfg = cfg.get("tool", {}).get("dmon", {})
|
|
65
65
|
else:
|
|
66
66
|
raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
|
|
67
|
+
if cfg is None:
|
|
68
|
+
cfg = {}
|
|
69
|
+
if not isinstance(cfg, dict):
|
|
70
|
+
raise TypeError(f"Config in '{path}' must be a table")
|
|
67
71
|
return cfg, path
|
|
68
72
|
|
|
69
73
|
|
|
@@ -121,12 +125,23 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
121
125
|
ret.log_rotate = task["log_rotate"]
|
|
122
126
|
|
|
123
127
|
if "log_max_size" in task:
|
|
124
|
-
if
|
|
128
|
+
if (
|
|
129
|
+
not isinstance(task["log_max_size"], (int, float))
|
|
130
|
+
or task["log_max_size"] <= 0
|
|
131
|
+
):
|
|
125
132
|
raise TypeError(
|
|
126
133
|
f"Task '{name}' 'log_max_size' field must be a positive number"
|
|
127
134
|
)
|
|
128
135
|
ret.log_max_size = task["log_max_size"]
|
|
129
136
|
|
|
137
|
+
if "log_backup_count" in task:
|
|
138
|
+
value = task["log_backup_count"]
|
|
139
|
+
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
|
140
|
+
raise TypeError(
|
|
141
|
+
f"Task '{name}' 'log_backup_count' field must be a positive integer"
|
|
142
|
+
)
|
|
143
|
+
ret.log_backup_count = value
|
|
144
|
+
|
|
130
145
|
if "rotate_log_path" in task:
|
|
131
146
|
if not isinstance(task["rotate_log_path"], str):
|
|
132
147
|
raise TypeError(
|
|
@@ -144,6 +159,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
144
159
|
)
|
|
145
160
|
ret.rotate_log_max_size = task["rotate_log_max_size"]
|
|
146
161
|
|
|
162
|
+
if "rotate_log_backup_count" in task:
|
|
163
|
+
value = task["rotate_log_backup_count"]
|
|
164
|
+
if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
|
|
165
|
+
raise TypeError(
|
|
166
|
+
f"Task '{name}' 'rotate_log_backup_count' field must be a positive integer"
|
|
167
|
+
)
|
|
168
|
+
ret.rotate_log_backup_count = value
|
|
169
|
+
|
|
147
170
|
if "meta_path" in task:
|
|
148
171
|
if not isinstance(task["meta_path"], str):
|
|
149
172
|
raise TypeError(f"Task '{name}' 'meta_path' field must be a string")
|
|
@@ -155,11 +178,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
155
178
|
return ret
|
|
156
179
|
|
|
157
180
|
|
|
158
|
-
def get_task_config(
|
|
181
|
+
def get_task_config(
|
|
182
|
+
names: Union[Sequence[str], str, None], cfg_path: Optional[str], all: bool = False
|
|
183
|
+
) -> Tuple[Sequence[str], List[DmonTaskConfig], Path]:
|
|
159
184
|
"""
|
|
160
|
-
Get the validated task
|
|
161
|
-
If
|
|
162
|
-
If
|
|
185
|
+
Get the validated task configurations for the given task names.
|
|
186
|
+
If 'all' is True, return all tasks.
|
|
187
|
+
If no name specified, and there is only one task, return that task; otherwise, raise ValueError.
|
|
188
|
+
If any task is not found, or required fields are missing, raise TypeError or ValueError.
|
|
163
189
|
|
|
164
190
|
The config is loaded from the given path, or searched for dmon.yaml or pyproject.toml.
|
|
165
191
|
"""
|
|
@@ -169,23 +195,37 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
|
|
|
169
195
|
if not isinstance(tasks, dict):
|
|
170
196
|
raise TypeError("'tasks' must be a table")
|
|
171
197
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
if all:
|
|
199
|
+
names = list(tasks.keys())
|
|
200
|
+
elif isinstance(names, str):
|
|
201
|
+
names = [names]
|
|
202
|
+
elif names is None or len(names) == 0:
|
|
203
|
+
default_task_name = cfg.get("default_task", None)
|
|
204
|
+
if default_task_name:
|
|
205
|
+
if not isinstance(default_task_name, str):
|
|
206
|
+
raise TypeError("'default_task' must be a string")
|
|
207
|
+
names = [default_task_name]
|
|
179
208
|
else:
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
if len(tasks) == 0:
|
|
210
|
+
raise ValueError(f"No task found in {path}")
|
|
211
|
+
elif len(tasks) == 1:
|
|
212
|
+
name = next(iter(tasks))
|
|
213
|
+
assert isinstance(name, str)
|
|
214
|
+
names = [name]
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError(f"Multiple tasks found in {path}; please specify one.")
|
|
217
|
+
|
|
218
|
+
ret_names = []
|
|
219
|
+
ret_tasks = []
|
|
220
|
+
for name in names:
|
|
182
221
|
name = name.lower()
|
|
183
222
|
if name not in tasks:
|
|
184
223
|
raise ValueError(f"Task '{name}' not found in {path}")
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
task = validate_task(tasks[name], name)
|
|
226
|
+
ret_names.append(name)
|
|
227
|
+
ret_tasks.append(task)
|
|
228
|
+
return ret_names, ret_tasks, path
|
|
189
229
|
|
|
190
230
|
|
|
191
231
|
def check_name_in_config(name: str) -> bool:
|
|
@@ -193,7 +233,10 @@ def check_name_in_config(name: str) -> bool:
|
|
|
193
233
|
Check if the given task name exists in the tasks.
|
|
194
234
|
Return True if found, False otherwise.
|
|
195
235
|
"""
|
|
196
|
-
|
|
236
|
+
try:
|
|
237
|
+
cfg, _ = load_config()
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
return False
|
|
197
240
|
tasks = cfg.get("tasks", {})
|
|
198
241
|
|
|
199
242
|
if not isinstance(tasks, dict):
|