python-dmon 0.3.1__tar.gz → 0.4.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.
@@ -0,0 +1,481 @@
1
+ Metadata-Version: 2.3
2
+ Name: python-dmon
3
+ Version: 0.4.0
4
+ Summary: A lightweight, cross-platform daemon manager that runs any command as a background process.
5
+ Keywords: python-dmon,dmon,daemon,background,detach,process management
6
+ Author: Atomie CHEN
7
+ Author-email: Atomie CHEN <atomic_cwh@163.com>
8
+ Requires-Dist: colorama>=0.4.6
9
+ Requires-Dist: psutil>=7.1.0
10
+ Requires-Dist: python-dotenv>=1.0.1,<1.1
11
+ Requires-Dist: pyyaml>=6.0.3
12
+ Requires-Dist: termcolor>=2.4.0
13
+ Requires-Dist: tomli>=2.2.1 ; python_full_version < '3.11'
14
+ Requires-Python: >=3.8
15
+ Project-URL: Bug Tracker, https://github.com/atomiechen/python-dmon/issues
16
+ Project-URL: Changelog, https://github.com/atomiechen/python-dmon/blob/main/CHANGELOG.md
17
+ Project-URL: Homepage, https://github.com/atomiechen/python-dmon
18
+ Description-Content-Type: text/markdown
19
+
20
+ # python-dmon
21
+
22
+
23
+ [![GitHub](https://img.shields.io/badge/github-python--dmon-blue?logo=github)](https://github.com/atomiechen/python-dmon)
24
+ [![PyPI](https://img.shields.io/pypi/v/python--dmon?logo=pypi&logoColor=white)](https://pypi.org/project/python-dmon/)
25
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/atomiechen/python-dmon)
26
+
27
+
28
+ A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
29
+ It also supports logging and log rotation out of the box.
30
+ **No external runtime required**.
31
+
32
+ Shipped as the CLI tool `dmon`.
33
+ It is a Python-based and more powerful successor to the [handy-backend shell scripts](https://github.com/atomiechen/handy-backend).
34
+
35
+
36
+ ## Features
37
+
38
+ - 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
39
+ - ⚡ **Lightweight:** No daemon service or container runtime required.
40
+ - 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
41
+ - 🔗 **Supervised stacks:** Start dependent tasks in order, wait for HTTP, TCP,
42
+ or command readiness, and report runtime degradation.
43
+ - 🌙 **Foreground or detached:** Keep a stack attached for development, or run
44
+ it under a recoverable background supervisor with `dmon stack up -d` and
45
+ `dmon stack down`.
46
+ - ⏱️ **Reusable readiness:** Wait for configured tasks or direct HTTP, TCP, and
47
+ command probes from scripts and deployment workflows.
48
+ - 🪵 **Logging & log rotation:** Keep active log files manageable, with optional archive retention limits.
49
+
50
+ ![dmon-demo-gif](https://github.com/user-attachments/assets/9bae2f46-5ef4-4784-aced-18d573204efc)
51
+
52
+
53
+ ## Installation
54
+
55
+ `python-dmon` is available on [PyPI](https://pypi.org/project/python-dmon/):
56
+
57
+ ```sh
58
+ pip install python-dmon
59
+ ```
60
+
61
+ We recommend installing into an isolated environment, e.g., with `uv` / `pipx`:
62
+
63
+ ```sh
64
+ # Install globally with uv tool
65
+ uv tool install python-dmon
66
+
67
+ # Or with pipx
68
+ pipx install python-dmon
69
+
70
+ # Add as a dev dependency in your project
71
+ uv add --dev python-dmon
72
+ ```
73
+
74
+ You can also invoke without installing:
75
+
76
+ ```sh
77
+ # With uvx (uv tool run)
78
+ uvx python-dmon
79
+
80
+ # Or with pipx
81
+ pipx run python-dmon
82
+ ```
83
+
84
+ To get the latest features, install from source:
85
+
86
+ ```sh
87
+ pip install git+https://github.com/atomiechen/python-dmon.git
88
+ ```
89
+
90
+ ## Getting Started
91
+
92
+ ### Prepare Configuration
93
+
94
+ Create a `dmon.yaml` file:
95
+
96
+ ```yaml
97
+ tasks:
98
+ app: ["python", "-u", "server.py"] # option 1: exec form
99
+ # app: "python -u server.py" # option 2: shell string
100
+ ```
101
+
102
+ Or add to your `pyproject.toml`:
103
+
104
+ ```toml
105
+ [tool.dmon.tasks]
106
+ app = ["python", "-u", "server.py"] # option 1: exec form
107
+ # app = "python -u server.py" # option 2: shell string
108
+ ```
109
+
110
+ Commands can be a single string (run in shell), or list of strings (exec form).
111
+ See [Example Configuration](#example-configuration) for more configuration options.
112
+ Without `--config`, dmon searches the current directory and its parents for
113
+ `dmon.yaml`, `dmon.yml`, or `pyproject.toml`.
114
+
115
+
116
+ ### Run tasks
117
+
118
+ Run a configured task by its name:
119
+
120
+ ```sh
121
+ # Start a task
122
+ dmon start app
123
+
124
+ # Stop a running task
125
+ dmon stop app
126
+
127
+ # Restart a task
128
+ dmon restart app
129
+
130
+ # Check task status
131
+ dmon status app
132
+
133
+ # Execute a task in the foreground (useful for debugging)
134
+ dmon exec app
135
+ ```
136
+
137
+ `dmon exec` runs one configured command directly in the current terminal for
138
+ debugging; it is not registered as a managed background task. Use `dmon start`
139
+ when the task should remain discoverable through `dmon status` and `dmon list`.
140
+
141
+ You can specify multiple tasks at once, e.g.: `dmon start app1 app2 app3`, except for `dmon exec` which only accepts one task.
142
+
143
+ Multi-task `start` is best-effort: dmon attempts every requested task and leaves
144
+ successful tasks running if another task cannot start. The command returns a
145
+ non-zero status and prints a summary naming the failed tasks. This is useful for
146
+ independent background services and does not provide atomic stack semantics.
147
+
148
+ For related services that should start and stop as one unit, define a stack and
149
+ run it in the foreground. Unlike a direct `dmon exec`, a foreground stack is a
150
+ managed multi-task lifecycle and remains discoverable from another terminal:
151
+
152
+ ```yaml
153
+ tasks:
154
+ database:
155
+ cmd: [python, database.py]
156
+ ready:
157
+ tcp: {host: 127.0.0.1, port: 5432}
158
+ timeout: 20
159
+ api:
160
+ cmd: [python, api.py]
161
+ depends_on: [database]
162
+ ready:
163
+ http: http://127.0.0.1:8000/health
164
+ worker:
165
+ cmd: [python, worker.py]
166
+ depends_on: [api]
167
+
168
+ stacks:
169
+ dev: [api, worker]
170
+ default_stack: dev
171
+ ```
172
+
173
+ ```sh
174
+ dmon stack up dev
175
+ # Or omit the name when default_stack (or only one stack) is configured
176
+ dmon stack up
177
+
178
+ # Keep the supervised stack running in the background
179
+ dmon stack up -d dev
180
+ dmon stack status dev
181
+ dmon stack restart dev
182
+ dmon stack logs --tail 100 dev
183
+ dmon stack logs -f dev
184
+ dmon stack list
185
+ dmon stack down dev
186
+
187
+ # Optional fail-fast policy for foreground or detached stacks
188
+ dmon stack up --abort-on-exit dev
189
+ ```
190
+
191
+ `dmon stack up` starts dependencies in order and waits for each task's optional
192
+ readiness probe. A startup failure or readiness timeout rolls back every task
193
+ started by that invocation. After startup, an exited task marks the stack as
194
+ degraded while unrelated tasks continue running, matching Docker Compose's
195
+ default behavior. Use `--abort-on-exit` when the whole stack should stop after
196
+ any runtime exit. Ctrl-C or SIGTERM cleans up a foreground stack in reverse
197
+ order. `dmon stack down` requests the same cleanup for an active foreground or
198
+ detached stack from another terminal.
199
+
200
+ Foreground `dmon stack up` displays new task output with task-name prefixes,
201
+ while retaining it in each task's configured `log_path`. Detached mode does not
202
+ attach output. `dmon stack logs` reads the latest 100 lines per task by default;
203
+ `--tail` changes that count and `-f` follows new output. Log viewing never
204
+ modifies the underlying files and never controls running processes. Docker
205
+ Compose is still appropriate when container behavior itself must be tested.
206
+
207
+ Terminal attachment and process management are separate choices: `dmon exec`
208
+ is foreground but intentionally unmanaged, while foreground `dmon stack up` is
209
+ supervised, recorded, and discoverable from another terminal. Detached stacks
210
+ use the same ownership model without attaching their output.
211
+
212
+ Detached mode waits for the same startup and readiness checks before returning.
213
+ A lightweight background supervisor keeps monitoring the stack; `dmon stack down`
214
+ requests the same graceful reverse-order cleanup on every platform. If that
215
+ supervisor is killed, its persisted ownership metadata lets `down` recover and
216
+ clean the tasks it started. Supervisor diagnostics are written to
217
+ `logs/<stack>.stack.log`. `dmon stack status` includes every owned task and its
218
+ process tree; `dmon stack list` summarizes all recorded foreground and detached
219
+ stacks. `dmon stack restart` applies to detached stacks: it performs a clean
220
+ `down` followed by a detached `up` and preserves the stack's exit policy.
221
+
222
+ ### Wait for readiness
223
+
224
+ `dmon wait` checks readiness without starting or stopping anything. For a
225
+ configured task, the task must already be managed by `dmon start` or a stack;
226
+ the command uses that task's configured probe, working directory, environment,
227
+ and process identity. The task name can be omitted when `default_task` is set or
228
+ the configuration contains only one task:
229
+
230
+ ```sh
231
+ dmon wait api
232
+ dmon wait api worker --timeout 60 --interval 0.5
233
+ ```
234
+
235
+ Direct probes need no dmon configuration:
236
+
237
+ ```sh
238
+ dmon wait --http http://127.0.0.1:8000/health
239
+ dmon wait --tcp 127.0.0.1:5432
240
+ dmon wait --timeout 30 --command -- python healthcheck.py --verbose
241
+ ```
242
+
243
+ For command probes, dmon options must precede `--command`; the optional second
244
+ `--` marks the child-command boundary. The command returns zero only when every
245
+ target is ready, one for a completed unsuccessful wait, and 130 when interrupted.
246
+ Configured timeouts and intervals can be overridden with positive values.
247
+
248
+ Or use `--all` to operate on all tasks:
249
+
250
+ ```sh
251
+ # All configured tasks
252
+ dmon start --all
253
+ dmon restart --all
254
+
255
+ # All recorded task metadata in the project
256
+ dmon status --all
257
+ dmon stop --all
258
+ ```
259
+
260
+ If you have defined `default_task`, or only one task is defined in the config file, you can omit the task name:
261
+
262
+ ```sh
263
+ dmon start
264
+ dmon stop
265
+ dmon restart
266
+ dmon status
267
+ dmon exec
268
+ ```
269
+
270
+ You can use `-c` / `--config` to specify a custom config file or the directory containing it:
271
+
272
+ ```sh
273
+ dmon start --config /path/to/dmon.yaml app # YAML
274
+ dmon start --config /path/to/pyproject.toml app # or TOML
275
+ dmon start -c /path/to/dir app # shorter, dir with `dmon.y(a)ml` or `pyproject.toml`
276
+ ```
277
+
278
+ The same config discovery and selection rules apply to stacks. A stack name can
279
+ be omitted when `default_stack` is set or only one stack is configured. Options
280
+ belonging to a stack operation go after that operation and may appear before or
281
+ after the stack name; for example, `dmon stack up -d dev` and `dmon stack up dev
282
+ -d` are equivalent.
283
+
284
+ And yes, you can use `dmon` to run in a nested manner:
285
+
286
+ ```yaml
287
+ tasks:
288
+ app: ["python", "-u", "server.py"]
289
+ nested: pwd && dmon exec app # nest `dmon exec`
290
+ subdir_task1:
291
+ cwd: /path/to/dir
292
+ cmd: ["dmon", "exec", "app"] # run task defined in another folder
293
+ subdir_task2: dmon exec app --config /path/to/dir/dmon.yaml # like above
294
+ ```
295
+
296
+
297
+ ### Run an ad-hoc command
298
+
299
+ ```sh
300
+ # Run a command with arguments in the background
301
+ dmon run --name myserver python -u server.py
302
+
303
+ # Optionally use -- to make the child-command boundary explicit
304
+ dmon run --name timer -- python -c 'import time; time.sleep(30)'
305
+
306
+ # Run a shell command in the background
307
+ dmon run --shell echo "Hello World"
308
+
309
+ # Run a shell script in the background
310
+ dmon run --cwd /path/to/script bash myscript.sh
311
+ ```
312
+
313
+ > [!NOTE]
314
+ > If no name is provided, `dmon` automatically assigns a fixed task name `default_run` to prevent duplicate runs.
315
+
316
+
317
+ ### List recorded tasks and their status
318
+
319
+ ```sh
320
+ dmon list
321
+ ```
322
+
323
+ Finite inspection and readiness commands also support machine-readable output:
324
+
325
+ ```sh
326
+ dmon status app --format json
327
+ dmon list --format json
328
+ dmon stack status dev --format json
329
+ dmon stack list --format json
330
+ dmon wait api --format json
331
+ ```
332
+
333
+ JSON is written only to stdout; actionable diagnostics remain on stderr. The
334
+ payload has a top-level `ok` field and a `tasks`, `stacks`, or `waits` array.
335
+ Inspection results contain `name`, `ok`, `error`, and an optional `snapshot`;
336
+ wait results contain the target, outcome, reason, elapsed time, and attempt
337
+ count. Existing exit-code semantics are unchanged. Interactive and streaming
338
+ commands do not offer JSON output.
339
+
340
+ ### Python API
341
+
342
+ The same task lifecycle and inspection logic is available without parsing CLI
343
+ output:
344
+
345
+ ```python
346
+ from dmon import Dmon
347
+
348
+ client = Dmon(config="dmon.yaml")
349
+ started = client.start("app")
350
+ ready = client.wait("app", timeout=30)
351
+ task = client.status("app")
352
+ stacks = client.list_stacks()
353
+ client.stop("app")
354
+ ```
355
+
356
+ API calls are synchronous and silent. They return immutable `ActionResult`,
357
+ `TaskResult`, `StackResult`, and `WaitResult` data; expected runtime states such
358
+ as missing or exited metadata are results, while invalid configuration raises
359
+ `DmonConfigError`. The initial API intentionally does not start a supervised
360
+ stack or create implicit background threads.
361
+
362
+
363
+ ## Example Configuration
364
+
365
+ A task can be a **string**, **list**, or **dictionary**.
366
+
367
+ When rotation is enabled, dmon keeps timestamped archives such as
368
+ `app.log.20260807-142106`. Both task and runner logs use this cross-platform
369
+ format; a same-second collision adds `.1`, `.2`, and so on. Archives are never
370
+ deleted by default. Set a backup count explicitly to enable retention cleanup.
371
+ `log_path` contains task output; `rotate_log_path` contains diagnostics from the
372
+ dmon process that captures and rotates that output. They are independent log
373
+ streams and use independent retention settings.
374
+ The size limit is checked at line boundaries, so a single long line may exceed
375
+ the configured limit.
376
+
377
+ Here is a more complete example with default values:
378
+
379
+ ```yaml
380
+ tasks:
381
+ your_task_name:
382
+ # Command to run; can be a string (run in shell) or list of strings (exec form)
383
+ cmd: ["python", "server.py"] # required
384
+ cwd: "/path/to/working/dir" # (default: current dir)
385
+ env_file: [".env", ".env.local"] # optional; later files override earlier files
386
+ env: # (default: inherit from parent process)
387
+ PYTHONUNBUFFERED: "1"
388
+ override_env: false # true omits parent env; use env_file and env only
389
+ log_path: "logs/<task>.log" # path to log file
390
+ log_rotate: false # enable log rotation
391
+ log_max_size: 5 # max log file size before rotation in MB
392
+ # log_backup_count: 10 # optional; omit to retain all task log archives
393
+ rotate_log_path: "logs/<task>.rotate.log" # path to rotation log
394
+ rotate_log_max_size: 5 # max rotation log file size in MB
395
+ # rotate_log_backup_count: 10 # optional; omit to retain all runner log archives
396
+ meta_path: ".dmon/<task>.meta.json" # path to meta file
397
+ depends_on: [another_task] # dependency order used by `dmon stack up`
398
+ ready: # optional; exactly one of http, tcp, or command
399
+ command: [python, healthcheck.py]
400
+ timeout: 30 # total seconds to wait (default: 30)
401
+ interval: 0.2 # seconds between attempts (default: 0.2)
402
+ default_task: your_task_name # the default task name
403
+ stacks:
404
+ dev: [your_task_name]
405
+ default_stack: dev
406
+ ```
407
+
408
+ `env_file` accepts one path or an ordered list of dotenv files. Paths are
409
+ relative to the dmon configuration file. Later files override earlier files,
410
+ the existing process environment overrides file values, and the explicit `env`
411
+ table has highest priority. Set `override_env: true` to omit the existing
412
+ process environment. Standard dotenv `${NAME}` expansion can refer to earlier
413
+ values in the same file or any earlier file in the list. Missing files and keys
414
+ without assigned values fail startup without creating task metadata.
415
+ Environment values and environment-file paths are never written to metadata or
416
+ JSON inspection output.
417
+
418
+ In TOML, write like this:
419
+
420
+ ```toml
421
+ [tool.dmon.tasks]
422
+ your_task_name = { cmd = [
423
+ "python", "-u", "server.py"
424
+ ], ... }
425
+ another_task = "cd subdir && ls && bash start.sh"
426
+
427
+ [tool.dmon]
428
+ default_task = "your_task_name"
429
+ ```
430
+
431
+ All paths, including `env_file`, can be absolute or relative to the **config
432
+ file location**.
433
+
434
+ YAML anchors and merge keys can reuse task fragments within one configuration
435
+ file. Task dependencies are resolved transitively and cycles are rejected.
436
+ dmon intentionally does not recursively include or merge other configuration
437
+ files; keeping one path owner makes command, environment, log, and metadata
438
+ paths unambiguous.
439
+
440
+
441
+ ## Under the Hood
442
+
443
+ Each task has `.dmon/<task>.meta.json`, which records its command, PID, process
444
+ creation time, and log paths. An active foreground or detached stack also has
445
+ `.dmon/<stack>.stack.json`, which records its mode, supervisor, and the exact
446
+ task processes it owns. Metadata paths are reserved exclusively and subsequent
447
+ updates replace the JSON atomically; a per-run ID isolates stop requests, while
448
+ PID plus creation time prevents a recycled PID from being mistaken for the
449
+ original process. Environment values are used only to launch and probe the task;
450
+ they are never written to metadata. Normal foreground cleanup removes its stack
451
+ metadata.
452
+
453
+ `dmon stack down` normally asks the foreground or detached supervisor to stop
454
+ tasks in reverse order.
455
+ If the supervisor has crashed, it uses the persisted process identities to
456
+ recover the orphaned stack without inferring ownership from current
457
+ configuration. Existing or unreadable stack metadata is preserved rather than
458
+ overwritten; use `dmon stack down` for stale, readable state. **Do not** edit or
459
+ delete `.dmon` files manually.
460
+
461
+ The log viewer is deliberately separate from process control. It opens task log
462
+ files only while reading, closes them before waiting for more output, and uses
463
+ file identity to reopen a replacement after rotation. Stopping `dmon stack logs
464
+ -f` cannot stop or restart a task or stack. The same read-only component powers
465
+ foreground log attachment; a display failure does not change stack lifecycle.
466
+
467
+ `dmon status` returns a non-zero status if a recorded task has exited. Starting
468
+ that task again removes its stale metadata automatically. `dmon stop` terminates
469
+ the complete process tree and also cleans stale metadata left by an exited task.
470
+
471
+
472
+ ## Development
473
+
474
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture, behavioral contracts,
475
+ validation, and the release workflow. Process, signal, log-rotation, and stack
476
+ changes must also pass the reproducible [manual test lab](tests/manual/README.md).
477
+
478
+
479
+ ## License
480
+
481
+ [python-dmon](https://github.com/atomiechen/python-dmon) © 2025 by [Atomie CHEN](https://github.com/atomiechen) is licensed under the [MIT License](https://github.com/atomiechen/python-dmon/blob/main/LICENSE).