meerschaum 2.9.5__py3-none-any.whl → 3.0.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.
- meerschaum/__init__.py +5 -2
- meerschaum/_internal/__init__.py +1 -0
- meerschaum/_internal/arguments/_parse_arguments.py +4 -4
- meerschaum/_internal/arguments/_parser.py +33 -4
- meerschaum/_internal/cli/__init__.py +6 -0
- meerschaum/_internal/cli/daemons.py +103 -0
- meerschaum/_internal/cli/entry.py +220 -0
- meerschaum/_internal/cli/workers.py +435 -0
- meerschaum/_internal/docs/index.py +48 -2
- meerschaum/_internal/entry.py +50 -14
- meerschaum/_internal/shell/Shell.py +121 -29
- meerschaum/_internal/shell/__init__.py +4 -1
- meerschaum/_internal/static.py +359 -0
- meerschaum/_internal/term/TermPageHandler.py +1 -2
- meerschaum/_internal/term/__init__.py +40 -6
- meerschaum/_internal/term/tools.py +33 -8
- meerschaum/actions/__init__.py +6 -4
- meerschaum/actions/api.py +53 -13
- meerschaum/actions/attach.py +1 -0
- meerschaum/actions/bootstrap.py +8 -8
- meerschaum/actions/delete.py +4 -2
- meerschaum/actions/edit.py +171 -25
- meerschaum/actions/login.py +8 -8
- meerschaum/actions/register.py +143 -6
- meerschaum/actions/reload.py +22 -5
- meerschaum/actions/restart.py +14 -0
- meerschaum/actions/show.py +184 -31
- meerschaum/actions/start.py +166 -17
- meerschaum/actions/stop.py +38 -2
- meerschaum/actions/sync.py +7 -2
- meerschaum/actions/tag.py +9 -8
- meerschaum/actions/verify.py +5 -8
- meerschaum/api/__init__.py +45 -15
- meerschaum/api/_events.py +46 -4
- meerschaum/api/_oauth2.py +162 -9
- meerschaum/api/_tokens.py +102 -0
- meerschaum/api/dash/__init__.py +0 -3
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/custom.py +4 -3
- meerschaum/api/dash/callbacks/dashboard.py +198 -118
- meerschaum/api/dash/callbacks/jobs.py +14 -7
- meerschaum/api/dash/callbacks/login.py +10 -1
- meerschaum/api/dash/callbacks/pipes.py +194 -14
- meerschaum/api/dash/callbacks/plugins.py +0 -1
- meerschaum/api/dash/callbacks/register.py +10 -3
- meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
- meerschaum/api/dash/callbacks/tokens.py +389 -0
- meerschaum/api/dash/components.py +36 -15
- meerschaum/api/dash/jobs.py +1 -1
- meerschaum/api/dash/keys.py +35 -93
- meerschaum/api/dash/pages/__init__.py +2 -1
- meerschaum/api/dash/pages/dashboard.py +1 -20
- meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
- meerschaum/api/dash/pages/login.py +2 -2
- meerschaum/api/dash/pages/pipes.py +16 -5
- meerschaum/api/dash/pages/settings/password_reset.py +1 -1
- meerschaum/api/dash/pages/tokens.py +53 -0
- meerschaum/api/dash/pipes.py +382 -95
- meerschaum/api/dash/sessions.py +12 -0
- meerschaum/api/dash/tokens.py +603 -0
- meerschaum/api/dash/websockets.py +1 -1
- meerschaum/api/dash/webterm.py +18 -6
- meerschaum/api/models/__init__.py +23 -3
- meerschaum/api/models/_actions.py +22 -0
- meerschaum/api/models/_pipes.py +91 -7
- meerschaum/api/models/_tokens.py +81 -0
- meerschaum/api/resources/static/js/terminado.js +3 -0
- meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
- meerschaum/api/resources/templates/termpage.html +13 -0
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +3 -4
- meerschaum/api/routes/_connectors.py +3 -7
- meerschaum/api/routes/_jobs.py +26 -35
- meerschaum/api/routes/_login.py +120 -15
- meerschaum/api/routes/_misc.py +5 -10
- meerschaum/api/routes/_pipes.py +178 -143
- meerschaum/api/routes/_plugins.py +38 -28
- meerschaum/api/routes/_tokens.py +236 -0
- meerschaum/api/routes/_users.py +47 -35
- meerschaum/api/routes/_version.py +3 -3
- meerschaum/api/routes/_webterm.py +3 -3
- meerschaum/config/__init__.py +100 -30
- meerschaum/config/_default.py +132 -64
- meerschaum/config/_edit.py +38 -32
- meerschaum/config/_formatting.py +2 -0
- meerschaum/config/_patch.py +10 -8
- meerschaum/config/_paths.py +133 -13
- meerschaum/config/_read_config.py +87 -36
- meerschaum/config/_sync.py +6 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/environment.py +262 -0
- meerschaum/config/stack/__init__.py +37 -15
- meerschaum/config/static.py +18 -0
- meerschaum/connectors/_Connector.py +11 -6
- meerschaum/connectors/__init__.py +41 -22
- meerschaum/connectors/api/_APIConnector.py +34 -6
- meerschaum/connectors/api/_actions.py +2 -2
- meerschaum/connectors/api/_jobs.py +12 -1
- meerschaum/connectors/api/_login.py +33 -7
- meerschaum/connectors/api/_misc.py +2 -2
- meerschaum/connectors/api/_pipes.py +23 -32
- meerschaum/connectors/api/_plugins.py +2 -2
- meerschaum/connectors/api/_request.py +1 -1
- meerschaum/connectors/api/_tokens.py +146 -0
- meerschaum/connectors/api/_users.py +70 -58
- meerschaum/connectors/instance/_InstanceConnector.py +83 -0
- meerschaum/connectors/instance/__init__.py +10 -0
- meerschaum/connectors/instance/_pipes.py +442 -0
- meerschaum/connectors/instance/_plugins.py +159 -0
- meerschaum/connectors/instance/_tokens.py +317 -0
- meerschaum/connectors/instance/_users.py +188 -0
- meerschaum/connectors/parse.py +5 -2
- meerschaum/connectors/sql/_SQLConnector.py +22 -5
- meerschaum/connectors/sql/_cli.py +12 -11
- meerschaum/connectors/sql/_create_engine.py +12 -168
- meerschaum/connectors/sql/_fetch.py +2 -18
- meerschaum/connectors/sql/_pipes.py +295 -278
- meerschaum/connectors/sql/_plugins.py +29 -0
- meerschaum/connectors/sql/_sql.py +46 -21
- meerschaum/connectors/sql/_users.py +36 -2
- meerschaum/connectors/sql/tables/__init__.py +254 -122
- meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
- meerschaum/connectors/valkey/_pipes.py +60 -31
- meerschaum/connectors/valkey/_plugins.py +2 -26
- meerschaum/core/Pipe/__init__.py +115 -85
- meerschaum/core/Pipe/_attributes.py +425 -124
- meerschaum/core/Pipe/_bootstrap.py +54 -24
- meerschaum/core/Pipe/_cache.py +555 -0
- meerschaum/core/Pipe/_clear.py +0 -11
- meerschaum/core/Pipe/_data.py +96 -68
- meerschaum/core/Pipe/_deduplicate.py +0 -13
- meerschaum/core/Pipe/_delete.py +12 -21
- meerschaum/core/Pipe/_drop.py +11 -23
- meerschaum/core/Pipe/_dtypes.py +49 -19
- meerschaum/core/Pipe/_edit.py +14 -4
- meerschaum/core/Pipe/_fetch.py +1 -1
- meerschaum/core/Pipe/_index.py +8 -14
- meerschaum/core/Pipe/_show.py +5 -5
- meerschaum/core/Pipe/_sync.py +123 -204
- meerschaum/core/Pipe/_verify.py +4 -4
- meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
- meerschaum/core/Plugin/__init__.py +1 -1
- meerschaum/core/Token/_Token.py +220 -0
- meerschaum/core/Token/__init__.py +12 -0
- meerschaum/core/User/_User.py +35 -10
- meerschaum/core/User/__init__.py +9 -1
- meerschaum/core/__init__.py +1 -0
- meerschaum/jobs/_Executor.py +88 -4
- meerschaum/jobs/_Job.py +149 -38
- meerschaum/jobs/__init__.py +3 -2
- meerschaum/jobs/systemd.py +8 -3
- meerschaum/models/__init__.py +35 -0
- meerschaum/models/pipes.py +247 -0
- meerschaum/models/tokens.py +38 -0
- meerschaum/models/users.py +26 -0
- meerschaum/plugins/__init__.py +301 -88
- meerschaum/plugins/bootstrap.py +510 -4
- meerschaum/utils/_get_pipes.py +97 -30
- meerschaum/utils/daemon/Daemon.py +199 -43
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
- meerschaum/utils/daemon/RotatingFile.py +63 -36
- meerschaum/utils/daemon/StdinFile.py +53 -13
- meerschaum/utils/daemon/__init__.py +47 -6
- meerschaum/utils/daemon/_names.py +6 -3
- meerschaum/utils/dataframe.py +479 -81
- meerschaum/utils/debug.py +49 -19
- meerschaum/utils/dtypes/__init__.py +476 -34
- meerschaum/utils/dtypes/sql.py +369 -29
- meerschaum/utils/formatting/__init__.py +5 -2
- meerschaum/utils/formatting/_jobs.py +1 -1
- meerschaum/utils/formatting/_pipes.py +52 -50
- meerschaum/utils/formatting/_pprint.py +1 -0
- meerschaum/utils/formatting/_shell.py +44 -18
- meerschaum/utils/misc.py +268 -186
- meerschaum/utils/packages/__init__.py +25 -40
- meerschaum/utils/packages/_packages.py +42 -34
- meerschaum/utils/pipes.py +213 -0
- meerschaum/utils/process.py +2 -2
- meerschaum/utils/prompt.py +175 -144
- meerschaum/utils/schedule.py +2 -1
- meerschaum/utils/sql.py +134 -47
- meerschaum/utils/threading.py +42 -0
- meerschaum/utils/typing.py +1 -4
- meerschaum/utils/venv/_Venv.py +2 -2
- meerschaum/utils/venv/__init__.py +7 -7
- meerschaum/utils/warnings.py +19 -13
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
- meerschaum-3.0.0.dist-info/RECORD +289 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
- meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
- meerschaum/api/models/_interfaces.py +0 -15
- meerschaum/api/models/_locations.py +0 -15
- meerschaum/api/models/_metrics.py +0 -15
- meerschaum/config/_environment.py +0 -145
- meerschaum/config/static/__init__.py +0 -186
- meerschaum-2.9.5.dist-info/RECORD +0 -263
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -24,10 +24,7 @@ from meerschaum.utils.typing import (
|
|
24
24
|
is_success_tuple, Tuple,
|
25
25
|
)
|
26
26
|
from meerschaum.config import get_config
|
27
|
-
from meerschaum.
|
28
|
-
from meerschaum.config._paths import (
|
29
|
-
DAEMON_RESOURCES_PATH, LOGS_RESOURCES_PATH, DAEMON_ERROR_LOG_PATH,
|
30
|
-
)
|
27
|
+
from meerschaum._internal.static import STATIC_CONFIG
|
31
28
|
from meerschaum.config._patch import apply_patch_to_config
|
32
29
|
from meerschaum.utils.warnings import warn, error
|
33
30
|
from meerschaum.utils.packages import attempt_import
|
@@ -144,6 +141,7 @@ class Daemon:
|
|
144
141
|
daemon_id: Optional[str] = None,
|
145
142
|
label: Optional[str] = None,
|
146
143
|
properties: Optional[Dict[str, Any]] = None,
|
144
|
+
pickle: bool = True,
|
147
145
|
):
|
148
146
|
"""
|
149
147
|
Parameters
|
@@ -211,6 +209,8 @@ class Daemon:
|
|
211
209
|
error("Cannot create a Daemon without a target.")
|
212
210
|
self.target = target
|
213
211
|
|
212
|
+
self.pickle = pickle
|
213
|
+
|
214
214
|
### NOTE: We have to check self.__dict__ in case we un-pickling.
|
215
215
|
if '_target_args' not in self.__dict__:
|
216
216
|
self._target_args = target_args
|
@@ -224,10 +224,17 @@ class Daemon:
|
|
224
224
|
else str(self.target)
|
225
225
|
)
|
226
226
|
self.label = label
|
227
|
+
elif label is not None:
|
228
|
+
self.label = label
|
229
|
+
|
227
230
|
if 'daemon_id' not in self.__dict__:
|
228
231
|
self.daemon_id = get_new_daemon_name()
|
229
232
|
if '_properties' not in self.__dict__:
|
230
233
|
self._properties = properties
|
234
|
+
elif properties:
|
235
|
+
if self._properties is None:
|
236
|
+
self._properties = {}
|
237
|
+
self._properties.update(properties)
|
231
238
|
if self._properties is None:
|
232
239
|
self._properties = {}
|
233
240
|
|
@@ -276,6 +283,24 @@ class Daemon:
|
|
276
283
|
|
277
284
|
self._setup(allow_dirty_run)
|
278
285
|
|
286
|
+
_daemons.append(self)
|
287
|
+
|
288
|
+
logs_cf = self.properties.get('logs', {})
|
289
|
+
log_refresh_seconds = logs_cf.get('refresh_files_seconds', None)
|
290
|
+
if log_refresh_seconds is None:
|
291
|
+
log_refresh_seconds = get_config('jobs', 'logs', 'refresh_files_seconds')
|
292
|
+
write_timestamps = logs_cf.get('write_timestamps', None)
|
293
|
+
if write_timestamps is None:
|
294
|
+
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled')
|
295
|
+
|
296
|
+
self._log_refresh_timer = RepeatTimer(
|
297
|
+
log_refresh_seconds,
|
298
|
+
partial(self.rotating_log.refresh_files, start_interception=write_timestamps),
|
299
|
+
)
|
300
|
+
|
301
|
+
capture_stdin = logs_cf.get('stdin', True)
|
302
|
+
cwd = self.properties.get('cwd', os.getcwd())
|
303
|
+
|
279
304
|
### NOTE: The SIGINT handler has been removed so that child processes may handle
|
280
305
|
### KeyboardInterrupts themselves.
|
281
306
|
### The previous aggressive approach was redundant because of the SIGTERM handler.
|
@@ -283,7 +308,8 @@ class Daemon:
|
|
283
308
|
pidfile=self.pid_lock,
|
284
309
|
stdout=self.rotating_log,
|
285
310
|
stderr=self.rotating_log,
|
286
|
-
|
311
|
+
stdin=(self.stdin_file if capture_stdin else None),
|
312
|
+
working_directory=cwd,
|
287
313
|
detach_process=True,
|
288
314
|
files_preserve=list(self.rotating_log.subfile_objects.values()),
|
289
315
|
signal_map={
|
@@ -291,18 +317,14 @@ class Daemon:
|
|
291
317
|
},
|
292
318
|
)
|
293
319
|
|
294
|
-
|
295
|
-
|
296
|
-
log_refresh_seconds = get_config('jobs', 'logs', 'refresh_files_seconds')
|
297
|
-
self._log_refresh_timer = RepeatTimer(
|
298
|
-
log_refresh_seconds,
|
299
|
-
partial(self.rotating_log.refresh_files, start_interception=True),
|
300
|
-
)
|
320
|
+
if capture_stdin and sys.stdin is None:
|
321
|
+
raise OSError("Cannot daemonize without stdin.")
|
301
322
|
|
302
323
|
try:
|
303
324
|
os.environ['LINES'], os.environ['COLUMNS'] = str(int(lines)), str(int(columns))
|
304
325
|
with self._daemon_context:
|
305
|
-
|
326
|
+
if capture_stdin:
|
327
|
+
sys.stdin = self.stdin_file
|
306
328
|
_ = os.environ.pop(STATIC_CONFIG['environment']['systemd_stdin_path'], None)
|
307
329
|
os.environ[STATIC_CONFIG['environment']['daemon_id']] = self.daemon_id
|
308
330
|
os.environ['PYTHONUNBUFFERED'] = '1'
|
@@ -338,6 +360,7 @@ class Daemon:
|
|
338
360
|
result = False, str(e)
|
339
361
|
finally:
|
340
362
|
_results[self.daemon_id] = result
|
363
|
+
self.properties['result'] = result
|
341
364
|
|
342
365
|
if keep_daemon_output:
|
343
366
|
self._capture_process_timestamp('ended')
|
@@ -359,8 +382,15 @@ class Daemon:
|
|
359
382
|
|
360
383
|
except Exception:
|
361
384
|
daemon_error = traceback.format_exc()
|
385
|
+
from meerschaum.config.paths import DAEMON_ERROR_LOG_PATH
|
362
386
|
with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
|
363
|
-
f.write(
|
387
|
+
f.write(
|
388
|
+
f"Error in Daemon '{self}':\n\n"
|
389
|
+
f"{sys.stdin=}\n"
|
390
|
+
f"{self.stdin_file_path=}\n"
|
391
|
+
f"{self.stdin_file_path.exists()=}\n\n"
|
392
|
+
f"{daemon_error}\n\n"
|
393
|
+
)
|
364
394
|
warn(f"Encountered an error while running the daemon '{self}':\n{daemon_error}")
|
365
395
|
|
366
396
|
def _capture_process_timestamp(
|
@@ -395,6 +425,8 @@ class Daemon:
|
|
395
425
|
self,
|
396
426
|
keep_daemon_output: bool = True,
|
397
427
|
allow_dirty_run: bool = False,
|
428
|
+
wait: bool = False,
|
429
|
+
timeout: Union[int, float] = 4,
|
398
430
|
debug: bool = False,
|
399
431
|
) -> SuccessTuple:
|
400
432
|
"""Run the daemon as a child process and continue executing the parent.
|
@@ -409,6 +441,12 @@ class Daemon:
|
|
409
441
|
This option is dangerous because if the same `daemon_id` runs concurrently,
|
410
442
|
the last to finish will overwrite the output of the first.
|
411
443
|
|
444
|
+
wait: bool, default True
|
445
|
+
If `True`, block until `Daemon.status` is running (or the timeout expires).
|
446
|
+
|
447
|
+
timeout: Union[int, float], default 4
|
448
|
+
If `wait` is `True`, block for up to `timeout` seconds before returning a failure.
|
449
|
+
|
412
450
|
Returns
|
413
451
|
-------
|
414
452
|
A SuccessTuple indicating success.
|
@@ -432,20 +470,44 @@ class Daemon:
|
|
432
470
|
return _write_pickle_success_tuple
|
433
471
|
|
434
472
|
_launch_daemon_code = (
|
435
|
-
"from meerschaum.utils.daemon import Daemon; "
|
436
|
-
|
437
|
-
|
438
|
-
|
473
|
+
"from meerschaum.utils.daemon import Daemon, _daemons; "
|
474
|
+
f"daemon = Daemon(daemon_id='{self.daemon_id}'); "
|
475
|
+
f"_daemons['{self.daemon_id}'] = daemon; "
|
476
|
+
f"daemon._run_exit(keep_daemon_output={keep_daemon_output}, "
|
477
|
+
"allow_dirty_run=True)"
|
439
478
|
)
|
440
479
|
env = dict(os.environ)
|
441
|
-
env[STATIC_CONFIG['environment']['noninteractive']] = 'true'
|
442
480
|
_launch_success_bool = venv_exec(_launch_daemon_code, debug=debug, venv=None, env=env)
|
443
481
|
msg = (
|
444
482
|
"Success"
|
445
483
|
if _launch_success_bool
|
446
484
|
else f"Failed to start daemon '{self.daemon_id}'."
|
447
485
|
)
|
448
|
-
|
486
|
+
if not wait or not _launch_success_bool:
|
487
|
+
return _launch_success_bool, msg
|
488
|
+
|
489
|
+
timeout = self.get_timeout_seconds(timeout)
|
490
|
+
check_timeout_interval = self.get_check_timeout_interval_seconds()
|
491
|
+
|
492
|
+
if not timeout:
|
493
|
+
success = self.status == 'running'
|
494
|
+
msg = "Success" if success else f"Failed to run daemon '{self.daemon_id}'."
|
495
|
+
if success:
|
496
|
+
self._capture_process_timestamp('began')
|
497
|
+
return success, msg
|
498
|
+
|
499
|
+
begin = time.perf_counter()
|
500
|
+
while (time.perf_counter() - begin) < timeout:
|
501
|
+
if self.status == 'running':
|
502
|
+
self._capture_process_timestamp('began')
|
503
|
+
return True, "Success"
|
504
|
+
time.sleep(check_timeout_interval)
|
505
|
+
|
506
|
+
return False, (
|
507
|
+
f"Failed to start daemon '{self.daemon_id}' within {timeout} second"
|
508
|
+
+ ('s' if timeout != 1 else '') + '.'
|
509
|
+
)
|
510
|
+
|
449
511
|
|
450
512
|
def kill(self, timeout: Union[int, float, None] = 8) -> SuccessTuple:
|
451
513
|
"""
|
@@ -465,10 +527,14 @@ class Daemon:
|
|
465
527
|
success, msg = self._send_signal(signal.SIGTERM, timeout=timeout)
|
466
528
|
if success:
|
467
529
|
self._write_stop_file('kill')
|
530
|
+
self.stdin_file.close()
|
531
|
+
self._remove_blocking_stdin_file()
|
468
532
|
return success, msg
|
469
533
|
|
470
534
|
if self.status == 'stopped':
|
471
535
|
self._write_stop_file('kill')
|
536
|
+
self.stdin_file.close()
|
537
|
+
self._remove_blocking_stdin_file()
|
472
538
|
return True, "Process has already stopped."
|
473
539
|
|
474
540
|
psutil = attempt_import('psutil')
|
@@ -493,6 +559,8 @@ class Daemon:
|
|
493
559
|
pass
|
494
560
|
|
495
561
|
self._write_stop_file('kill')
|
562
|
+
self.stdin_file.close()
|
563
|
+
self._remove_blocking_stdin_file()
|
496
564
|
return True, "Success"
|
497
565
|
|
498
566
|
def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
|
@@ -503,6 +571,8 @@ class Daemon:
|
|
503
571
|
signal_success, signal_msg = self._send_signal(signal.SIGINT, timeout=timeout)
|
504
572
|
if signal_success:
|
505
573
|
self._write_stop_file('quit')
|
574
|
+
self.stdin_file.close()
|
575
|
+
self._remove_blocking_stdin_file()
|
506
576
|
return signal_success, signal_msg
|
507
577
|
|
508
578
|
def pause(
|
@@ -525,6 +595,8 @@ class Daemon:
|
|
525
595
|
-------
|
526
596
|
A `SuccessTuple` indicating whether the `Daemon` process was successfully suspended.
|
527
597
|
"""
|
598
|
+
self._remove_blocking_stdin_file()
|
599
|
+
|
528
600
|
if self.process is None:
|
529
601
|
return False, f"Daemon '{self.daemon_id}' is not running and cannot be paused."
|
530
602
|
|
@@ -532,6 +604,8 @@ class Daemon:
|
|
532
604
|
return True, f"Daemon '{self.daemon_id}' is already paused."
|
533
605
|
|
534
606
|
self._write_stop_file('pause')
|
607
|
+
self.stdin_file.close()
|
608
|
+
self._remove_blocking_stdin_file()
|
535
609
|
try:
|
536
610
|
self.process.suspend()
|
537
611
|
except Exception as e:
|
@@ -597,6 +671,9 @@ class Daemon:
|
|
597
671
|
|
598
672
|
self._remove_stop_file()
|
599
673
|
try:
|
674
|
+
if self.process is None:
|
675
|
+
return False, f"Cannot resume daemon '{self.daemon_id}'."
|
676
|
+
|
600
677
|
self.process.resume()
|
601
678
|
except Exception as e:
|
602
679
|
return False, f"Failed to resume daemon '{self.daemon_id}':\n{e}"
|
@@ -670,6 +747,18 @@ class Daemon:
|
|
670
747
|
except Exception:
|
671
748
|
return {}
|
672
749
|
|
750
|
+
def _remove_blocking_stdin_file(self) -> mrsm.SuccessTuple:
|
751
|
+
"""
|
752
|
+
Remove the blocking STDIN file if it exists.
|
753
|
+
"""
|
754
|
+
try:
|
755
|
+
if self.blocking_stdin_file_path.exists():
|
756
|
+
self.blocking_stdin_file_path.unlink()
|
757
|
+
except Exception as e:
|
758
|
+
return False, str(e)
|
759
|
+
|
760
|
+
return True, "Success"
|
761
|
+
|
673
762
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
674
763
|
"""
|
675
764
|
Handle `SIGTERM` within the `Daemon` context.
|
@@ -798,7 +887,7 @@ class Daemon:
|
|
798
887
|
if self.process is None:
|
799
888
|
return 'stopped'
|
800
889
|
|
801
|
-
psutil = attempt_import('psutil')
|
890
|
+
psutil = attempt_import('psutil', lazy=False)
|
802
891
|
try:
|
803
892
|
if self.process.status() == 'stopped':
|
804
893
|
return 'paused'
|
@@ -819,6 +908,7 @@ class Daemon:
|
|
819
908
|
"""
|
820
909
|
Return a Daemon's path from its `daemon_id`.
|
821
910
|
"""
|
911
|
+
from meerschaum.config.paths import DAEMON_RESOURCES_PATH
|
822
912
|
return DAEMON_RESOURCES_PATH / daemon_id
|
823
913
|
|
824
914
|
@property
|
@@ -854,7 +944,12 @@ class Daemon:
|
|
854
944
|
"""
|
855
945
|
Return the log path.
|
856
946
|
"""
|
857
|
-
|
947
|
+
logs_cf = self.properties.get('logs', None) or {}
|
948
|
+
if 'path' not in logs_cf:
|
949
|
+
from meerschaum.config.paths import LOGS_RESOURCES_PATH
|
950
|
+
return LOGS_RESOURCES_PATH / (self.daemon_id + '.log')
|
951
|
+
|
952
|
+
return pathlib.Path(logs_cf['path'])
|
858
953
|
|
859
954
|
@property
|
860
955
|
def stdin_file_path(self) -> pathlib.Path:
|
@@ -873,13 +968,33 @@ class Daemon:
|
|
873
968
|
|
874
969
|
return self.path / 'input.stdin.block'
|
875
970
|
|
971
|
+
@property
|
972
|
+
def prompt_kwargs_file_path(self) -> pathlib.Path:
|
973
|
+
"""
|
974
|
+
Return the file path to the kwargs for the invoking `prompt()`.
|
975
|
+
"""
|
976
|
+
return self.path / 'prompt_kwargs.json'
|
977
|
+
|
876
978
|
@property
|
877
979
|
def log_offset_path(self) -> pathlib.Path:
|
878
980
|
"""
|
879
981
|
Return the log offset file path.
|
880
982
|
"""
|
983
|
+
from meerschaum.config.paths import LOGS_RESOURCES_PATH
|
881
984
|
return LOGS_RESOURCES_PATH / ('.' + self.daemon_id + '.log.offset')
|
882
985
|
|
986
|
+
@property
|
987
|
+
def log_offset_lock(self) -> 'fasteners.InterProcessLock':
|
988
|
+
"""
|
989
|
+
Return the process lock context manager.
|
990
|
+
"""
|
991
|
+
if '_log_offset_lock' in self.__dict__:
|
992
|
+
return self._log_offset_lock
|
993
|
+
|
994
|
+
fasteners = attempt_import('fasteners')
|
995
|
+
self._log_offset_lock = fasteners.InterProcessLock(self.log_offset_path)
|
996
|
+
return self._log_offset_lock
|
997
|
+
|
883
998
|
@property
|
884
999
|
def rotating_log(self) -> RotatingFile:
|
885
1000
|
"""
|
@@ -888,17 +1003,32 @@ class Daemon:
|
|
888
1003
|
if '_rotating_log' in self.__dict__:
|
889
1004
|
return self._rotating_log
|
890
1005
|
|
891
|
-
|
892
|
-
|
893
|
-
)
|
1006
|
+
logs_cf = self.properties.get('logs', None) or {}
|
1007
|
+
write_timestamps = logs_cf.get('write_timestamps', None)
|
894
1008
|
if write_timestamps is None:
|
895
1009
|
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled')
|
896
1010
|
|
1011
|
+
timestamp_format = logs_cf.get('timestamp_format', None)
|
1012
|
+
if timestamp_format is None:
|
1013
|
+
timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
|
1014
|
+
|
1015
|
+
num_files_to_keep = logs_cf.get('num_files_to_keep', None)
|
1016
|
+
if num_files_to_keep is None:
|
1017
|
+
num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
|
1018
|
+
|
1019
|
+
max_file_size = logs_cf.get('max_file_size', None)
|
1020
|
+
if max_file_size is None:
|
1021
|
+
max_file_size = get_config('jobs', 'logs', 'max_file_size')
|
1022
|
+
|
1023
|
+
redirect_streams = logs_cf.get('redirect_streams', True)
|
1024
|
+
|
897
1025
|
self._rotating_log = RotatingFile(
|
898
1026
|
self.log_path,
|
899
|
-
redirect_streams=
|
1027
|
+
redirect_streams=redirect_streams,
|
900
1028
|
write_timestamps=write_timestamps,
|
901
|
-
timestamp_format=
|
1029
|
+
timestamp_format=timestamp_format,
|
1030
|
+
num_files_to_keep=num_files_to_keep,
|
1031
|
+
max_file_size=max_file_size,
|
902
1032
|
)
|
903
1033
|
return self._rotating_log
|
904
1034
|
|
@@ -907,8 +1037,8 @@ class Daemon:
|
|
907
1037
|
"""
|
908
1038
|
Return the file handler for the stdin file.
|
909
1039
|
"""
|
910
|
-
if (
|
911
|
-
return
|
1040
|
+
if (_stdin_file := self.__dict__.get('_stdin_file', None)):
|
1041
|
+
return _stdin_file
|
912
1042
|
|
913
1043
|
self._stdin_file = StdinFile(
|
914
1044
|
self.stdin_file_path,
|
@@ -917,17 +1047,34 @@ class Daemon:
|
|
917
1047
|
return self._stdin_file
|
918
1048
|
|
919
1049
|
@property
|
920
|
-
def log_text(self) ->
|
1050
|
+
def log_text(self) -> Union[str, None]:
|
921
1051
|
"""
|
922
1052
|
Read the log files and return their contents.
|
923
1053
|
Returns `None` if the log file does not exist.
|
924
1054
|
"""
|
1055
|
+
logs_cf = self.properties.get('logs', None) or {}
|
1056
|
+
write_timestamps = logs_cf.get('write_timestamps', None)
|
1057
|
+
if write_timestamps is None:
|
1058
|
+
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled')
|
1059
|
+
|
1060
|
+
timestamp_format = logs_cf.get('timestamp_format', None)
|
1061
|
+
if timestamp_format is None:
|
1062
|
+
timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
|
1063
|
+
|
1064
|
+
num_files_to_keep = logs_cf.get('num_files_to_keep', None)
|
1065
|
+
if num_files_to_keep is None:
|
1066
|
+
num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
|
1067
|
+
|
1068
|
+
max_file_size = logs_cf.get('max_file_size', None)
|
1069
|
+
if max_file_size is None:
|
1070
|
+
max_file_size = get_config('jobs', 'logs', 'max_file_size')
|
1071
|
+
|
925
1072
|
new_rotating_log = RotatingFile(
|
926
1073
|
self.rotating_log.file_path,
|
927
|
-
num_files_to_keep
|
928
|
-
max_file_size
|
929
|
-
write_timestamps
|
930
|
-
timestamp_format
|
1074
|
+
num_files_to_keep=num_files_to_keep,
|
1075
|
+
max_file_size=max_file_size,
|
1076
|
+
write_timestamps=write_timestamps,
|
1077
|
+
timestamp_format=timestamp_format,
|
931
1078
|
)
|
932
1079
|
return new_rotating_log.read()
|
933
1080
|
|
@@ -952,20 +1099,25 @@ class Daemon:
|
|
952
1099
|
if not self.log_offset_path.exists():
|
953
1100
|
return 0, 0
|
954
1101
|
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
1102
|
+
try:
|
1103
|
+
with open(self.log_offset_path, 'r', encoding='utf-8') as f:
|
1104
|
+
cursor_text = f.read()
|
1105
|
+
cursor_parts = cursor_text.split(' ')
|
1106
|
+
subfile_index, subfile_position = int(cursor_parts[0]), int(cursor_parts[1])
|
1107
|
+
return subfile_index, subfile_position
|
1108
|
+
except Exception as e:
|
1109
|
+
warn(f"Failed to read cursor:\n{e}")
|
1110
|
+
return 0, 0
|
960
1111
|
|
961
1112
|
def _write_log_offset(self) -> None:
|
962
1113
|
"""
|
963
1114
|
Write the current log offset file.
|
964
1115
|
"""
|
965
|
-
with
|
966
|
-
|
967
|
-
|
968
|
-
|
1116
|
+
with self.log_offset_lock:
|
1117
|
+
with open(self.log_offset_path, 'w+', encoding='utf-8') as f:
|
1118
|
+
subfile_index = self.rotating_log._cursor[0]
|
1119
|
+
subfile_position = self.rotating_log._cursor[1]
|
1120
|
+
f.write(f"{subfile_index} {subfile_position}")
|
969
1121
|
|
970
1122
|
@property
|
971
1123
|
def pid(self) -> Union[int, None]:
|
@@ -1123,6 +1275,10 @@ class Daemon:
|
|
1123
1275
|
import pickle
|
1124
1276
|
import traceback
|
1125
1277
|
from meerschaum.utils.misc import generate_password
|
1278
|
+
|
1279
|
+
if not self.pickle:
|
1280
|
+
return True, "Success"
|
1281
|
+
|
1126
1282
|
backup_path = self.pickle_path.parent / (generate_password(7) + '.pkl')
|
1127
1283
|
try:
|
1128
1284
|
self.path.mkdir(parents=True, exist_ok=True)
|