python-dmon 0.2.0__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/__init__.py +0 -0
- dmon/__main__.py +5 -0
- dmon/cli.py +232 -0
- dmon/config.py +195 -0
- dmon/constants.py +14 -0
- dmon/control.py +542 -0
- dmon/py.typed +0 -0
- dmon/runner.py +188 -0
- dmon/types.py +63 -0
- dmon/utils.py +32 -0
- python_dmon-0.2.0.dist-info/METADATA +156 -0
- python_dmon-0.2.0.dist-info/RECORD +14 -0
- python_dmon-0.2.0.dist-info/WHEEL +4 -0
- python_dmon-0.2.0.dist-info/entry_points.txt +3 -0
dmon/__init__.py
ADDED
|
File without changes
|
dmon/__main__.py
ADDED
dmon/cli.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import shlex
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from colorama import just_fix_windows_console
|
|
6
|
+
|
|
7
|
+
from .config import check_name_in_config, get_task_config
|
|
8
|
+
from .control import list_processes, restart, start, stop, status
|
|
9
|
+
from .constants import (
|
|
10
|
+
DEFAULT_META_DIR,
|
|
11
|
+
DEFAULT_RUN_NAME,
|
|
12
|
+
LOG_PATH_TEMPLATE,
|
|
13
|
+
META_PATH_TEMPLATE,
|
|
14
|
+
ROTATE_LOG_PATH_TEMPLATE,
|
|
15
|
+
)
|
|
16
|
+
from .types import DmonTaskConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
just_fix_windows_console()
|
|
21
|
+
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="dmon",
|
|
24
|
+
description="Minimal cross-platform daemon manager",
|
|
25
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
26
|
+
)
|
|
27
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
28
|
+
|
|
29
|
+
# start subcommand
|
|
30
|
+
sp_start = subparsers.add_parser(
|
|
31
|
+
"start",
|
|
32
|
+
help="Start a configured task as a background process",
|
|
33
|
+
description="Start a configured task as a background process",
|
|
34
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
35
|
+
)
|
|
36
|
+
sp_start.add_argument(
|
|
37
|
+
"name",
|
|
38
|
+
help="Configured task name (default: the only task if there's just one)",
|
|
39
|
+
nargs="?",
|
|
40
|
+
)
|
|
41
|
+
sp_start.add_argument(
|
|
42
|
+
"--meta-file",
|
|
43
|
+
help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
|
|
44
|
+
)
|
|
45
|
+
sp_start.add_argument(
|
|
46
|
+
"--log-file",
|
|
47
|
+
help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# stop subcommand
|
|
51
|
+
sp_stop = subparsers.add_parser(
|
|
52
|
+
"stop",
|
|
53
|
+
help="Stop a background process",
|
|
54
|
+
description="Stop a background process given name or meta file",
|
|
55
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
56
|
+
)
|
|
57
|
+
sp_stop.add_argument(
|
|
58
|
+
"name",
|
|
59
|
+
help="Configured task name (default: the only task if there's just one)",
|
|
60
|
+
nargs="?",
|
|
61
|
+
)
|
|
62
|
+
sp_stop.add_argument("--meta-file", help="Path to meta file")
|
|
63
|
+
|
|
64
|
+
# restart subcommand
|
|
65
|
+
sp_restart = subparsers.add_parser(
|
|
66
|
+
"restart",
|
|
67
|
+
help="Restart a configured task as a background process",
|
|
68
|
+
description="Restart a configured task as a background process",
|
|
69
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
70
|
+
)
|
|
71
|
+
sp_restart.add_argument(
|
|
72
|
+
"name",
|
|
73
|
+
help="Configured task name (default: the only task if there's just one)",
|
|
74
|
+
nargs="?",
|
|
75
|
+
)
|
|
76
|
+
sp_restart.add_argument(
|
|
77
|
+
"--meta-file",
|
|
78
|
+
help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
|
|
79
|
+
)
|
|
80
|
+
sp_restart.add_argument(
|
|
81
|
+
"--log-file",
|
|
82
|
+
help=f"Path to log file (default: task configured or {LOG_PATH_TEMPLATE})",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# status subcommand
|
|
86
|
+
sp_status = subparsers.add_parser(
|
|
87
|
+
"status",
|
|
88
|
+
help="Check process status",
|
|
89
|
+
description="Check status of a background process given name or meta file",
|
|
90
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
91
|
+
)
|
|
92
|
+
sp_status.add_argument(
|
|
93
|
+
"name",
|
|
94
|
+
help="Configured task name (default: the only task if there's just one)",
|
|
95
|
+
nargs="?",
|
|
96
|
+
)
|
|
97
|
+
sp_status.add_argument(
|
|
98
|
+
"--meta-file",
|
|
99
|
+
help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# list subcommand
|
|
103
|
+
sp_list = subparsers.add_parser(
|
|
104
|
+
"list",
|
|
105
|
+
help="List all processes and their status",
|
|
106
|
+
description="List all processes and their status managed by dmon in the given directory",
|
|
107
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
108
|
+
)
|
|
109
|
+
sp_list.add_argument(
|
|
110
|
+
"dir",
|
|
111
|
+
help=f"Directory to look for meta files (default: {DEFAULT_META_DIR})",
|
|
112
|
+
nargs="?",
|
|
113
|
+
)
|
|
114
|
+
sp_list.add_argument(
|
|
115
|
+
"--full",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="Show full width without truncating column (default: False)",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# run subcommand
|
|
121
|
+
sp_run = subparsers.add_parser(
|
|
122
|
+
"run",
|
|
123
|
+
help="Run a custom task (not in config) as a background process",
|
|
124
|
+
description="Run a custom task (not in config) as a background process",
|
|
125
|
+
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
126
|
+
)
|
|
127
|
+
sp_run.add_argument(
|
|
128
|
+
"--name",
|
|
129
|
+
"-n",
|
|
130
|
+
default=DEFAULT_RUN_NAME,
|
|
131
|
+
help=f"Name for this task (default: {DEFAULT_RUN_NAME})",
|
|
132
|
+
)
|
|
133
|
+
sp_run.add_argument(
|
|
134
|
+
"--cwd",
|
|
135
|
+
help="Working directory to run the command in (default: current directory)",
|
|
136
|
+
default="",
|
|
137
|
+
)
|
|
138
|
+
sp_run.add_argument(
|
|
139
|
+
"--shell", action="store_true", help="Run task in shell (default: False)"
|
|
140
|
+
)
|
|
141
|
+
sp_run.add_argument(
|
|
142
|
+
"--meta-file",
|
|
143
|
+
help=f"Path to meta file (default: {META_PATH_TEMPLATE})",
|
|
144
|
+
)
|
|
145
|
+
sp_run.add_argument(
|
|
146
|
+
"--log-file",
|
|
147
|
+
help=f"Path to log file (default: {LOG_PATH_TEMPLATE})",
|
|
148
|
+
)
|
|
149
|
+
sp_run.add_argument(
|
|
150
|
+
"--log-rotate",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="Whether to rotate log file (default: False)",
|
|
153
|
+
)
|
|
154
|
+
sp_run.add_argument(
|
|
155
|
+
"--rotate-log-path",
|
|
156
|
+
help=f"Path to rotation log file (default: {ROTATE_LOG_PATH_TEMPLATE})",
|
|
157
|
+
)
|
|
158
|
+
sp_run.add_argument(
|
|
159
|
+
"command_list",
|
|
160
|
+
metavar="command",
|
|
161
|
+
nargs=argparse.ONE_OR_MORE,
|
|
162
|
+
help="Command (with args) to run",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
args = parser.parse_args()
|
|
166
|
+
|
|
167
|
+
if args.command in ["start", "restart"]:
|
|
168
|
+
sp = sp_start if args.command == "start" else sp_restart
|
|
169
|
+
try:
|
|
170
|
+
name, task_cfg = get_task_config(args.name)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
sp.error(str(e))
|
|
173
|
+
|
|
174
|
+
task_cfg.meta_path = (
|
|
175
|
+
args.meta_file or task_cfg.meta_path or META_PATH_TEMPLATE.format(name=name)
|
|
176
|
+
)
|
|
177
|
+
task_cfg.log_path = (
|
|
178
|
+
args.log_file or task_cfg.log_path or LOG_PATH_TEMPLATE.format(name=name)
|
|
179
|
+
)
|
|
180
|
+
task_cfg.rotate_log_path = (
|
|
181
|
+
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(name=name)
|
|
182
|
+
)
|
|
183
|
+
if args.command == "start":
|
|
184
|
+
sys.exit(start(task_cfg))
|
|
185
|
+
else:
|
|
186
|
+
sys.exit(restart(task_cfg))
|
|
187
|
+
elif args.command in ["stop", "status"]:
|
|
188
|
+
sp = sp_stop if args.command == "stop" else sp_status
|
|
189
|
+
if args.meta_file:
|
|
190
|
+
meta_path = args.meta_file
|
|
191
|
+
else:
|
|
192
|
+
if args.name:
|
|
193
|
+
name = args.name
|
|
194
|
+
else:
|
|
195
|
+
try:
|
|
196
|
+
name, _ = get_task_config(args.name)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
sp.error(str(e))
|
|
199
|
+
meta_path = META_PATH_TEMPLATE.format(name=name)
|
|
200
|
+
if args.command == "stop":
|
|
201
|
+
sys.exit(stop(meta_path))
|
|
202
|
+
else:
|
|
203
|
+
sys.exit(status(meta_path))
|
|
204
|
+
elif args.command == "list":
|
|
205
|
+
dir = args.dir or DEFAULT_META_DIR
|
|
206
|
+
sys.exit(list_processes(dir, args.full))
|
|
207
|
+
elif args.command == "run":
|
|
208
|
+
if not args.name:
|
|
209
|
+
sp_run.error("Please provide a non-empty name for the task.")
|
|
210
|
+
elif check_name_in_config(args.name):
|
|
211
|
+
sp_run.error(
|
|
212
|
+
f"Name '{args.name}' already exists in config. Please choose another name."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
task_cfg = DmonTaskConfig(
|
|
216
|
+
name=args.name,
|
|
217
|
+
cmd=shlex.join(args.command_list) if args.shell else args.command_list,
|
|
218
|
+
cwd=args.cwd,
|
|
219
|
+
meta_path=args.meta_file or META_PATH_TEMPLATE.format(name=args.name),
|
|
220
|
+
log_path=args.log_file or LOG_PATH_TEMPLATE.format(name=args.name),
|
|
221
|
+
log_rotate=args.log_rotate,
|
|
222
|
+
rotate_log_path=args.rotate_log_path
|
|
223
|
+
or ROTATE_LOG_PATH_TEMPLATE.format(name=args.name),
|
|
224
|
+
)
|
|
225
|
+
sys.exit(start(task_cfg))
|
|
226
|
+
else:
|
|
227
|
+
parser.print_help()
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main()
|
dmon/config.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, Optional, cast
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 11):
|
|
6
|
+
import tomllib
|
|
7
|
+
else:
|
|
8
|
+
import tomli as tomllib
|
|
9
|
+
|
|
10
|
+
from .types import CmdType, DmonTaskConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_pyproject_toml(start: Optional[Path] = None):
|
|
14
|
+
"""
|
|
15
|
+
Search for pyproject.toml from the current directory (or specified directory) upwards.
|
|
16
|
+
Return a Path object if found.
|
|
17
|
+
"""
|
|
18
|
+
start = start or Path.cwd()
|
|
19
|
+
current = start.resolve()
|
|
20
|
+
|
|
21
|
+
for parent in [current, *current.parents]:
|
|
22
|
+
candidate = parent / "pyproject.toml"
|
|
23
|
+
if candidate.is_file():
|
|
24
|
+
return candidate
|
|
25
|
+
|
|
26
|
+
# raise FileNotFoundError(f"No pyproject.toml found from {start} upwards.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_dmon_yaml(start: Optional[Path] = None):
|
|
30
|
+
"""
|
|
31
|
+
Search for dmon.yaml / dmon.yml from the current directory (or specified directory) upwards.
|
|
32
|
+
Return a Path object if found.
|
|
33
|
+
"""
|
|
34
|
+
start = start or Path.cwd()
|
|
35
|
+
current = start.resolve()
|
|
36
|
+
|
|
37
|
+
for parent in [current, *current.parents]:
|
|
38
|
+
for filename in ["dmon.yaml", "dmon.yml"]:
|
|
39
|
+
candidate = parent / filename
|
|
40
|
+
if candidate.is_file():
|
|
41
|
+
return candidate
|
|
42
|
+
|
|
43
|
+
# raise FileNotFoundError(f"No dmon.yaml or dmon.yml found from {start} upwards.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_config():
|
|
47
|
+
path = find_dmon_yaml()
|
|
48
|
+
if path:
|
|
49
|
+
import yaml
|
|
50
|
+
|
|
51
|
+
with path.open("r", encoding="utf-8") as f:
|
|
52
|
+
cfg = yaml.safe_load(f)
|
|
53
|
+
else:
|
|
54
|
+
path = find_pyproject_toml()
|
|
55
|
+
if not path:
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
"No dmon.yaml or pyproject.toml found in current or any parent directory."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
with path.open("rb") as f:
|
|
61
|
+
cfg = tomllib.load(f)
|
|
62
|
+
cfg = cfg.get("tool", {}).get("dmon", {})
|
|
63
|
+
return cfg, path
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_cmd_type(cmd, name: str) -> CmdType:
|
|
67
|
+
if isinstance(cmd, str):
|
|
68
|
+
return cmd
|
|
69
|
+
elif isinstance(cmd, list):
|
|
70
|
+
if not all(isinstance(item, str) for item in cmd):
|
|
71
|
+
# check if it's a list of strings
|
|
72
|
+
raise TypeError(f"Task '{name}' list items must be strings")
|
|
73
|
+
return cmd
|
|
74
|
+
else:
|
|
75
|
+
raise TypeError(
|
|
76
|
+
f"Task '{name}' 'cmd' field must be a string, or list of strings; got {type(cmd)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_task(task, name: str) -> DmonTaskConfig:
|
|
81
|
+
ret = DmonTaskConfig(name=name)
|
|
82
|
+
if isinstance(task, str) or isinstance(task, list):
|
|
83
|
+
ret.cmd = validate_cmd_type(task, name)
|
|
84
|
+
elif isinstance(task, dict):
|
|
85
|
+
if "cmd" not in task:
|
|
86
|
+
raise TypeError(f"Task '{name}' must have a 'cmd' field")
|
|
87
|
+
ret.cmd = validate_cmd_type(task["cmd"], name)
|
|
88
|
+
|
|
89
|
+
if "cwd" in task:
|
|
90
|
+
if not isinstance(task["cwd"], str):
|
|
91
|
+
raise TypeError(f"Task '{name}' 'cwd' field must be a string")
|
|
92
|
+
ret.cwd = task["cwd"]
|
|
93
|
+
|
|
94
|
+
if "env" in task:
|
|
95
|
+
if not isinstance(task["env"], dict) or not all(
|
|
96
|
+
isinstance(k, str) and isinstance(v, str)
|
|
97
|
+
for k, v in task["env"].items()
|
|
98
|
+
):
|
|
99
|
+
raise TypeError(
|
|
100
|
+
f"Task '{name}' 'env' field must be a table of string to string"
|
|
101
|
+
)
|
|
102
|
+
ret.env = cast(Dict[str, str], task["env"])
|
|
103
|
+
|
|
104
|
+
if "override_env" in task:
|
|
105
|
+
if not isinstance(task["override_env"], bool):
|
|
106
|
+
raise TypeError(f"Task '{name}' 'override_env' field must be a boolean")
|
|
107
|
+
ret.override_env = task["override_env"]
|
|
108
|
+
|
|
109
|
+
if "log_path" in task:
|
|
110
|
+
if not isinstance(task["log_path"], str):
|
|
111
|
+
raise TypeError(f"Task '{name}' 'log_path' field must be a string")
|
|
112
|
+
ret.log_path = task["log_path"]
|
|
113
|
+
|
|
114
|
+
if "log_rotate" in task:
|
|
115
|
+
if not isinstance(task["log_rotate"], bool):
|
|
116
|
+
raise TypeError(f"Task '{name}' 'log_rotate' field must be a boolean")
|
|
117
|
+
ret.log_rotate = task["log_rotate"]
|
|
118
|
+
|
|
119
|
+
if "log_max_size" in task:
|
|
120
|
+
if not isinstance(task["log_max_size"], int) or task["log_max_size"] <= 0:
|
|
121
|
+
raise TypeError(
|
|
122
|
+
f"Task '{name}' 'log_max_size' field must be a positive integer"
|
|
123
|
+
)
|
|
124
|
+
ret.log_max_size = task["log_max_size"]
|
|
125
|
+
|
|
126
|
+
if "rotate_log_path" in task:
|
|
127
|
+
if not isinstance(task["rotate_log_path"], str):
|
|
128
|
+
raise TypeError(
|
|
129
|
+
f"Task '{name}' 'rotate_log_path' field must be a string"
|
|
130
|
+
)
|
|
131
|
+
ret.rotate_log_path = task["rotate_log_path"]
|
|
132
|
+
|
|
133
|
+
if "rotate_log_max_size" in task:
|
|
134
|
+
if (
|
|
135
|
+
not isinstance(task["rotate_log_max_size"], int)
|
|
136
|
+
or task["rotate_log_max_size"] <= 0
|
|
137
|
+
):
|
|
138
|
+
raise TypeError(
|
|
139
|
+
f"Task '{name}' 'rotate_log_max_size' field must be a positive integer"
|
|
140
|
+
)
|
|
141
|
+
ret.rotate_log_max_size = task["rotate_log_max_size"]
|
|
142
|
+
|
|
143
|
+
if "meta_path" in task:
|
|
144
|
+
if not isinstance(task["meta_path"], str):
|
|
145
|
+
raise TypeError(f"Task '{name}' 'meta_path' field must be a string")
|
|
146
|
+
ret.meta_path = task["meta_path"]
|
|
147
|
+
else:
|
|
148
|
+
raise TypeError(
|
|
149
|
+
f"Task '{name}' must be a string, list of strings, or a table; got {type(task)}"
|
|
150
|
+
)
|
|
151
|
+
return ret
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_task_config(name: Optional[str] = None):
|
|
155
|
+
"""
|
|
156
|
+
Get the validated task configuration for the given task name from [tool.dmon.tasks] or dmon.yaml.
|
|
157
|
+
|
|
158
|
+
If name is None or empty, and there is only one task, return that task; otherwise, raise ValueError.
|
|
159
|
+
If the task is not found, or required fields are missing, raise TypeError or ValueError.
|
|
160
|
+
"""
|
|
161
|
+
cfg, path = load_config()
|
|
162
|
+
tasks = cfg.get("tasks", {})
|
|
163
|
+
|
|
164
|
+
if not isinstance(tasks, dict):
|
|
165
|
+
raise TypeError("'tasks' must be a table")
|
|
166
|
+
|
|
167
|
+
if not name:
|
|
168
|
+
if len(tasks) == 0:
|
|
169
|
+
raise ValueError(f"No task found in {path}")
|
|
170
|
+
elif len(tasks) == 1:
|
|
171
|
+
name = next(iter(tasks))
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError(f"Multiple tasks found in {path}; please specify one.")
|
|
174
|
+
else:
|
|
175
|
+
name = name.lower()
|
|
176
|
+
if name not in tasks:
|
|
177
|
+
raise ValueError(f"Task '{name}' not found in {path}")
|
|
178
|
+
|
|
179
|
+
assert isinstance(name, str)
|
|
180
|
+
task = validate_task(tasks[name], name)
|
|
181
|
+
return name, task
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def check_name_in_config(name: str) -> bool:
|
|
185
|
+
"""
|
|
186
|
+
Check if the given task name exists in the tasks.
|
|
187
|
+
Return True if found, False otherwise.
|
|
188
|
+
"""
|
|
189
|
+
cfg, _ = load_config()
|
|
190
|
+
tasks = cfg.get("tasks", {})
|
|
191
|
+
|
|
192
|
+
if not isinstance(tasks, dict):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
return name.lower() in tasks
|
dmon/constants.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DEFAULT_META_DIR = Path(".dmon")
|
|
5
|
+
DEFAULT_LOG_DIR = Path("logs")
|
|
6
|
+
|
|
7
|
+
META_SUFFIX = ".meta.json"
|
|
8
|
+
|
|
9
|
+
META_PATH_TEMPLATE = str(DEFAULT_META_DIR / ("{name}" + META_SUFFIX))
|
|
10
|
+
LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{name}.log")
|
|
11
|
+
ROTATE_LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{name}.rotate.log")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_RUN_NAME = "default_run"
|