meerschaum 2.3.0.dev1__py3-none-any.whl → 2.3.0rc1__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.
- meerschaum/__init__.py +5 -2
- meerschaum/__main__.py +0 -5
- meerschaum/_internal/arguments/_parse_arguments.py +10 -3
- meerschaum/_internal/arguments/_parser.py +6 -2
- meerschaum/_internal/entry.py +36 -6
- meerschaum/_internal/shell/Shell.py +32 -20
- meerschaum/actions/__init__.py +8 -6
- meerschaum/actions/attach.py +31 -13
- meerschaum/actions/copy.py +68 -41
- meerschaum/actions/delete.py +64 -21
- meerschaum/actions/edit.py +3 -3
- meerschaum/actions/install.py +40 -32
- meerschaum/actions/pause.py +44 -27
- meerschaum/actions/restart.py +107 -0
- meerschaum/actions/show.py +8 -8
- meerschaum/actions/start.py +26 -41
- meerschaum/actions/stop.py +11 -4
- meerschaum/api/_events.py +10 -3
- meerschaum/api/dash/jobs.py +69 -70
- meerschaum/api/routes/_actions.py +8 -3
- meerschaum/api/routes/_jobs.py +86 -37
- meerschaum/config/_default.py +1 -1
- meerschaum/config/_paths.py +5 -0
- meerschaum/config/_shell.py +1 -1
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +6 -1
- meerschaum/connectors/Connector.py +13 -7
- meerschaum/connectors/__init__.py +21 -5
- meerschaum/connectors/api/APIConnector.py +3 -0
- meerschaum/connectors/api/_jobs.py +108 -11
- meerschaum/connectors/parse.py +10 -13
- meerschaum/core/Pipe/_bootstrap.py +16 -8
- meerschaum/jobs/_Executor.py +69 -0
- meerschaum/{utils/jobs → jobs}/_Job.py +206 -40
- meerschaum/jobs/_LocalExecutor.py +88 -0
- meerschaum/jobs/_SystemdExecutor.py +608 -0
- meerschaum/jobs/__init__.py +365 -0
- meerschaum/plugins/__init__.py +6 -6
- meerschaum/utils/daemon/Daemon.py +7 -0
- meerschaum/utils/daemon/RotatingFile.py +5 -2
- meerschaum/utils/daemon/StdinFile.py +12 -2
- meerschaum/utils/daemon/__init__.py +2 -0
- meerschaum/utils/formatting/_jobs.py +52 -16
- meerschaum/utils/misc.py +23 -5
- meerschaum/utils/packages/_packages.py +7 -4
- meerschaum/utils/process.py +9 -9
- meerschaum/utils/venv/__init__.py +2 -2
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/METADATA +14 -17
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/RECORD +55 -51
- meerschaum/utils/jobs/__init__.py +0 -245
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/WHEEL +0 -0
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/zip-safe +0 -0
@@ -6,6 +6,8 @@
|
|
6
6
|
Define the Meerschaum abstraction atop daemons.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import shlex
|
10
12
|
import asyncio
|
11
13
|
import threading
|
@@ -18,11 +20,17 @@ from functools import partial
|
|
18
20
|
from datetime import datetime, timezone
|
19
21
|
|
20
22
|
import meerschaum as mrsm
|
21
|
-
from meerschaum.utils.typing import
|
23
|
+
from meerschaum.utils.typing import (
|
24
|
+
List, Optional, Union, SuccessTuple, Any, Dict, Callable, TYPE_CHECKING,
|
25
|
+
)
|
22
26
|
from meerschaum._internal.entry import entry
|
23
27
|
from meerschaum.utils.warnings import warn
|
24
28
|
from meerschaum.config.paths import LOGS_RESOURCES_PATH
|
25
29
|
from meerschaum.config import get_config
|
30
|
+
from meerschaum.config.static import STATIC_CONFIG
|
31
|
+
|
32
|
+
if TYPE_CHECKING:
|
33
|
+
from meerschaum.jobs._Executor import Executor
|
26
34
|
|
27
35
|
BANNED_CHARS: List[str] = [
|
28
36
|
',', ';', "'", '"',
|
@@ -52,6 +60,10 @@ class Job:
|
|
52
60
|
sysargs: Union[List[str], str, None] = None,
|
53
61
|
executor_keys: Optional[str] = None,
|
54
62
|
_properties: Optional[Dict[str, Any]] = None,
|
63
|
+
_rotating_log = None,
|
64
|
+
_stdin_file = None,
|
65
|
+
_status_hook = None,
|
66
|
+
_result_hook = None,
|
55
67
|
):
|
56
68
|
"""
|
57
69
|
Create a new job to manage a `meerschaum.utils.daemon.Daemon`.
|
@@ -79,19 +91,40 @@ class Job:
|
|
79
91
|
if isinstance(sysargs, str):
|
80
92
|
sysargs = shlex.split(sysargs)
|
81
93
|
|
82
|
-
|
83
|
-
|
94
|
+
### NOTE: 'local' and 'systemd' executors are being coalesced.
|
95
|
+
if executor_keys is None:
|
96
|
+
from meerschaum.jobs import get_executor_keys_from_context
|
97
|
+
executor_keys = get_executor_keys_from_context()
|
98
|
+
|
84
99
|
self.executor_keys = executor_keys
|
85
100
|
self.name = name
|
86
101
|
try:
|
87
102
|
self._daemon = (
|
88
103
|
Daemon(daemon_id=name)
|
89
|
-
if executor_keys
|
104
|
+
if executor_keys == 'local'
|
90
105
|
else None
|
91
106
|
)
|
92
107
|
except Exception:
|
93
108
|
self._daemon = None
|
94
109
|
|
110
|
+
### Handle any injected dependencies.
|
111
|
+
if _rotating_log is not None:
|
112
|
+
self._rotating_log = _rotating_log
|
113
|
+
if self._daemon is not None:
|
114
|
+
self._daemon._rotating_log = _rotating_log
|
115
|
+
|
116
|
+
if _stdin_file is not None:
|
117
|
+
self._stdin_file = _stdin_file
|
118
|
+
if self._daemon is not None:
|
119
|
+
self._daemon._stdin_file = _stdin_file
|
120
|
+
self._daemon._blocking_stdin_file_path = _stdin_file.blocking_file_path
|
121
|
+
|
122
|
+
if _status_hook is not None:
|
123
|
+
self._status_hook = _status_hook
|
124
|
+
|
125
|
+
if _result_hook is not None:
|
126
|
+
self._result_hook = _result_hook
|
127
|
+
|
95
128
|
self._properties_patch = _properties or {}
|
96
129
|
|
97
130
|
daemon_sysargs = (
|
@@ -113,6 +146,71 @@ class Job:
|
|
113
146
|
self._properties_patch.update({'restart': True})
|
114
147
|
break
|
115
148
|
|
149
|
+
if '--systemd' in self._sysargs:
|
150
|
+
self._properties_patch.update({'systemd': True})
|
151
|
+
|
152
|
+
@staticmethod
|
153
|
+
def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job:
|
154
|
+
"""
|
155
|
+
Build a `Job` from the PID of a running Meerschaum process.
|
156
|
+
|
157
|
+
Parameters
|
158
|
+
----------
|
159
|
+
pid: int
|
160
|
+
The PID of the process.
|
161
|
+
|
162
|
+
executor_keys: Optional[str], default None
|
163
|
+
The executor keys to assign to the job.
|
164
|
+
"""
|
165
|
+
from meerschaum.config.paths import DAEMON_RESOURCES_PATH
|
166
|
+
|
167
|
+
psutil = mrsm.attempt_import('psutil')
|
168
|
+
try:
|
169
|
+
process = psutil.Process(pid)
|
170
|
+
except psutil.NoSuchProcess as e:
|
171
|
+
warn(f"Process with PID {pid} does not exist.", stack=False)
|
172
|
+
raise e
|
173
|
+
|
174
|
+
command_args = process.cmdline()
|
175
|
+
is_daemon = command_args[1] == '-c'
|
176
|
+
|
177
|
+
if is_daemon:
|
178
|
+
daemon_id = command_args[-1].split('daemon_id=')[-1].split(')')[0].replace("'", '')
|
179
|
+
root_dir = process.environ().get(STATIC_CONFIG['environment']['root'], None)
|
180
|
+
if root_dir is None:
|
181
|
+
from meerschaum.config.paths import ROOT_DIR_PATH
|
182
|
+
root_dir = ROOT_DIR_PATH
|
183
|
+
jobs_dir = root_dir / DAEMON_RESOURCES_PATH.name
|
184
|
+
daemon_dir = jobs_dir / daemon_id
|
185
|
+
pid_file = daemon_dir / 'process.pid'
|
186
|
+
properties_path = daemon_dir / 'properties.json'
|
187
|
+
pickle_path = daemon_dir / 'pickle.pkl'
|
188
|
+
|
189
|
+
if pid_file.exists():
|
190
|
+
with open(pid_file, 'r', encoding='utf-8') as f:
|
191
|
+
daemon_pid = int(f.read())
|
192
|
+
|
193
|
+
if pid != daemon_pid:
|
194
|
+
raise EnvironmentError(f"Differing PIDs: {pid=}, {daemon_pid=}")
|
195
|
+
else:
|
196
|
+
raise EnvironmentError(f"Is job '{daemon_id}' running?")
|
197
|
+
|
198
|
+
return Job(daemon_id, executor_keys=executor_keys)
|
199
|
+
|
200
|
+
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
201
|
+
from meerschaum.utils.daemon import get_new_daemon_name
|
202
|
+
|
203
|
+
mrsm_ix = 0
|
204
|
+
for i, arg in enumerate(command_args):
|
205
|
+
if 'mrsm' in arg or 'meerschaum' in arg.lower():
|
206
|
+
mrsm_ix = i
|
207
|
+
break
|
208
|
+
|
209
|
+
sysargs = command_args[mrsm_ix+1:]
|
210
|
+
kwargs = parse_arguments(sysargs)
|
211
|
+
name = kwargs.get('name', get_new_daemon_name())
|
212
|
+
return Job(name, sysargs, executor_keys=executor_keys)
|
213
|
+
|
116
214
|
def start(self, debug: bool = False) -> SuccessTuple:
|
117
215
|
"""
|
118
216
|
Start the job's daemon.
|
@@ -144,6 +242,8 @@ class Job:
|
|
144
242
|
if self.daemon.status == 'stopped':
|
145
243
|
if not self.restart:
|
146
244
|
return True, f"{self} is not running."
|
245
|
+
elif self.stop_time is not None:
|
246
|
+
return True, f"{self} will not restart until manually started."
|
147
247
|
|
148
248
|
quit_success, quit_msg = self.daemon.quit(timeout=timeout_seconds)
|
149
249
|
if quit_success:
|
@@ -218,7 +318,8 @@ class Job:
|
|
218
318
|
self,
|
219
319
|
callback_function: Callable[[str], None] = partial(print, end=''),
|
220
320
|
input_callback_function: Optional[Callable[[], str]] = None,
|
221
|
-
|
321
|
+
stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
|
322
|
+
stop_event: Optional[asyncio.Event] = None,
|
222
323
|
stop_on_exit: bool = False,
|
223
324
|
strip_timestamps: bool = False,
|
224
325
|
accept_input: bool = True,
|
@@ -237,9 +338,13 @@ class Job:
|
|
237
338
|
If provided, execute this callback when the daemon is blocking on stdin.
|
238
339
|
Defaults to `sys.stdin.readline()`.
|
239
340
|
|
341
|
+
stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None
|
342
|
+
If provided, execute this callback when the daemon stops.
|
343
|
+
The job's SuccessTuple will be passed to the callback.
|
344
|
+
|
240
345
|
stop_event: Optional[asyncio.Event], default None
|
241
346
|
If provided, stop monitoring when this event is set.
|
242
|
-
You may instead raise `meerschaum.
|
347
|
+
You may instead raise `meerschaum.jobs.StopMonitoringLogs`
|
243
348
|
from within `callback_function` to stop monitoring.
|
244
349
|
|
245
350
|
stop_on_exit: bool, default False
|
@@ -251,13 +356,29 @@ class Job:
|
|
251
356
|
accept_input: bool, default True
|
252
357
|
If `True`, accept input when the daemon blocks on stdin.
|
253
358
|
"""
|
359
|
+
def default_input_callback_function():
|
360
|
+
return sys.stdin.readline()
|
361
|
+
|
362
|
+
if input_callback_function is None:
|
363
|
+
input_callback_function = default_input_callback_function
|
364
|
+
|
254
365
|
if self.executor is not None:
|
255
|
-
self.executor.monitor_logs(
|
366
|
+
self.executor.monitor_logs(
|
367
|
+
self.name,
|
368
|
+
callback_function,
|
369
|
+
input_callback_function=input_callback_function,
|
370
|
+
stop_callback_function=stop_callback_function,
|
371
|
+
stop_on_exit=stop_on_exit,
|
372
|
+
accept_input=accept_input,
|
373
|
+
strip_timestamps=strip_timestamps,
|
374
|
+
debug=debug,
|
375
|
+
)
|
256
376
|
return
|
257
377
|
|
258
378
|
monitor_logs_coroutine = self.monitor_logs_async(
|
259
379
|
callback_function=callback_function,
|
260
380
|
input_callback_function=input_callback_function,
|
381
|
+
stop_callback_function=stop_callback_function,
|
261
382
|
stop_event=stop_event,
|
262
383
|
stop_on_exit=stop_on_exit,
|
263
384
|
strip_timestamps=strip_timestamps,
|
@@ -270,10 +391,14 @@ class Job:
|
|
270
391
|
self,
|
271
392
|
callback_function: Callable[[str], None] = partial(print, end='', flush=True),
|
272
393
|
input_callback_function: Optional[Callable[[], str]] = None,
|
394
|
+
stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
|
273
395
|
stop_event: Optional[asyncio.Event] = None,
|
274
396
|
stop_on_exit: bool = False,
|
275
397
|
strip_timestamps: bool = False,
|
276
398
|
accept_input: bool = True,
|
399
|
+
_logs_path: Optional[pathlib.Path] = None,
|
400
|
+
_log = None,
|
401
|
+
_stdin_file = None,
|
277
402
|
debug: bool = False,
|
278
403
|
):
|
279
404
|
"""
|
@@ -289,9 +414,13 @@ class Job:
|
|
289
414
|
If provided, execute this callback when the daemon is blocking on stdin.
|
290
415
|
Defaults to `sys.stdin.readline()`.
|
291
416
|
|
417
|
+
stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None
|
418
|
+
If provided, execute this callback when the daemon stops.
|
419
|
+
The job's SuccessTuple will be passed to the callback.
|
420
|
+
|
292
421
|
stop_event: Optional[asyncio.Event], default None
|
293
422
|
If provided, stop monitoring when this event is set.
|
294
|
-
You may instead raise `meerschaum.
|
423
|
+
You may instead raise `meerschaum.jobs.StopMonitoringLogs`
|
295
424
|
from within `callback_function` to stop monitoring.
|
296
425
|
|
297
426
|
stop_on_exit: bool, default False
|
@@ -314,6 +443,8 @@ class Job:
|
|
314
443
|
self.name,
|
315
444
|
callback_function,
|
316
445
|
input_callback_function=input_callback_function,
|
446
|
+
stop_callback_function=stop_callback_function,
|
447
|
+
stop_on_exit=stop_on_exit,
|
317
448
|
accept_input=accept_input,
|
318
449
|
debug=debug,
|
319
450
|
)
|
@@ -323,16 +454,18 @@ class Job:
|
|
323
454
|
|
324
455
|
events = {
|
325
456
|
'user': stop_event,
|
326
|
-
'stopped':
|
457
|
+
'stopped': asyncio.Event(),
|
327
458
|
}
|
328
459
|
combined_event = asyncio.Event()
|
329
460
|
emitted_text = False
|
461
|
+
stdin_file = _stdin_file if _stdin_file is not None else self.daemon.stdin_file
|
330
462
|
|
331
463
|
async def check_job_status():
|
332
464
|
nonlocal emitted_text
|
333
465
|
stopped_event = events.get('stopped', None)
|
334
466
|
if stopped_event is None:
|
335
467
|
return
|
468
|
+
|
336
469
|
sleep_time = 0.1
|
337
470
|
while sleep_time < 60:
|
338
471
|
if self.status == 'stopped':
|
@@ -340,7 +473,19 @@ class Job:
|
|
340
473
|
await asyncio.sleep(sleep_time)
|
341
474
|
sleep_time = round(sleep_time * 1.1, 2)
|
342
475
|
continue
|
343
|
-
|
476
|
+
|
477
|
+
if stop_callback_function is not None:
|
478
|
+
try:
|
479
|
+
if asyncio.iscoroutinefunction(stop_callback_function):
|
480
|
+
await stop_callback_function(self.result)
|
481
|
+
else:
|
482
|
+
stop_callback_function(self.result)
|
483
|
+
except Exception:
|
484
|
+
warn(traceback.format_exc())
|
485
|
+
|
486
|
+
if stop_on_exit:
|
487
|
+
events['stopped'].set()
|
488
|
+
|
344
489
|
break
|
345
490
|
await asyncio.sleep(0.1)
|
346
491
|
|
@@ -360,7 +505,7 @@ class Job:
|
|
360
505
|
|
361
506
|
try:
|
362
507
|
print('', end='', flush=True)
|
363
|
-
if asyncio.iscoroutinefunction(
|
508
|
+
if asyncio.iscoroutinefunction(input_callback_function):
|
364
509
|
data = await input_callback_function()
|
365
510
|
else:
|
366
511
|
data = input_callback_function()
|
@@ -368,7 +513,8 @@ class Job:
|
|
368
513
|
break
|
369
514
|
if not data.endswith('\n'):
|
370
515
|
data += '\n'
|
371
|
-
|
516
|
+
|
517
|
+
stdin_file.write(data)
|
372
518
|
await asyncio.sleep(0.1)
|
373
519
|
|
374
520
|
async def combine_events():
|
@@ -396,7 +542,7 @@ class Job:
|
|
396
542
|
check_blocking_on_input_task = asyncio.create_task(check_blocking_on_input())
|
397
543
|
combine_events_task = asyncio.create_task(combine_events())
|
398
544
|
|
399
|
-
log = self.daemon.rotating_log
|
545
|
+
log = _log if _log is not None else self.daemon.rotating_log
|
400
546
|
lines_to_show = get_config('jobs', 'logs', 'lines_to_show')
|
401
547
|
|
402
548
|
async def emit_latest_lines():
|
@@ -434,7 +580,7 @@ class Job:
|
|
434
580
|
|
435
581
|
watchfiles = mrsm.attempt_import('watchfiles')
|
436
582
|
async for changes in watchfiles.awatch(
|
437
|
-
LOGS_RESOURCES_PATH,
|
583
|
+
_logs_path or LOGS_RESOURCES_PATH,
|
438
584
|
stop_event=combined_event,
|
439
585
|
):
|
440
586
|
for change in changes:
|
@@ -444,23 +590,7 @@ class Job:
|
|
444
590
|
if latest_subfile_path != file_path:
|
445
591
|
continue
|
446
592
|
|
447
|
-
|
448
|
-
for line in lines:
|
449
|
-
if strip_timestamps:
|
450
|
-
line = strip_timestamp_from_line(line)
|
451
|
-
try:
|
452
|
-
if asyncio.iscoroutinefunction(callback_function):
|
453
|
-
await callback_function(line)
|
454
|
-
else:
|
455
|
-
callback_function(line)
|
456
|
-
emitted_text = True
|
457
|
-
except RuntimeError:
|
458
|
-
return
|
459
|
-
except StopMonitoringLogs:
|
460
|
-
return
|
461
|
-
except Exception:
|
462
|
-
warn(f"Error in logs callback:\n{traceback.format_exc()}")
|
463
|
-
return
|
593
|
+
await emit_latest_lines()
|
464
594
|
|
465
595
|
await emit_latest_lines()
|
466
596
|
|
@@ -477,19 +607,16 @@ class Job:
|
|
477
607
|
"""
|
478
608
|
Write to a job's daemon's `stdin`.
|
479
609
|
"""
|
480
|
-
if self.executor is not None:
|
481
|
-
pass
|
482
|
-
|
483
610
|
self.daemon.stdin_file.write(data)
|
484
611
|
|
485
612
|
@property
|
486
|
-
def executor(self) -> Union[
|
613
|
+
def executor(self) -> Union[Executor, None]:
|
487
614
|
"""
|
488
615
|
If the job is remote, return the connector to the remote API instance.
|
489
616
|
"""
|
490
617
|
return (
|
491
618
|
mrsm.get_connector(self.executor_keys)
|
492
|
-
if self.executor_keys
|
619
|
+
if self.executor_keys != 'local'
|
493
620
|
else None
|
494
621
|
)
|
495
622
|
|
@@ -498,10 +625,11 @@ class Job:
|
|
498
625
|
"""
|
499
626
|
Return the running status of the job's daemon.
|
500
627
|
"""
|
628
|
+
if '_status_hook' in self.__dict__:
|
629
|
+
return self._status_hook()
|
630
|
+
|
501
631
|
if self.executor is not None:
|
502
|
-
return self.executor.
|
503
|
-
self.name
|
504
|
-
).get('daemon', {}).get('status', 'stopped')
|
632
|
+
return self.executor.get_job_status(self.name)
|
505
633
|
|
506
634
|
return self.daemon.status
|
507
635
|
|
@@ -520,6 +648,9 @@ class Job:
|
|
520
648
|
"""
|
521
649
|
Return whether to restart a stopped job.
|
522
650
|
"""
|
651
|
+
if self.executor is not None:
|
652
|
+
return self.executor.get_job_metadata(self.name).get('restart', False)
|
653
|
+
|
523
654
|
return self.daemon.properties.get('restart', False)
|
524
655
|
|
525
656
|
@property
|
@@ -530,6 +661,15 @@ class Job:
|
|
530
661
|
if self.is_running():
|
531
662
|
return True, f"{self} is running."
|
532
663
|
|
664
|
+
if '_result_hook' in self.__dict__:
|
665
|
+
return self._result_hook()
|
666
|
+
|
667
|
+
if self.executor is not None:
|
668
|
+
return (
|
669
|
+
self.executor.get_job_metadata(self.name)
|
670
|
+
.get('result', (False, "No result available."))
|
671
|
+
)
|
672
|
+
|
533
673
|
_result = self.daemon.properties.get('result', None)
|
534
674
|
if _result is None:
|
535
675
|
return False, "No result available."
|
@@ -544,7 +684,9 @@ class Job:
|
|
544
684
|
if self._sysargs:
|
545
685
|
return self._sysargs
|
546
686
|
|
547
|
-
|
687
|
+
if self.executor is not None:
|
688
|
+
return self.executor.get_job_metadata(self.name).get('sysargs', [])
|
689
|
+
|
548
690
|
target_args = self.daemon.target_args
|
549
691
|
if target_args is None:
|
550
692
|
return []
|
@@ -575,6 +717,12 @@ class Job:
|
|
575
717
|
label=shlex.join(self._sysargs),
|
576
718
|
properties=properties,
|
577
719
|
)
|
720
|
+
if '_rotating_log' in self.__dict__:
|
721
|
+
self._daemon._rotating_log = self._rotating_log
|
722
|
+
|
723
|
+
if '_stdin_file' in self.__dict__:
|
724
|
+
self._daemon._stdin_file = self._stdin_file
|
725
|
+
self._daemon._blocking_stdin_file_path = self._stdin_file.blocking_file_path
|
578
726
|
|
579
727
|
return self._daemon
|
580
728
|
|
@@ -583,6 +731,16 @@ class Job:
|
|
583
731
|
"""
|
584
732
|
The datetime when the job began running.
|
585
733
|
"""
|
734
|
+
if self.executor is not None:
|
735
|
+
began_str = self.executor.get_job_began(name)
|
736
|
+
if began_str is None:
|
737
|
+
return None
|
738
|
+
return (
|
739
|
+
datetime.fromisoformat(began_str)
|
740
|
+
.astimezone(timezone.utc)
|
741
|
+
.replace(tzinfo=None)
|
742
|
+
)
|
743
|
+
|
586
744
|
began_str = self.daemon.properties.get('process', {}).get('began', None)
|
587
745
|
if began_str is None:
|
588
746
|
return None
|
@@ -594,6 +752,14 @@ class Job:
|
|
594
752
|
"""
|
595
753
|
The datetime when the job stopped running.
|
596
754
|
"""
|
755
|
+
if self.executor is not None:
|
756
|
+
ended_str = self.executor.get_job_ended(self.name)
|
757
|
+
return (
|
758
|
+
datetime.fromisoformat(ended_str)
|
759
|
+
.astimezone(timezone.utc)
|
760
|
+
.replace(tzinfo=None)
|
761
|
+
)
|
762
|
+
|
597
763
|
ended_str = self.daemon.properties.get('process', {}).get('ended', None)
|
598
764
|
if ended_str is None:
|
599
765
|
return None
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# vim:fenc=utf-8
|
3
|
+
|
4
|
+
"""
|
5
|
+
Run jobs locally.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union
|
9
|
+
from meerschaum.jobs import Job, Executor, make_executor
|
10
|
+
from meerschaum.utils.daemon import Daemon, get_daemons
|
11
|
+
from meerschaum._internal.entry import entry
|
12
|
+
|
13
|
+
|
14
|
+
@make_executor
|
15
|
+
class LocalExecutor(Executor):
|
16
|
+
"""
|
17
|
+
Run jobs locally as Unix daemons.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def get_job_daemon(
|
21
|
+
self,
|
22
|
+
name: str,
|
23
|
+
# sysargs: Opt
|
24
|
+
debug: bool = False,
|
25
|
+
) -> Union[Daemon, None]:
|
26
|
+
"""
|
27
|
+
Return a job's daemon if it exists.
|
28
|
+
"""
|
29
|
+
try:
|
30
|
+
daemon = Daemon(name)
|
31
|
+
except Exception:
|
32
|
+
daemon = None
|
33
|
+
|
34
|
+
return daemon
|
35
|
+
|
36
|
+
def get_daemon_syargs(self, name: str, debug: bool = False) -> Union[List[str], None]:
|
37
|
+
"""
|
38
|
+
Return the list of sysargs from the job's daemon.
|
39
|
+
"""
|
40
|
+
daemon = self.get_job_daemon(name, debug=debug)
|
41
|
+
|
42
|
+
if daemon is None:
|
43
|
+
return None
|
44
|
+
|
45
|
+
return daemon.properties.get('target', {}).get('args', [None])[0]
|
46
|
+
|
47
|
+
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
48
|
+
"""
|
49
|
+
Return whether a job exists.
|
50
|
+
"""
|
51
|
+
daemon = self.get_job_daemon(name, debug=debug)
|
52
|
+
if daemon is None:
|
53
|
+
return False
|
54
|
+
|
55
|
+
def get_jobs(self) -> Dict[str, Job]:
|
56
|
+
"""
|
57
|
+
Return a dictionary of names -> Jobs.
|
58
|
+
"""
|
59
|
+
|
60
|
+
def create_job(self, name: str, sysargs: List[str], debug: bool = False) -> SuccessTuple:
|
61
|
+
"""
|
62
|
+
Create a new job.
|
63
|
+
"""
|
64
|
+
|
65
|
+
def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
66
|
+
"""
|
67
|
+
Start a job.
|
68
|
+
"""
|
69
|
+
|
70
|
+
def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
71
|
+
"""
|
72
|
+
Stop a job.
|
73
|
+
"""
|
74
|
+
|
75
|
+
def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
76
|
+
"""
|
77
|
+
Pause a job.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
|
81
|
+
"""
|
82
|
+
Delete a job.
|
83
|
+
"""
|
84
|
+
|
85
|
+
def get_logs(self, name: str, debug: bool = False) -> str:
|
86
|
+
"""
|
87
|
+
Return a job's log output.
|
88
|
+
"""
|