python-dmon 0.2.0__py3-none-any.whl → 0.2.2__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 +57 -19
- dmon/config.py +33 -43
- dmon/constants.py +6 -3
- dmon/control.py +50 -8
- dmon/runner.py +16 -12
- dmon/types.py +1 -1
- {python_dmon-0.2.0.dist-info → python_dmon-0.2.2.dist-info}/METADATA +29 -6
- python_dmon-0.2.2.dist-info/RECORD +14 -0
- python_dmon-0.2.0.dist-info/RECORD +0 -14
- {python_dmon-0.2.0.dist-info → python_dmon-0.2.2.dist-info}/WHEEL +0 -0
- {python_dmon-0.2.0.dist-info → python_dmon-0.2.2.dist-info}/entry_points.txt +0 -0
dmon/cli.py
CHANGED
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from colorama import just_fix_windows_console
|
|
6
6
|
|
|
7
7
|
from .config import check_name_in_config, get_task_config
|
|
8
|
-
from .control import list_processes, restart, start, stop, status
|
|
8
|
+
from .control import execute, list_processes, restart, start, stop, status
|
|
9
9
|
from .constants import (
|
|
10
10
|
DEFAULT_META_DIR,
|
|
11
11
|
DEFAULT_RUN_NAME,
|
|
@@ -16,14 +16,28 @@ from .constants import (
|
|
|
16
16
|
from .types import DmonTaskConfig
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def get_version():
|
|
20
|
+
# if python 3.8 or later, use importlib.metadata
|
|
21
|
+
import importlib.metadata
|
|
22
|
+
|
|
23
|
+
return importlib.metadata.version("python-dmon")
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
def main():
|
|
20
27
|
just_fix_windows_console()
|
|
21
28
|
|
|
22
29
|
parser = argparse.ArgumentParser(
|
|
23
30
|
prog="dmon",
|
|
24
|
-
description="
|
|
31
|
+
description=f"dmon v{get_version()} - Lightweight cross-platform daemon manager",
|
|
25
32
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
26
33
|
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-v",
|
|
36
|
+
"--version",
|
|
37
|
+
action="version",
|
|
38
|
+
version=get_version(),
|
|
39
|
+
)
|
|
40
|
+
|
|
27
41
|
subparsers = parser.add_subparsers(dest="command")
|
|
28
42
|
|
|
29
43
|
# start subcommand
|
|
@@ -34,7 +48,7 @@ def main():
|
|
|
34
48
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
35
49
|
)
|
|
36
50
|
sp_start.add_argument(
|
|
37
|
-
"
|
|
51
|
+
"task",
|
|
38
52
|
help="Configured task name (default: the only task if there's just one)",
|
|
39
53
|
nargs="?",
|
|
40
54
|
)
|
|
@@ -55,7 +69,7 @@ def main():
|
|
|
55
69
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
56
70
|
)
|
|
57
71
|
sp_stop.add_argument(
|
|
58
|
-
"
|
|
72
|
+
"task",
|
|
59
73
|
help="Configured task name (default: the only task if there's just one)",
|
|
60
74
|
nargs="?",
|
|
61
75
|
)
|
|
@@ -69,7 +83,7 @@ def main():
|
|
|
69
83
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
70
84
|
)
|
|
71
85
|
sp_restart.add_argument(
|
|
72
|
-
"
|
|
86
|
+
"task",
|
|
73
87
|
help="Configured task name (default: the only task if there's just one)",
|
|
74
88
|
nargs="?",
|
|
75
89
|
)
|
|
@@ -90,7 +104,7 @@ def main():
|
|
|
90
104
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
91
105
|
)
|
|
92
106
|
sp_status.add_argument(
|
|
93
|
-
"
|
|
107
|
+
"task",
|
|
94
108
|
help="Configured task name (default: the only task if there's just one)",
|
|
95
109
|
nargs="?",
|
|
96
110
|
)
|
|
@@ -162,41 +176,65 @@ def main():
|
|
|
162
176
|
help="Command (with args) to run",
|
|
163
177
|
)
|
|
164
178
|
|
|
179
|
+
sp_exec = subparsers.add_parser(
|
|
180
|
+
"exec",
|
|
181
|
+
help="Execute a configured task in the foreground",
|
|
182
|
+
description="Execute a configured task in the foreground",
|
|
183
|
+
)
|
|
184
|
+
sp_exec.add_argument(
|
|
185
|
+
"task",
|
|
186
|
+
help="Configured task name (default: the only task if there's just one)",
|
|
187
|
+
nargs="?",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# add custom config file option
|
|
191
|
+
for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
|
|
192
|
+
sp.add_argument(
|
|
193
|
+
"--config",
|
|
194
|
+
help="Path to config file (YAML or TOML) (default: search from current directory upwards)",
|
|
195
|
+
)
|
|
196
|
+
|
|
165
197
|
args = parser.parse_args()
|
|
166
198
|
|
|
167
199
|
if args.command in ["start", "restart"]:
|
|
168
200
|
sp = sp_start if args.command == "start" else sp_restart
|
|
169
201
|
try:
|
|
170
|
-
|
|
202
|
+
task, task_cfg = get_task_config(args.task, args.config)
|
|
171
203
|
except Exception as e:
|
|
172
204
|
sp.error(str(e))
|
|
173
205
|
|
|
174
206
|
task_cfg.meta_path = (
|
|
175
|
-
args.meta_file or task_cfg.meta_path or META_PATH_TEMPLATE.format(
|
|
207
|
+
args.meta_file or task_cfg.meta_path or META_PATH_TEMPLATE.format(task=task)
|
|
176
208
|
)
|
|
177
209
|
task_cfg.log_path = (
|
|
178
|
-
args.log_file or task_cfg.log_path or LOG_PATH_TEMPLATE.format(
|
|
210
|
+
args.log_file or task_cfg.log_path or LOG_PATH_TEMPLATE.format(task=task)
|
|
179
211
|
)
|
|
180
212
|
task_cfg.rotate_log_path = (
|
|
181
|
-
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(
|
|
213
|
+
task_cfg.rotate_log_path or ROTATE_LOG_PATH_TEMPLATE.format(task=task)
|
|
182
214
|
)
|
|
183
215
|
if args.command == "start":
|
|
184
216
|
sys.exit(start(task_cfg))
|
|
185
217
|
else:
|
|
186
218
|
sys.exit(restart(task_cfg))
|
|
219
|
+
elif args.command == "exec":
|
|
220
|
+
try:
|
|
221
|
+
task, task_cfg = get_task_config(args.task, args.config)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
sp_exec.error(str(e))
|
|
224
|
+
sys.exit(execute(task_cfg))
|
|
187
225
|
elif args.command in ["stop", "status"]:
|
|
188
226
|
sp = sp_stop if args.command == "stop" else sp_status
|
|
189
227
|
if args.meta_file:
|
|
190
228
|
meta_path = args.meta_file
|
|
191
229
|
else:
|
|
192
|
-
if args.
|
|
193
|
-
|
|
230
|
+
if args.task:
|
|
231
|
+
task = args.task
|
|
194
232
|
else:
|
|
195
233
|
try:
|
|
196
|
-
|
|
234
|
+
task, _ = get_task_config(args.task, args.config)
|
|
197
235
|
except Exception as e:
|
|
198
236
|
sp.error(str(e))
|
|
199
|
-
meta_path = META_PATH_TEMPLATE.format(
|
|
237
|
+
meta_path = META_PATH_TEMPLATE.format(task=task)
|
|
200
238
|
if args.command == "stop":
|
|
201
239
|
sys.exit(stop(meta_path))
|
|
202
240
|
else:
|
|
@@ -209,18 +247,18 @@ def main():
|
|
|
209
247
|
sp_run.error("Please provide a non-empty name for the task.")
|
|
210
248
|
elif check_name_in_config(args.name):
|
|
211
249
|
sp_run.error(
|
|
212
|
-
f"
|
|
250
|
+
f"Task '{args.name}' already exists in config. Please choose another name."
|
|
213
251
|
)
|
|
214
252
|
|
|
215
253
|
task_cfg = DmonTaskConfig(
|
|
216
|
-
|
|
254
|
+
task=args.name,
|
|
217
255
|
cmd=shlex.join(args.command_list) if args.shell else args.command_list,
|
|
218
256
|
cwd=args.cwd,
|
|
219
|
-
meta_path=args.meta_file or META_PATH_TEMPLATE.format(
|
|
220
|
-
log_path=args.log_file or LOG_PATH_TEMPLATE.format(
|
|
257
|
+
meta_path=args.meta_file or META_PATH_TEMPLATE.format(task=args.name),
|
|
258
|
+
log_path=args.log_file or LOG_PATH_TEMPLATE.format(task=args.name),
|
|
221
259
|
log_rotate=args.log_rotate,
|
|
222
260
|
rotate_log_path=args.rotate_log_path
|
|
223
|
-
or ROTATE_LOG_PATH_TEMPLATE.format(
|
|
261
|
+
or ROTATE_LOG_PATH_TEMPLATE.format(task=args.name),
|
|
224
262
|
)
|
|
225
263
|
sys.exit(start(task_cfg))
|
|
226
264
|
else:
|
dmon/config.py
CHANGED
|
@@ -10,56 +10,45 @@ else:
|
|
|
10
10
|
from .types import CmdType, DmonTaskConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def load_config(cfg_path: Optional[str] = None):
|
|
14
14
|
"""
|
|
15
|
-
|
|
16
|
-
Return a Path object if found.
|
|
15
|
+
Load configuration from the given path, or search it from the current working directory upwards.
|
|
17
16
|
"""
|
|
18
|
-
start = start or Path.cwd()
|
|
19
|
-
current = start.resolve()
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
18
|
+
if cfg_path:
|
|
19
|
+
# Load configuration from the given path
|
|
20
|
+
path = Path(cfg_path).resolve()
|
|
21
|
+
if not path.exists():
|
|
22
|
+
raise FileNotFoundError(f"Config file '{path}' does not exist.")
|
|
53
23
|
else:
|
|
54
|
-
path
|
|
24
|
+
# No path provided, search for config files
|
|
25
|
+
# starting from the current working directory upwards
|
|
26
|
+
current = Path.cwd().resolve()
|
|
27
|
+
path = None
|
|
28
|
+
for parent in [current, *current.parents]:
|
|
29
|
+
for filename in ["dmon.yaml", "dmon.yml", "pyproject.toml"]:
|
|
30
|
+
path = parent / filename
|
|
31
|
+
if path.is_file():
|
|
32
|
+
break
|
|
33
|
+
else:
|
|
34
|
+
continue # Only break out of the inner loop if a file was found
|
|
35
|
+
break # Break out of the outer loop if a file was found
|
|
55
36
|
if not path:
|
|
56
37
|
raise FileNotFoundError(
|
|
57
38
|
"No dmon.yaml or pyproject.toml found in current or any parent directory."
|
|
58
39
|
)
|
|
59
40
|
|
|
41
|
+
if path.suffix in [".yaml", ".yml"]:
|
|
42
|
+
import yaml
|
|
43
|
+
|
|
44
|
+
with path.open("r", encoding="utf-8") as f:
|
|
45
|
+
cfg = yaml.safe_load(f)
|
|
46
|
+
elif path.suffix == ".toml":
|
|
60
47
|
with path.open("rb") as f:
|
|
61
48
|
cfg = tomllib.load(f)
|
|
62
49
|
cfg = cfg.get("tool", {}).get("dmon", {})
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
|
|
63
52
|
return cfg, path
|
|
64
53
|
|
|
65
54
|
|
|
@@ -78,7 +67,7 @@ def validate_cmd_type(cmd, name: str) -> CmdType:
|
|
|
78
67
|
|
|
79
68
|
|
|
80
69
|
def validate_task(task, name: str) -> DmonTaskConfig:
|
|
81
|
-
ret = DmonTaskConfig(
|
|
70
|
+
ret = DmonTaskConfig(task=name)
|
|
82
71
|
if isinstance(task, str) or isinstance(task, list):
|
|
83
72
|
ret.cmd = validate_cmd_type(task, name)
|
|
84
73
|
elif isinstance(task, dict):
|
|
@@ -151,14 +140,15 @@ def validate_task(task, name: str) -> DmonTaskConfig:
|
|
|
151
140
|
return ret
|
|
152
141
|
|
|
153
142
|
|
|
154
|
-
def get_task_config(name: Optional[str]
|
|
143
|
+
def get_task_config(name: Optional[str], cfg_path: Optional[str]):
|
|
155
144
|
"""
|
|
156
|
-
Get the validated task configuration for the given task name
|
|
157
|
-
|
|
145
|
+
Get the validated task configuration for the given task name.
|
|
158
146
|
If name is None or empty, and there is only one task, return that task; otherwise, raise ValueError.
|
|
159
147
|
If the task is not found, or required fields are missing, raise TypeError or ValueError.
|
|
148
|
+
|
|
149
|
+
The config is loaded from the given path, or searched for dmon.yaml or pyproject.toml.
|
|
160
150
|
"""
|
|
161
|
-
cfg, path = load_config()
|
|
151
|
+
cfg, path = load_config(cfg_path)
|
|
162
152
|
tasks = cfg.get("tasks", {})
|
|
163
153
|
|
|
164
154
|
if not isinstance(tasks, dict):
|
dmon/constants.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
import sys
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
DEFAULT_META_DIR = Path(".dmon")
|
|
@@ -6,9 +7,11 @@ DEFAULT_LOG_DIR = Path("logs")
|
|
|
6
7
|
|
|
7
8
|
META_SUFFIX = ".meta.json"
|
|
8
9
|
|
|
9
|
-
META_PATH_TEMPLATE = str(DEFAULT_META_DIR / ("{
|
|
10
|
-
LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{
|
|
11
|
-
ROTATE_LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{
|
|
10
|
+
META_PATH_TEMPLATE = str(DEFAULT_META_DIR / ("{task}" + META_SUFFIX))
|
|
11
|
+
LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{task}.log")
|
|
12
|
+
ROTATE_LOG_PATH_TEMPLATE = str(DEFAULT_LOG_DIR / "{task}.rotate.log")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
DEFAULT_RUN_NAME = "default_run"
|
|
16
|
+
|
|
17
|
+
ON_WINDOWS = sys.platform.startswith("win")
|
dmon/control.py
CHANGED
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
import shlex
|
|
4
4
|
import shutil
|
|
5
|
+
import signal
|
|
5
6
|
import sys
|
|
6
7
|
import subprocess
|
|
7
8
|
import time
|
|
@@ -10,7 +11,7 @@ from typing import List
|
|
|
10
11
|
import psutil
|
|
11
12
|
from termcolor import colored
|
|
12
13
|
|
|
13
|
-
from .constants import DEFAULT_META_DIR, META_SUFFIX
|
|
14
|
+
from .constants import DEFAULT_META_DIR, META_SUFFIX, ON_WINDOWS
|
|
14
15
|
from .types import DmonTaskConfig, DmonMeta, PathType
|
|
15
16
|
from .utils import len_ansi, pad_ansi
|
|
16
17
|
|
|
@@ -66,7 +67,7 @@ def start(cfg: DmonTaskConfig):
|
|
|
66
67
|
|
|
67
68
|
# Platform-specific parameters to run the process in background detached from parent
|
|
68
69
|
kwargs = {}
|
|
69
|
-
if
|
|
70
|
+
if ON_WINDOWS:
|
|
70
71
|
# DETACHED_PROCESS = 0x00000008
|
|
71
72
|
CREATE_NO_WINDOW = 0x08000000
|
|
72
73
|
kwargs["creationflags"] = CREATE_NO_WINDOW
|
|
@@ -78,7 +79,7 @@ def start(cfg: DmonTaskConfig):
|
|
|
78
79
|
kwargs["start_new_session"] = True
|
|
79
80
|
|
|
80
81
|
meta = DmonMeta(
|
|
81
|
-
|
|
82
|
+
task=cfg.task,
|
|
82
83
|
meta_path=str(meta_path),
|
|
83
84
|
log_path=str(log_path),
|
|
84
85
|
log_rotate=cfg.log_rotate,
|
|
@@ -213,7 +214,7 @@ def stop(
|
|
|
213
214
|
meta_path.unlink(missing_ok=True)
|
|
214
215
|
return 1
|
|
215
216
|
|
|
216
|
-
if
|
|
217
|
+
if ON_WINDOWS:
|
|
217
218
|
ret = terminate_win(proc, timeout)
|
|
218
219
|
else:
|
|
219
220
|
ret = terminate_posix(proc, timeout)
|
|
@@ -416,7 +417,7 @@ def print_status(meta: DmonMeta):
|
|
|
416
417
|
|
|
417
418
|
# key-value pairs with aligned keys
|
|
418
419
|
rows = [
|
|
419
|
-
("TASK", colored(meta.
|
|
420
|
+
("TASK", colored(meta.task, "cyan", attrs=["bold"])),
|
|
420
421
|
("PID", colored(str(meta.pid), "cyan", attrs=["bold"])),
|
|
421
422
|
("STATUS", status),
|
|
422
423
|
("CMD", meta.cmd),
|
|
@@ -477,7 +478,7 @@ def print_process_table(metas: List[DmonMeta], full_width: bool = False):
|
|
|
477
478
|
ppid = "N/A"
|
|
478
479
|
rows.append(
|
|
479
480
|
(
|
|
480
|
-
colored(meta.
|
|
481
|
+
colored(meta.task, "cyan", attrs=["bold"]),
|
|
481
482
|
colored(meta.pid, "cyan", attrs=["bold"]),
|
|
482
483
|
ppid,
|
|
483
484
|
status,
|
|
@@ -524,14 +525,14 @@ def print_process_table(metas: List[DmonMeta], full_width: bool = False):
|
|
|
524
525
|
|
|
525
526
|
def list_processes(dir: PathType, full_width: bool):
|
|
526
527
|
target_dmon_dir = Path(dir).resolve()
|
|
527
|
-
metas = []
|
|
528
|
+
metas: List[DmonMeta] = []
|
|
528
529
|
if target_dmon_dir.exists() and target_dmon_dir.is_dir():
|
|
529
530
|
for meta_file in target_dmon_dir.glob(f"*{META_SUFFIX}"):
|
|
530
531
|
meta = DmonMeta.load(meta_file)
|
|
531
532
|
if meta is not None:
|
|
532
533
|
metas.append(meta)
|
|
533
534
|
# sort by name (case-insensitive)
|
|
534
|
-
metas.sort(key=lambda m: m.
|
|
535
|
+
metas.sort(key=lambda m: m.task.lower())
|
|
535
536
|
n_task = len(metas)
|
|
536
537
|
processes = print_process_table(metas, full_width)
|
|
537
538
|
n_proc = len(processes)
|
|
@@ -540,3 +541,44 @@ def list_processes(dir: PathType, full_width: bool):
|
|
|
540
541
|
file=sys.stderr,
|
|
541
542
|
)
|
|
542
543
|
return 0
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def execute(cfg: DmonTaskConfig):
|
|
547
|
+
"""
|
|
548
|
+
Execute the command in the foreground.
|
|
549
|
+
This is used for the 'dmon exec' command.
|
|
550
|
+
"""
|
|
551
|
+
cwd = Path(cfg.cwd).resolve()
|
|
552
|
+
|
|
553
|
+
env = None # default behavior of Popen
|
|
554
|
+
if cfg.override_env:
|
|
555
|
+
env = cfg.env
|
|
556
|
+
elif cfg.env:
|
|
557
|
+
env = {**os.environ, **cfg.env}
|
|
558
|
+
|
|
559
|
+
shell = isinstance(cfg.cmd, str)
|
|
560
|
+
if ON_WINDOWS and isinstance(cfg.cmd, list) and len(cfg.cmd) > 0:
|
|
561
|
+
# On Windows, use full path for the executable when shell=False
|
|
562
|
+
cfg.cmd[0] = shutil.which(cfg.cmd[0]) or cfg.cmd[0]
|
|
563
|
+
|
|
564
|
+
def signal_handler(signum: int, frame) -> None:
|
|
565
|
+
if ON_WINDOWS and signum == signal.SIGINT:
|
|
566
|
+
signum = signal.SIGTERM
|
|
567
|
+
proc.send_signal(signum)
|
|
568
|
+
|
|
569
|
+
prev_handle_int = signal.signal(signal.SIGINT, signal_handler)
|
|
570
|
+
prev_handle_term = signal.signal(signal.SIGTERM, signal_handler)
|
|
571
|
+
|
|
572
|
+
proc = subprocess.Popen(
|
|
573
|
+
cfg.cmd,
|
|
574
|
+
cwd=cwd,
|
|
575
|
+
env=env,
|
|
576
|
+
shell=shell,
|
|
577
|
+
bufsize=0, # unbuffered
|
|
578
|
+
close_fds=False,
|
|
579
|
+
)
|
|
580
|
+
ret = proc.wait()
|
|
581
|
+
|
|
582
|
+
signal.signal(signal.SIGTERM, prev_handle_term)
|
|
583
|
+
signal.signal(signal.SIGINT, prev_handle_int)
|
|
584
|
+
return ret
|
dmon/runner.py
CHANGED
|
@@ -116,21 +116,13 @@ def main(
|
|
|
116
116
|
)
|
|
117
117
|
|
|
118
118
|
shell = isinstance(cmd, str)
|
|
119
|
-
# Start the child process with stdout/stderr redirected to the log
|
|
120
|
-
# env will be inherited from parent process
|
|
121
|
-
# cwd will be inherited from parent process
|
|
122
|
-
proc = subprocess.Popen(
|
|
123
|
-
cmd,
|
|
124
|
-
stdout=subprocess.PIPE,
|
|
125
|
-
stderr=subprocess.STDOUT,
|
|
126
|
-
shell=shell,
|
|
127
|
-
text=False, # binary mode
|
|
128
|
-
bufsize=0, # unbuffered
|
|
129
|
-
)
|
|
130
119
|
|
|
131
120
|
# register signal handler to terminate the child process
|
|
132
|
-
def signal_handler(signum, frame):
|
|
121
|
+
def signal_handler(signum: int, frame):
|
|
133
122
|
logger.info(f"Received signal {signum}, forwarding to child process...")
|
|
123
|
+
if sys.platform.startswith("win") and signum == signal.SIGINT:
|
|
124
|
+
signum = signal.SIGTERM
|
|
125
|
+
logger.info(f"On Windows, convert SIGINT to SIGTERM ({signum})")
|
|
134
126
|
# proc.terminate()
|
|
135
127
|
proc.send_signal(signum)
|
|
136
128
|
logger.info("Waiting for child process to exit...")
|
|
@@ -142,6 +134,18 @@ def main(
|
|
|
142
134
|
signal.signal(signal.SIGINT, signal_handler)
|
|
143
135
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
144
136
|
|
|
137
|
+
# Start the child process with stdout/stderr redirected to the log
|
|
138
|
+
# env will be inherited from parent process
|
|
139
|
+
# cwd will be inherited from parent process
|
|
140
|
+
proc = subprocess.Popen(
|
|
141
|
+
cmd,
|
|
142
|
+
stdout=subprocess.PIPE,
|
|
143
|
+
stderr=subprocess.STDOUT,
|
|
144
|
+
shell=shell,
|
|
145
|
+
text=False, # binary mode
|
|
146
|
+
bufsize=0, # unbuffered
|
|
147
|
+
)
|
|
148
|
+
|
|
145
149
|
logger.info(f"Started process {proc.pid} with command: {cmd} (shell={shell})")
|
|
146
150
|
|
|
147
151
|
make_file_dir(log_path)
|
dmon/types.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-dmon
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -12,7 +12,7 @@ Requires-Dist: termcolor>=2.4.0
|
|
|
12
12
|
Requires-Dist: tomli>=2.2.1 ; python_full_version < '3.11'
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Project-URL: Bug Tracker, https://github.com/atomiechen/python-dmon/issues
|
|
15
|
-
Project-URL: Changelog, https://github.com/atomiechen/python-dmon/blob/
|
|
15
|
+
Project-URL: Changelog, https://github.com/atomiechen/python-dmon/blob/main/CHANGELOG.md
|
|
16
16
|
Project-URL: Homepage, https://github.com/atomiechen/python-dmon
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
@@ -86,6 +86,9 @@ dmon stop app
|
|
|
86
86
|
|
|
87
87
|
# Check task status
|
|
88
88
|
dmon status app
|
|
89
|
+
|
|
90
|
+
# Execute a task in the foreground (useful for debugging)
|
|
91
|
+
dmon exec app
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
If only one task is defined in the config file, you can omit the task name:
|
|
@@ -94,6 +97,26 @@ If only one task is defined in the config file, you can omit the task name:
|
|
|
94
97
|
dmon start
|
|
95
98
|
dmon stop
|
|
96
99
|
dmon status
|
|
100
|
+
dmon exec
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can use `--config` to specify a custom config file:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
dmon start --config /path/to/dmon.yaml app # YAML
|
|
107
|
+
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
And yes, you can use `dmon` to run in a nested manner:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
tasks:
|
|
114
|
+
app: ["python", "-u", "server.py"]
|
|
115
|
+
nested: pwd && dmon exec app # nest `dmon exec`
|
|
116
|
+
subdir_task1:
|
|
117
|
+
cwd: /path/to/dir
|
|
118
|
+
cmd: ["dmon", "exec", "app"] # run task defined in another folder
|
|
119
|
+
subdir_task2: dmon exec app --config /path/to/dir/dmon.yaml # like above
|
|
97
120
|
```
|
|
98
121
|
|
|
99
122
|
|
|
@@ -136,17 +159,17 @@ tasks:
|
|
|
136
159
|
env: # (default: inherit from parent process)
|
|
137
160
|
PYTHONUNBUFFERED: "1"
|
|
138
161
|
override_env: false # override parent env and only use env defined here
|
|
139
|
-
log_path: "logs/<
|
|
162
|
+
log_path: "logs/<task>.log" # path to log file
|
|
140
163
|
log_rotate: false # enable log rotation
|
|
141
164
|
log_max_size: 5 # max log file size before rotation in MB
|
|
142
|
-
rotate_log_path: "logs/<
|
|
165
|
+
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
143
166
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
144
|
-
meta_path: ".dmon/<
|
|
167
|
+
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
145
168
|
```
|
|
146
169
|
|
|
147
170
|
## Under the Hood
|
|
148
171
|
|
|
149
|
-
Each task is associated with a meta file (e.g. `.dmon/<
|
|
172
|
+
Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
|
|
150
173
|
The file contains details such as the command, PID, log path, and more.
|
|
151
174
|
**Do not** modify or delete these files manually.
|
|
152
175
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
dmon/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
+
dmon/cli.py,sha256=Dqu43_hcsx8iCJXAL1YXqBpp1jFt0Y7qBEr9mYzkAn8,8764
|
|
4
|
+
dmon/config.py,sha256=Q8TvcYDwUTlL_8bcjxVsA-lLE_AlcSmxYpbnMaCkFrw,6650
|
|
5
|
+
dmon/constants.py,sha256=uJPI66IJvT6vhamqEDeqHjZQS12RxCPe3DD5yCU0UR8,407
|
|
6
|
+
dmon/control.py,sha256=KpGNPPsuAsDNX3FTJxnGNnt9f4Q18omvG42zO1CwTy4,17494
|
|
7
|
+
dmon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
dmon/runner.py,sha256=iyotNIrbO6I_u02EcqwO7R7N3BEBP6b_8eL8yb3zeJQ,6082
|
|
9
|
+
dmon/types.py,sha256=tTVoJzWk-Z8XiZCUt0VE8Z7-URpXMoS54HfVYXhW-0Q,1795
|
|
10
|
+
dmon/utils.py,sha256=zVQC9YNhMQu4hrAp9k2ds2egnbWZZv7S5w8i2oVIPJg,808
|
|
11
|
+
python_dmon-0.2.2.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
+
python_dmon-0.2.2.dist-info/entry_points.txt,sha256=rad5ugz4K_IyNXS_IrFPu0tS-UXlBCSJRhJv-OWd_yk,45
|
|
13
|
+
python_dmon-0.2.2.dist-info/METADATA,sha256=tncJt5ouNhwjFYaGHEKEQgxas34QDLW6IRY1Qd2bO6g,5106
|
|
14
|
+
python_dmon-0.2.2.dist-info/RECORD,,
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
dmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
dmon/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
-
dmon/cli.py,sha256=IVJ5jCHkaLGTQjMmWCuJbhnt4MWEOaNWQajC-mm3LUQ,7599
|
|
4
|
-
dmon/config.py,sha256=COzwN0Ms8q32pV6dqsZU59RFE2GQmqyAmfygDotc4CA,6610
|
|
5
|
-
dmon/constants.py,sha256=y5S532iTeklaR28BFbvpdOHFj1udbEJ-reD1mTCcC6E,351
|
|
6
|
-
dmon/control.py,sha256=0_NrrOA0sUM_lFHNyKGAmQsBbJhXwXfj8kxOp2S8Ni8,16311
|
|
7
|
-
dmon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
dmon/runner.py,sha256=zEW7TnHMfIw6g3UTjOTUcLFKueXpqSmNDP3ZOptXQKs,5892
|
|
9
|
-
dmon/types.py,sha256=ZtTAK-cfs2MgUFRwDH61zeIU2B4dHZDbx1644tYI1h4,1795
|
|
10
|
-
dmon/utils.py,sha256=zVQC9YNhMQu4hrAp9k2ds2egnbWZZv7S5w8i2oVIPJg,808
|
|
11
|
-
python_dmon-0.2.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
-
python_dmon-0.2.0.dist-info/entry_points.txt,sha256=rad5ugz4K_IyNXS_IrFPu0tS-UXlBCSJRhJv-OWd_yk,45
|
|
13
|
-
python_dmon-0.2.0.dist-info/METADATA,sha256=s1AoObf2NB_kK_Era10wpm0X6OwH-J0UVeAS8Dg6pcs,4498
|
|
14
|
-
python_dmon-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|