meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc8__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.
Files changed (117) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +435 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +115 -24
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +4 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +2 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +29 -0
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/register.py +9 -2
  34. meerschaum/api/dash/pages/login.py +2 -2
  35. meerschaum/api/dash/pipes.py +72 -36
  36. meerschaum/api/dash/webterm.py +14 -6
  37. meerschaum/api/models/_pipes.py +7 -1
  38. meerschaum/api/resources/static/js/terminado.js +3 -0
  39. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  40. meerschaum/api/resources/templates/termpage.html +1 -0
  41. meerschaum/api/routes/_jobs.py +23 -11
  42. meerschaum/api/routes/_login.py +73 -5
  43. meerschaum/api/routes/_pipes.py +6 -4
  44. meerschaum/api/routes/_webterm.py +3 -3
  45. meerschaum/config/__init__.py +60 -13
  46. meerschaum/config/_default.py +89 -61
  47. meerschaum/config/_edit.py +10 -8
  48. meerschaum/config/_formatting.py +2 -0
  49. meerschaum/config/_patch.py +4 -2
  50. meerschaum/config/_paths.py +127 -12
  51. meerschaum/config/_read_config.py +32 -12
  52. meerschaum/config/_version.py +1 -1
  53. meerschaum/config/environment.py +262 -0
  54. meerschaum/config/stack/__init__.py +7 -5
  55. meerschaum/connectors/_Connector.py +1 -2
  56. meerschaum/connectors/__init__.py +37 -2
  57. meerschaum/connectors/api/_APIConnector.py +1 -1
  58. meerschaum/connectors/api/_jobs.py +11 -0
  59. meerschaum/connectors/api/_pipes.py +7 -1
  60. meerschaum/connectors/instance/_plugins.py +9 -1
  61. meerschaum/connectors/instance/_tokens.py +20 -3
  62. meerschaum/connectors/instance/_users.py +8 -1
  63. meerschaum/connectors/parse.py +1 -1
  64. meerschaum/connectors/sql/_create_engine.py +3 -0
  65. meerschaum/connectors/sql/_pipes.py +93 -79
  66. meerschaum/connectors/sql/_users.py +8 -1
  67. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  68. meerschaum/connectors/valkey/_pipes.py +7 -5
  69. meerschaum/core/Pipe/__init__.py +45 -71
  70. meerschaum/core/Pipe/_attributes.py +66 -90
  71. meerschaum/core/Pipe/_cache.py +555 -0
  72. meerschaum/core/Pipe/_clear.py +0 -11
  73. meerschaum/core/Pipe/_data.py +0 -50
  74. meerschaum/core/Pipe/_deduplicate.py +0 -13
  75. meerschaum/core/Pipe/_delete.py +12 -21
  76. meerschaum/core/Pipe/_drop.py +11 -23
  77. meerschaum/core/Pipe/_dtypes.py +1 -1
  78. meerschaum/core/Pipe/_index.py +8 -14
  79. meerschaum/core/Pipe/_sync.py +12 -18
  80. meerschaum/core/Plugin/_Plugin.py +7 -1
  81. meerschaum/core/Token/_Token.py +1 -1
  82. meerschaum/core/User/_User.py +1 -2
  83. meerschaum/jobs/_Executor.py +88 -4
  84. meerschaum/jobs/_Job.py +146 -36
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +197 -42
  88. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  89. meerschaum/utils/daemon/RotatingFile.py +63 -36
  90. meerschaum/utils/daemon/StdinFile.py +53 -13
  91. meerschaum/utils/daemon/__init__.py +18 -5
  92. meerschaum/utils/daemon/_names.py +6 -3
  93. meerschaum/utils/debug.py +34 -4
  94. meerschaum/utils/dtypes/__init__.py +5 -1
  95. meerschaum/utils/formatting/__init__.py +4 -1
  96. meerschaum/utils/formatting/_jobs.py +1 -1
  97. meerschaum/utils/formatting/_pipes.py +47 -46
  98. meerschaum/utils/formatting/_shell.py +33 -9
  99. meerschaum/utils/misc.py +22 -38
  100. meerschaum/utils/packages/__init__.py +15 -13
  101. meerschaum/utils/packages/_packages.py +1 -0
  102. meerschaum/utils/pipes.py +33 -5
  103. meerschaum/utils/process.py +1 -1
  104. meerschaum/utils/prompt.py +172 -143
  105. meerschaum/utils/sql.py +12 -2
  106. meerschaum/utils/threading.py +42 -0
  107. meerschaum/utils/venv/__init__.py +2 -0
  108. meerschaum/utils/warnings.py +19 -13
  109. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/zip-safe +0 -0
@@ -25,9 +25,6 @@ from meerschaum.utils.typing import (
25
25
  )
26
26
  from meerschaum.config import get_config
27
27
  from meerschaum._internal.static import STATIC_CONFIG
28
- from meerschaum.config._paths import (
29
- DAEMON_RESOURCES_PATH, LOGS_RESOURCES_PATH, DAEMON_ERROR_LOG_PATH,
30
- )
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
- working_directory=os.getcwd(),
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
- _daemons.append(self)
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
- sys.stdin = self.stdin_file
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'
@@ -360,8 +382,15 @@ class Daemon:
360
382
 
361
383
  except Exception:
362
384
  daemon_error = traceback.format_exc()
385
+ from meerschaum.config.paths import DAEMON_ERROR_LOG_PATH
363
386
  with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
364
- f.write(daemon_error)
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
+ )
365
394
  warn(f"Encountered an error while running the daemon '{self}':\n{daemon_error}")
366
395
 
367
396
  def _capture_process_timestamp(
@@ -396,6 +425,8 @@ class Daemon:
396
425
  self,
397
426
  keep_daemon_output: bool = True,
398
427
  allow_dirty_run: bool = False,
428
+ wait: bool = False,
429
+ timeout: Union[int, float] = 4,
399
430
  debug: bool = False,
400
431
  ) -> SuccessTuple:
401
432
  """Run the daemon as a child process and continue executing the parent.
@@ -410,6 +441,12 @@ class Daemon:
410
441
  This option is dangerous because if the same `daemon_id` runs concurrently,
411
442
  the last to finish will overwrite the output of the first.
412
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
+
413
450
  Returns
414
451
  -------
415
452
  A SuccessTuple indicating success.
@@ -433,20 +470,44 @@ class Daemon:
433
470
  return _write_pickle_success_tuple
434
471
 
435
472
  _launch_daemon_code = (
436
- "from meerschaum.utils.daemon import Daemon; "
437
- + f"daemon = Daemon(daemon_id='{self.daemon_id}'); "
438
- + f"daemon._run_exit(keep_daemon_output={keep_daemon_output}, "
439
- + "allow_dirty_run=True)"
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)"
440
478
  )
441
479
  env = dict(os.environ)
442
- env[STATIC_CONFIG['environment']['noninteractive']] = 'true'
443
480
  _launch_success_bool = venv_exec(_launch_daemon_code, debug=debug, venv=None, env=env)
444
481
  msg = (
445
482
  "Success"
446
483
  if _launch_success_bool
447
484
  else f"Failed to start daemon '{self.daemon_id}'."
448
485
  )
449
- return _launch_success_bool, msg
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
+
450
511
 
451
512
  def kill(self, timeout: Union[int, float, None] = 8) -> SuccessTuple:
452
513
  """
@@ -466,10 +527,14 @@ class Daemon:
466
527
  success, msg = self._send_signal(signal.SIGTERM, timeout=timeout)
467
528
  if success:
468
529
  self._write_stop_file('kill')
530
+ self.stdin_file.close()
531
+ self._remove_blocking_stdin_file()
469
532
  return success, msg
470
533
 
471
534
  if self.status == 'stopped':
472
535
  self._write_stop_file('kill')
536
+ self.stdin_file.close()
537
+ self._remove_blocking_stdin_file()
473
538
  return True, "Process has already stopped."
474
539
 
475
540
  psutil = attempt_import('psutil')
@@ -494,6 +559,8 @@ class Daemon:
494
559
  pass
495
560
 
496
561
  self._write_stop_file('kill')
562
+ self.stdin_file.close()
563
+ self._remove_blocking_stdin_file()
497
564
  return True, "Success"
498
565
 
499
566
  def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
@@ -504,6 +571,8 @@ class Daemon:
504
571
  signal_success, signal_msg = self._send_signal(signal.SIGINT, timeout=timeout)
505
572
  if signal_success:
506
573
  self._write_stop_file('quit')
574
+ self.stdin_file.close()
575
+ self._remove_blocking_stdin_file()
507
576
  return signal_success, signal_msg
508
577
 
509
578
  def pause(
@@ -526,6 +595,8 @@ class Daemon:
526
595
  -------
527
596
  A `SuccessTuple` indicating whether the `Daemon` process was successfully suspended.
528
597
  """
598
+ self._remove_blocking_stdin_file()
599
+
529
600
  if self.process is None:
530
601
  return False, f"Daemon '{self.daemon_id}' is not running and cannot be paused."
531
602
 
@@ -533,6 +604,8 @@ class Daemon:
533
604
  return True, f"Daemon '{self.daemon_id}' is already paused."
534
605
 
535
606
  self._write_stop_file('pause')
607
+ self.stdin_file.close()
608
+ self._remove_blocking_stdin_file()
536
609
  try:
537
610
  self.process.suspend()
538
611
  except Exception as e:
@@ -598,6 +671,9 @@ class Daemon:
598
671
 
599
672
  self._remove_stop_file()
600
673
  try:
674
+ if self.process is None:
675
+ return False, f"Cannot resume daemon '{self.daemon_id}'."
676
+
601
677
  self.process.resume()
602
678
  except Exception as e:
603
679
  return False, f"Failed to resume daemon '{self.daemon_id}':\n{e}"
@@ -671,6 +747,18 @@ class Daemon:
671
747
  except Exception:
672
748
  return {}
673
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
+
674
762
  def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
675
763
  """
676
764
  Handle `SIGTERM` within the `Daemon` context.
@@ -799,7 +887,7 @@ class Daemon:
799
887
  if self.process is None:
800
888
  return 'stopped'
801
889
 
802
- psutil = attempt_import('psutil')
890
+ psutil = attempt_import('psutil', lazy=False)
803
891
  try:
804
892
  if self.process.status() == 'stopped':
805
893
  return 'paused'
@@ -820,6 +908,7 @@ class Daemon:
820
908
  """
821
909
  Return a Daemon's path from its `daemon_id`.
822
910
  """
911
+ from meerschaum.config.paths import DAEMON_RESOURCES_PATH
823
912
  return DAEMON_RESOURCES_PATH / daemon_id
824
913
 
825
914
  @property
@@ -855,7 +944,12 @@ class Daemon:
855
944
  """
856
945
  Return the log path.
857
946
  """
858
- return LOGS_RESOURCES_PATH / (self.daemon_id + '.log')
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'])
859
953
 
860
954
  @property
861
955
  def stdin_file_path(self) -> pathlib.Path:
@@ -874,13 +968,33 @@ class Daemon:
874
968
 
875
969
  return self.path / 'input.stdin.block'
876
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
+
877
978
  @property
878
979
  def log_offset_path(self) -> pathlib.Path:
879
980
  """
880
981
  Return the log offset file path.
881
982
  """
983
+ from meerschaum.config.paths import LOGS_RESOURCES_PATH
882
984
  return LOGS_RESOURCES_PATH / ('.' + self.daemon_id + '.log.offset')
883
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
+
884
998
  @property
885
999
  def rotating_log(self) -> RotatingFile:
886
1000
  """
@@ -889,17 +1003,32 @@ class Daemon:
889
1003
  if '_rotating_log' in self.__dict__:
890
1004
  return self._rotating_log
891
1005
 
892
- write_timestamps = (
893
- self.properties.get('logs', {}).get('write_timestamps', None)
894
- )
1006
+ logs_cf = self.properties.get('logs', None) or {}
1007
+ write_timestamps = logs_cf.get('write_timestamps', None)
895
1008
  if write_timestamps is None:
896
1009
  write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled')
897
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
+
898
1025
  self._rotating_log = RotatingFile(
899
1026
  self.log_path,
900
- redirect_streams=True,
1027
+ redirect_streams=redirect_streams,
901
1028
  write_timestamps=write_timestamps,
902
- timestamp_format=get_config('jobs', 'logs', 'timestamps', 'format'),
1029
+ timestamp_format=timestamp_format,
1030
+ num_files_to_keep=num_files_to_keep,
1031
+ max_file_size=max_file_size,
903
1032
  )
904
1033
  return self._rotating_log
905
1034
 
@@ -908,8 +1037,8 @@ class Daemon:
908
1037
  """
909
1038
  Return the file handler for the stdin file.
910
1039
  """
911
- if (stdin_file := self.__dict__.get('_stdin_file', None)):
912
- return stdin_file
1040
+ if (_stdin_file := self.__dict__.get('_stdin_file', None)):
1041
+ return _stdin_file
913
1042
 
914
1043
  self._stdin_file = StdinFile(
915
1044
  self.stdin_file_path,
@@ -918,17 +1047,34 @@ class Daemon:
918
1047
  return self._stdin_file
919
1048
 
920
1049
  @property
921
- def log_text(self) -> Optional[str]:
1050
+ def log_text(self) -> Union[str, None]:
922
1051
  """
923
1052
  Read the log files and return their contents.
924
1053
  Returns `None` if the log file does not exist.
925
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
+
926
1072
  new_rotating_log = RotatingFile(
927
1073
  self.rotating_log.file_path,
928
- num_files_to_keep = self.rotating_log.num_files_to_keep,
929
- max_file_size = self.rotating_log.max_file_size,
930
- write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled'),
931
- timestamp_format = get_config('jobs', 'logs', 'timestamps', '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,
932
1078
  )
933
1079
  return new_rotating_log.read()
934
1080
 
@@ -953,20 +1099,25 @@ class Daemon:
953
1099
  if not self.log_offset_path.exists():
954
1100
  return 0, 0
955
1101
 
956
- with open(self.log_offset_path, 'r', encoding='utf-8') as f:
957
- cursor_text = f.read()
958
- cursor_parts = cursor_text.split(' ')
959
- subfile_index, subfile_position = int(cursor_parts[0]), int(cursor_parts[1])
960
- return subfile_index, subfile_position
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
961
1111
 
962
1112
  def _write_log_offset(self) -> None:
963
1113
  """
964
1114
  Write the current log offset file.
965
1115
  """
966
- with open(self.log_offset_path, 'w+', encoding='utf-8') as f:
967
- subfile_index = self.rotating_log._cursor[0]
968
- subfile_position = self.rotating_log._cursor[1]
969
- f.write(f"{subfile_index} {subfile_position}")
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}")
970
1121
 
971
1122
  @property
972
1123
  def pid(self) -> Union[int, None]:
@@ -1124,6 +1275,10 @@ class Daemon:
1124
1275
  import pickle
1125
1276
  import traceback
1126
1277
  from meerschaum.utils.misc import generate_password
1278
+
1279
+ if not self.pickle:
1280
+ return True, "Success"
1281
+
1127
1282
  backup_path = self.pickle_path.parent / (generate_password(7) + '.pkl')
1128
1283
  try:
1129
1284
  self.path.mkdir(parents=True, exist_ok=True)
@@ -77,7 +77,6 @@ class FileDescriptorInterceptor:
77
77
  break
78
78
 
79
79
  try:
80
- first_char_is_newline = data[0] == b'\n'
81
80
  last_char_is_newline = data[-1] == b'\n'
82
81
 
83
82
  injected_str = self.injection_hook()