dron 0.1.20241008__py3-none-any.whl → 0.2.20251011__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.
- dron/api.py +6 -5
- dron/cli.py +176 -289
- dron/common.py +27 -20
- dron/dron.py +42 -133
- dron/launchd.py +49 -37
- dron/launchd_wrapper.py +16 -4
- dron/monitor.py +2 -4
- dron/notify/common.py +1 -1
- dron/notify/email.py +1 -1
- dron/notify/ntfy_common.py +3 -2
- dron/notify/telegram.py +1 -1
- dron/systemd.py +129 -107
- dron/tests/test_dron.py +81 -10
- {dron-0.1.20241008.dist-info → dron-0.2.20251011.dist-info}/METADATA +11 -17
- dron-0.2.20251011.dist-info/RECORD +23 -0
- {dron-0.1.20241008.dist-info → dron-0.2.20251011.dist-info}/WHEEL +1 -2
- dron/__init__.py +0 -6
- dron-0.1.20241008.dist-info/RECORD +0 -25
- dron-0.1.20241008.dist-info/top_level.txt +0 -1
- {dron-0.1.20241008.dist-info → dron-0.2.20251011.dist-info}/entry_points.txt +0 -0
- {dron-0.1.20241008.dist-info → dron-0.2.20251011.dist-info/licenses}/LICENSE.txt +0 -0
dron/api.py
CHANGED
@@ -4,8 +4,8 @@ import getpass
|
|
4
4
|
import inspect
|
5
5
|
import re
|
6
6
|
import sys
|
7
|
+
from collections.abc import Sequence
|
7
8
|
from dataclasses import dataclass
|
8
|
-
from typing import Sequence
|
9
9
|
|
10
10
|
from .common import (
|
11
11
|
IS_SYSTEMD,
|
@@ -19,6 +19,7 @@ OnFailureAction = str
|
|
19
19
|
|
20
20
|
UnitName = str
|
21
21
|
|
22
|
+
|
22
23
|
@dataclass
|
23
24
|
class Job:
|
24
25
|
when: When | None
|
@@ -95,12 +96,12 @@ def job(
|
|
95
96
|
|
96
97
|
|
97
98
|
__all__ = (
|
98
|
-
'
|
99
|
+
'Command',
|
100
|
+
'Job', # todo maybe don't expose it?
|
99
101
|
'OnCalendar',
|
100
102
|
'OnFailureAction',
|
101
|
-
'
|
102
|
-
'wrap',
|
103
|
+
'When',
|
103
104
|
'job',
|
104
105
|
'notify',
|
105
|
-
'
|
106
|
+
'wrap',
|
106
107
|
)
|
dron/cli.py
CHANGED
@@ -1,32 +1,25 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import
|
4
|
-
import os
|
5
|
-
import shutil
|
6
|
-
import subprocess
|
3
|
+
import socket
|
7
4
|
import sys
|
8
|
-
from pathlib import Path
|
9
5
|
from pprint import pprint
|
10
|
-
from tempfile import TemporaryDirectory
|
11
6
|
|
12
7
|
import click
|
13
8
|
|
14
|
-
from . import launchd, systemd
|
9
|
+
from . import common, launchd, systemd
|
15
10
|
from .api import UnitName
|
16
11
|
from .common import (
|
17
12
|
IS_SYSTEMD,
|
18
|
-
MANAGED_MARKER,
|
19
13
|
MonitorParams,
|
20
14
|
Unit,
|
21
15
|
escape,
|
22
16
|
logger,
|
23
17
|
print_monitor,
|
18
|
+
set_verify_off,
|
24
19
|
)
|
25
20
|
from .dron import (
|
26
|
-
DRONTAB,
|
27
21
|
apply,
|
28
22
|
do_lint,
|
29
|
-
drontab_dir,
|
30
23
|
get_entries_for_monitor,
|
31
24
|
load_jobs,
|
32
25
|
manage,
|
@@ -34,128 +27,8 @@ from .dron import (
|
|
34
27
|
)
|
35
28
|
|
36
29
|
|
37
|
-
def cmd_edit() -> None:
|
38
|
-
drontab = DRONTAB
|
39
|
-
if not drontab.exists():
|
40
|
-
if click.confirm(f"tabfile {drontab} doesn't exist. Create?", default=True):
|
41
|
-
drontab.write_text(
|
42
|
-
'''\
|
43
|
-
#!/usr/bin/env python3
|
44
|
-
from dron.api import job
|
45
|
-
|
46
|
-
def jobs():
|
47
|
-
# yield job(
|
48
|
-
# 'hourly',
|
49
|
-
# '/bin/echo 123',
|
50
|
-
# unit_name='test_unit'
|
51
|
-
# )
|
52
|
-
pass
|
53
|
-
'''.lstrip()
|
54
|
-
)
|
55
|
-
else:
|
56
|
-
raise RuntimeError()
|
57
|
-
|
58
|
-
editor = os.environ.get('EDITOR')
|
59
|
-
if editor is None:
|
60
|
-
logger.warning('No EDITOR! Fallback to nano')
|
61
|
-
editor = 'nano'
|
62
|
-
|
63
|
-
with TemporaryDirectory() as tdir:
|
64
|
-
tpath = Path(tdir) / 'drontab'
|
65
|
-
shutil.copy2(drontab, tpath)
|
66
|
-
|
67
|
-
orig_mtime = tpath.stat().st_mtime
|
68
|
-
while True:
|
69
|
-
res = subprocess.run([editor, str(tpath)], check=True)
|
70
|
-
|
71
|
-
new_mtime = tpath.stat().st_mtime
|
72
|
-
if new_mtime == orig_mtime:
|
73
|
-
logger.warning('No notification made')
|
74
|
-
return
|
75
|
-
|
76
|
-
ex: Exception | None = None
|
77
|
-
try:
|
78
|
-
state = do_lint(tabfile=tpath)
|
79
|
-
except Exception as e:
|
80
|
-
logger.exception(e)
|
81
|
-
ex = e
|
82
|
-
else:
|
83
|
-
try:
|
84
|
-
manage(state=state)
|
85
|
-
except Exception as ee:
|
86
|
-
logger.exception(ee)
|
87
|
-
ex = ee
|
88
|
-
if ex is not None:
|
89
|
-
if click.confirm('Got errors. Try again?', default=True):
|
90
|
-
continue
|
91
|
-
raise ex
|
92
|
-
|
93
|
-
drontab.write_text(tpath.read_text()) # handles symlinks correctly
|
94
|
-
logger.info(f"Wrote changes to {drontab}. Don't forget to commit!")
|
95
|
-
break
|
96
|
-
|
97
|
-
# TODO show git diff?
|
98
|
-
# TODO perhaps allow to carry on regardless? not sure..
|
99
|
-
# not sure how much we can do without modifying anything...
|
100
|
-
|
101
|
-
|
102
|
-
def cmd_lint(tabfile: Path) -> None:
|
103
|
-
_state = do_lint(tabfile)
|
104
|
-
logger.info('all good')
|
105
|
-
|
106
|
-
|
107
|
-
def cmd_apply(tabfile: Path) -> None:
|
108
|
-
apply(tabfile)
|
109
|
-
|
110
|
-
|
111
|
-
def cmd_print(*, tabfile: Path, pretty: bool) -> None:
|
112
|
-
dtab_dir = Path(drontab_dir())
|
113
|
-
jobs = list(load_jobs(tabfile=tabfile, ppath=dtab_dir))
|
114
|
-
|
115
|
-
if pretty:
|
116
|
-
import tabulate
|
117
|
-
|
118
|
-
items = [
|
119
|
-
{
|
120
|
-
'UNIT': job.unit_name,
|
121
|
-
'SCHEDULE': job.when,
|
122
|
-
'COMMAND': escape(job.command),
|
123
|
-
}
|
124
|
-
for job in jobs
|
125
|
-
]
|
126
|
-
print(tabulate.tabulate(items, headers="keys"))
|
127
|
-
else:
|
128
|
-
for j in jobs:
|
129
|
-
print(j)
|
130
|
-
|
131
|
-
|
132
|
-
def cmd_run(*, unit: Unit, do_exec: bool) -> None:
|
133
|
-
if IS_SYSTEMD:
|
134
|
-
return systemd.cmd_run(unit=unit, do_exec=do_exec)
|
135
|
-
else:
|
136
|
-
return launchd.cmd_run(unit=unit, do_exec=do_exec)
|
137
|
-
|
138
|
-
|
139
|
-
def cmd_past(unit: Unit) -> None:
|
140
|
-
if IS_SYSTEMD:
|
141
|
-
return systemd.cmd_past(unit)
|
142
|
-
else:
|
143
|
-
return launchd.cmd_past(unit)
|
144
|
-
|
145
|
-
|
146
|
-
# TODO think if it's worth integrating with timers?
|
147
|
-
def cmd_monitor(params: MonitorParams) -> None:
|
148
|
-
managed = list(managed_units(with_body=False)) # body slows down this call quite a bit
|
149
|
-
if len(managed) == 0:
|
150
|
-
logger.warning('no managed units!')
|
151
|
-
|
152
|
-
logger.debug('starting monitor...')
|
153
|
-
|
154
|
-
entries = get_entries_for_monitor(managed=managed, params=params)
|
155
|
-
print_monitor(entries)
|
156
|
-
|
157
|
-
|
158
30
|
# TODO test it on CI?
|
31
|
+
# TODO explicitly inject it into readme?
|
159
32
|
def _drontab_example() -> str:
|
160
33
|
return '''
|
161
34
|
from dron.api import job
|
@@ -190,193 +63,207 @@ def jobs():
|
|
190
63
|
'''.lstrip()
|
191
64
|
|
192
65
|
|
193
|
-
def
|
194
|
-
|
66
|
+
def _get_epilog() -> str:
|
67
|
+
return '''
|
68
|
+
* Why?
|
69
|
+
|
70
|
+
In short, because I want to benefit from the heavy lifting that Systemd does: timeouts, resource management, restart policies, powerful scheduling specs and logging,
|
71
|
+
while not having to manually manipulate numerous unit files and restart the daemon all over.
|
72
|
+
|
73
|
+
I elaborate on what led me to implement it and motivation [[https://beepb00p.xyz/scheduler.html#what_do_i_want][here]]. Also:
|
74
|
+
|
75
|
+
\b
|
76
|
+
- why not just use [[https://beepb00p.xyz/scheduler.html#cron][cron]]?
|
77
|
+
- why not just use [[https://beepb00p.xyz/scheduler.html#systemd][systemd]]?
|
78
|
+
'''.strip()
|
195
79
|
|
196
|
-
def add_verify(p: argparse.ArgumentParser) -> None:
|
197
|
-
# specify in readme???
|
198
|
-
# would be nice to use external checker..
|
199
|
-
# https://github.com/systemd/systemd/issues/8072
|
200
|
-
# https://unix.stackexchange.com/questions/493187/systemd-under-ubuntu-18-04-1-fails-with-failed-to-create-user-slice-serv
|
201
|
-
p.add_argument('--no-verify', action=VerifyOff, nargs=0, help='Skip systemctl verify step')
|
202
80
|
|
203
|
-
|
204
|
-
|
205
|
-
|
81
|
+
@click.group(
|
82
|
+
context_settings={'show_default': True},
|
83
|
+
help="""
|
206
84
|
dron -- simple frontend for Systemd, inspired by cron.
|
207
85
|
|
86
|
+
\b
|
208
87
|
- *d* stands for 'Systemd'
|
209
88
|
- *ron* stands for 'cron'
|
210
89
|
|
211
90
|
dron is my attempt to overcome things that make working with Systemd tedious
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
91
|
+
""".strip(),
|
92
|
+
epilog=_get_epilog(),
|
93
|
+
)
|
94
|
+
@click.option(
|
95
|
+
'--marker',
|
96
|
+
required=False,
|
97
|
+
help=f'Use custom marker instead of default `{common.MANAGED_MARKER}`. Useful for developing/testing.',
|
98
|
+
)
|
99
|
+
def cli(*, marker: str | None) -> None:
|
100
|
+
if marker is not None:
|
101
|
+
common.MANAGED_MARKER = marker
|
221
102
|
|
222
|
-
{example}
|
223
103
|
|
224
|
-
|
104
|
+
arg_tab_module = click.option(
|
105
|
+
'--module',
|
106
|
+
'tab_module',
|
107
|
+
type=str,
|
108
|
+
default=f'drontab.{socket.gethostname()}',
|
109
|
+
)
|
225
110
|
|
226
|
-
- if checks have passed, your jobs are mapped onto Systemd units and started up
|
227
|
-
- if there are potential errors, you are prompted to fix them before retrying
|
228
111
|
|
229
|
-
|
230
|
-
|
231
|
-
|
112
|
+
# specify in readme???
|
113
|
+
# would be nice to use external checker..
|
114
|
+
# https://github.com/systemd/systemd/issues/8072
|
115
|
+
# https://unix.stackexchange.com/questions/493187/systemd-under-ubuntu-18-04-1-fails-with-failed-to-create-user-slice-serv
|
116
|
+
def _set_verify_off(ctx, param, value) -> None: # noqa: ARG001
|
117
|
+
if value is True:
|
118
|
+
set_verify_off()
|
232
119
|
|
233
|
-
I elaborate on what led me to implement it and motivation [[https://beepb00p.xyz/scheduler.html#what_do_i_want][here]]. Also:
|
234
120
|
|
235
|
-
|
236
|
-
-
|
237
|
-
|
121
|
+
arg_no_verify = click.option(
|
122
|
+
'--no-verify',
|
123
|
+
is_flag=True,
|
124
|
+
callback=_set_verify_off,
|
125
|
+
expose_value=False,
|
126
|
+
help='Skip systemctl verify step',
|
127
|
+
)
|
238
128
|
|
239
|
-
p.add_argument(
|
240
|
-
'--marker', required=False, help=f'Use custom marker instead of default `{MANAGED_MARKER}`. Useful for developing/testing.'
|
241
|
-
)
|
242
129
|
|
243
|
-
|
244
|
-
|
130
|
+
@cli.command('lint')
|
131
|
+
@arg_tab_module
|
132
|
+
@arg_no_verify
|
133
|
+
def cmd_lint(*, tab_module: str) -> None:
|
134
|
+
# FIXME how to disable verity?
|
135
|
+
# FIXME lint command isn't very interesting now btw?
|
136
|
+
# perhaps instead, either add dry mode to apply
|
137
|
+
# or split into the 'diff' part and side effect apply part
|
138
|
+
_state = do_lint(tab_module)
|
139
|
+
logger.info('all good')
|
245
140
|
|
246
|
-
sp = p.add_subparsers(dest='mode')
|
247
141
|
|
248
|
-
|
249
|
-
|
250
|
-
|
142
|
+
@cli.command('print')
|
143
|
+
@arg_tab_module
|
144
|
+
@click.option('--pretty', is_flag=True, help='Pretty print')
|
145
|
+
@arg_no_verify
|
146
|
+
def cmd_print(*, tab_module: str, pretty: bool) -> None:
|
147
|
+
"""Parse and print drontab"""
|
148
|
+
jobs = list(load_jobs(tab_module=tab_module))
|
251
149
|
|
252
|
-
|
253
|
-
|
254
|
-
add_verify(apply_parser)
|
255
|
-
# TODO --force?
|
256
|
-
lint_parser = sp.add_parser('lint', help="Check drontab (no 'crontab' alternative, sadly!)")
|
257
|
-
add_tabfile_arg(lint_parser)
|
258
|
-
add_verify(lint_parser)
|
150
|
+
if pretty:
|
151
|
+
import tabulate
|
259
152
|
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
153
|
+
items = [
|
154
|
+
{
|
155
|
+
'UNIT': job.unit_name,
|
156
|
+
'SCHEDULE': job.when,
|
157
|
+
'COMMAND': escape(job.command),
|
158
|
+
}
|
159
|
+
for job in jobs
|
160
|
+
]
|
161
|
+
print(tabulate.tabulate(items, headers="keys"))
|
162
|
+
else:
|
163
|
+
for j in jobs:
|
164
|
+
print(j)
|
264
165
|
|
265
|
-
### actions on managed jobs
|
266
|
-
debug_parser = sp.add_parser('debug', help='Print some debug info')
|
267
166
|
|
268
|
-
|
269
|
-
|
167
|
+
# TODO --force?
|
168
|
+
@cli.command('apply')
|
169
|
+
@arg_tab_module
|
170
|
+
def cmd_apply(*, tab_module: str) -> None:
|
171
|
+
"""Apply drontab (like 'crontab' with no args)"""
|
172
|
+
apply(tab_module)
|
270
173
|
|
271
|
-
run_parser = sp.add_parser('run', help='Run the job right now, ignoring the timer')
|
272
|
-
run_parser.add_argument('unit', type=str, nargs='?') # TODO add shell completion?
|
273
|
-
run_parser.add_argument('--exec', action='store_true', dest='do_exec', help='Run directly, not via systemd/launchd')
|
274
174
|
|
275
|
-
|
276
|
-
|
277
|
-
|
175
|
+
@cli.command('debug')
|
176
|
+
def cmd_debug() -> None:
|
177
|
+
"""Print some debug info"""
|
178
|
+
managed = managed_units(with_body=False) # TODO not sure about body
|
179
|
+
for x in managed:
|
180
|
+
pprint(x, stream=sys.stderr)
|
278
181
|
|
279
|
-
### misc actions
|
280
|
-
mp = sp.add_parser('monitor', help='Monitor services/timers managed by dron')
|
281
|
-
mp.add_argument('-n', type=int, default=1, help='refresh every n seconds')
|
282
|
-
mp.add_argument('--once', action='store_true', help='only call once')
|
283
|
-
mp.add_argument('--rate', action='store_true', help='Display success rate (unstable and potentially slow)')
|
284
|
-
mp.add_argument('--command', action='store_true', help='Display command')
|
285
|
-
###
|
286
|
-
return p
|
287
182
|
|
183
|
+
@cli.command('uninstall')
|
184
|
+
def cmd_uninstall() -> None:
|
185
|
+
"""Remove all managed jobs (will ask for confirmation)"""
|
186
|
+
click.confirm('Going to remove all dron managed jobs. Continue?', default=True, abort=True)
|
187
|
+
manage([])
|
288
188
|
|
289
|
-
def main() -> None:
|
290
|
-
from .cli import make_parser
|
291
189
|
|
292
|
-
|
293
|
-
|
190
|
+
@cli.group('job')
|
191
|
+
def cli_job() -> None:
|
192
|
+
"""Actions on individual jobs"""
|
193
|
+
pass
|
294
194
|
|
295
|
-
marker: str | None = args.marker
|
296
|
-
if marker is not None:
|
297
|
-
from . import common
|
298
195
|
|
299
|
-
|
196
|
+
def _prompt_for_unit() -> UnitName:
|
197
|
+
from prompt_toolkit import PromptSession
|
198
|
+
from prompt_toolkit.completion import WordCompleter
|
300
199
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
return selected
|
325
|
-
|
326
|
-
if mode == 'edit':
|
327
|
-
cmd_edit()
|
328
|
-
elif mode == 'apply':
|
329
|
-
tabfile = tabfile_or_default()
|
330
|
-
cmd_apply(tabfile)
|
331
|
-
elif mode == 'lint':
|
332
|
-
tabfile = tabfile_or_default()
|
333
|
-
cmd_lint(tabfile)
|
334
|
-
elif mode == 'print':
|
335
|
-
tabfile = tabfile_or_default()
|
336
|
-
|
337
|
-
from .cli import cmd_print # lazy due to circular import
|
338
|
-
|
339
|
-
cmd_print(tabfile=tabfile, pretty=args.pretty)
|
340
|
-
elif mode == 'debug':
|
341
|
-
managed = managed_units(with_body=False) # TODO not sure about body
|
342
|
-
for x in managed:
|
343
|
-
pprint(x, stream=sys.stderr)
|
344
|
-
elif mode == 'uninstall':
|
345
|
-
click.confirm('Going to remove all dron managed jobs. Continue?', default=True, abort=True)
|
346
|
-
with TemporaryDirectory() as td:
|
347
|
-
empty = Path(td) / 'empty'
|
348
|
-
empty.write_text(
|
349
|
-
'''\
|
350
|
-
def jobs():
|
351
|
-
yield from []
|
352
|
-
'''
|
353
|
-
)
|
354
|
-
cmd_apply(empty)
|
355
|
-
elif mode == 'run':
|
356
|
-
unit = args.unit if args.unit is not None else prompt_for_unit()
|
357
|
-
do_exec = args.do_exec
|
358
|
-
cmd_run(unit=unit, do_exec=do_exec)
|
359
|
-
elif mode == 'past':
|
360
|
-
unit = args.unit if args.unit is not None else prompt_for_unit()
|
361
|
-
cmd_past(unit=unit)
|
362
|
-
elif mode == 'monitor':
|
363
|
-
once = args.once
|
364
|
-
|
365
|
-
params = MonitorParams(
|
366
|
-
with_success_rate=args.rate,
|
367
|
-
with_command=args.command,
|
368
|
-
)
|
369
|
-
|
370
|
-
if once:
|
371
|
-
# fallback on old style monitor for now?
|
372
|
-
# this can be quite useful for grepping etc..
|
373
|
-
cmd_monitor(params=params)
|
374
|
-
else:
|
375
|
-
from .monitor import MonitorApp
|
376
|
-
|
377
|
-
app = MonitorApp(monitor_params=params, refresh_every=args.n)
|
378
|
-
app.run()
|
200
|
+
# TODO print options
|
201
|
+
managed = list(managed_units(with_body=False))
|
202
|
+
units = [x.unit_file.stem for x in managed]
|
203
|
+
|
204
|
+
print('Units under dron:', file=sys.stderr)
|
205
|
+
for u in units:
|
206
|
+
print(f'- {u}', file=sys.stderr)
|
207
|
+
|
208
|
+
completer = WordCompleter(units, ignore_case=True)
|
209
|
+
session = PromptSession("Select a unit: ", completer=completer) # type: ignore[var-annotated]
|
210
|
+
selected = session.prompt()
|
211
|
+
return selected
|
212
|
+
|
213
|
+
|
214
|
+
arg_unit = click.argument('unit', type=Unit, default=_prompt_for_unit)
|
215
|
+
|
216
|
+
|
217
|
+
@cli_job.command('past')
|
218
|
+
@arg_unit
|
219
|
+
def cmd_past(unit: Unit) -> None:
|
220
|
+
if IS_SYSTEMD:
|
221
|
+
# TODO hmm seems like this just exit with 0 if unit diesn't exist
|
222
|
+
return systemd.cmd_past(unit)
|
379
223
|
else:
|
380
|
-
|
381
|
-
|
382
|
-
|
224
|
+
return launchd.cmd_past(unit)
|
225
|
+
|
226
|
+
|
227
|
+
@cli_job.command('run')
|
228
|
+
@arg_unit
|
229
|
+
@click.option('--exec', 'do_exec', is_flag=True, help='Run directly, not via systemd/launchd')
|
230
|
+
def cmd_run(*, unit: Unit, do_exec: bool) -> None:
|
231
|
+
"""Run the job right now, ignoring the timer"""
|
232
|
+
if IS_SYSTEMD:
|
233
|
+
return systemd.cmd_run(unit=unit, do_exec=do_exec)
|
234
|
+
else:
|
235
|
+
return launchd.cmd_run(unit=unit, do_exec=do_exec)
|
236
|
+
|
237
|
+
|
238
|
+
@cli.command('monitor')
|
239
|
+
@click.option('-n', type=float, default=1.0, help='refresh every n seconds')
|
240
|
+
@click.option('--once', is_flag=True, help='only call once')
|
241
|
+
@click.option('--rate', is_flag=True, help='Display success rate (unstable and potentially slow)')
|
242
|
+
@click.option('--command', is_flag=True, help='Display command')
|
243
|
+
def cmd_monitor(*, n: float, once: bool, rate: bool, command: bool) -> None:
|
244
|
+
"""Monitor services/timers managed by dron"""
|
245
|
+
params = MonitorParams(
|
246
|
+
with_success_rate=rate,
|
247
|
+
with_command=command,
|
248
|
+
)
|
249
|
+
|
250
|
+
if once:
|
251
|
+
# old style monitor
|
252
|
+
# TODO think if it's worth integrating with timers?
|
253
|
+
managed = list(managed_units(with_body=False)) # body slows down this call quite a bit
|
254
|
+
if len(managed) == 0:
|
255
|
+
logger.warning('no managed units!')
|
256
|
+
|
257
|
+
logger.debug('starting monitor...')
|
258
|
+
|
259
|
+
entries = get_entries_for_monitor(managed=managed, params=params)
|
260
|
+
print_monitor(entries)
|
261
|
+
else:
|
262
|
+
from .monitor import MonitorApp
|
263
|
+
|
264
|
+
app = MonitorApp(monitor_params=params, refresh_every=n)
|
265
|
+
app.run()
|
266
|
+
|
267
|
+
|
268
|
+
def main() -> None:
|
269
|
+
cli()
|
dron/common.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import argparse
|
4
3
|
import platform
|
5
4
|
import shlex
|
6
5
|
import sys
|
6
|
+
from collections.abc import Iterable, Sequence
|
7
7
|
from dataclasses import asdict, dataclass, replace
|
8
8
|
from datetime import datetime
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Any
|
10
|
+
from typing import Any
|
11
11
|
|
12
12
|
from loguru import logger # noqa: F401
|
13
13
|
|
@@ -16,15 +16,15 @@ datetime_naive = datetime
|
|
16
16
|
|
17
17
|
|
18
18
|
# TODO can remove this? although might be useful for tests
|
19
|
-
VERIFY_UNITS = True
|
19
|
+
VERIFY_UNITS: bool = True
|
20
20
|
# TODO ugh. verify tries using already installed unit files so if they were bad, everything would fail
|
21
21
|
# I guess could do two stages, i.e. units first, then timers
|
22
22
|
# dunno, a bit less atomic though...
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
|
25
|
+
def set_verify_off() -> None:
|
26
|
+
global VERIFY_UNITS
|
27
|
+
VERIFY_UNITS = False
|
28
28
|
|
29
29
|
|
30
30
|
@dataclass
|
@@ -45,6 +45,11 @@ class UnitState:
|
|
45
45
|
cmdline: Sequence[str] | None # can be None for timers
|
46
46
|
|
47
47
|
|
48
|
+
@dataclass
|
49
|
+
class SystemdUnitState(UnitState):
|
50
|
+
dbus_properties: Any # seems like keeping this around massively speeds up dbus access...
|
51
|
+
|
52
|
+
|
48
53
|
@dataclass
|
49
54
|
class LaunchdUnitState(UnitState):
|
50
55
|
# NOTE: can legit be str (e.g. if unit was never ran before)
|
@@ -59,44 +64,46 @@ State = Iterable[UnitState]
|
|
59
64
|
IS_SYSTEMD = platform.system() != 'Darwin' # if not systemd it's launchd
|
60
65
|
|
61
66
|
|
62
|
-
|
63
|
-
T = TypeVar('T')
|
64
|
-
def unwrap(x: T | None) -> T:
|
67
|
+
def unwrap[T](x: T | None) -> T:
|
65
68
|
assert x is not None
|
66
69
|
return x
|
67
70
|
|
68
71
|
|
69
|
-
PathIsh =
|
72
|
+
PathIsh = str | Path
|
70
73
|
|
71
74
|
# if it's an str, assume it's already escaped
|
72
75
|
# otherwise we are responsible for escaping..
|
73
|
-
Command =
|
76
|
+
Command = PathIsh | Sequence[PathIsh]
|
74
77
|
|
75
78
|
|
76
79
|
OnCalendar = str
|
77
|
-
TimerSpec =
|
80
|
+
TimerSpec = dict[str, str] # meh # TODO why is it a dict???
|
78
81
|
ALWAYS = 'always'
|
79
|
-
When =
|
82
|
+
When = OnCalendar | TimerSpec
|
83
|
+
|
84
|
+
|
85
|
+
MANAGED_MARKER: str = '(MANAGED BY DRON)'
|
80
86
|
|
81
87
|
|
82
|
-
MANAGED_MARKER = '(MANAGED BY DRON)'
|
83
88
|
def is_managed(body: str) -> bool:
|
84
89
|
# switching off it because it's unfriendly to launchd
|
85
90
|
legacy_marker = '<MANAGED BY DRON>'
|
86
91
|
return MANAGED_MARKER in body or legacy_marker in body
|
87
92
|
|
88
93
|
|
89
|
-
|
90
94
|
pytest_fixture: Any
|
91
95
|
under_pytest = 'pytest' in sys.modules
|
92
96
|
if under_pytest:
|
93
97
|
import pytest
|
98
|
+
|
94
99
|
pytest_fixture = pytest.fixture
|
95
100
|
else:
|
96
|
-
pytest_fixture = lambda f: f
|
101
|
+
pytest_fixture = lambda f: f # no-op otherwise to prevent pytest import
|
97
102
|
|
98
103
|
|
99
104
|
Escaped = str
|
105
|
+
|
106
|
+
|
100
107
|
def escape(command: Command) -> Escaped:
|
101
108
|
if isinstance(command, Escaped):
|
102
109
|
return command
|
@@ -118,7 +125,6 @@ def test_wrap() -> None:
|
|
118
125
|
assert wrap('cat', bin_) == "cat /bin/bash"
|
119
126
|
|
120
127
|
|
121
|
-
|
122
128
|
@dataclass(order=True)
|
123
129
|
class MonitorEntry:
|
124
130
|
unit: str
|
@@ -143,8 +149,9 @@ def print_monitor(entries: Iterable[MonitorEntry]) -> None:
|
|
143
149
|
key=lambda e: (e.pid is None, e.status_ok, e),
|
144
150
|
)
|
145
151
|
|
146
|
-
import termcolor # noqa: I001
|
147
152
|
import tabulate
|
153
|
+
import termcolor
|
154
|
+
|
148
155
|
tabulate.PRESERVE_WHITESPACE = True
|
149
156
|
|
150
157
|
headers = [
|
@@ -170,5 +177,5 @@ def print_monitor(entries: Iterable[MonitorEntry]) -> None:
|
|
170
177
|
next=termcolor.colored('running', 'yellow'),
|
171
178
|
left='--',
|
172
179
|
)
|
173
|
-
items.append(list(asdict(e).values())[:len(headers)])
|
180
|
+
items.append(list(asdict(e).values())[: len(headers)])
|
174
181
|
print(tabulate.tabulate(items, headers=headers))
|