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/control.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import shlex
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
import psutil
|
|
11
|
+
from termcolor import colored
|
|
12
|
+
|
|
13
|
+
from .constants import DEFAULT_META_DIR, META_SUFFIX
|
|
14
|
+
from .types import DmonTaskConfig, DmonMeta, PathType
|
|
15
|
+
from .utils import len_ansi, pad_ansi
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ensure_meta_dir(meta_path: Path):
|
|
19
|
+
"""
|
|
20
|
+
Ensure the meta directory exists.
|
|
21
|
+
If it is under the default meta directory, create .gitignore to ignore all files in it.
|
|
22
|
+
"""
|
|
23
|
+
meta_dir = meta_path.resolve().parent
|
|
24
|
+
meta_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
if meta_dir == DEFAULT_META_DIR.resolve():
|
|
26
|
+
gitignore = DEFAULT_META_DIR / ".gitignore"
|
|
27
|
+
if not gitignore.exists():
|
|
28
|
+
gitignore.write_text("# Auto-generated by dmon\n*\n")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ensure_log_dir(log_path: Path):
|
|
32
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def start(cfg: DmonTaskConfig):
|
|
36
|
+
meta_path = Path(cfg.meta_path).resolve()
|
|
37
|
+
log_path = Path(cfg.log_path).resolve()
|
|
38
|
+
cwd = Path(cfg.cwd).resolve()
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
ret_meta = DmonMeta.load(meta_path)
|
|
42
|
+
except Exception:
|
|
43
|
+
ret_meta = None
|
|
44
|
+
if ret_meta:
|
|
45
|
+
print(
|
|
46
|
+
f"{colored('Start failed: meta file already exists', color='red', attrs=['bold'])}",
|
|
47
|
+
file=sys.stderr,
|
|
48
|
+
)
|
|
49
|
+
print_status(ret_meta)
|
|
50
|
+
print(
|
|
51
|
+
"\nRun 'dmon status' / 'dmon list' to check, or 'dmon stop' to stop it.",
|
|
52
|
+
file=sys.stderr,
|
|
53
|
+
)
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
ensure_meta_dir(meta_path)
|
|
57
|
+
ensure_log_dir(log_path)
|
|
58
|
+
|
|
59
|
+
env = None # default behavior of Popen
|
|
60
|
+
if cfg.override_env:
|
|
61
|
+
env = cfg.env
|
|
62
|
+
elif cfg.env:
|
|
63
|
+
env = {**os.environ, **cfg.env}
|
|
64
|
+
|
|
65
|
+
shell = isinstance(cfg.cmd, str)
|
|
66
|
+
|
|
67
|
+
# Platform-specific parameters to run the process in background detached from parent
|
|
68
|
+
kwargs = {}
|
|
69
|
+
if sys.platform.startswith("win"):
|
|
70
|
+
# DETACHED_PROCESS = 0x00000008
|
|
71
|
+
CREATE_NO_WINDOW = 0x08000000
|
|
72
|
+
kwargs["creationflags"] = CREATE_NO_WINDOW
|
|
73
|
+
if isinstance(cfg.cmd, list) and len(cfg.cmd) > 0:
|
|
74
|
+
# On Windows, use full path for the executable when shell=False
|
|
75
|
+
cfg.cmd[0] = shutil.which(cfg.cmd[0]) or cfg.cmd[0]
|
|
76
|
+
else:
|
|
77
|
+
# Make the child process independent of the parent process in Unix-like systems
|
|
78
|
+
kwargs["start_new_session"] = True
|
|
79
|
+
|
|
80
|
+
meta = DmonMeta(
|
|
81
|
+
name=cfg.name,
|
|
82
|
+
meta_path=str(meta_path),
|
|
83
|
+
log_path=str(log_path),
|
|
84
|
+
log_rotate=cfg.log_rotate,
|
|
85
|
+
cmd=cfg.cmd,
|
|
86
|
+
cwd=str(cwd),
|
|
87
|
+
env=cfg.env,
|
|
88
|
+
override_env=cfg.override_env,
|
|
89
|
+
shell=shell,
|
|
90
|
+
popen_kwargs=kwargs,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if cfg.log_rotate:
|
|
94
|
+
rotate_log_path = Path(cfg.rotate_log_path).resolve()
|
|
95
|
+
|
|
96
|
+
meta.rotate_log_path = str(rotate_log_path)
|
|
97
|
+
meta.log_max_size = cfg.log_max_size
|
|
98
|
+
meta.rotate_log_max_size = cfg.rotate_log_max_size
|
|
99
|
+
|
|
100
|
+
ensure_log_dir(rotate_log_path)
|
|
101
|
+
|
|
102
|
+
# use runner to start user process and handle log rotation
|
|
103
|
+
if isinstance(cfg.cmd, str):
|
|
104
|
+
# when cmd is str, we need to split it into list for subprocess
|
|
105
|
+
cmd = shlex.split(cfg.cmd)
|
|
106
|
+
else:
|
|
107
|
+
cmd = cfg.cmd
|
|
108
|
+
args = [
|
|
109
|
+
sys.executable,
|
|
110
|
+
"-m",
|
|
111
|
+
"dmon.runner",
|
|
112
|
+
"--log-path",
|
|
113
|
+
str(log_path),
|
|
114
|
+
"--max-log-size",
|
|
115
|
+
str(cfg.log_max_size),
|
|
116
|
+
"--rotate-log-path",
|
|
117
|
+
str(rotate_log_path),
|
|
118
|
+
"--max-rotate-log-size",
|
|
119
|
+
str(cfg.rotate_log_max_size),
|
|
120
|
+
]
|
|
121
|
+
if shell:
|
|
122
|
+
args.append("--shell")
|
|
123
|
+
args.append("--")
|
|
124
|
+
args.extend(cmd)
|
|
125
|
+
proc = subprocess.Popen(
|
|
126
|
+
args,
|
|
127
|
+
cwd=cwd,
|
|
128
|
+
env=env,
|
|
129
|
+
stdout=subprocess.DEVNULL,
|
|
130
|
+
stderr=subprocess.STDOUT,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
# Open the log file (append binary mode)
|
|
135
|
+
with open(log_path, "ab", buffering=0) as lof:
|
|
136
|
+
# Start the child process with stdout/stderr redirected to the log
|
|
137
|
+
proc = subprocess.Popen(
|
|
138
|
+
cfg.cmd,
|
|
139
|
+
stdout=lof,
|
|
140
|
+
stderr=subprocess.STDOUT,
|
|
141
|
+
cwd=cwd,
|
|
142
|
+
env=env,
|
|
143
|
+
shell=shell,
|
|
144
|
+
text=False, # binary mode
|
|
145
|
+
bufsize=0, # unbuffered
|
|
146
|
+
**kwargs,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
meta.pid = proc.pid
|
|
150
|
+
try:
|
|
151
|
+
p = psutil.Process(proc.pid)
|
|
152
|
+
create_time = p.create_time()
|
|
153
|
+
create_time_human = time.strftime(
|
|
154
|
+
"%Y-%m-%d %H:%M:%S", time.localtime(create_time)
|
|
155
|
+
)
|
|
156
|
+
meta.create_time = create_time
|
|
157
|
+
meta.create_time_human = create_time_human
|
|
158
|
+
except psutil.NoSuchProcess:
|
|
159
|
+
# process already exited?
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
meta.dump(meta_path)
|
|
163
|
+
|
|
164
|
+
print_status(meta)
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def stop(
|
|
169
|
+
meta_path: PathType,
|
|
170
|
+
timeout=5.0,
|
|
171
|
+
):
|
|
172
|
+
meta_path = Path(meta_path).resolve()
|
|
173
|
+
meta = DmonMeta.load(meta_path)
|
|
174
|
+
if meta is None:
|
|
175
|
+
print(
|
|
176
|
+
colored(
|
|
177
|
+
"Stop failed: meta file not found (maybe not started)\n",
|
|
178
|
+
color="red",
|
|
179
|
+
attrs=["bold"],
|
|
180
|
+
)
|
|
181
|
+
+ str(meta_path),
|
|
182
|
+
file=sys.stderr,
|
|
183
|
+
)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
pid = meta.pid
|
|
187
|
+
try:
|
|
188
|
+
proc = psutil.Process(pid)
|
|
189
|
+
except psutil.NoSuchProcess:
|
|
190
|
+
print(
|
|
191
|
+
colored(
|
|
192
|
+
f"Process {pid} not found (already exited); removing stale meta file",
|
|
193
|
+
color="yellow",
|
|
194
|
+
attrs=["bold"],
|
|
195
|
+
),
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
print_status(meta)
|
|
199
|
+
meta_path.unlink(missing_ok=True)
|
|
200
|
+
return 1
|
|
201
|
+
|
|
202
|
+
# check if it's the same process by comparing create_time
|
|
203
|
+
if not check_same_process(proc, meta.create_time):
|
|
204
|
+
print(
|
|
205
|
+
colored(
|
|
206
|
+
f"Warning: PID {pid} exists but create_time does not match (maybe reused by another process); removing stale meta file",
|
|
207
|
+
color="yellow",
|
|
208
|
+
attrs=["bold"],
|
|
209
|
+
),
|
|
210
|
+
file=sys.stderr,
|
|
211
|
+
)
|
|
212
|
+
print_status(meta)
|
|
213
|
+
meta_path.unlink(missing_ok=True)
|
|
214
|
+
return 1
|
|
215
|
+
|
|
216
|
+
if sys.platform.startswith("win"):
|
|
217
|
+
ret = terminate_win(proc, timeout)
|
|
218
|
+
else:
|
|
219
|
+
ret = terminate_posix(proc, timeout)
|
|
220
|
+
print_status(meta)
|
|
221
|
+
if ret == 0:
|
|
222
|
+
meta_path.unlink(missing_ok=True)
|
|
223
|
+
return ret
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def terminate_posix(proc: psutil.Process, timeout):
|
|
227
|
+
# send SIGTERM first for graceful shutdown (if platform supports)
|
|
228
|
+
proc.terminate()
|
|
229
|
+
|
|
230
|
+
# wait for the process to exit or timeout
|
|
231
|
+
try:
|
|
232
|
+
ret = proc.wait(timeout)
|
|
233
|
+
print(
|
|
234
|
+
colored(
|
|
235
|
+
f"Process {proc.pid} exited with code {ret}; removing meta file",
|
|
236
|
+
color="green",
|
|
237
|
+
attrs=["bold"],
|
|
238
|
+
),
|
|
239
|
+
file=sys.stderr,
|
|
240
|
+
)
|
|
241
|
+
except psutil.TimeoutExpired:
|
|
242
|
+
print(
|
|
243
|
+
colored(
|
|
244
|
+
f"Process {proc.pid} did not exit in time; shutting down child processes",
|
|
245
|
+
color="yellow",
|
|
246
|
+
attrs=["bold"],
|
|
247
|
+
),
|
|
248
|
+
file=sys.stderr,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# first shut down child processes (leaf nodes first)
|
|
252
|
+
for child in reversed(proc.children(recursive=True)):
|
|
253
|
+
try:
|
|
254
|
+
child.terminate()
|
|
255
|
+
child.wait(timeout=2)
|
|
256
|
+
except Exception:
|
|
257
|
+
try:
|
|
258
|
+
child.kill()
|
|
259
|
+
except Exception:
|
|
260
|
+
pass
|
|
261
|
+
# then shut down the parent process
|
|
262
|
+
try:
|
|
263
|
+
# check if process is already exited
|
|
264
|
+
ret = proc.wait(timeout=2)
|
|
265
|
+
print(
|
|
266
|
+
colored(
|
|
267
|
+
f"Process {proc.pid} exited with code {ret} after terminating children; removing meta file",
|
|
268
|
+
color="green",
|
|
269
|
+
attrs=["bold"],
|
|
270
|
+
),
|
|
271
|
+
file=sys.stderr,
|
|
272
|
+
)
|
|
273
|
+
except psutil.TimeoutExpired:
|
|
274
|
+
print(
|
|
275
|
+
colored(
|
|
276
|
+
f"Process {proc.pid} did not exit in time after terminating children; killing it",
|
|
277
|
+
color="yellow",
|
|
278
|
+
attrs=["bold"],
|
|
279
|
+
),
|
|
280
|
+
file=sys.stderr,
|
|
281
|
+
)
|
|
282
|
+
try:
|
|
283
|
+
proc.kill()
|
|
284
|
+
print(
|
|
285
|
+
colored(
|
|
286
|
+
f"Killed process {proc.pid}; removing meta file",
|
|
287
|
+
color="green",
|
|
288
|
+
attrs=["bold"],
|
|
289
|
+
),
|
|
290
|
+
file=sys.stderr,
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
print(
|
|
294
|
+
colored(
|
|
295
|
+
f"Failed to kill process {proc.pid}: {e}",
|
|
296
|
+
color="red",
|
|
297
|
+
attrs=["bold"],
|
|
298
|
+
),
|
|
299
|
+
file=sys.stderr,
|
|
300
|
+
)
|
|
301
|
+
return 1
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def terminate_win(proc: psutil.Process, timeout):
|
|
306
|
+
# On Windows, we need to stop child processes first
|
|
307
|
+
children = proc.children(recursive=True)
|
|
308
|
+
# reverse to kill leaf nodes first
|
|
309
|
+
for child in reversed(children):
|
|
310
|
+
try:
|
|
311
|
+
child.kill()
|
|
312
|
+
child.wait(timeout=2)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass
|
|
315
|
+
try:
|
|
316
|
+
ret = proc.wait(timeout=timeout)
|
|
317
|
+
print(
|
|
318
|
+
colored(
|
|
319
|
+
f"Process {proc.pid} exited with code {ret}; removing meta file",
|
|
320
|
+
color="green",
|
|
321
|
+
attrs=["bold"],
|
|
322
|
+
),
|
|
323
|
+
file=sys.stderr,
|
|
324
|
+
)
|
|
325
|
+
except psutil.TimeoutExpired:
|
|
326
|
+
print(
|
|
327
|
+
colored(
|
|
328
|
+
f"Process {proc.pid} did not exit in time after killing children; killing it",
|
|
329
|
+
color="yellow",
|
|
330
|
+
attrs=["bold"],
|
|
331
|
+
),
|
|
332
|
+
file=sys.stderr,
|
|
333
|
+
)
|
|
334
|
+
proc.kill()
|
|
335
|
+
print(
|
|
336
|
+
colored(
|
|
337
|
+
f"Killed process {proc.pid}; removing meta file",
|
|
338
|
+
color="green",
|
|
339
|
+
attrs=["bold"],
|
|
340
|
+
),
|
|
341
|
+
file=sys.stderr,
|
|
342
|
+
)
|
|
343
|
+
return 0
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def restart(
|
|
347
|
+
cfg: DmonTaskConfig,
|
|
348
|
+
timeout=5.0,
|
|
349
|
+
):
|
|
350
|
+
stop(cfg.meta_path, timeout)
|
|
351
|
+
print("--- Restarting ---", file=sys.stderr)
|
|
352
|
+
return start(cfg)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def status(meta_path: PathType):
|
|
356
|
+
meta_path = Path(meta_path).resolve()
|
|
357
|
+
meta = DmonMeta.load(meta_path)
|
|
358
|
+
if meta is None:
|
|
359
|
+
print(
|
|
360
|
+
colored(
|
|
361
|
+
"Status failed: meta file not found (maybe not started)\n",
|
|
362
|
+
color="red",
|
|
363
|
+
attrs=["bold"],
|
|
364
|
+
)
|
|
365
|
+
+ str(meta_path),
|
|
366
|
+
file=sys.stderr,
|
|
367
|
+
)
|
|
368
|
+
return 1
|
|
369
|
+
|
|
370
|
+
print_status(meta)
|
|
371
|
+
print("\nProcess Tree:", file=sys.stderr)
|
|
372
|
+
print_process_table([meta])
|
|
373
|
+
return 0
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def check_same_process(proc: psutil.Process, create_time: float) -> bool:
|
|
377
|
+
if create_time < 0:
|
|
378
|
+
return False
|
|
379
|
+
return abs(proc.create_time() - create_time) < 1e-3
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_unique_process(pid: int, create_time: float):
|
|
383
|
+
"""
|
|
384
|
+
Get the process with given PID and create_time if it is still the same process.
|
|
385
|
+
"""
|
|
386
|
+
if create_time >= 0:
|
|
387
|
+
try:
|
|
388
|
+
p = psutil.Process(pid)
|
|
389
|
+
if check_same_process(p, create_time):
|
|
390
|
+
return p
|
|
391
|
+
except psutil.NoSuchProcess:
|
|
392
|
+
pass
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def check_running(pid: int, create_time: float) -> bool:
|
|
397
|
+
"""
|
|
398
|
+
Check if a process with given PID and create_time is running.
|
|
399
|
+
"""
|
|
400
|
+
# return psutil.pid_exists(pid)
|
|
401
|
+
if create_time < 0:
|
|
402
|
+
return False
|
|
403
|
+
try:
|
|
404
|
+
p = psutil.Process(pid)
|
|
405
|
+
return abs(p.create_time() - create_time) < 1e-3
|
|
406
|
+
except psutil.NoSuchProcess:
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def print_status(meta: DmonMeta):
|
|
411
|
+
status = (
|
|
412
|
+
colored("Running", on_color="on_green")
|
|
413
|
+
if check_running(meta.pid, meta.create_time)
|
|
414
|
+
else colored("Exited", on_color="on_light_red")
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# key-value pairs with aligned keys
|
|
418
|
+
rows = [
|
|
419
|
+
("TASK", colored(meta.name, "cyan", attrs=["bold"])),
|
|
420
|
+
("PID", colored(str(meta.pid), "cyan", attrs=["bold"])),
|
|
421
|
+
("STATUS", status),
|
|
422
|
+
("CMD", meta.cmd),
|
|
423
|
+
("WORKING DIR", meta.cwd),
|
|
424
|
+
("CREATE TIME", meta.create_time_human),
|
|
425
|
+
("META PATH", meta.meta_path),
|
|
426
|
+
("LOG ROTATE", meta.log_rotate),
|
|
427
|
+
("LOG PATH", meta.log_path),
|
|
428
|
+
]
|
|
429
|
+
if meta.log_rotate:
|
|
430
|
+
rows.append(("ROTATE LOG PATH", meta.rotate_log_path))
|
|
431
|
+
rows.append(("LOG MAX SIZE", f"{meta.log_max_size} MB"))
|
|
432
|
+
rows.append(("ROTATE LOG MAX SIZE", f"{meta.rotate_log_max_size} MB"))
|
|
433
|
+
|
|
434
|
+
# calculate the max width of the keys
|
|
435
|
+
key_width = max(len(key) for key, _ in rows)
|
|
436
|
+
# align the keys and print
|
|
437
|
+
lines = [
|
|
438
|
+
f"{colored(f'{key:<{key_width}}: ', attrs=['dark'])}{value}"
|
|
439
|
+
for key, value in rows
|
|
440
|
+
]
|
|
441
|
+
print("\n".join(lines), file=sys.stderr)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def get_table_row(p: psutil.Process, target_ppid: int, prefix=""):
|
|
445
|
+
ppid = p.ppid()
|
|
446
|
+
ppid_str = (
|
|
447
|
+
colored(str(ppid), "cyan", attrs=["bold"]) if ppid == target_ppid else str(ppid)
|
|
448
|
+
)
|
|
449
|
+
return (
|
|
450
|
+
prefix + p.name(),
|
|
451
|
+
p.pid,
|
|
452
|
+
ppid_str,
|
|
453
|
+
p.status(),
|
|
454
|
+
p.cmdline(),
|
|
455
|
+
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(p.create_time())),
|
|
456
|
+
"N/A",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def print_process_table(metas: List[DmonMeta], full_width: bool = False):
|
|
461
|
+
headers = ("TASK", "PID", "PPID", "STATUS", "CMD", "CREATE TIME", "LOG PATH")
|
|
462
|
+
align = ("<", ">", ">", "<", "<", "<", "<")
|
|
463
|
+
|
|
464
|
+
# add table header
|
|
465
|
+
rows = []
|
|
466
|
+
rows.append(headers)
|
|
467
|
+
|
|
468
|
+
processes = []
|
|
469
|
+
for meta in metas:
|
|
470
|
+
proc = get_unique_process(meta.pid, meta.create_time)
|
|
471
|
+
if proc:
|
|
472
|
+
status = colored("Running", on_color="on_green")
|
|
473
|
+
ppid = proc.ppid()
|
|
474
|
+
processes.append(proc)
|
|
475
|
+
else:
|
|
476
|
+
status = colored("Exited", on_color="on_light_red")
|
|
477
|
+
ppid = "N/A"
|
|
478
|
+
rows.append(
|
|
479
|
+
(
|
|
480
|
+
colored(meta.name, "cyan", attrs=["bold"]),
|
|
481
|
+
colored(meta.pid, "cyan", attrs=["bold"]),
|
|
482
|
+
ppid,
|
|
483
|
+
status,
|
|
484
|
+
meta.cmd,
|
|
485
|
+
meta.create_time_human,
|
|
486
|
+
meta.log_path,
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
# add child processes indented
|
|
490
|
+
if proc:
|
|
491
|
+
children = proc.children(recursive=True)
|
|
492
|
+
for idx, child in enumerate(children):
|
|
493
|
+
prefix = "├ " if idx < len(children) - 1 else "└ "
|
|
494
|
+
rows.append(get_table_row(child, target_ppid=proc.pid, prefix=prefix))
|
|
495
|
+
processes.append(child)
|
|
496
|
+
|
|
497
|
+
# calculate column widths
|
|
498
|
+
widths = [max(len_ansi(str(row[i])) for row in rows) for i in range(len(headers))]
|
|
499
|
+
|
|
500
|
+
term_width = shutil.get_terminal_size().columns
|
|
501
|
+
diff = sum(widths) + 2 * (len(headers) - 1) - term_width
|
|
502
|
+
if diff > 0 and not full_width:
|
|
503
|
+
# truncate CMD column
|
|
504
|
+
cmd_idx = headers.index("CMD")
|
|
505
|
+
widths[cmd_idx] = max(widths[cmd_idx] - diff, 10) # at least 10 chars
|
|
506
|
+
|
|
507
|
+
# print the table with proper padding
|
|
508
|
+
lines = []
|
|
509
|
+
for row in rows:
|
|
510
|
+
line = " ".join(
|
|
511
|
+
# pad the cell except the last one if left-aligned (no need for extra spaces)
|
|
512
|
+
(
|
|
513
|
+
pad_ansi(str(cell), widths[i], align[i])
|
|
514
|
+
if i < len(align) - 1 or align[i] != "<"
|
|
515
|
+
else str(cell)
|
|
516
|
+
)
|
|
517
|
+
for i, cell in enumerate(row)
|
|
518
|
+
)
|
|
519
|
+
lines.append(line)
|
|
520
|
+
print("\n".join(lines), file=sys.stderr)
|
|
521
|
+
|
|
522
|
+
return processes
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def list_processes(dir: PathType, full_width: bool):
|
|
526
|
+
target_dmon_dir = Path(dir).resolve()
|
|
527
|
+
metas = []
|
|
528
|
+
if target_dmon_dir.exists() and target_dmon_dir.is_dir():
|
|
529
|
+
for meta_file in target_dmon_dir.glob(f"*{META_SUFFIX}"):
|
|
530
|
+
meta = DmonMeta.load(meta_file)
|
|
531
|
+
if meta is not None:
|
|
532
|
+
metas.append(meta)
|
|
533
|
+
# sort by name (case-insensitive)
|
|
534
|
+
metas.sort(key=lambda m: m.name.lower())
|
|
535
|
+
n_task = len(metas)
|
|
536
|
+
processes = print_process_table(metas, full_width)
|
|
537
|
+
n_proc = len(processes)
|
|
538
|
+
print(
|
|
539
|
+
f"\nFound {n_task} task{'s' if n_task > 1 else ''} ({n_proc} process{'es' if n_proc > 1 else ''}) in {target_dmon_dir}",
|
|
540
|
+
file=sys.stderr,
|
|
541
|
+
)
|
|
542
|
+
return 0
|
dmon/py.typed
ADDED
|
File without changes
|