python-dmon 0.2.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,156 @@
1
+ Metadata-Version: 2.3
2
+ Name: python-dmon
3
+ Version: 0.2.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: pyyaml>=6.0.3
11
+ Requires-Dist: termcolor>=2.4.0
12
+ Requires-Dist: tomli>=2.2.1 ; python_full_version < '3.11'
13
+ Requires-Python: >=3.8
14
+ Project-URL: Bug Tracker, https://github.com/atomiechen/python-dmon/issues
15
+ Project-URL: Changelog, https://github.com/atomiechen/python-dmon/blob/master/CHANGELOG.md
16
+ Project-URL: Homepage, https://github.com/atomiechen/python-dmon
17
+ Description-Content-Type: text/markdown
18
+
19
+ # python-dmon
20
+
21
+
22
+ [![GitHub](https://img.shields.io/badge/github-python--dmon-blue?logo=github)](https://github.com/atomiechen/python-dmon)
23
+ [![PyPI](https://img.shields.io/pypi/v/python--dmon?logo=pypi&logoColor=white)](https://pypi.org/project/python-dmon/)
24
+
25
+
26
+ A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
27
+ It also supports logging and log rotation out of the box.
28
+ **No Docker or extra dependencies required**.
29
+
30
+ Shipped as the CLI tool `dmon`.
31
+ It is a Python-based and more powerful successor to the [handy-backend shell scripts](https://github.com/atomiechen/handy-backend).
32
+
33
+
34
+ ## Features
35
+
36
+ - 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
37
+ - ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
38
+ - 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
39
+ - 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
40
+
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ pip install python-dmon
46
+ ```
47
+
48
+ To get the latest features, install from source:
49
+
50
+ ```sh
51
+ pip install git+https://github.com/atomiechen/python-dmon.git
52
+ ```
53
+
54
+ ## Getting Started
55
+
56
+ ### Prepare Configuration
57
+
58
+ Create a `dmon.yaml` file:
59
+
60
+ ```yaml
61
+ tasks:
62
+ app: ["python", "-u", "server.py"]
63
+ ```
64
+
65
+ Or add to your `pyproject.toml`:
66
+
67
+ ```toml
68
+ [tool.dmon.tasks]
69
+ app = ["python", "-u", "server.py"]
70
+ ```
71
+
72
+ Commands can be a single string (run in shell), or list of strings (exec form).
73
+ See [Example Task Configuration](#example-task-configuration) for more configuration options.
74
+
75
+
76
+ ### Run tasks
77
+
78
+ Run a configured task by its name:
79
+
80
+ ```sh
81
+ # Start a task
82
+ dmon start app
83
+
84
+ # Stop a running task
85
+ dmon stop app
86
+
87
+ # Check task status
88
+ dmon status app
89
+ ```
90
+
91
+ If only one task is defined in the config file, you can omit the task name:
92
+
93
+ ```sh
94
+ dmon start
95
+ dmon stop
96
+ dmon status
97
+ ```
98
+
99
+
100
+ ### Run an ad-hoc command
101
+
102
+ ```sh
103
+ # Run a command with arguments in the background
104
+ dmon run --name myserver python -u server.py
105
+
106
+ # Run a shell command in the background
107
+ dmon run --shell echo "Hello World"
108
+
109
+ # Run a shell script in the background
110
+ dmon run --cwd /path/to/script bash myscript.sh
111
+ ```
112
+
113
+ > [!NOTE]
114
+ > If no name is provided, `dmon` automatically assigns a fixed task name `default_run` to prevent duplicate runs.
115
+
116
+
117
+ ### List all running tasks
118
+
119
+ ```sh
120
+ dmon list
121
+ ```
122
+
123
+
124
+ ## Example Task Configuration
125
+
126
+ A task can be a **string**, **list**, or **dictionary**.
127
+
128
+ Here is a more complete example with default values:
129
+
130
+ ```yaml
131
+ tasks:
132
+ your_task_name:
133
+ # Command to run; can be a string (run in shell) or list of strings (exec form)
134
+ cmd: ["python", "server.py"] # required
135
+ cwd: "/path/to/working/dir" # (default: current dir)
136
+ env: # (default: inherit from parent process)
137
+ PYTHONUNBUFFERED: "1"
138
+ override_env: false # override parent env and only use env defined here
139
+ log_path: "logs/<name>.log" # path to log file
140
+ log_rotate: false # enable log rotation
141
+ log_max_size: 5 # max log file size before rotation in MB
142
+ rotate_log_path: "logs/<name>.rotate.log" # path to rotation log
143
+ rotate_log_max_size: 5 # max rotation log file size in MB
144
+ meta_path: ".dmon/<name>.meta.json" # path to meta file
145
+ ```
146
+
147
+ ## Under the Hood
148
+
149
+ Each task is associated with a meta file (e.g. `.dmon/<name>.meta.json`) stored in the current working directory.
150
+ The file contains details such as the command, PID, log path, and more.
151
+ **Do not** modify or delete these files manually.
152
+
153
+
154
+ ## License
155
+
156
+ [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).
@@ -0,0 +1,138 @@
1
+ # python-dmon
2
+
3
+
4
+ [![GitHub](https://img.shields.io/badge/github-python--dmon-blue?logo=github)](https://github.com/atomiechen/python-dmon)
5
+ [![PyPI](https://img.shields.io/pypi/v/python--dmon?logo=pypi&logoColor=white)](https://pypi.org/project/python-dmon/)
6
+
7
+
8
+ A lightweight, cross-platform daemon manager that runs any command — called a *task* — as a background process.
9
+ It also supports logging and log rotation out of the box.
10
+ **No Docker or extra dependencies required**.
11
+
12
+ Shipped as the CLI tool `dmon`.
13
+ It is a Python-based and more powerful successor to the [handy-backend shell scripts](https://github.com/atomiechen/handy-backend).
14
+
15
+
16
+ ## Features
17
+
18
+ - 🖥️ **Cross-platform:** Works on Linux, macOS, and Windows.
19
+ - ⚡ **Lightweight:** Pure Python, no Docker or external dependencies needed.
20
+ - 🧩 **Flexible tasks:** Tasks can be configured in `pyproject.toml` or `dmon.yaml`; or run ad-hoc commands directly.
21
+ - 🪵 **Logging & log rotation:** Automatically manage log files to prevent uncontrolled growth.
22
+
23
+
24
+ ## Installation
25
+
26
+ ```sh
27
+ pip install python-dmon
28
+ ```
29
+
30
+ To get the latest features, install from source:
31
+
32
+ ```sh
33
+ pip install git+https://github.com/atomiechen/python-dmon.git
34
+ ```
35
+
36
+ ## Getting Started
37
+
38
+ ### Prepare Configuration
39
+
40
+ Create a `dmon.yaml` file:
41
+
42
+ ```yaml
43
+ tasks:
44
+ app: ["python", "-u", "server.py"]
45
+ ```
46
+
47
+ Or add to your `pyproject.toml`:
48
+
49
+ ```toml
50
+ [tool.dmon.tasks]
51
+ app = ["python", "-u", "server.py"]
52
+ ```
53
+
54
+ Commands can be a single string (run in shell), or list of strings (exec form).
55
+ See [Example Task Configuration](#example-task-configuration) for more configuration options.
56
+
57
+
58
+ ### Run tasks
59
+
60
+ Run a configured task by its name:
61
+
62
+ ```sh
63
+ # Start a task
64
+ dmon start app
65
+
66
+ # Stop a running task
67
+ dmon stop app
68
+
69
+ # Check task status
70
+ dmon status app
71
+ ```
72
+
73
+ If only one task is defined in the config file, you can omit the task name:
74
+
75
+ ```sh
76
+ dmon start
77
+ dmon stop
78
+ dmon status
79
+ ```
80
+
81
+
82
+ ### Run an ad-hoc command
83
+
84
+ ```sh
85
+ # Run a command with arguments in the background
86
+ dmon run --name myserver python -u server.py
87
+
88
+ # Run a shell command in the background
89
+ dmon run --shell echo "Hello World"
90
+
91
+ # Run a shell script in the background
92
+ dmon run --cwd /path/to/script bash myscript.sh
93
+ ```
94
+
95
+ > [!NOTE]
96
+ > If no name is provided, `dmon` automatically assigns a fixed task name `default_run` to prevent duplicate runs.
97
+
98
+
99
+ ### List all running tasks
100
+
101
+ ```sh
102
+ dmon list
103
+ ```
104
+
105
+
106
+ ## Example Task Configuration
107
+
108
+ A task can be a **string**, **list**, or **dictionary**.
109
+
110
+ Here is a more complete example with default values:
111
+
112
+ ```yaml
113
+ tasks:
114
+ your_task_name:
115
+ # Command to run; can be a string (run in shell) or list of strings (exec form)
116
+ cmd: ["python", "server.py"] # required
117
+ cwd: "/path/to/working/dir" # (default: current dir)
118
+ env: # (default: inherit from parent process)
119
+ PYTHONUNBUFFERED: "1"
120
+ override_env: false # override parent env and only use env defined here
121
+ log_path: "logs/<name>.log" # path to log file
122
+ log_rotate: false # enable log rotation
123
+ log_max_size: 5 # max log file size before rotation in MB
124
+ rotate_log_path: "logs/<name>.rotate.log" # path to rotation log
125
+ rotate_log_max_size: 5 # max rotation log file size in MB
126
+ meta_path: ".dmon/<name>.meta.json" # path to meta file
127
+ ```
128
+
129
+ ## Under the Hood
130
+
131
+ Each task is associated with a meta file (e.g. `.dmon/<name>.meta.json`) stored in the current working directory.
132
+ The file contains details such as the command, PID, log path, and more.
133
+ **Do not** modify or delete these files manually.
134
+
135
+
136
+ ## License
137
+
138
+ [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).
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "python-dmon"
3
+ version = "0.2.0"
4
+ description = "A lightweight, cross-platform daemon manager that runs any command as a background process."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Atomie CHEN", email = "atomic_cwh@163.com" }
8
+ ]
9
+ requires-python = ">=3.8"
10
+ dependencies = [
11
+ "colorama>=0.4.6",
12
+ "psutil>=7.1.0",
13
+ "pyyaml>=6.0.3",
14
+ "termcolor>=2.4.0",
15
+ "tomli>=2.2.1 ; python_full_version < '3.11'",
16
+ ]
17
+ keywords = ["python-dmon", "dmon", "daemon", "background", "detach", "process management"]
18
+
19
+ [project.urls]
20
+ "Homepage" = "https://github.com/atomiechen/python-dmon"
21
+ "Bug Tracker" = "https://github.com/atomiechen/python-dmon/issues"
22
+ Changelog = "https://github.com/atomiechen/python-dmon/blob/master/CHANGELOG.md"
23
+
24
+ [project.scripts]
25
+ dmon = "dmon.__main__:main"
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.8.4,<0.9.0"]
29
+ build-backend = "uv_build"
30
+
31
+ [tool.uv.build-backend]
32
+ module-name = "dmon"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "ruff>=0.13.3",
37
+ ]
File without changes
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -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()