python-dmon 0.2.3__tar.gz → 0.3.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.
- {python_dmon-0.2.3 → python_dmon-0.3.0}/PKG-INFO +23 -3
- {python_dmon-0.2.3 → python_dmon-0.3.0}/README.md +22 -2
- {python_dmon-0.2.3 → python_dmon-0.3.0}/pyproject.toml +1 -1
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/cli.py +83 -36
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/config.py +39 -21
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/control.py +66 -29
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/runner.py +4 -4
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/types.py +2 -2
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/__init__.py +0 -0
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/__main__.py +0 -0
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/constants.py +0 -0
- {python_dmon-0.2.3 → python_dmon-0.3.0}/src/dmon/py.typed +0 -0
- {python_dmon-0.2.3 → python_dmon-0.3.0}/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.0
|
|
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.
|
|
@@ -38,6 +39,8 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
38
39
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
39
40
|
- 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
|
|
40
41
|
|
|
42
|
+

|
|
43
|
+
|
|
41
44
|
|
|
42
45
|
## Installation
|
|
43
46
|
|
|
@@ -84,14 +87,16 @@ Create a `dmon.yaml` file:
|
|
|
84
87
|
|
|
85
88
|
```yaml
|
|
86
89
|
tasks:
|
|
87
|
-
app: ["python", "-u", "server.py"]
|
|
90
|
+
app: ["python", "-u", "server.py"] # option 1: exec form
|
|
91
|
+
# app: "python -u server.py" # option 2: shell string
|
|
88
92
|
```
|
|
89
93
|
|
|
90
94
|
Or add to your `pyproject.toml`:
|
|
91
95
|
|
|
92
96
|
```toml
|
|
93
97
|
[tool.dmon.tasks]
|
|
94
|
-
app = ["python", "-u", "server.py"]
|
|
98
|
+
app = ["python", "-u", "server.py"] # option 1: exec form
|
|
99
|
+
# app = "python -u server.py" # option 2: shell string
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
@@ -109,6 +114,9 @@ dmon start app
|
|
|
109
114
|
# Stop a running task
|
|
110
115
|
dmon stop app
|
|
111
116
|
|
|
117
|
+
# Restart a task
|
|
118
|
+
dmon restart app
|
|
119
|
+
|
|
112
120
|
# Check task status
|
|
113
121
|
dmon status app
|
|
114
122
|
|
|
@@ -116,11 +124,23 @@ dmon status app
|
|
|
116
124
|
dmon exec app
|
|
117
125
|
```
|
|
118
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
|
+
Or use `--all` to operate on all tasks:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
# all configured tasks
|
|
133
|
+
dmon start/stop/restart --all
|
|
134
|
+
# all running tasks
|
|
135
|
+
dmon stop/status --all
|
|
136
|
+
```
|
|
137
|
+
|
|
119
138
|
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
120
139
|
|
|
121
140
|
```sh
|
|
122
141
|
dmon start
|
|
123
142
|
dmon stop
|
|
143
|
+
dmon restart
|
|
124
144
|
dmon status
|
|
125
145
|
dmon exec
|
|
126
146
|
```
|
|
@@ -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.
|
|
@@ -20,6 +21,8 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
20
21
|
- 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
|
|
21
22
|
- 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
|
|
22
23
|
|
|
24
|
+

|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
## Installation
|
|
25
28
|
|
|
@@ -66,14 +69,16 @@ Create a `dmon.yaml` file:
|
|
|
66
69
|
|
|
67
70
|
```yaml
|
|
68
71
|
tasks:
|
|
69
|
-
app: ["python", "-u", "server.py"]
|
|
72
|
+
app: ["python", "-u", "server.py"] # option 1: exec form
|
|
73
|
+
# app: "python -u server.py" # option 2: shell string
|
|
70
74
|
```
|
|
71
75
|
|
|
72
76
|
Or add to your `pyproject.toml`:
|
|
73
77
|
|
|
74
78
|
```toml
|
|
75
79
|
[tool.dmon.tasks]
|
|
76
|
-
app = ["python", "-u", "server.py"]
|
|
80
|
+
app = ["python", "-u", "server.py"] # option 1: exec form
|
|
81
|
+
# app = "python -u server.py" # option 2: shell string
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
@@ -91,6 +96,9 @@ dmon start app
|
|
|
91
96
|
# Stop a running task
|
|
92
97
|
dmon stop app
|
|
93
98
|
|
|
99
|
+
# Restart a task
|
|
100
|
+
dmon restart app
|
|
101
|
+
|
|
94
102
|
# Check task status
|
|
95
103
|
dmon status app
|
|
96
104
|
|
|
@@ -98,11 +106,23 @@ dmon status app
|
|
|
98
106
|
dmon exec app
|
|
99
107
|
```
|
|
100
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
|
+
Or use `--all` to operate on all tasks:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
# all configured tasks
|
|
115
|
+
dmon start/stop/restart --all
|
|
116
|
+
# all running tasks
|
|
117
|
+
dmon stop/status --all
|
|
118
|
+
```
|
|
119
|
+
|
|
101
120
|
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
102
121
|
|
|
103
122
|
```sh
|
|
104
123
|
dmon start
|
|
105
124
|
dmon stop
|
|
125
|
+
dmon restart
|
|
106
126
|
dmon status
|
|
107
127
|
dmon exec
|
|
108
128
|
```
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
import shlex
|
|
3
4
|
import sys
|
|
4
5
|
|
|
5
6
|
from colorama import just_fix_windows_console
|
|
6
7
|
|
|
7
8
|
from .config import check_name_in_config, get_task_config
|
|
8
|
-
from .control import
|
|
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(
|
|
@@ -199,46 +221,71 @@ def main():
|
|
|
199
221
|
if args.command in ["start", "restart"]:
|
|
200
222
|
sp = sp_start if args.command == "start" else sp_restart
|
|
201
223
|
try:
|
|
202
|
-
|
|
224
|
+
tasks, task_cfgs = get_task_config(args.task, args.config, args.all)
|
|
203
225
|
except Exception as e:
|
|
204
226
|
sp.error(str(e))
|
|
205
227
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
# check if meta_file or log_file path is provided;
|
|
229
|
+
# if so, only one task should be specified
|
|
230
|
+
if args.meta_file or args.log_file:
|
|
231
|
+
if len(tasks) == 1:
|
|
232
|
+
task_cfgs[0].meta_path = args.meta_file or task_cfgs[0].meta_path
|
|
233
|
+
task_cfgs[0].log_path = args.log_file or task_cfgs[0].log_path
|
|
234
|
+
else:
|
|
235
|
+
sp.error(
|
|
236
|
+
f"'--meta-file' and '--log-file' can only be specified when {args.command}ing a single task"
|
|
237
|
+
)
|
|
238
|
+
# fill in default values if not provided
|
|
239
|
+
for task, task_cfg in zip(tasks, task_cfgs):
|
|
240
|
+
task_cfg.meta_path = task_cfg.meta_path or META_PATH_TEMPLATE.format(
|
|
241
|
+
task=task
|
|
242
|
+
)
|
|
243
|
+
task_cfg.log_path = task_cfg.log_path or LOG_PATH_TEMPLATE.format(task=task)
|
|
244
|
+
task_cfg.rotate_log_path = (
|
|
245
|
+
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
|
|
246
|
+
)
|
|
215
247
|
if args.command == "start":
|
|
216
|
-
sys.exit(start(
|
|
248
|
+
sys.exit(start(task_cfgs))
|
|
217
249
|
else:
|
|
218
|
-
sys.exit(restart(
|
|
250
|
+
sys.exit(restart(task_cfgs))
|
|
219
251
|
elif args.command == "exec":
|
|
220
252
|
try:
|
|
221
|
-
|
|
253
|
+
_, task_cfgs = get_task_config(args.task, args.config)
|
|
222
254
|
except Exception as e:
|
|
223
255
|
sp_exec.error(str(e))
|
|
224
|
-
sys.exit(execute(
|
|
256
|
+
sys.exit(execute(task_cfgs[0]))
|
|
225
257
|
elif args.command in ["stop", "status"]:
|
|
226
258
|
sp = sp_stop if args.command == "stop" else sp_status
|
|
259
|
+
meta_paths = []
|
|
260
|
+
|
|
261
|
+
# Collect meta paths from --all
|
|
262
|
+
if args.all:
|
|
263
|
+
meta_paths.extend(get_meta_paths(DEFAULT_META_DIR))
|
|
264
|
+
|
|
265
|
+
# Collect meta paths from --meta-file
|
|
227
266
|
if args.meta_file:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
meta_paths.append(args.meta_file)
|
|
268
|
+
|
|
269
|
+
# Collect meta paths from task names
|
|
270
|
+
if len(args.task) > 0:
|
|
271
|
+
tasks = args.task
|
|
272
|
+
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
273
|
+
|
|
274
|
+
# If no meta paths collected, use default task
|
|
275
|
+
if len(meta_paths) == 0:
|
|
276
|
+
try:
|
|
277
|
+
tasks, _ = get_task_config(args.task, args.config)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
sp.error(str(e))
|
|
280
|
+
meta_paths.extend([META_PATH_TEMPLATE.format(task=task) for task in tasks])
|
|
281
|
+
|
|
282
|
+
# Remove duplicates
|
|
283
|
+
unique_meta_paths = sorted(set(Path(p).resolve() for p in meta_paths))
|
|
284
|
+
|
|
238
285
|
if args.command == "stop":
|
|
239
|
-
sys.exit(stop(
|
|
286
|
+
sys.exit(stop(unique_meta_paths))
|
|
240
287
|
else:
|
|
241
|
-
sys.exit(status(
|
|
288
|
+
sys.exit(status(unique_meta_paths))
|
|
242
289
|
elif args.command == "list":
|
|
243
290
|
dir = args.dir or DEFAULT_META_DIR
|
|
244
291
|
sys.exit(list_processes(dir, args.full))
|
|
@@ -260,7 +307,7 @@ def main():
|
|
|
260
307
|
rotate_log_path=args.rotate_log_path
|
|
261
308
|
or ROTATE_LOG_PATH_TEMPLATE.format(task=args.name),
|
|
262
309
|
)
|
|
263
|
-
sys.exit(start(task_cfg))
|
|
310
|
+
sys.exit(start([task_cfg]))
|
|
264
311
|
else:
|
|
265
312
|
parser.print_help()
|
|
266
313
|
sys.exit(1)
|
|
@@ -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
|
|
@@ -121,9 +121,12 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
121
121
|
ret.log_rotate = task["log_rotate"]
|
|
122
122
|
|
|
123
123
|
if "log_max_size" in task:
|
|
124
|
-
if
|
|
124
|
+
if (
|
|
125
|
+
not isinstance(task["log_max_size"], (int, float))
|
|
126
|
+
or task["log_max_size"] <= 0
|
|
127
|
+
):
|
|
125
128
|
raise TypeError(
|
|
126
|
-
f"Task '{name}' 'log_max_size' field must be a positive
|
|
129
|
+
f"Task '{name}' 'log_max_size' field must be a positive number"
|
|
127
130
|
)
|
|
128
131
|
ret.log_max_size = task["log_max_size"]
|
|
129
132
|
|
|
@@ -136,11 +139,11 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
136
139
|
|
|
137
140
|
if "rotate_log_max_size" in task:
|
|
138
141
|
if (
|
|
139
|
-
not isinstance(task["rotate_log_max_size"], int)
|
|
142
|
+
not isinstance(task["rotate_log_max_size"], (int, float))
|
|
140
143
|
or task["rotate_log_max_size"] <= 0
|
|
141
144
|
):
|
|
142
145
|
raise TypeError(
|
|
143
|
-
f"Task '{name}' 'rotate_log_max_size' field must be a positive
|
|
146
|
+
f"Task '{name}' 'rotate_log_max_size' field must be a positive number"
|
|
144
147
|
)
|
|
145
148
|
ret.rotate_log_max_size = task["rotate_log_max_size"]
|
|
146
149
|
|
|
@@ -155,11 +158,14 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
155
158
|
return ret
|
|
156
159
|
|
|
157
160
|
|
|
158
|
-
def get_task_config(
|
|
161
|
+
def get_task_config(
|
|
162
|
+
names: Union[Sequence[str], str, None], cfg_path: Optional[str], all: bool = False
|
|
163
|
+
) -> Tuple[Sequence[str], List[DmonTaskConfig]]:
|
|
159
164
|
"""
|
|
160
|
-
Get the validated task
|
|
161
|
-
If
|
|
162
|
-
If
|
|
165
|
+
Get the validated task configurations for the given task names.
|
|
166
|
+
If 'all' is True, return all tasks.
|
|
167
|
+
If no name specified, and there is only one task, return that task; otherwise, raise ValueError.
|
|
168
|
+
If any task is not found, or required fields are missing, raise TypeError or ValueError.
|
|
163
169
|
|
|
164
170
|
The config is loaded from the given path, or searched for dmon.yaml or pyproject.toml.
|
|
165
171
|
"""
|
|
@@ -169,23 +175,35 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
|
|
|
169
175
|
if not isinstance(tasks, dict):
|
|
170
176
|
raise TypeError("'tasks' must be a table")
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
if all:
|
|
179
|
+
names = list(tasks.keys())
|
|
180
|
+
elif isinstance(names, str):
|
|
181
|
+
names = [names]
|
|
182
|
+
elif names is None or len(names) == 0:
|
|
183
|
+
default_task_name = cfg.get("default_task", None)
|
|
184
|
+
if default_task_name:
|
|
185
|
+
if not isinstance(default_task_name, str):
|
|
186
|
+
raise TypeError("'default_task' must be a string")
|
|
187
|
+
names = [default_task_name]
|
|
179
188
|
else:
|
|
180
|
-
|
|
181
|
-
|
|
189
|
+
if len(tasks) == 0:
|
|
190
|
+
raise ValueError(f"No task found in {path}")
|
|
191
|
+
elif len(tasks) == 1:
|
|
192
|
+
name = next(iter(tasks))
|
|
193
|
+
assert isinstance(name, str)
|
|
194
|
+
names = [name]
|
|
195
|
+
else:
|
|
196
|
+
raise ValueError(f"Multiple tasks found in {path}; please specify one.")
|
|
197
|
+
|
|
198
|
+
ret_tasks = []
|
|
199
|
+
for name in names:
|
|
182
200
|
name = name.lower()
|
|
183
201
|
if name not in tasks:
|
|
184
202
|
raise ValueError(f"Task '{name}' not found in {path}")
|
|
185
203
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return
|
|
204
|
+
task = validate_task(tasks[name], name)
|
|
205
|
+
ret_tasks.append(task)
|
|
206
|
+
return names, ret_tasks
|
|
189
207
|
|
|
190
208
|
|
|
191
209
|
def check_name_in_config(name: str) -> bool:
|
|
@@ -6,7 +6,7 @@ import signal
|
|
|
6
6
|
import sys
|
|
7
7
|
import subprocess
|
|
8
8
|
import time
|
|
9
|
-
from typing import List
|
|
9
|
+
from typing import List, Sequence
|
|
10
10
|
|
|
11
11
|
import psutil
|
|
12
12
|
from termcolor import colored
|
|
@@ -33,7 +33,17 @@ def ensure_log_dir(log_path: Path):
|
|
|
33
33
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def start(
|
|
36
|
+
def start(cfgs: Sequence[DmonTaskConfig]):
|
|
37
|
+
ret = 0
|
|
38
|
+
for idx, cfg in enumerate(cfgs):
|
|
39
|
+
# non-zero if any start() fails
|
|
40
|
+
ret |= start_single(cfg)
|
|
41
|
+
if idx < len(cfgs) - 1:
|
|
42
|
+
print("---", file=sys.stderr) # print a blank line between tasks
|
|
43
|
+
return ret
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def start_single(cfg: DmonTaskConfig):
|
|
37
47
|
meta_path = Path(cfg.meta_path).resolve()
|
|
38
48
|
log_path = Path(cfg.log_path).resolve()
|
|
39
49
|
cwd = Path(cfg.cwd).resolve()
|
|
@@ -166,7 +176,17 @@ def start(cfg: DmonTaskConfig):
|
|
|
166
176
|
return 0
|
|
167
177
|
|
|
168
178
|
|
|
169
|
-
def stop(
|
|
179
|
+
def stop(meta_paths: Sequence[PathType], timeout=5.0):
|
|
180
|
+
ret = 0
|
|
181
|
+
for idx, meta_path in enumerate(meta_paths):
|
|
182
|
+
# non-zero if any stop() fails
|
|
183
|
+
ret |= stop_single(meta_path=meta_path, timeout=timeout)
|
|
184
|
+
if idx < len(meta_paths) - 1:
|
|
185
|
+
print(file=sys.stderr) # print a blank line between tasks
|
|
186
|
+
return ret
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def stop_single(
|
|
170
190
|
meta_path: PathType,
|
|
171
191
|
timeout=5.0,
|
|
172
192
|
):
|
|
@@ -345,33 +365,41 @@ def terminate_win(proc: psutil.Process, timeout):
|
|
|
345
365
|
|
|
346
366
|
|
|
347
367
|
def restart(
|
|
348
|
-
|
|
368
|
+
cfgs: Sequence[DmonTaskConfig],
|
|
349
369
|
timeout=5.0,
|
|
350
370
|
):
|
|
351
|
-
|
|
371
|
+
meta_paths = [cfg.meta_path for cfg in cfgs]
|
|
372
|
+
stop(meta_paths, timeout=timeout)
|
|
352
373
|
print("--- Restarting ---", file=sys.stderr)
|
|
353
|
-
return start(
|
|
374
|
+
return start(cfgs)
|
|
354
375
|
|
|
355
376
|
|
|
356
|
-
def status(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
def status(meta_paths: Sequence[PathType]):
|
|
378
|
+
ret = 0
|
|
379
|
+
metas = []
|
|
380
|
+
for idx, meta_path in enumerate(meta_paths):
|
|
381
|
+
meta_path = Path(meta_path).resolve()
|
|
382
|
+
meta = DmonMeta.load(meta_path)
|
|
383
|
+
if meta is None:
|
|
384
|
+
print(
|
|
385
|
+
colored(
|
|
386
|
+
"Status failed: meta file not found (maybe not started)\n",
|
|
387
|
+
color="red",
|
|
388
|
+
attrs=["bold"],
|
|
389
|
+
)
|
|
390
|
+
+ str(meta_path),
|
|
391
|
+
file=sys.stderr,
|
|
365
392
|
)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
393
|
+
ret |= 1
|
|
394
|
+
else:
|
|
395
|
+
print_status(meta)
|
|
396
|
+
metas.append(meta)
|
|
397
|
+
if idx < len(meta_paths) - 1:
|
|
398
|
+
print("---", file=sys.stderr)
|
|
399
|
+
if metas:
|
|
400
|
+
print("\nProcess Tree:", file=sys.stderr)
|
|
401
|
+
print_process_table(metas)
|
|
402
|
+
return ret
|
|
375
403
|
|
|
376
404
|
|
|
377
405
|
def check_same_process(proc: psutil.Process, create_time: float) -> bool:
|
|
@@ -523,14 +551,23 @@ def print_process_table(metas: List[DmonMeta], full_width: bool = False):
|
|
|
523
551
|
return processes
|
|
524
552
|
|
|
525
553
|
|
|
526
|
-
def
|
|
554
|
+
def get_meta_paths(dir: PathType) -> List[Path]:
|
|
527
555
|
target_dmon_dir = Path(dir).resolve()
|
|
528
|
-
|
|
556
|
+
meta_paths: List[Path] = []
|
|
529
557
|
if target_dmon_dir.exists() and target_dmon_dir.is_dir():
|
|
530
558
|
for meta_file in target_dmon_dir.glob(f"*{META_SUFFIX}"):
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
559
|
+
meta_paths.append(meta_file)
|
|
560
|
+
return meta_paths
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def list_processes(dir: PathType, full_width: bool):
|
|
564
|
+
target_dmon_dir = Path(dir).resolve()
|
|
565
|
+
meta_paths = get_meta_paths(target_dmon_dir)
|
|
566
|
+
metas = []
|
|
567
|
+
for meta_path in meta_paths:
|
|
568
|
+
meta = DmonMeta.load(meta_path)
|
|
569
|
+
if meta is not None:
|
|
570
|
+
metas.append(meta)
|
|
534
571
|
# sort by name (case-insensitive)
|
|
535
572
|
metas.sort(key=lambda m: m.task.lower())
|
|
536
573
|
n_task = len(metas)
|
|
@@ -167,7 +167,7 @@ if __name__ == "__main__":
|
|
|
167
167
|
parser.add_argument(
|
|
168
168
|
"--max-log-size",
|
|
169
169
|
help="Max log file size (MB); 0 for no rotation",
|
|
170
|
-
type=
|
|
170
|
+
type=float,
|
|
171
171
|
default=5,
|
|
172
172
|
)
|
|
173
173
|
parser.add_argument(
|
|
@@ -178,15 +178,15 @@ if __name__ == "__main__":
|
|
|
178
178
|
parser.add_argument(
|
|
179
179
|
"--max-rotate-log-size",
|
|
180
180
|
help="Max file size (MB) for this rotation log; 0 for no rotation",
|
|
181
|
-
type=
|
|
181
|
+
type=float,
|
|
182
182
|
default=5,
|
|
183
183
|
)
|
|
184
184
|
args = parser.parse_args()
|
|
185
185
|
main(
|
|
186
186
|
" ".join(args.command) if args.shell else args.command,
|
|
187
187
|
args.log_path,
|
|
188
|
-
args.max_log_size * 1024 * 1024,
|
|
188
|
+
int(args.max_log_size * 1024 * 1024),
|
|
189
189
|
args.rotate_log_path,
|
|
190
|
-
args.max_rotate_log_size * 1024 * 1024,
|
|
190
|
+
int(args.max_rotate_log_size * 1024 * 1024),
|
|
191
191
|
)
|
|
192
192
|
logger.info("Process finished.")
|
|
@@ -31,11 +31,11 @@ class DmonTaskConfig:
|
|
|
31
31
|
"""Path to log file"""
|
|
32
32
|
log_rotate: bool = False
|
|
33
33
|
"""Whether to rotate log file"""
|
|
34
|
-
log_max_size:
|
|
34
|
+
log_max_size: float = 5
|
|
35
35
|
"""Size in MB to rotate log file"""
|
|
36
36
|
rotate_log_path: str = ""
|
|
37
37
|
"""Path to rotation log file"""
|
|
38
|
-
rotate_log_max_size:
|
|
38
|
+
rotate_log_max_size: float = 5
|
|
39
39
|
"""Size in MB to rotation log file"""
|
|
40
40
|
meta_path: str = ""
|
|
41
41
|
"""Path to meta file"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|