python-dmon 0.2.4__py3-none-any.whl → 0.3.1__py3-none-any.whl
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.
- dmon/cli.py +116 -44
- dmon/config.py +62 -19
- dmon/control.py +406 -215
- dmon/runner.py +74 -27
- dmon/types.py +40 -3
- {python_dmon-0.2.4.dist-info → python_dmon-0.3.1.dist-info}/METADATA +46 -4
- python_dmon-0.3.1.dist-info/RECORD +14 -0
- python_dmon-0.2.4.dist-info/RECORD +0 -14
- {python_dmon-0.2.4.dist-info → python_dmon-0.3.1.dist-info}/WHEEL +0 -0
- {python_dmon-0.2.4.dist-info → python_dmon-0.3.1.dist-info}/entry_points.txt +0 -0
dmon/cli.py
CHANGED
|
@@ -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__":
|
dmon/config.py
CHANGED
|
@@ -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):
|