python-dmon 0.2.1__py3-none-any.whl → 0.2.3__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 +1 -1
- dmon/config.py +36 -53
- dmon/control.py +5 -2
- {python_dmon-0.2.1.dist-info → python_dmon-0.2.3.dist-info}/METADATA +68 -4
- {python_dmon-0.2.1.dist-info → python_dmon-0.2.3.dist-info}/RECORD +7 -7
- {python_dmon-0.2.1.dist-info → python_dmon-0.2.3.dist-info}/entry_points.txt +1 -0
- {python_dmon-0.2.1.dist-info → python_dmon-0.2.3.dist-info}/WHEEL +0 -0
dmon/cli.py
CHANGED
|
@@ -191,7 +191,7 @@ def main():
|
|
|
191
191
|
for sp in [sp_start, sp_stop, sp_restart, sp_status, sp_exec]:
|
|
192
192
|
sp.add_argument(
|
|
193
193
|
"--config",
|
|
194
|
-
help="Path to config file
|
|
194
|
+
help="Path to config file or the directory containing it (default: search from current directory upwards)",
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
args = parser.parse_args()
|
dmon/config.py
CHANGED
|
@@ -10,79 +10,60 @@ else:
|
|
|
10
10
|
from .types import CmdType, DmonTaskConfig
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def search_config(start_dir: Path, recursive: bool) -> Optional[Path]:
|
|
14
14
|
"""
|
|
15
|
-
Search for pyproject.toml from the
|
|
16
|
-
Return
|
|
15
|
+
Search for dmon.yaml, dmon.yml, or pyproject.toml from the given directory upwards.
|
|
16
|
+
Return the path if found, None otherwise.
|
|
17
17
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.")
|
|
18
|
+
current = start_dir.resolve()
|
|
19
|
+
directories = [current] if not recursive else [current, *current.parents]
|
|
20
|
+
for parent in directories:
|
|
21
|
+
for filename in ["dmon.yaml", "dmon.yml", "pyproject.toml"]:
|
|
22
|
+
path = parent / filename
|
|
23
|
+
if path.is_file():
|
|
24
|
+
return path
|
|
25
|
+
return None
|
|
44
26
|
|
|
45
27
|
|
|
46
28
|
def load_config(cfg_path: Optional[str] = None):
|
|
47
29
|
"""
|
|
48
|
-
Load configuration from the given path, or search
|
|
30
|
+
Load configuration from the given path, or search it from the current working directory upwards.
|
|
49
31
|
"""
|
|
50
32
|
|
|
51
|
-
# Load configuration from the given path
|
|
52
33
|
if cfg_path:
|
|
34
|
+
# Load configuration from the given path
|
|
53
35
|
path = Path(cfg_path).resolve()
|
|
54
36
|
if not path.exists():
|
|
55
|
-
raise FileNotFoundError(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
|
|
67
|
-
return cfg, path
|
|
68
|
-
|
|
69
|
-
# No path provided, search for dmon.yaml first, then pyproject.toml
|
|
70
|
-
path = find_dmon_yaml()
|
|
71
|
-
if path:
|
|
72
|
-
import yaml
|
|
73
|
-
|
|
74
|
-
with path.open("r", encoding="utf-8") as f:
|
|
75
|
-
cfg = yaml.safe_load(f)
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
f"Config file or directory '{path}' does not exist."
|
|
39
|
+
)
|
|
40
|
+
elif path.is_dir():
|
|
41
|
+
# If it's a directory, search for config files in it
|
|
42
|
+
result = search_config(path, recursive=False)
|
|
43
|
+
if not result:
|
|
44
|
+
raise FileNotFoundError(
|
|
45
|
+
f"No dmon.yaml or pyproject.toml found in directory '{path}'."
|
|
46
|
+
)
|
|
47
|
+
path = result
|
|
76
48
|
else:
|
|
77
|
-
path
|
|
49
|
+
# No path provided, search from the current working directory upwards
|
|
50
|
+
path = search_config(Path.cwd(), recursive=True)
|
|
78
51
|
if not path:
|
|
79
52
|
raise FileNotFoundError(
|
|
80
53
|
"No dmon.yaml or pyproject.toml found in current or any parent directory."
|
|
81
54
|
)
|
|
82
55
|
|
|
56
|
+
if path.suffix in [".yaml", ".yml"]:
|
|
57
|
+
import yaml
|
|
58
|
+
|
|
59
|
+
with path.open("r", encoding="utf-8") as f:
|
|
60
|
+
cfg = yaml.safe_load(f)
|
|
61
|
+
elif path.suffix == ".toml":
|
|
83
62
|
with path.open("rb") as f:
|
|
84
63
|
cfg = tomllib.load(f)
|
|
85
64
|
cfg = cfg.get("tool", {}).get("dmon", {})
|
|
65
|
+
else:
|
|
66
|
+
raise ValueError("Config file must be YAML (.yaml/.yml) or TOML (.toml)")
|
|
86
67
|
return cfg, path
|
|
87
68
|
|
|
88
69
|
|
|
@@ -188,6 +169,8 @@ def get_task_config(name: Optional[str], cfg_path: Optional[str]):
|
|
|
188
169
|
if not isinstance(tasks, dict):
|
|
189
170
|
raise TypeError("'tasks' must be a table")
|
|
190
171
|
|
|
172
|
+
name = name or cfg.get("default_task", None)
|
|
173
|
+
|
|
191
174
|
if not name:
|
|
192
175
|
if len(tasks) == 0:
|
|
193
176
|
raise ValueError(f"No task found in {path}")
|
dmon/control.py
CHANGED
|
@@ -525,14 +525,14 @@ def print_process_table(metas: List[DmonMeta], full_width: bool = False):
|
|
|
525
525
|
|
|
526
526
|
def list_processes(dir: PathType, full_width: bool):
|
|
527
527
|
target_dmon_dir = Path(dir).resolve()
|
|
528
|
-
metas = []
|
|
528
|
+
metas: List[DmonMeta] = []
|
|
529
529
|
if target_dmon_dir.exists() and target_dmon_dir.is_dir():
|
|
530
530
|
for meta_file in target_dmon_dir.glob(f"*{META_SUFFIX}"):
|
|
531
531
|
meta = DmonMeta.load(meta_file)
|
|
532
532
|
if meta is not None:
|
|
533
533
|
metas.append(meta)
|
|
534
534
|
# sort by name (case-insensitive)
|
|
535
|
-
metas.sort(key=lambda m: m.
|
|
535
|
+
metas.sort(key=lambda m: m.task.lower())
|
|
536
536
|
n_task = len(metas)
|
|
537
537
|
processes = print_process_table(metas, full_width)
|
|
538
538
|
n_proc = len(processes)
|
|
@@ -557,6 +557,9 @@ def execute(cfg: DmonTaskConfig):
|
|
|
557
557
|
env = {**os.environ, **cfg.env}
|
|
558
558
|
|
|
559
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]
|
|
560
563
|
|
|
561
564
|
def signal_handler(signum: int, frame) -> None:
|
|
562
565
|
if ON_WINDOWS and signum == signal.SIGINT:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: python-dmon
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -41,10 +41,35 @@ It is a Python-based and more powerful successor to the [handy-backend shell scr
|
|
|
41
41
|
|
|
42
42
|
## Installation
|
|
43
43
|
|
|
44
|
+
`python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
|
|
45
|
+
|
|
44
46
|
```sh
|
|
45
47
|
pip install python-dmon
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
# Install globally with uv tool
|
|
54
|
+
uv tool install python-dmon
|
|
55
|
+
|
|
56
|
+
# Or with pipx
|
|
57
|
+
pipx install python-dmon
|
|
58
|
+
|
|
59
|
+
# Add as a dev dependency in your project
|
|
60
|
+
uv add --dev python-dmon
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
You can also invoke without installing:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
# With uvx (uv tool run)
|
|
67
|
+
uvx python-dmon
|
|
68
|
+
|
|
69
|
+
# Or with pipx
|
|
70
|
+
pipx run python-dmon
|
|
71
|
+
```
|
|
72
|
+
|
|
48
73
|
To get the latest features, install from source:
|
|
49
74
|
|
|
50
75
|
```sh
|
|
@@ -70,7 +95,7 @@ app = ["python", "-u", "server.py"]
|
|
|
70
95
|
```
|
|
71
96
|
|
|
72
97
|
Commands can be a single string (run in shell), or list of strings (exec form).
|
|
73
|
-
See [Example
|
|
98
|
+
See [Example Configuration](#example-configuration) for more configuration options.
|
|
74
99
|
|
|
75
100
|
|
|
76
101
|
### Run tasks
|
|
@@ -86,14 +111,38 @@ dmon stop app
|
|
|
86
111
|
|
|
87
112
|
# Check task status
|
|
88
113
|
dmon status app
|
|
114
|
+
|
|
115
|
+
# Execute a task in the foreground (useful for debugging)
|
|
116
|
+
dmon exec app
|
|
89
117
|
```
|
|
90
118
|
|
|
91
|
-
If only one task is defined in the config file, you can omit the task name:
|
|
119
|
+
If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
|
|
92
120
|
|
|
93
121
|
```sh
|
|
94
122
|
dmon start
|
|
95
123
|
dmon stop
|
|
96
124
|
dmon status
|
|
125
|
+
dmon exec
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
You can use `--config` to specify a custom config file or the directory containing it:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
dmon start --config /path/to/dmon.yaml app # YAML
|
|
132
|
+
dmon start --config /path/to/pyproject.toml app # or TOML
|
|
133
|
+
dmon start --config /path/to/dir app # dir with `dmon.y(a)ml` or `pyproject.toml`
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
And yes, you can use `dmon` to run in a nested manner:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
tasks:
|
|
140
|
+
app: ["python", "-u", "server.py"]
|
|
141
|
+
nested: pwd && dmon exec app # nest `dmon exec`
|
|
142
|
+
subdir_task1:
|
|
143
|
+
cwd: /path/to/dir
|
|
144
|
+
cmd: ["dmon", "exec", "app"] # run task defined in another folder
|
|
145
|
+
subdir_task2: dmon exec app --config /path/to/dir/dmon.yaml # like above
|
|
97
146
|
```
|
|
98
147
|
|
|
99
148
|
|
|
@@ -121,7 +170,7 @@ dmon list
|
|
|
121
170
|
```
|
|
122
171
|
|
|
123
172
|
|
|
124
|
-
## Example
|
|
173
|
+
## Example Configuration
|
|
125
174
|
|
|
126
175
|
A task can be a **string**, **list**, or **dictionary**.
|
|
127
176
|
|
|
@@ -142,8 +191,23 @@ tasks:
|
|
|
142
191
|
rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
|
|
143
192
|
rotate_log_max_size: 5 # max rotation log file size in MB
|
|
144
193
|
meta_path: ".dmon/<task>.meta.json" # path to meta file
|
|
194
|
+
default_task: your_task_name # the default task name
|
|
145
195
|
```
|
|
146
196
|
|
|
197
|
+
In TOML, write like this:
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[tool.dmon.tasks]
|
|
201
|
+
your_task_name = { cmd = [
|
|
202
|
+
"python", "-u", "server.py"
|
|
203
|
+
], ... }
|
|
204
|
+
another_task = "cd subdir && ls && bash start.sh"
|
|
205
|
+
|
|
206
|
+
[tool.dmon]
|
|
207
|
+
default_task = "your_task_name"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
|
|
147
211
|
## Under the Hood
|
|
148
212
|
|
|
149
213
|
Each task is associated with a meta file (e.g. `.dmon/<task>.meta.json`) stored in the current working directory.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
dmon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
dmon/__main__.py,sha256=wu5N2wk8mvBgyvr2ghmQf4prezAe0_i-p123VVreyYc,62
|
|
3
|
-
dmon/cli.py,sha256=
|
|
4
|
-
dmon/config.py,sha256=
|
|
3
|
+
dmon/cli.py,sha256=CdlgGTWWbTbJ-yz1Cct1EoJtaNGI3zs_3puoMRtrqbg,8780
|
|
4
|
+
dmon/config.py,sha256=RXcQEgyXSmx_sIXzcrMnc_XkyV0uc20MEv5DTNxFHrw,7204
|
|
5
5
|
dmon/constants.py,sha256=uJPI66IJvT6vhamqEDeqHjZQS12RxCPe3DD5yCU0UR8,407
|
|
6
|
-
dmon/control.py,sha256=
|
|
6
|
+
dmon/control.py,sha256=KpGNPPsuAsDNX3FTJxnGNnt9f4Q18omvG42zO1CwTy4,17494
|
|
7
7
|
dmon/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
dmon/runner.py,sha256=iyotNIrbO6I_u02EcqwO7R7N3BEBP6b_8eL8yb3zeJQ,6082
|
|
9
9
|
dmon/types.py,sha256=tTVoJzWk-Z8XiZCUt0VE8Z7-URpXMoS54HfVYXhW-0Q,1795
|
|
10
10
|
dmon/utils.py,sha256=zVQC9YNhMQu4hrAp9k2ds2egnbWZZv7S5w8i2oVIPJg,808
|
|
11
|
-
python_dmon-0.2.
|
|
12
|
-
python_dmon-0.2.
|
|
13
|
-
python_dmon-0.2.
|
|
14
|
-
python_dmon-0.2.
|
|
11
|
+
python_dmon-0.2.3.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
12
|
+
python_dmon-0.2.3.dist-info/entry_points.txt,sha256=1XmEpyjShANXEHMIMa4p3WXpcllIZfGt5DQyTFuSI74,78
|
|
13
|
+
python_dmon-0.2.3.dist-info/METADATA,sha256=7k6z_ZfbmzpHEAiUKY_O0MQVwkEuWE53PLPz1Th66wA,5984
|
|
14
|
+
python_dmon-0.2.3.dist-info/RECORD,,
|
|
File without changes
|