meerschaum 2.2.7__py3-none-any.whl → 2.3.0.dev3__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 +6 -1
- meerschaum/_internal/arguments/_parse_arguments.py +10 -3
- meerschaum/_internal/arguments/_parser.py +44 -15
- meerschaum/_internal/entry.py +22 -1
- meerschaum/_internal/shell/Shell.py +129 -31
- meerschaum/actions/__init__.py +8 -6
- meerschaum/actions/api.py +12 -12
- meerschaum/actions/attach.py +108 -0
- meerschaum/actions/delete.py +35 -26
- meerschaum/actions/show.py +119 -148
- meerschaum/actions/start.py +85 -75
- meerschaum/actions/stop.py +68 -39
- meerschaum/api/_events.py +18 -1
- meerschaum/api/_oauth2.py +2 -0
- meerschaum/api/_websockets.py +2 -2
- meerschaum/api/dash/jobs.py +5 -2
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +122 -44
- meerschaum/api/routes/_jobs.py +371 -0
- meerschaum/api/routes/_pipes.py +5 -5
- meerschaum/config/_default.py +1 -0
- meerschaum/config/_paths.py +1 -0
- meerschaum/config/_shell.py +8 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +10 -0
- meerschaum/connectors/__init__.py +9 -11
- meerschaum/connectors/api/APIConnector.py +18 -1
- meerschaum/connectors/api/_actions.py +60 -71
- meerschaum/connectors/api/_jobs.py +330 -0
- meerschaum/connectors/parse.py +23 -7
- meerschaum/plugins/__init__.py +89 -5
- meerschaum/utils/daemon/Daemon.py +255 -30
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
- meerschaum/utils/daemon/RotatingFile.py +10 -6
- meerschaum/utils/daemon/StdinFile.py +110 -0
- meerschaum/utils/daemon/__init__.py +13 -7
- meerschaum/utils/formatting/__init__.py +2 -1
- meerschaum/utils/formatting/_jobs.py +83 -54
- meerschaum/utils/formatting/_shell.py +6 -0
- meerschaum/utils/jobs/_Job.py +710 -0
- meerschaum/utils/jobs/__init__.py +245 -0
- meerschaum/utils/misc.py +18 -17
- meerschaum/utils/packages/_packages.py +2 -2
- meerschaum/utils/prompt.py +16 -8
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/METADATA +9 -9
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/RECORD +52 -46
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev3.dist-info}/zip-safe +0 -0
meerschaum/plugins/__init__.py
CHANGED
@@ -8,6 +8,7 @@ Expose plugin management APIs from the `meerschaum.plugins` module.
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
import functools
|
11
|
+
import meerschaum as mrsm
|
11
12
|
from meerschaum.utils.typing import Callable, Any, Union, Optional, Dict, List, Tuple
|
12
13
|
from meerschaum.utils.threading import Lock, RLock
|
13
14
|
from meerschaum.plugins._Plugin import Plugin
|
@@ -429,11 +430,11 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
|
|
429
430
|
|
430
431
|
|
431
432
|
def import_plugins(
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
433
|
+
*plugins_to_import: Union[str, List[str], None],
|
434
|
+
warn: bool = True,
|
435
|
+
) -> Union[
|
436
|
+
'ModuleType', Tuple['ModuleType', None]
|
437
|
+
]:
|
437
438
|
"""
|
438
439
|
Import the Meerschaum plugins directory.
|
439
440
|
|
@@ -524,6 +525,89 @@ def import_plugins(
|
|
524
525
|
return imported_plugins
|
525
526
|
|
526
527
|
|
528
|
+
def from_plugin_import(plugin_import_name: str, *attrs: str) -> Any:
|
529
|
+
"""
|
530
|
+
Emulate the `from module import x` behavior.
|
531
|
+
|
532
|
+
Parameters
|
533
|
+
----------
|
534
|
+
plugin_import_name: str
|
535
|
+
The import name of the plugin's module.
|
536
|
+
Separate submodules with '.' (e.g. 'compose.utils.pipes')
|
537
|
+
|
538
|
+
attrs: str
|
539
|
+
Names of the attributes to return.
|
540
|
+
|
541
|
+
Returns
|
542
|
+
-------
|
543
|
+
Objects from a plugin's submodule.
|
544
|
+
If multiple objects are provided, return a tuple.
|
545
|
+
|
546
|
+
Examples
|
547
|
+
--------
|
548
|
+
>>> init = from_plugin_import('compose.utils', 'init')
|
549
|
+
>>> with mrsm.Venv('compose'):
|
550
|
+
... cf = init()
|
551
|
+
>>> build_parent_pipe, get_defined_pipes = from_plugin_import(
|
552
|
+
... 'compose.utils.pipes',
|
553
|
+
... 'build_parent_pipe',
|
554
|
+
... 'get_defined_pipes',
|
555
|
+
... )
|
556
|
+
>>> parent_pipe = build_parent_pipe(cf)
|
557
|
+
>>> defined_pipes = get_defined_pipes(cf)
|
558
|
+
"""
|
559
|
+
import importlib
|
560
|
+
from meerschaum.config._paths import PLUGINS_RESOURCES_PATH
|
561
|
+
from meerschaum.utils.warnings import warn as _warn
|
562
|
+
if plugin_import_name.startswith('plugins.'):
|
563
|
+
plugin_import_name = plugin_import_name[len('plugins.'):]
|
564
|
+
plugin_import_parts = plugin_import_name.split('.')
|
565
|
+
plugin_root_name = plugin_import_parts[0]
|
566
|
+
plugin = mrsm.Plugin(plugin_root_name)
|
567
|
+
|
568
|
+
submodule_import_name = '.'.join(
|
569
|
+
[PLUGINS_RESOURCES_PATH.stem]
|
570
|
+
+ plugin_import_parts
|
571
|
+
)
|
572
|
+
if len(attrs) == 0:
|
573
|
+
raise ValueError(f"Provide which attributes to return from '{submodule_import_name}'.")
|
574
|
+
|
575
|
+
attrs_to_return = []
|
576
|
+
with mrsm.Venv(plugin):
|
577
|
+
if plugin.module is None:
|
578
|
+
return None
|
579
|
+
|
580
|
+
try:
|
581
|
+
submodule = importlib.import_module(submodule_import_name)
|
582
|
+
except ImportError as e:
|
583
|
+
_warn(
|
584
|
+
f"Failed to import plugin '{submodule_import_name}':\n "
|
585
|
+
+ f"{e}\n\nHere's a stacktrace:",
|
586
|
+
stack=False,
|
587
|
+
)
|
588
|
+
from meerschaum.utils.formatting import get_console
|
589
|
+
get_console().print_exception(
|
590
|
+
suppress=[
|
591
|
+
'meerschaum/plugins/__init__.py',
|
592
|
+
importlib,
|
593
|
+
importlib._bootstrap,
|
594
|
+
]
|
595
|
+
)
|
596
|
+
return None
|
597
|
+
|
598
|
+
for attr in attrs:
|
599
|
+
try:
|
600
|
+
attrs_to_return.append(getattr(submodule, attr))
|
601
|
+
except Exception:
|
602
|
+
_warn(f"Failed to access '{attr}' from '{submodule_import_name}'.")
|
603
|
+
attrs_to_return.append(None)
|
604
|
+
|
605
|
+
if len(attrs) == 1:
|
606
|
+
return attrs_to_return[0]
|
607
|
+
|
608
|
+
return tuple(attrs_to_return)
|
609
|
+
|
610
|
+
|
527
611
|
def load_plugins(debug: bool = False, shell: bool = False) -> None:
|
528
612
|
"""
|
529
613
|
Import Meerschaum plugins and update the actions dictionary.
|
@@ -8,6 +8,7 @@ Manage running daemons via the Daemon class.
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
import os
|
11
|
+
import importlib
|
11
12
|
import pathlib
|
12
13
|
import json
|
13
14
|
import shutil
|
@@ -21,7 +22,7 @@ from datetime import datetime, timezone
|
|
21
22
|
import meerschaum as mrsm
|
22
23
|
from meerschaum.utils.typing import (
|
23
24
|
Optional, Dict, Any, SuccessTuple, Callable, List, Union,
|
24
|
-
is_success_tuple,
|
25
|
+
is_success_tuple, Tuple,
|
25
26
|
)
|
26
27
|
from meerschaum.config import get_config
|
27
28
|
from meerschaum.config.static import STATIC_CONFIG
|
@@ -34,6 +35,7 @@ from meerschaum.utils.packages import attempt_import
|
|
34
35
|
from meerschaum.utils.venv import venv_exec
|
35
36
|
from meerschaum.utils.daemon._names import get_new_daemon_name
|
36
37
|
from meerschaum.utils.daemon.RotatingFile import RotatingFile
|
38
|
+
from meerschaum.utils.daemon.StdinFile import StdinFile
|
37
39
|
from meerschaum.utils.threading import RepeatTimer
|
38
40
|
from meerschaum.__main__ import _close_pools
|
39
41
|
|
@@ -43,6 +45,32 @@ _results = {}
|
|
43
45
|
class Daemon:
|
44
46
|
"""
|
45
47
|
Daemonize Python functions into background processes.
|
48
|
+
|
49
|
+
Examples
|
50
|
+
--------
|
51
|
+
>>> import meerschaum as mrsm
|
52
|
+
>>> from meerschaum.utils.daemons import Daemon
|
53
|
+
>>> daemon = Daemon(print, ('hi',))
|
54
|
+
>>> success, msg = daemon.run()
|
55
|
+
>>> print(daemon.log_text)
|
56
|
+
|
57
|
+
2024-07-29 18:03 | hi
|
58
|
+
2024-07-29 18:03 |
|
59
|
+
>>> daemon.run(allow_dirty_run=True)
|
60
|
+
>>> print(daemon.log_text)
|
61
|
+
|
62
|
+
2024-07-29 18:03 | hi
|
63
|
+
2024-07-29 18:03 |
|
64
|
+
2024-07-29 18:05 | hi
|
65
|
+
2024-07-29 18:05 |
|
66
|
+
>>> mrsm.pprint(daemon.properties)
|
67
|
+
{
|
68
|
+
'label': 'print',
|
69
|
+
'target': {'name': 'print', 'module': 'builtins', 'args': ['hi'], 'kw': {}},
|
70
|
+
'result': None,
|
71
|
+
'process': {'ended': '2024-07-29T18:03:33.752806'}
|
72
|
+
}
|
73
|
+
|
46
74
|
"""
|
47
75
|
|
48
76
|
def __new__(
|
@@ -61,10 +89,57 @@ class Daemon:
|
|
61
89
|
instance = instance.read_pickle()
|
62
90
|
return instance
|
63
91
|
|
92
|
+
@classmethod
|
93
|
+
def from_properties_file(cls, daemon_id: str) -> Daemon:
|
94
|
+
"""
|
95
|
+
Return a Daemon from a properties dictionary.
|
96
|
+
"""
|
97
|
+
properties_path = cls._get_properties_path_from_daemon_id(daemon_id)
|
98
|
+
if not properties_path.exists():
|
99
|
+
raise OSError(f"Properties file '{properties_path}' does not exist.")
|
100
|
+
|
101
|
+
try:
|
102
|
+
with open(properties_path, 'r', encoding='utf-8') as f:
|
103
|
+
properties = json.load(f)
|
104
|
+
except Exception:
|
105
|
+
properties = {}
|
106
|
+
|
107
|
+
if not properties:
|
108
|
+
raise ValueError(f"No properties could be read for daemon '{daemon_id}'.")
|
109
|
+
|
110
|
+
daemon_id = properties_path.parent.name
|
111
|
+
target_cf = properties.get('target', {})
|
112
|
+
target_module_name = target_cf.get('module', None)
|
113
|
+
target_function_name = target_cf.get('name', None)
|
114
|
+
target_args = target_cf.get('args', None)
|
115
|
+
target_kw = target_cf.get('kw', None)
|
116
|
+
label = properties.get('label', None)
|
117
|
+
|
118
|
+
if None in [
|
119
|
+
target_module_name,
|
120
|
+
target_function_name,
|
121
|
+
target_args,
|
122
|
+
target_kw,
|
123
|
+
]:
|
124
|
+
raise ValueError("Missing target function information.")
|
125
|
+
|
126
|
+
target_module = importlib.import_module(target_module_name)
|
127
|
+
target_function = getattr(target_module, target_function_name)
|
128
|
+
|
129
|
+
return Daemon(
|
130
|
+
daemon_id=daemon_id,
|
131
|
+
target=target_function,
|
132
|
+
target_args=target_args,
|
133
|
+
target_kw=target_kw,
|
134
|
+
properties=properties,
|
135
|
+
label=label,
|
136
|
+
)
|
137
|
+
|
138
|
+
|
64
139
|
def __init__(
|
65
140
|
self,
|
66
141
|
target: Optional[Callable[[Any], Any]] = None,
|
67
|
-
target_args:
|
142
|
+
target_args: Union[List[Any], Tuple[Any], None] = None,
|
68
143
|
target_kw: Optional[Dict[str, Any]] = None,
|
69
144
|
daemon_id: Optional[str] = None,
|
70
145
|
label: Optional[str] = None,
|
@@ -76,7 +151,7 @@ class Daemon:
|
|
76
151
|
target: Optional[Callable[[Any], Any]], default None,
|
77
152
|
The function to execute in a child process.
|
78
153
|
|
79
|
-
target_args:
|
154
|
+
target_args: Union[List[Any], Tuple[Any], None], default None
|
80
155
|
Positional arguments to pass to the target function.
|
81
156
|
|
82
157
|
target_kw: Optional[Dict[str, Any]], default None
|
@@ -91,25 +166,54 @@ class Daemon:
|
|
91
166
|
Label string to help identifiy a daemon.
|
92
167
|
If `None`, use the function name instead.
|
93
168
|
|
94
|
-
|
169
|
+
properties: Optional[Dict[str, Any]], default None
|
95
170
|
Override reading from the properties JSON by providing an existing dictionary.
|
96
171
|
"""
|
97
172
|
_pickle = self.__dict__.get('_pickle', False)
|
98
173
|
if daemon_id is not None:
|
99
174
|
self.daemon_id = daemon_id
|
100
175
|
if not self.pickle_path.exists() and not target and ('target' not in self.__dict__):
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
176
|
+
|
177
|
+
if not self.properties_path.exists():
|
178
|
+
raise Exception(
|
179
|
+
f"Daemon '{self.daemon_id}' does not exist. "
|
180
|
+
+ "Pass a target to create a new Daemon."
|
181
|
+
)
|
182
|
+
|
183
|
+
try:
|
184
|
+
new_daemon = self.from_properties_file(daemon_id)
|
185
|
+
except Exception:
|
186
|
+
new_daemon = None
|
187
|
+
|
188
|
+
if new_daemon is not None:
|
189
|
+
new_daemon.write_pickle()
|
190
|
+
target = new_daemon.target
|
191
|
+
target_args = new_daemon.target_args
|
192
|
+
target_kw = new_daemon.target_kw
|
193
|
+
label = new_daemon.label
|
194
|
+
self._properties = new_daemon.properties
|
195
|
+
else:
|
196
|
+
try:
|
197
|
+
self.properties_path.unlink()
|
198
|
+
except Exception:
|
199
|
+
pass
|
200
|
+
|
201
|
+
raise Exception(
|
202
|
+
f"Could not recover daemon '{self.daemon_id}' "
|
203
|
+
+ "from its properties file."
|
204
|
+
)
|
205
|
+
|
105
206
|
if 'target' not in self.__dict__:
|
106
207
|
if target is None:
|
107
208
|
error("Cannot create a Daemon without a target.")
|
108
209
|
self.target = target
|
109
|
-
|
110
|
-
|
111
|
-
if '
|
112
|
-
self.
|
210
|
+
|
211
|
+
### NOTE: We have to check self.__dict__ in case we un-pickling.
|
212
|
+
if '_target_args' not in self.__dict__:
|
213
|
+
self._target_args = target_args
|
214
|
+
if '_target_kw' not in self.__dict__:
|
215
|
+
self._target_kw = target_kw
|
216
|
+
|
113
217
|
if 'label' not in self.__dict__:
|
114
218
|
if label is None:
|
115
219
|
label = (
|
@@ -188,6 +292,7 @@ class Daemon:
|
|
188
292
|
try:
|
189
293
|
os.environ['LINES'], os.environ['COLUMNS'] = str(int(lines)), str(int(columns))
|
190
294
|
with self._daemon_context:
|
295
|
+
sys.stdin = self.stdin_file
|
191
296
|
os.environ[STATIC_CONFIG['environment']['daemon_id']] = self.daemon_id
|
192
297
|
self.rotating_log.refresh_files(start_interception=True)
|
193
298
|
result = None
|
@@ -197,7 +302,7 @@ class Daemon:
|
|
197
302
|
|
198
303
|
self._log_refresh_timer.start()
|
199
304
|
self.properties['result'] = None
|
200
|
-
self.
|
305
|
+
self._capture_process_timestamp('began')
|
201
306
|
result = self.target(*self.target_args, **self.target_kw)
|
202
307
|
self.properties['result'] = result
|
203
308
|
except (BrokenPipeError, KeyboardInterrupt, SystemExit):
|
@@ -250,7 +355,7 @@ class Daemon:
|
|
250
355
|
if 'process' not in self.properties:
|
251
356
|
self.properties['process'] = {}
|
252
357
|
|
253
|
-
if process_key not in ('began', 'ended', 'paused'):
|
358
|
+
if process_key not in ('began', 'ended', 'paused', 'stopped'):
|
254
359
|
raise ValueError(f"Invalid key '{process_key}'.")
|
255
360
|
|
256
361
|
self.properties['process'][process_key] = (
|
@@ -290,6 +395,10 @@ class Daemon:
|
|
290
395
|
if self.status == 'paused':
|
291
396
|
return self.resume()
|
292
397
|
|
398
|
+
self._remove_stop_file()
|
399
|
+
if self.status == 'running':
|
400
|
+
return True, f"Daemon '{self}' is already running."
|
401
|
+
|
293
402
|
self.mkdir_if_not_exists(allow_dirty_run)
|
294
403
|
_write_pickle_success_tuple = self.write_pickle()
|
295
404
|
if not _write_pickle_success_tuple[0]:
|
@@ -312,7 +421,8 @@ class Daemon:
|
|
312
421
|
return _launch_success_bool, msg
|
313
422
|
|
314
423
|
def kill(self, timeout: Union[int, float, None] = 8) -> SuccessTuple:
|
315
|
-
"""
|
424
|
+
"""
|
425
|
+
Forcibly terminate a running daemon.
|
316
426
|
Sends a SIGTERM signal to the process.
|
317
427
|
|
318
428
|
Parameters
|
@@ -327,9 +437,11 @@ class Daemon:
|
|
327
437
|
if self.status != 'paused':
|
328
438
|
success, msg = self._send_signal(signal.SIGTERM, timeout=timeout)
|
329
439
|
if success:
|
440
|
+
self._write_stop_file('kill')
|
330
441
|
return success, msg
|
331
442
|
|
332
443
|
if self.status == 'stopped':
|
444
|
+
self._write_stop_file('kill')
|
333
445
|
return True, "Process has already stopped."
|
334
446
|
|
335
447
|
process = self.process
|
@@ -345,13 +457,18 @@ class Daemon:
|
|
345
457
|
self.pid_path.unlink()
|
346
458
|
except Exception as e:
|
347
459
|
pass
|
460
|
+
|
461
|
+
self._write_stop_file('kill')
|
348
462
|
return True, "Success"
|
349
463
|
|
350
464
|
def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
|
351
465
|
"""Gracefully quit a running daemon."""
|
352
466
|
if self.status == 'paused':
|
353
467
|
return self.kill(timeout)
|
468
|
+
|
354
469
|
signal_success, signal_msg = self._send_signal(signal.SIGINT, timeout=timeout)
|
470
|
+
if signal_success:
|
471
|
+
self._write_stop_file('quit')
|
355
472
|
return signal_success, signal_msg
|
356
473
|
|
357
474
|
def pause(
|
@@ -380,6 +497,7 @@ class Daemon:
|
|
380
497
|
if self.status == 'paused':
|
381
498
|
return True, f"Daemon '{self.daemon_id}' is already paused."
|
382
499
|
|
500
|
+
self._write_stop_file('pause')
|
383
501
|
try:
|
384
502
|
self.process.suspend()
|
385
503
|
except Exception as e:
|
@@ -418,10 +536,10 @@ class Daemon:
|
|
418
536
|
)
|
419
537
|
|
420
538
|
def resume(
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
539
|
+
self,
|
540
|
+
timeout: Union[int, float, None] = None,
|
541
|
+
check_timeout_interval: Union[float, int, None] = None,
|
542
|
+
) -> SuccessTuple:
|
425
543
|
"""
|
426
544
|
Resume the daemon if it is paused.
|
427
545
|
|
@@ -443,6 +561,7 @@ class Daemon:
|
|
443
561
|
if self.status == 'stopped':
|
444
562
|
return False, f"Daemon '{self.daemon_id}' is stopped and cannot be resumed."
|
445
563
|
|
564
|
+
self._remove_stop_file()
|
446
565
|
try:
|
447
566
|
self.process.resume()
|
448
567
|
except Exception as e:
|
@@ -472,6 +591,33 @@ class Daemon:
|
|
472
591
|
+ ('s' if timeout != 1 else '') + '.'
|
473
592
|
)
|
474
593
|
|
594
|
+
def _write_stop_file(self, action: str) -> SuccessTuple:
|
595
|
+
"""Write the stop file timestamp and action."""
|
596
|
+
if action not in ('quit', 'kill', 'pause'):
|
597
|
+
return False, f"Unsupported action '{action}'."
|
598
|
+
|
599
|
+
with open(self.stop_path, 'w+', encoding='utf-8') as f:
|
600
|
+
json.dump(
|
601
|
+
{
|
602
|
+
'stop_time': datetime.now(timezone.utc).isoformat(),
|
603
|
+
'action': action,
|
604
|
+
},
|
605
|
+
f
|
606
|
+
)
|
607
|
+
|
608
|
+
return True, "Success"
|
609
|
+
|
610
|
+
def _remove_stop_file(self) -> SuccessTuple:
|
611
|
+
"""Remove the stop file"""
|
612
|
+
if not self.stop_path.exists():
|
613
|
+
return True, "Stop file does not exist."
|
614
|
+
|
615
|
+
try:
|
616
|
+
self.stop_path.unlink()
|
617
|
+
except Exception as e:
|
618
|
+
return False, f"Failed to remove stop file:\n{e}"
|
619
|
+
|
620
|
+
return True, "Success"
|
475
621
|
|
476
622
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
477
623
|
"""
|
@@ -638,6 +784,13 @@ class Daemon:
|
|
638
784
|
"""
|
639
785
|
return self._get_properties_path_from_daemon_id(self.daemon_id)
|
640
786
|
|
787
|
+
@property
|
788
|
+
def stop_path(self) -> pathlib.Path:
|
789
|
+
"""
|
790
|
+
Return the path for the stop file (created when manually stopped).
|
791
|
+
"""
|
792
|
+
return self.path / '.stop.json'
|
793
|
+
|
641
794
|
@property
|
642
795
|
def log_path(self) -> pathlib.Path:
|
643
796
|
"""
|
@@ -645,6 +798,20 @@ class Daemon:
|
|
645
798
|
"""
|
646
799
|
return LOGS_RESOURCES_PATH / (self.daemon_id + '.log')
|
647
800
|
|
801
|
+
@property
|
802
|
+
def stdin_file_path(self) -> pathlib.Path:
|
803
|
+
"""
|
804
|
+
Return the stdin file path.
|
805
|
+
"""
|
806
|
+
return self.path / 'input.stdin'
|
807
|
+
|
808
|
+
@property
|
809
|
+
def blocking_stdin_file_path(self) -> pathlib.Path:
|
810
|
+
"""
|
811
|
+
Return the stdin file path.
|
812
|
+
"""
|
813
|
+
return self.path / 'input.stdin.block'
|
814
|
+
|
648
815
|
@property
|
649
816
|
def log_offset_path(self) -> pathlib.Path:
|
650
817
|
"""
|
@@ -654,20 +821,44 @@ class Daemon:
|
|
654
821
|
|
655
822
|
@property
|
656
823
|
def rotating_log(self) -> RotatingFile:
|
824
|
+
"""
|
825
|
+
The rotating log file for the daemon's output.
|
826
|
+
"""
|
657
827
|
if '_rotating_log' in self.__dict__:
|
658
828
|
return self._rotating_log
|
659
829
|
|
830
|
+
write_timestamps = (
|
831
|
+
self.properties.get('logs', {}).get('write_timestamps', None)
|
832
|
+
)
|
833
|
+
if write_timestamps is None:
|
834
|
+
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled')
|
835
|
+
|
660
836
|
self._rotating_log = RotatingFile(
|
661
837
|
self.log_path,
|
662
|
-
redirect_streams
|
663
|
-
write_timestamps
|
664
|
-
timestamp_format
|
838
|
+
redirect_streams=True,
|
839
|
+
write_timestamps=write_timestamps,
|
840
|
+
timestamp_format=get_config('jobs', 'logs', 'timestamps', 'format'),
|
665
841
|
)
|
666
842
|
return self._rotating_log
|
667
843
|
|
844
|
+
@property
|
845
|
+
def stdin_file(self):
|
846
|
+
"""
|
847
|
+
Return the file handler for the stdin file.
|
848
|
+
"""
|
849
|
+
if '_stdin_file' in self.__dict__:
|
850
|
+
return self._stdin_file
|
851
|
+
|
852
|
+
self._stdin_file = StdinFile(
|
853
|
+
self.stdin_file_path,
|
854
|
+
lock_file_path=self.blocking_stdin_file_path,
|
855
|
+
)
|
856
|
+
return self._stdin_file
|
857
|
+
|
668
858
|
@property
|
669
859
|
def log_text(self) -> Optional[str]:
|
670
|
-
"""
|
860
|
+
"""
|
861
|
+
Read the log files and return their contents.
|
671
862
|
Returns `None` if the log file does not exist.
|
672
863
|
"""
|
673
864
|
new_rotating_log = RotatingFile(
|
@@ -717,7 +908,8 @@ class Daemon:
|
|
717
908
|
|
718
909
|
@property
|
719
910
|
def pid(self) -> Union[int, None]:
|
720
|
-
"""
|
911
|
+
"""
|
912
|
+
Read the PID file and return its contents.
|
721
913
|
Returns `None` if the PID file does not exist.
|
722
914
|
"""
|
723
915
|
if not self.pid_path.exists():
|
@@ -725,6 +917,8 @@ class Daemon:
|
|
725
917
|
try:
|
726
918
|
with open(self.pid_path, 'r', encoding='utf-8') as f:
|
727
919
|
text = f.read()
|
920
|
+
if len(text) == 0:
|
921
|
+
return None
|
728
922
|
pid = int(text.rstrip())
|
729
923
|
except Exception as e:
|
730
924
|
warn(e)
|
@@ -764,17 +958,21 @@ class Daemon:
|
|
764
958
|
return None
|
765
959
|
try:
|
766
960
|
with open(self.properties_path, 'r', encoding='utf-8') as file:
|
767
|
-
|
961
|
+
properties = json.load(file)
|
768
962
|
except Exception as e:
|
769
|
-
|
963
|
+
properties = {}
|
964
|
+
|
965
|
+
return properties
|
770
966
|
|
771
967
|
def read_pickle(self) -> Daemon:
|
772
968
|
"""Read a Daemon's pickle file and return the `Daemon`."""
|
773
969
|
import pickle, traceback
|
774
970
|
if not self.pickle_path.exists():
|
775
971
|
error(f"Pickle file does not exist for daemon '{self.daemon_id}'.")
|
972
|
+
|
776
973
|
if self.pickle_path.stat().st_size == 0:
|
777
974
|
error(f"Pickle was empty for daemon '{self.daemon_id}'.")
|
975
|
+
|
778
976
|
try:
|
779
977
|
with open(self.pickle_path, 'rb') as pickle_file:
|
780
978
|
daemon = pickle.load(pickle_file)
|
@@ -921,9 +1119,9 @@ class Daemon:
|
|
921
1119
|
|
922
1120
|
|
923
1121
|
def get_check_timeout_interval_seconds(
|
924
|
-
|
925
|
-
|
926
|
-
|
1122
|
+
self,
|
1123
|
+
check_timeout_interval: Union[int, float, None] = None,
|
1124
|
+
) -> Union[int, float]:
|
927
1125
|
"""
|
928
1126
|
Return the interval value to check the status of timeouts.
|
929
1127
|
"""
|
@@ -931,6 +1129,33 @@ class Daemon:
|
|
931
1129
|
return check_timeout_interval
|
932
1130
|
return get_config('jobs', 'check_timeout_interval_seconds')
|
933
1131
|
|
1132
|
+
@property
|
1133
|
+
def target_args(self) -> Union[Tuple[Any], None]:
|
1134
|
+
"""
|
1135
|
+
Return the positional arguments to pass to the target function.
|
1136
|
+
"""
|
1137
|
+
target_args = (
|
1138
|
+
self.__dict__.get('_target_args', None)
|
1139
|
+
or self.properties.get('target', {}).get('args', None)
|
1140
|
+
)
|
1141
|
+
if target_args is None:
|
1142
|
+
return tuple([])
|
1143
|
+
|
1144
|
+
return tuple(target_args)
|
1145
|
+
|
1146
|
+
@property
|
1147
|
+
def target_kw(self) -> Union[Dict[str, Any], None]:
|
1148
|
+
"""
|
1149
|
+
Return the keyword arguments to pass to the target function.
|
1150
|
+
"""
|
1151
|
+
target_kw = (
|
1152
|
+
self.__dict__.get('_target_kw', None)
|
1153
|
+
or self.properties.get('target', {}).get('kw', None)
|
1154
|
+
)
|
1155
|
+
if target_kw is None:
|
1156
|
+
return {}
|
1157
|
+
|
1158
|
+
return {key: val for key, val in target_kw.items()}
|
934
1159
|
|
935
1160
|
def __getstate__(self):
|
936
1161
|
"""
|
@@ -981,4 +1206,4 @@ class Daemon:
|
|
981
1206
|
return self.daemon_id == other.daemon_id
|
982
1207
|
|
983
1208
|
def __hash__(self):
|
984
|
-
return hash(self.daemon_id)
|
1209
|
+
return hash(self.daemon_id)
|
@@ -112,7 +112,7 @@ class FileDescriptorInterceptor:
|
|
112
112
|
except OSError as e:
|
113
113
|
if e.errno != FD_CLOSED:
|
114
114
|
warn(
|
115
|
-
|
115
|
+
"Error while trying to close the duplicated file descriptor:\n"
|
116
116
|
+ f"{traceback.format_exc()}"
|
117
117
|
)
|
118
118
|
|
@@ -121,7 +121,7 @@ class FileDescriptorInterceptor:
|
|
121
121
|
except OSError as e:
|
122
122
|
if e.errno != FD_CLOSED:
|
123
123
|
warn(
|
124
|
-
|
124
|
+
"Error while trying to close the write-pipe "
|
125
125
|
+ "to the intercepted file descriptor:\n"
|
126
126
|
+ f"{traceback.format_exc()}"
|
127
127
|
)
|
@@ -130,7 +130,7 @@ class FileDescriptorInterceptor:
|
|
130
130
|
except OSError as e:
|
131
131
|
if e.errno != FD_CLOSED:
|
132
132
|
warn(
|
133
|
-
|
133
|
+
"Error while trying to close the read-pipe "
|
134
134
|
+ "to the intercepted file descriptor:\n"
|
135
135
|
+ f"{traceback.format_exc()}"
|
136
136
|
)
|
@@ -140,7 +140,7 @@ class FileDescriptorInterceptor:
|
|
140
140
|
except OSError as e:
|
141
141
|
if e.errno != FD_CLOSED:
|
142
142
|
warn(
|
143
|
-
|
143
|
+
"Error while trying to close the signal-read-pipe "
|
144
144
|
+ "to the intercepted file descriptor:\n"
|
145
145
|
+ f"{traceback.format_exc()}"
|
146
146
|
)
|
@@ -150,7 +150,7 @@ class FileDescriptorInterceptor:
|
|
150
150
|
except OSError as e:
|
151
151
|
if e.errno != FD_CLOSED:
|
152
152
|
warn(
|
153
|
-
|
153
|
+
"Error while trying to close the signal-write-pipe "
|
154
154
|
+ "to the intercepted file descriptor:\n"
|
155
155
|
+ f"{traceback.format_exc()}"
|
156
156
|
)
|