meerschaum 2.3.0.dev3__py3-none-any.whl → 2.3.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. meerschaum/__init__.py +3 -2
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/_parser.py +6 -2
  4. meerschaum/_internal/entry.py +36 -6
  5. meerschaum/_internal/shell/Shell.py +32 -20
  6. meerschaum/actions/attach.py +12 -7
  7. meerschaum/actions/copy.py +68 -41
  8. meerschaum/actions/delete.py +64 -21
  9. meerschaum/actions/edit.py +3 -3
  10. meerschaum/actions/install.py +40 -32
  11. meerschaum/actions/pause.py +44 -27
  12. meerschaum/actions/restart.py +107 -0
  13. meerschaum/actions/show.py +8 -8
  14. meerschaum/actions/start.py +25 -40
  15. meerschaum/actions/stop.py +11 -4
  16. meerschaum/api/_events.py +10 -3
  17. meerschaum/api/dash/jobs.py +69 -70
  18. meerschaum/api/routes/_actions.py +8 -3
  19. meerschaum/api/routes/_jobs.py +37 -19
  20. meerschaum/config/_default.py +1 -1
  21. meerschaum/config/_paths.py +5 -0
  22. meerschaum/config/_version.py +1 -1
  23. meerschaum/config/static/__init__.py +3 -0
  24. meerschaum/connectors/Connector.py +13 -7
  25. meerschaum/connectors/__init__.py +21 -5
  26. meerschaum/connectors/api/APIConnector.py +3 -0
  27. meerschaum/connectors/api/_jobs.py +30 -3
  28. meerschaum/connectors/parse.py +10 -13
  29. meerschaum/core/Pipe/_bootstrap.py +16 -8
  30. meerschaum/jobs/_Executor.py +69 -0
  31. meerschaum/{utils/jobs → jobs}/_Job.py +160 -20
  32. meerschaum/jobs/_LocalExecutor.py +88 -0
  33. meerschaum/jobs/_SystemdExecutor.py +608 -0
  34. meerschaum/jobs/__init__.py +365 -0
  35. meerschaum/plugins/__init__.py +6 -6
  36. meerschaum/utils/daemon/Daemon.py +7 -0
  37. meerschaum/utils/daemon/RotatingFile.py +5 -2
  38. meerschaum/utils/daemon/StdinFile.py +12 -2
  39. meerschaum/utils/daemon/__init__.py +2 -0
  40. meerschaum/utils/formatting/_jobs.py +52 -16
  41. meerschaum/utils/misc.py +23 -5
  42. meerschaum/utils/packages/_packages.py +7 -4
  43. meerschaum/utils/process.py +9 -9
  44. meerschaum/utils/venv/__init__.py +2 -2
  45. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/METADATA +14 -17
  46. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/RECORD +52 -48
  47. meerschaum/utils/jobs/__init__.py +0 -245
  48. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/LICENSE +0 -0
  49. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/NOTICE +0 -0
  50. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/WHEEL +0 -0
  51. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/entry_points.txt +0 -0
  52. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/top_level.txt +0 -0
  53. {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc1.dist-info}/zip-safe +0 -0
@@ -0,0 +1,365 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Higher-level utilities for managing `meerschaum.utils.daemon.Daemon`.
7
+ """
8
+
9
+ import pathlib
10
+
11
+ import meerschaum as mrsm
12
+ from meerschaum.utils.typing import Dict, Optional, List, Callable, Any, SuccessTuple
13
+
14
+ from meerschaum.jobs._Job import Job, StopMonitoringLogs
15
+ from meerschaum.jobs._Executor import Executor
16
+
17
+ __all__ = (
18
+ 'Job',
19
+ 'get_jobs',
20
+ 'get_filtered_jobs',
21
+ 'get_restart_jobs',
22
+ 'get_running_jobs',
23
+ 'get_stopped_jobs',
24
+ 'get_paused_jobs',
25
+ 'get_restart_jobs',
26
+ 'Executor',
27
+ 'make_executor',
28
+ 'check_restart_jobs',
29
+ 'start_check_jobs_thread',
30
+ 'stop_check_jobs_thread',
31
+ )
32
+
33
+ executor_types: List[str] = ['api', 'local']
34
+
35
+
36
+ def get_jobs(
37
+ executor_keys: Optional[str] = None,
38
+ include_hidden: bool = False,
39
+ combine_local_and_systemd: bool = True,
40
+ debug: bool = False,
41
+ ) -> Dict[str, Job]:
42
+ """
43
+ Return a dictionary of the existing jobs.
44
+
45
+ Parameters
46
+ ----------
47
+ executor_keys: Optional[str], default None
48
+ If provided, return remote jobs on the given API instance.
49
+ Otherwise return local jobs.
50
+
51
+ include_hidden: bool, default False
52
+ If `True`, include jobs with the `hidden` attribute.
53
+
54
+ Returns
55
+ -------
56
+ A dictionary mapping job names to jobs.
57
+ """
58
+ from meerschaum.connectors.parse import parse_connector_keys
59
+ include_local_and_system = (
60
+ combine_local_and_systemd
61
+ and str(executor_keys).split(':')[0] in ('None', 'local', 'systemd')
62
+ )
63
+
64
+ def _get_local_jobs():
65
+ from meerschaum.utils.daemon import get_daemons
66
+ daemons = get_daemons()
67
+ jobs = {
68
+ daemon.daemon_id: Job(name=daemon.daemon_id, executor_keys='local')
69
+ for daemon in daemons
70
+ }
71
+ return {
72
+ name: job
73
+ for name, job in jobs.items()
74
+ if include_hidden or not job.hidden
75
+ }
76
+
77
+ def _get_systemd_jobs():
78
+ conn = mrsm.get_connector('systemd')
79
+ return conn.get_jobs(debug=debug)
80
+
81
+ if include_local_and_system:
82
+ local_jobs = _get_local_jobs()
83
+ systemd_jobs = _get_systemd_jobs()
84
+ shared_jobs = set(local_jobs) & set(systemd_jobs)
85
+ if shared_jobs:
86
+ from meerschaum.utils.misc import items_str
87
+ from meerschaum.utils.warnings import warn
88
+ warn(
89
+ "Job"
90
+ + ('s' if len(shared_jobs) != 1 else '')
91
+ + f" {items_str(list(shared_jobs))} "
92
+ + "exist"
93
+ + ('s' if len(shared_jobs) == 1 else '')
94
+ + " in both `local` and `systemd`.",
95
+ stack=False,
96
+ )
97
+ return {**local_jobs, **systemd_jobs}
98
+
99
+ try:
100
+ _ = parse_connector_keys(executor_keys, construct=False)
101
+ conn = mrsm.get_connector(executor_keys)
102
+ return conn.get_jobs(debug=debug)
103
+ except Exception:
104
+ return {}
105
+
106
+
107
+ def get_filtered_jobs(
108
+ executor_keys: Optional[str] = None,
109
+ filter_list: Optional[List[str]] = None,
110
+ include_hidden: bool = False,
111
+ warn: bool = False,
112
+ debug: bool = False,
113
+ ) -> Dict[str, Job]:
114
+ """
115
+ Return a list of jobs filtered by the user.
116
+ """
117
+ from meerschaum.utils.warnings import warn as _warn
118
+ jobs = get_jobs(executor_keys, include_hidden=include_hidden, debug=debug)
119
+
120
+ if not filter_list:
121
+ return jobs
122
+
123
+ jobs_to_return = {}
124
+ for name in filter_list:
125
+ job = jobs.get(name, None)
126
+ if job is None:
127
+ if warn:
128
+ _warn(
129
+ f"Job '{name}' does not exist.",
130
+ stack=False,
131
+ )
132
+ continue
133
+ jobs_to_return[name] = job
134
+
135
+ return jobs_to_return
136
+
137
+
138
+ def get_restart_jobs(
139
+ executor_keys: Optional[str] = None,
140
+ jobs: Optional[Dict[str, Job]] = None,
141
+ include_hidden: bool = False,
142
+ debug: bool = False,
143
+ ) -> Dict[str, Job]:
144
+ """
145
+ Return jobs which were created with `--restart` or `--loop`.
146
+ """
147
+ if jobs is None:
148
+ jobs = get_jobs(executor_keys, include_hidden=include_hidden, debug=debug)
149
+
150
+ return {
151
+ name: job
152
+ for name, job in jobs.items()
153
+ if job.restart
154
+ }
155
+
156
+
157
+ def get_running_jobs(
158
+ executor_keys: Optional[str] = None,
159
+ jobs: Optional[Dict[str, Job]] = None,
160
+ include_hidden: bool = False,
161
+ debug: bool = False,
162
+ ) -> Dict[str, Job]:
163
+ """
164
+ Return a dictionary of running jobs.
165
+ """
166
+ if jobs is None:
167
+ jobs = get_jobs(executor_keys, include_hidden=include_hidden, debug=debug)
168
+
169
+ return {
170
+ name: job
171
+ for name, job in jobs.items()
172
+ if job.status == 'running'
173
+ }
174
+
175
+
176
+ def get_paused_jobs(
177
+ executor_keys: Optional[str] = None,
178
+ jobs: Optional[Dict[str, Job]] = None,
179
+ include_hidden: bool = False,
180
+ debug: bool = False,
181
+ ) -> Dict[str, Job]:
182
+ """
183
+ Return a dictionary of paused jobs.
184
+ """
185
+ if jobs is None:
186
+ jobs = get_jobs(executor_keys, include_hidden=include_hidden, debug=debug)
187
+
188
+ return {
189
+ name: job
190
+ for name, job in jobs.items()
191
+ if job.status == 'paused'
192
+ }
193
+
194
+
195
+ def get_stopped_jobs(
196
+ executor_keys: Optional[str] = None,
197
+ jobs: Optional[Dict[str, Job]] = None,
198
+ include_hidden: bool = False,
199
+ debug: bool = False,
200
+ ) -> Dict[str, Job]:
201
+ """
202
+ Return a dictionary of stopped jobs.
203
+ """
204
+ if jobs is None:
205
+ jobs = get_jobs(executor_keys, include_hidden=include_hidden, debug=debug)
206
+
207
+ return {
208
+ name: job
209
+ for name, job in jobs.items()
210
+ if job.status == 'stopped'
211
+ }
212
+
213
+
214
+ def make_executor(cls):
215
+ """
216
+ Register a class as an `Executor`.
217
+ """
218
+ import re
219
+ from meerschaum.connectors import make_connector
220
+ suffix_regex = r'executor$'
221
+ typ = re.sub(suffix_regex, '', cls.__name__.lower())
222
+ if typ not in executor_types:
223
+ executor_types.append(typ)
224
+ return make_connector(cls, _is_executor=True)
225
+
226
+
227
+ def check_restart_jobs(
228
+ executor_keys: Optional[str] = 'local',
229
+ jobs: Optional[Dict[str, Job]] = None,
230
+ include_hidden: bool = True,
231
+ silent: bool = False,
232
+ debug: bool = False,
233
+ ) -> SuccessTuple:
234
+ """
235
+ Restart any stopped jobs which were created with `--restart`.
236
+
237
+ Parameters
238
+ ----------
239
+ executor_keys: Optional[str], default None
240
+ If provided, check jobs on the given remote API instance.
241
+ Otherwise check local jobs.
242
+
243
+ include_hidden: bool, default True
244
+ If `True`, include hidden jobs in the check.
245
+
246
+ silent: bool, default False
247
+ If `True`, do not print the restart success message.
248
+ """
249
+ from meerschaum.utils.misc import items_str
250
+
251
+ if jobs is None:
252
+ jobs = get_jobs(
253
+ executor_keys,
254
+ include_hidden=include_hidden,
255
+ debug=debug,
256
+ )
257
+
258
+ if not jobs:
259
+ return True, "No jobs to restart."
260
+
261
+ results = {}
262
+ for name, job in jobs.items():
263
+ check_success, check_msg = job.check_restart()
264
+ results[job.name] = (check_success, check_msg)
265
+ if not silent:
266
+ mrsm.pprint((check_success, check_msg))
267
+
268
+ success_names = [name for name, (check_success, check_msg) in results.items() if check_success]
269
+ fail_names = [name for name, (check_success, check_msg) in results.items() if not check_success]
270
+ success = len(success_names) == len(jobs)
271
+ msg = (
272
+ (
273
+ "Successfully restarted job"
274
+ + ('s' if len(success_names) != 1 else '')
275
+ + ' ' + items_str(success_names) + '.'
276
+ )
277
+ if success
278
+ else (
279
+ "Failed to restart job"
280
+ + ('s' if len(success_names) != 1 else '')
281
+ + ' ' + items_str(fail_names) + '.'
282
+ )
283
+ )
284
+ return success, msg
285
+
286
+
287
+ def _check_restart_jobs_against_lock(*args, **kwargs):
288
+ from meerschaum.config.paths import CHECK_JOBS_LOCK_PATH
289
+ fasteners = mrsm.attempt_import('fasteners')
290
+ lock = fasteners.InterProcessLock(CHECK_JOBS_LOCK_PATH)
291
+ with lock:
292
+ check_restart_jobs(*args, **kwargs)
293
+
294
+
295
+ _check_loop_stop_thread = None
296
+ def start_check_jobs_thread():
297
+ """
298
+ Start a thread to regularly monitor jobs.
299
+ """
300
+ import atexit
301
+ from functools import partial
302
+ from meerschaum.utils.threading import RepeatTimer
303
+ from meerschaum.config.static import STATIC_CONFIG
304
+
305
+ global _check_loop_stop_thread
306
+ sleep_seconds = STATIC_CONFIG['jobs']['check_restart_seconds']
307
+
308
+ _check_loop_stop_thread = RepeatTimer(
309
+ sleep_seconds,
310
+ partial(
311
+ _check_restart_jobs_against_lock,
312
+ silent=True,
313
+ )
314
+ )
315
+ _check_loop_stop_thread.daemon = True
316
+ atexit.register(stop_check_jobs_thread)
317
+
318
+ _check_loop_stop_thread.start()
319
+ return _check_loop_stop_thread
320
+
321
+
322
+ def stop_check_jobs_thread():
323
+ """
324
+ Stop the job monitoring thread.
325
+ """
326
+ from meerschaum.config.paths import CHECK_JOBS_LOCK_PATH
327
+ from meerschaum.utils.warnings import warn
328
+ if _check_loop_stop_thread is None:
329
+ return
330
+
331
+ _check_loop_stop_thread.cancel()
332
+
333
+ try:
334
+ if CHECK_JOBS_LOCK_PATH.exists():
335
+ CHECK_JOBS_LOCK_PATH.unlink()
336
+ except Exception as e:
337
+ warn(f"Failed to remove check jobs lock file:\n{e}")
338
+
339
+
340
+ def get_executor_keys_from_context() -> str:
341
+ """
342
+ If we are running on the host with the default root, default to `'systemd'`.
343
+ Otherwise return `'local'`.
344
+ """
345
+ from meerschaum.config.paths import ROOT_DIR_PATH, DEFAULT_ROOT_DIR_PATH
346
+ from meerschaum.utils.misc import is_systemd_available
347
+ if is_systemd_available() and ROOT_DIR_PATH == DEFAULT_ROOT_DIR_PATH:
348
+ return 'systemd'
349
+
350
+ return 'local'
351
+
352
+
353
+ def _install_healthcheck_job() -> SuccessTuple:
354
+ """
355
+ Install the systemd job which checks local jobs.
356
+ """
357
+ if get_executor_keys_from_context() != 'systemd':
358
+ return False, "Not running systemd."
359
+
360
+ job = Job(
361
+ '.local_healthcheck',
362
+ ['restart', 'jobs', '-e', 'local', '--loop'],
363
+ executor_keys='systemd',
364
+ )
365
+ return job.start()
@@ -37,12 +37,12 @@ __pdoc__ = {
37
37
  }
38
38
 
39
39
  def make_action(
40
- function: Callable[[Any], Any],
41
- shell: bool = False,
42
- activate: bool = True,
43
- deactivate: bool = True,
44
- debug: bool = False
45
- ) -> Callable[[Any], Any]:
40
+ function: Callable[[Any], Any],
41
+ shell: bool = False,
42
+ activate: bool = True,
43
+ deactivate: bool = True,
44
+ debug: bool = False
45
+ ) -> Callable[[Any], Any]:
46
46
  """
47
47
  Make a function a Meerschaum action. Useful for plugins that are adding multiple actions.
48
48
 
@@ -294,6 +294,7 @@ class Daemon:
294
294
  with self._daemon_context:
295
295
  sys.stdin = self.stdin_file
296
296
  os.environ[STATIC_CONFIG['environment']['daemon_id']] = self.daemon_id
297
+ os.environ['PYTHONUNBUFFERED'] = '1'
297
298
  self.rotating_log.refresh_files(start_interception=True)
298
299
  result = None
299
300
  try:
@@ -596,6 +597,9 @@ class Daemon:
596
597
  if action not in ('quit', 'kill', 'pause'):
597
598
  return False, f"Unsupported action '{action}'."
598
599
 
600
+ if not self.stop_path.parent.exists():
601
+ self.stop_path.parent.mkdir(parents=True, exist_ok=True)
602
+
599
603
  with open(self.stop_path, 'w+', encoding='utf-8') as f:
600
604
  json.dump(
601
605
  {
@@ -810,6 +814,9 @@ class Daemon:
810
814
  """
811
815
  Return the stdin file path.
812
816
  """
817
+ if '_blocking_stdin_file_path' in self.__dict__:
818
+ return self._blocking_stdin_file_path
819
+
813
820
  return self.path / 'input.stdin.block'
814
821
 
815
822
  @property
@@ -38,7 +38,7 @@ class RotatingFile(io.IOBase):
38
38
  max_file_size: Optional[int] = None,
39
39
  redirect_streams: bool = False,
40
40
  write_timestamps: bool = False,
41
- timestamp_format: str = '%Y-%m-%d %H:%M',
41
+ timestamp_format: Optional[str] = None,
42
42
  ):
43
43
  """
44
44
  Create a file-like object which manages other files.
@@ -64,14 +64,17 @@ class RotatingFile(io.IOBase):
64
64
  write_timestamps: bool, default False
65
65
  If `True`, prepend the current UTC timestamp to each line of the file.
66
66
 
67
- timestamp_format: str, default '%Y-%m-%d %H:%M'
67
+ timestamp_format: str, default None
68
68
  If `write_timestamps` is `True`, use this format for the timestamps.
69
+ Defaults to `'%Y-%m-%d %H:%M'`.
69
70
  """
70
71
  self.file_path = pathlib.Path(file_path)
71
72
  if num_files_to_keep is None:
72
73
  num_files_to_keep = get_config('jobs', 'logs', 'num_files_to_keep')
73
74
  if max_file_size is None:
74
75
  max_file_size = get_config('jobs', 'logs', 'max_file_size')
76
+ if timestamp_format is None:
77
+ timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
75
78
  if num_files_to_keep < 2:
76
79
  raise ValueError("At least 2 files must be kept.")
77
80
  if max_file_size < 1:
@@ -13,7 +13,7 @@ import os
13
13
  import selectors
14
14
  import traceback
15
15
 
16
- from meerschaum.utils.typing import Optional
16
+ from meerschaum.utils.typing import Optional, Union
17
17
  from meerschaum.utils.warnings import warn
18
18
 
19
19
 
@@ -23,9 +23,12 @@ class StdinFile(io.TextIOBase):
23
23
  """
24
24
  def __init__(
25
25
  self,
26
- file_path: pathlib.Path,
26
+ file_path: Union[pathlib.Path, str],
27
27
  lock_file_path: Optional[pathlib.Path] = None,
28
28
  ):
29
+ if isinstance(file_path, str):
30
+ file_path = pathlib.Path(file_path)
31
+
29
32
  self.file_path = file_path
30
33
  self.blocking_file_path = (
31
34
  lock_file_path
@@ -108,3 +111,10 @@ class StdinFile(io.TextIOBase):
108
111
 
109
112
  def is_open(self):
110
113
  return self._file_handler is not None
114
+
115
+
116
+ def __str__(self) -> str:
117
+ return f"StdinFile('{self.file_path}')"
118
+
119
+ def __repr__(self) -> str:
120
+ return str(self)
@@ -10,9 +10,11 @@ from __future__ import annotations
10
10
  import os, pathlib, shutil, json, datetime, threading, shlex
11
11
  from meerschaum.utils.typing import SuccessTuple, List, Optional, Callable, Any, Dict
12
12
  from meerschaum.config._paths import DAEMON_RESOURCES_PATH
13
+ from meerschaum.utils.daemon.StdinFile import StdinFile
13
14
  from meerschaum.utils.daemon.Daemon import Daemon
14
15
  from meerschaum.utils.daemon.RotatingFile import RotatingFile
15
16
  from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInterceptor
17
+ from meerschaum.utils.daemon._names import get_new_daemon_name
16
18
 
17
19
 
18
20
  def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
@@ -12,12 +12,13 @@ from datetime import datetime, timezone
12
12
 
13
13
  import meerschaum as mrsm
14
14
  from meerschaum.utils.typing import List, Optional, Any, is_success_tuple, Dict
15
- from meerschaum.utils.jobs import (
15
+ from meerschaum.jobs import (
16
16
  Job,
17
17
  get_jobs,
18
18
  get_running_jobs,
19
19
  get_stopped_jobs,
20
20
  get_paused_jobs,
21
+ get_executor_keys_from_context,
21
22
  )
22
23
  from meerschaum.config import get_config
23
24
 
@@ -28,11 +29,17 @@ def pprint_jobs(
28
29
  ):
29
30
  """Pretty-print a list of Daemons."""
30
31
  from meerschaum.utils.formatting import make_header
32
+ from meerschaum.utils.misc import items_str
31
33
 
32
34
  running_jobs = get_running_jobs(jobs=jobs)
33
35
  paused_jobs = get_paused_jobs(jobs=jobs)
34
36
  stopped_jobs = get_stopped_jobs(jobs=jobs)
35
- executor_keys = (list(jobs.values())[0].executor_keys if jobs else None) or 'local'
37
+ executor_keys_list = list(set(
38
+ [
39
+ job.executor_keys.replace('systemd:main', 'systemd')
40
+ for job in jobs.values()
41
+ ]
42
+ )) if jobs else [get_executor_keys_from_context()]
36
43
 
37
44
  def _nopretty_print():
38
45
  from meerschaum.utils.misc import print_options
@@ -62,12 +69,20 @@ def pprint_jobs(
62
69
  'rich.table', 'rich.text', 'rich.box', 'rich.json', 'rich.panel', 'rich.console',
63
70
  )
64
71
  table = rich_table.Table(
65
- title=rich_text.Text(f"\nJobs on Executor '{executor_keys}'"),
72
+ title=rich_text.Text(
73
+ f"\nJobs on Executor"
74
+ + ('s' if len(executor_keys_list) != 1 else '')
75
+ + f" {items_str(executor_keys_list)}"
76
+ ),
66
77
  box=(rich_box.ROUNDED if UNICODE else rich_box.ASCII),
67
78
  show_lines=True,
68
79
  show_header=ANSI,
69
80
  )
70
81
  table.add_column("Name", justify='right', style=('magenta' if ANSI else ''))
82
+ table.add_column(
83
+ "Executor",
84
+ style=(get_config('shell', 'ansi', 'executor', 'rich', 'style') if ANSI else ''),
85
+ )
71
86
  table.add_column("Command")
72
87
  table.add_column("Status")
73
88
 
@@ -100,40 +115,61 @@ def pprint_jobs(
100
115
  if job.hidden:
101
116
  continue
102
117
 
103
- status_text = (
104
- rich_text.Text(job.status, style=('green' if ANSI else ''))
105
- if not job.is_blocking_on_stdin()
106
- else rich_text.Text('waiting for input', style=('yellow' if ANSI else ''))
107
- )
118
+ status_group = [
119
+ (
120
+ rich_text.Text(job.status, style=('green' if ANSI else ''))
121
+ if not job.is_blocking_on_stdin()
122
+ else rich_text.Text('waiting for input', style=('yellow' if ANSI else ''))
123
+ ),
124
+ rich_text.Text(f'PID: {job.pid}'),
125
+ ]
126
+ if job.restart:
127
+ status_group.append(rich_text.Text('(restarts)'))
108
128
 
109
129
  table.add_row(
110
130
  job.name,
131
+ job.executor_keys.replace('systemd:main', 'systemd'),
111
132
  job.label,
112
- rich_console.Group(status_text),
133
+ rich_console.Group(*status_group),
113
134
  )
114
135
 
115
136
  for name, job in paused_jobs.items():
116
137
  if job.hidden:
117
138
  continue
139
+
140
+ status_group = [
141
+ rich_text.Text(job.status, style=('yellow' if ANSI else '')),
142
+ ]
143
+ if job.restart:
144
+ status_group.append(rich_text.Text('(restarts)'))
145
+
118
146
  table.add_row(
119
147
  job.name,
148
+ job.executor_keys.replace('systemd:main', 'systemd'),
120
149
  job.label,
121
- rich_console.Group(
122
- rich_text.Text(job.status, style=('yellow' if ANSI else '')),
123
- ),
150
+ rich_console.Group(*status_group),
124
151
  )
125
152
 
126
153
  for name, job in stopped_jobs.items():
127
154
  if job.hidden:
128
155
  continue
129
156
 
157
+ status_group = [
158
+ rich_text.Text(job.status, style=('red' if ANSI else '')),
159
+ ]
160
+ if job.restart:
161
+ if job.stop_time is None:
162
+ status_group.append(rich_text.Text('(restarts)'))
163
+ else:
164
+ status_group.append(rich_text.Text('(start to resume restarts)'))
165
+
166
+ status_group.append(get_success_text(job))
167
+
130
168
  table.add_row(
131
169
  job.name,
170
+ job.executor_keys.replace('systemd:main', 'systemd'),
132
171
  job.label,
133
- rich_console.Group(
134
- rich_text.Text(job.status, style=('red' if ANSI else '')),
135
- get_success_text(job)
136
- ),
172
+ rich_console.Group(*status_group),
137
173
  )
138
174
  get_console().print(table)
139
175
 
meerschaum/utils/misc.py CHANGED
@@ -902,6 +902,7 @@ def get_connector_labels(
902
902
  *types: str,
903
903
  search_term: str = '',
904
904
  ignore_exact_match = True,
905
+ _additional_options: Optional[List[str]] = None,
905
906
  ) -> List[str]:
906
907
  """
907
908
  Read connector labels from the configuration dictionary.
@@ -941,12 +942,16 @@ def get_connector_labels(
941
942
  continue
942
943
  conns += [ f'{t}:{label}' for label in connectors.get(t, {}) if label != 'default' ]
943
944
 
945
+ if _additional_options:
946
+ conns += _additional_options
947
+
944
948
  possibilities = [
945
- c for c in conns
946
- if c.startswith(search_term)
947
- and c != (
948
- search_term if ignore_exact_match else ''
949
- )
949
+ c
950
+ for c in conns
951
+ if c.startswith(search_term)
952
+ and c != (
953
+ search_term if ignore_exact_match else ''
954
+ )
950
955
  ]
951
956
  return sorted(possibilities)
952
957
 
@@ -1218,6 +1223,19 @@ def is_bcp_available() -> bool:
1218
1223
  return has_bcp
1219
1224
 
1220
1225
 
1226
+ def is_systemd_available() -> bool:
1227
+ """Check if running on systemd."""
1228
+ import subprocess
1229
+ try:
1230
+ has_systemctl = subprocess.call(
1231
+ ['systemctl', '-h'],
1232
+ stdout=subprocess.DEVNULL,
1233
+ stderr=subprocess.STDOUT,
1234
+ ) == 0
1235
+ except Exception:
1236
+ has_systemctl = False
1237
+ return has_systemctl
1238
+
1221
1239
  def get_last_n_lines(file_name: str, N: int):
1222
1240
  """
1223
1241
  https://thispointer.com/python-get-last-n-lines-of-a-text-file-like-tail-command/
@@ -47,15 +47,17 @@ packages: Dict[str, Dict[str, str]] = {
47
47
  'packaging' : 'packaging>=21.3.0',
48
48
  'prompt_toolkit' : 'prompt-toolkit>=3.0.39',
49
49
  'more_itertools' : 'more-itertools>=8.7.0',
50
- 'daemon' : 'python-daemon>=0.2.3',
51
50
  'fasteners' : 'fasteners>=0.19.0',
52
- 'psutil' : 'psutil>=5.8.0',
53
- 'watchfiles' : 'watchfiles>=0.21.0',
54
- 'dill' : 'dill>=0.3.3',
55
51
  'virtualenv' : 'virtualenv>=20.1.0',
56
52
  'apscheduler' : 'APScheduler>=4.0.0a5',
57
53
  'uv' : 'uv>=0.2.11',
58
54
  },
55
+ 'jobs': {
56
+ 'dill' : 'dill>=0.3.3',
57
+ 'daemon' : 'python-daemon>=0.2.3',
58
+ 'watchfiles' : 'watchfiles>=0.21.0',
59
+ 'psutil' : 'psutil>=5.8.0',
60
+ },
59
61
  'drivers': {
60
62
  'cryptography' : 'cryptography>=38.0.1',
61
63
  'psycopg' : 'psycopg[binary]>=3.2.1',
@@ -156,6 +158,7 @@ packages['api'] = {
156
158
  packages['api'].update(packages['sql'])
157
159
  packages['api'].update(packages['formatting'])
158
160
  packages['api'].update(packages['dash'])
161
+ packages['api'].update(packages['jobs'])
159
162
 
160
163
  all_packages = {}
161
164
  for group, import_names in packages.items():