meerschaum 2.9.4__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.
Files changed (201) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +228 -117
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +438 -88
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.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.config.static import STATIC_CONFIG
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
- 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'
@@ -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(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
+ )
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
- + f"daemon = Daemon(daemon_id='{self.daemon_id}'); "
437
- + f"daemon._run_exit(keep_daemon_output={keep_daemon_output}, "
438
- + "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)"
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
- 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
+
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
- 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'])
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
- write_timestamps = (
892
- self.properties.get('logs', {}).get('write_timestamps', None)
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=True,
1027
+ redirect_streams=redirect_streams,
900
1028
  write_timestamps=write_timestamps,
901
- 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,
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 (stdin_file := self.__dict__.get('_stdin_file', None)):
911
- return stdin_file
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) -> Optional[str]:
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 = self.rotating_log.num_files_to_keep,
928
- max_file_size = self.rotating_log.max_file_size,
929
- write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled'),
930
- 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,
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
- with open(self.log_offset_path, 'r', encoding='utf-8') as f:
956
- cursor_text = f.read()
957
- cursor_parts = cursor_text.split(' ')
958
- subfile_index, subfile_position = int(cursor_parts[0]), int(cursor_parts[1])
959
- 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
960
1111
 
961
1112
  def _write_log_offset(self) -> None:
962
1113
  """
963
1114
  Write the current log offset file.
964
1115
  """
965
- with open(self.log_offset_path, 'w+', encoding='utf-8') as f:
966
- subfile_index = self.rotating_log._cursor[0]
967
- subfile_position = self.rotating_log._cursor[1]
968
- 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}")
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)
@@ -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()