meerschaum 2.2.7__py3-none-any.whl → 2.3.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 (70) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +72 -6
  5. meerschaum/_internal/arguments/_parser.py +45 -15
  6. meerschaum/_internal/docs/index.py +265 -8
  7. meerschaum/_internal/entry.py +154 -24
  8. meerschaum/_internal/shell/Shell.py +264 -77
  9. meerschaum/actions/__init__.py +29 -17
  10. meerschaum/actions/api.py +12 -12
  11. meerschaum/actions/attach.py +113 -0
  12. meerschaum/actions/copy.py +68 -41
  13. meerschaum/actions/delete.py +112 -50
  14. meerschaum/actions/edit.py +3 -3
  15. meerschaum/actions/install.py +40 -32
  16. meerschaum/actions/pause.py +44 -27
  17. meerschaum/actions/restart.py +107 -0
  18. meerschaum/actions/show.py +130 -159
  19. meerschaum/actions/start.py +161 -100
  20. meerschaum/actions/stop.py +78 -42
  21. meerschaum/api/_events.py +25 -1
  22. meerschaum/api/_oauth2.py +2 -0
  23. meerschaum/api/_websockets.py +2 -2
  24. meerschaum/api/dash/callbacks/jobs.py +36 -44
  25. meerschaum/api/dash/jobs.py +89 -78
  26. meerschaum/api/routes/__init__.py +1 -0
  27. meerschaum/api/routes/_actions.py +148 -17
  28. meerschaum/api/routes/_jobs.py +407 -0
  29. meerschaum/api/routes/_pipes.py +5 -5
  30. meerschaum/config/_default.py +1 -0
  31. meerschaum/config/_jobs.py +1 -1
  32. meerschaum/config/_paths.py +7 -0
  33. meerschaum/config/_shell.py +8 -3
  34. meerschaum/config/_version.py +1 -1
  35. meerschaum/config/static/__init__.py +17 -0
  36. meerschaum/connectors/Connector.py +13 -7
  37. meerschaum/connectors/__init__.py +28 -15
  38. meerschaum/connectors/api/APIConnector.py +27 -1
  39. meerschaum/connectors/api/_actions.py +71 -6
  40. meerschaum/connectors/api/_jobs.py +368 -0
  41. meerschaum/connectors/api/_pipes.py +85 -84
  42. meerschaum/connectors/parse.py +27 -15
  43. meerschaum/core/Pipe/_bootstrap.py +16 -8
  44. meerschaum/jobs/_Executor.py +69 -0
  45. meerschaum/jobs/_Job.py +899 -0
  46. meerschaum/jobs/__init__.py +396 -0
  47. meerschaum/jobs/systemd.py +694 -0
  48. meerschaum/plugins/__init__.py +97 -12
  49. meerschaum/utils/daemon/Daemon.py +276 -30
  50. meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
  51. meerschaum/utils/daemon/RotatingFile.py +14 -7
  52. meerschaum/utils/daemon/StdinFile.py +121 -0
  53. meerschaum/utils/daemon/__init__.py +15 -7
  54. meerschaum/utils/daemon/_names.py +15 -13
  55. meerschaum/utils/formatting/__init__.py +2 -1
  56. meerschaum/utils/formatting/_jobs.py +115 -62
  57. meerschaum/utils/formatting/_shell.py +6 -0
  58. meerschaum/utils/misc.py +41 -22
  59. meerschaum/utils/packages/_packages.py +9 -6
  60. meerschaum/utils/process.py +9 -9
  61. meerschaum/utils/prompt.py +16 -8
  62. meerschaum/utils/venv/__init__.py +2 -2
  63. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  64. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/RECORD +70 -61
  65. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  66. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  67. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  68. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  69. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  70. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
@@ -0,0 +1,694 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Manage `meerschaum.jobs.Job` via `systemd`.
6
+ """
7
+
8
+ import os
9
+ import pathlib
10
+ import shlex
11
+ import sys
12
+ import asyncio
13
+ import json
14
+ import time
15
+ import traceback
16
+ import shutil
17
+ from datetime import datetime, timezone
18
+ from functools import partial
19
+
20
+ import meerschaum as mrsm
21
+ from meerschaum.jobs import Job, Executor, make_executor
22
+ from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union, Optional, Callable
23
+ from meerschaum.config import get_config
24
+ from meerschaum.config.static import STATIC_CONFIG
25
+ from meerschaum.utils.warnings import warn, dprint
26
+ from meerschaum._internal.arguments._parse_arguments import parse_arguments
27
+
28
+ JOB_METADATA_CACHE_SECONDS: int = STATIC_CONFIG['api']['jobs']['metadata_cache_seconds']
29
+
30
+
31
+ @make_executor
32
+ class SystemdExecutor(Executor):
33
+ """
34
+ Execute Meerschaum jobs via `systemd`.
35
+ """
36
+
37
+ def get_job_names(self, debug: bool = False) -> List[str]:
38
+ """
39
+ Return a list of existing jobs, including hidden ones.
40
+ """
41
+ from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
42
+ return [
43
+ service_name[len('mrsm-'):(-1 * len('.service'))]
44
+ for service_name in os.listdir(SYSTEMD_USER_RESOURCES_PATH)
45
+ if service_name.startswith('mrsm-') and service_name.endswith('.service')
46
+ ]
47
+
48
+ def get_job_exists(self, name: str, debug: bool = False) -> bool:
49
+ """
50
+ Return whether a job exists.
51
+ """
52
+ user_services = self.get_job_names(debug=debug)
53
+ if debug:
54
+ dprint(f'Existing services: {user_services}')
55
+ return name in user_services
56
+
57
+ def get_jobs(self, debug: bool = False) -> Dict[str, Job]:
58
+ """
59
+ Return a dictionary of `systemd` Jobs (including hidden jobs).
60
+ """
61
+ user_services = self.get_job_names(debug=debug)
62
+ jobs = {
63
+ name: Job(name, executor_keys=str(self))
64
+ for name in user_services
65
+ }
66
+ return {
67
+ name: job
68
+ for name, job in jobs.items()
69
+ }
70
+
71
+ def get_service_name(self, name: str, debug: bool = False) -> str:
72
+ """
73
+ Return a job's service name.
74
+ """
75
+ return f"mrsm-{name.replace(' ', '-')}.service"
76
+
77
+ def get_service_job_path(self, name: str, debug: bool = False) -> pathlib.Path:
78
+ """
79
+ Return the path for the job's files under the root directory.
80
+ """
81
+ from meerschaum.config.paths import SYSTEMD_JOBS_RESOURCES_PATH
82
+ return SYSTEMD_JOBS_RESOURCES_PATH / name
83
+
84
+ def get_service_symlink_file_path(self, name: str, debug: bool = False) -> pathlib.Path:
85
+ """
86
+ Return the path to where to create the service symlink.
87
+ """
88
+ from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
89
+ return SYSTEMD_USER_RESOURCES_PATH / self.get_service_name(name, debug=debug)
90
+
91
+ def get_service_file_path(self, name: str, debug: bool = False) -> pathlib.Path:
92
+ """
93
+ Return the path to a Job's service file.
94
+ """
95
+ return (
96
+ self.get_service_job_path(name, debug=debug)
97
+ / self.get_service_name(name, debug=debug)
98
+ )
99
+
100
+ def get_service_logs_path(self, name: str, debug: bool = False) -> pathlib.Path:
101
+ """
102
+ Return the path to direct service logs to.
103
+ """
104
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
105
+ return SYSTEMD_LOGS_RESOURCES_PATH / (self.get_service_name(name, debug=debug) + '.log')
106
+
107
+ def get_socket_path(self, name: str, debug: bool = False) -> pathlib.Path:
108
+ """
109
+ Return the path to the FIFO file.
110
+ """
111
+ return (
112
+ self.get_service_job_path(name, debug=debug)
113
+ / (self.get_service_name(name, debug=debug) + '.stdin')
114
+ )
115
+
116
+ def get_result_path(self, name: str, debug: bool = False) -> pathlib.Path:
117
+ """
118
+ Return the path to the result file.
119
+ """
120
+ return (
121
+ self.get_service_job_path(name, debug=debug)
122
+ / (self.get_service_name(name, debug=debug) + '.result.json')
123
+ )
124
+
125
+ def get_service_file_text(self, name: str, sysargs: List[str], debug: bool = False) -> str:
126
+ """
127
+ Return the contents of the unit file.
128
+ """
129
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
130
+ socket_path = self.get_socket_path(name, debug=debug)
131
+ result_path = self.get_result_path(name, debug=debug)
132
+
133
+ sysargs_str = shlex.join(sysargs)
134
+ exec_str = f'{sys.executable} -m meerschaum {sysargs_str}'
135
+ mrsm_env_var_names = set([var for var in STATIC_CONFIG['environment'].values()])
136
+ mrsm_env_vars = {
137
+ key: val
138
+ for key, val in os.environ.items()
139
+ if key in mrsm_env_var_names
140
+ }
141
+
142
+ ### Add new environment variables for the service process.
143
+ mrsm_env_vars.update({
144
+ STATIC_CONFIG['environment']['daemon_id']: name,
145
+ STATIC_CONFIG['environment']['systemd_log_path']: service_logs_path.as_posix(),
146
+ STATIC_CONFIG['environment']['systemd_result_path']: result_path.as_posix(),
147
+ STATIC_CONFIG['environment']['systemd_stdin_path']: socket_path.as_posix(),
148
+ 'LINES': get_config('jobs', 'terminal', 'lines'),
149
+ 'COLUMNS': get_config('jobs', 'terminal', 'columns'),
150
+ })
151
+ environment_lines = [
152
+ f"Environment={key}={val}"
153
+ for key, val in mrsm_env_vars.items()
154
+ ]
155
+ environment_str = '\n'.join(environment_lines)
156
+ service_name = self.get_service_name(name, debug=debug)
157
+
158
+ service_text = (
159
+ "[Unit]\n"
160
+ f"Description=Run the job '{name}'\n"
161
+ "\n"
162
+ "[Service]\n"
163
+ f"ExecStart={exec_str}\n"
164
+ "KillSignal=SIGTERM\n"
165
+ "Restart=always\n"
166
+ "RestartPreventExitStatus=0\n"
167
+ f"SyslogIdentifier={service_name}\n"
168
+ f"{environment_str}\n"
169
+ "\n"
170
+ "[Install]\n"
171
+ "WantedBy=default.target\n"
172
+ )
173
+ return service_text
174
+
175
+ def get_socket_file_text(self, name: str, debug: bool = False) -> str:
176
+ """
177
+ Return the contents of the socket file.
178
+ """
179
+ service_name = self.get_service_name(name, debug=debug)
180
+ socket_path = self.get_socket_path(name, debug=debug)
181
+ socket_text = (
182
+ "[Unit]\n"
183
+ f"BindsTo={service_name}\n"
184
+ "\n"
185
+ "[Socket]\n"
186
+ f"ListenFIFO={socket_path.as_posix()}\n"
187
+ "FileDescriptorName=stdin\n"
188
+ "RemoveOnStop=true\n"
189
+ "SocketMode=0660\n"
190
+ )
191
+ return socket_text
192
+
193
+ def get_hidden_job(self, name: str, sysargs: Optional[List[str]] = None, debug: bool = False):
194
+ """
195
+ Return the hidden "sister" job to store a job's parameters.
196
+ """
197
+ job = Job(
198
+ name,
199
+ sysargs,
200
+ executor_keys='local',
201
+ _rotating_log=self.get_job_rotating_file(name, debug=debug),
202
+ _stdin_file=self.get_job_stdin_file(name, debug=debug),
203
+ _status_hook=partial(self.get_job_status, name),
204
+ _result_hook=partial(self.get_job_result, name),
205
+ _externally_managed=True,
206
+ )
207
+ job._set_externally_managed()
208
+ return job
209
+
210
+
211
+ def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]:
212
+ """
213
+ Return metadata about a job.
214
+ """
215
+ now = time.perf_counter()
216
+ if '_jobs_metadata' not in self.__dict__:
217
+ self._jobs_metadata: Dict[str, Any] = {}
218
+
219
+ if name in self._jobs_metadata:
220
+ ts = self._jobs_metadata[name].get('timestamp', None)
221
+
222
+ if ts is not None and (now - ts) <= JOB_METADATA_CACHE_SECONDS:
223
+ if debug:
224
+ dprint(f"Retuning cached metadata for job '{name}'.")
225
+ return self._jobs_metadata[name]['metadata']
226
+
227
+ metadata = {
228
+ 'sysargs': self.get_job_sysargs(name, debug=debug),
229
+ 'result': self.get_job_result(name, debug=debug),
230
+ 'restart': self.get_job_restart(name, debug=debug),
231
+ 'daemon': {
232
+ 'status': self.get_job_status(name, debug=debug),
233
+ 'pid': self.get_job_pid(name, debug=debug),
234
+ },
235
+ }
236
+ self._jobs_metadata[name] = {
237
+ 'timestamp': now,
238
+ 'metadata': metadata,
239
+ }
240
+ return metadata
241
+
242
+ def get_job_restart(self, name: str, debug: bool = False) -> bool:
243
+ """
244
+ Return whether a job restarts.
245
+ """
246
+ from meerschaum.jobs._Job import RESTART_FLAGS
247
+ sysargs = self.get_job_sysargs(name, debug=debug)
248
+ for flag in RESTART_FLAGS:
249
+ if flag in sysargs:
250
+ return True
251
+ return False
252
+
253
+ def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]:
254
+ """
255
+ Return the properties for a job.
256
+ """
257
+ metadata = self.get_job_metadata(name, debug=debug)
258
+ return metadata.get('daemon', {}).get('properties', {})
259
+
260
+ def get_job_process(self, name: str, debug: bool = False):
261
+ """
262
+ Return a `psutil.Process` for the job's PID.
263
+ """
264
+ pid = self.get_job_pid(name, debug=debug)
265
+ if pid is None:
266
+ return None
267
+
268
+ psutil = mrsm.attempt_import('psutil')
269
+ try:
270
+ return psutil.Process(pid)
271
+ except Exception:
272
+ return None
273
+
274
+ def get_job_status(self, name: str, debug: bool = False) -> str:
275
+ """
276
+ Return the job's service status.
277
+ """
278
+ output = self.run_command(
279
+ ['is-active', self.get_service_name(name, debug=debug)],
280
+ as_output=True,
281
+ debug=debug,
282
+ )
283
+
284
+ if output == 'activating':
285
+ return 'running'
286
+
287
+ if output == 'active':
288
+ process = self.get_job_process(name, debug=debug)
289
+ if process is None:
290
+ return 'stopped'
291
+
292
+ try:
293
+ if process.status() == 'stopped':
294
+ return 'paused'
295
+ except Exception:
296
+ return 'stopped'
297
+
298
+ return 'running'
299
+
300
+ return 'stopped'
301
+
302
+ def get_job_pid(self, name: str, debug: bool = False) -> Union[int, None]:
303
+ """
304
+ Return the job's service PID.
305
+ """
306
+ from meerschaum.utils.misc import is_int
307
+
308
+ output = self.run_command(
309
+ [
310
+ 'show',
311
+ self.get_service_name(name, debug=debug),
312
+ '--property=MainPID',
313
+ ],
314
+ as_output=True,
315
+ debug=debug,
316
+ )
317
+ if not output.startswith('MainPID='):
318
+ return None
319
+
320
+ pid_str = output[len('MainPID='):]
321
+ if pid_str == '0':
322
+ return None
323
+
324
+ if is_int(pid_str):
325
+ return int(pid_str)
326
+
327
+ return None
328
+
329
+ def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
330
+ """
331
+ Return when a job began running.
332
+ """
333
+ output = self.run_command(
334
+ [
335
+ 'show',
336
+ self.get_service_name(name, debug=debug),
337
+ '--property=ActiveEnterTimestamp'
338
+ ],
339
+ as_output=True,
340
+ debug=debug,
341
+ )
342
+ if not output.startswith('ActiveEnterTimestamp'):
343
+ return None
344
+
345
+ dt_str = output.split('=')[-1]
346
+ if not dt_str:
347
+ return None
348
+
349
+ dateutil_parser = mrsm.attempt_import('dateutil.parser')
350
+ try:
351
+ dt = dateutil_parser.parse(dt_str)
352
+ except Exception as e:
353
+ warn(f"Cannot parse '{output}' as a datetime:\n{e}")
354
+ return None
355
+
356
+ return dt.astimezone(timezone.utc).isoformat()
357
+
358
+ def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
359
+ """
360
+ Return when a job began running.
361
+ """
362
+ output = self.run_command(
363
+ [
364
+ 'show',
365
+ self.get_service_name(name, debug=debug),
366
+ '--property=InactiveEnterTimestamp'
367
+ ],
368
+ as_output=True,
369
+ debug=debug,
370
+ )
371
+ if not output.startswith('InactiveEnterTimestamp'):
372
+ return None
373
+
374
+ dt_str = output.split('=')[-1]
375
+ if not dt_str:
376
+ return None
377
+
378
+ dateutil_parser = mrsm.attempt_import('dateutil.parser')
379
+
380
+ try:
381
+ dt = dateutil_parser.parse(dt_str)
382
+ except Exception as e:
383
+ warn(f"Cannot parse '{output}' as a datetime:\n{e}")
384
+ return None
385
+ return dt.astimezone(timezone.utc).isoformat()
386
+
387
+ def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
388
+ """
389
+ Return when a job was paused.
390
+ """
391
+ job = self.get_hidden_job(name, debug=debug)
392
+ if self.get_job_status(name, debug=debug) != 'paused':
393
+ return None
394
+
395
+ stop_time = job.stop_time
396
+ if stop_time is None:
397
+ return None
398
+
399
+ return stop_time.isoformat()
400
+
401
+ def get_job_result(self, name: str, debug: bool = False) -> SuccessTuple:
402
+ """
403
+ Return the job's result SuccessTuple.
404
+ """
405
+ result_path = self.get_result_path(name, debug=debug)
406
+ if not result_path.exists():
407
+ return False, "No result available."
408
+
409
+ try:
410
+ with open(result_path, 'r', encoding='utf-8') as f:
411
+ result = json.load(f)
412
+ except Exception:
413
+ return False, f"Could not read result for Job '{name}'."
414
+
415
+ return tuple(result)
416
+
417
+ def get_job_sysargs(self, name: str, debug: bool = False) -> Union[List[str], None]:
418
+ """
419
+ Return the sysargs from the service file.
420
+ """
421
+ service_file_path = self.get_service_file_path(name, debug=debug)
422
+ if not service_file_path.exists():
423
+ return []
424
+
425
+ with open(service_file_path, 'r', encoding='utf-8') as f:
426
+ service_lines = f.readlines()
427
+
428
+ for line in service_lines:
429
+ if line.startswith('ExecStart='):
430
+ sysargs_str = line.split(' -m meerschaum ')[-1].split('<')[0]
431
+ return shlex.split(sysargs_str)
432
+
433
+ return []
434
+
435
+ def run_command(
436
+ self,
437
+ command_args: List[str],
438
+ as_output: bool = False,
439
+ debug: bool = False,
440
+ ) -> Union[SuccessTuple, str]:
441
+ """
442
+ Run a `systemd` command and return success.
443
+
444
+ Parameters
445
+ ----------
446
+ command_args: List[str]
447
+ The command to pass to `systemctl --user`.
448
+
449
+ as_output: bool, default False
450
+ If `True`, return the process stdout output.
451
+ Defaults to a `SuccessTuple`.
452
+
453
+ Returns
454
+ -------
455
+ A `SuccessTuple` indicating success or a str for the process output.
456
+ """
457
+ from meerschaum.utils.process import run_process
458
+
459
+ if command_args[:2] != ['systemctl', '--user']:
460
+ command_args = ['systemctl', '--user'] + command_args
461
+
462
+ if debug:
463
+ dprint(shlex.join(command_args))
464
+
465
+ proc = run_process(
466
+ command_args,
467
+ foreground=False,
468
+ as_proc=True,
469
+ capture_output=True,
470
+ text=True,
471
+ )
472
+ stdout, stderr = proc.communicate()
473
+ if debug:
474
+ dprint(f"{stdout}")
475
+
476
+ if as_output:
477
+ return stdout.strip()
478
+
479
+ command_success = proc.wait() == 0
480
+ command_msg = (
481
+ "Success"
482
+ if command_success
483
+ else f"Failed to execute command `{shlex.join(command_args)}`."
484
+ )
485
+ return command_success, command_msg
486
+
487
+ def get_job_stdin_file(self, name: str, debug: bool = False):
488
+ """
489
+ Return a `StdinFile` for the job.
490
+ """
491
+ from meerschaum.utils.daemon import StdinFile
492
+ if '_stdin_files' not in self.__dict__:
493
+ self._stdin_files: Dict[str, StdinFile] = {}
494
+
495
+ if name not in self._stdin_files:
496
+ socket_path = self.get_socket_path(name, debug=debug)
497
+ socket_path.parent.mkdir(parents=True, exist_ok=True)
498
+ self._stdin_files[name] = StdinFile(socket_path)
499
+
500
+ return self._stdin_files[name]
501
+
502
+ def create_job(self, name: str, sysargs: List[str], debug: bool = False) -> SuccessTuple:
503
+ """
504
+ Create a job as a service to be run by `systemd`.
505
+ """
506
+ from meerschaum.utils.misc import make_symlink
507
+ service_name = self.get_service_name(name, debug=debug)
508
+ service_file_path = self.get_service_file_path(name, debug=debug)
509
+ service_symlink_file_path = self.get_service_symlink_file_path(name, debug=debug)
510
+ socket_stdin = self.get_job_stdin_file(name, debug=debug)
511
+ _ = socket_stdin.file_handler
512
+
513
+ ### Init the `externally_managed file`.
514
+ _ = self.get_hidden_job(name, debug=debug)
515
+
516
+ with open(service_file_path, 'w+', encoding='utf-8') as f:
517
+ f.write(self.get_service_file_text(name, sysargs, debug=debug))
518
+
519
+ symlink_success, symlink_msg = make_symlink(service_file_path, service_symlink_file_path)
520
+ if not symlink_success:
521
+ return symlink_success, symlink_msg
522
+
523
+ commands = [
524
+ ['daemon-reload'],
525
+ ['enable', service_name],
526
+ ['start', service_name],
527
+ ]
528
+
529
+ fails = 0
530
+ for command_list in commands:
531
+ command_success, command_msg = self.run_command(command_list, debug=debug)
532
+ if not command_success:
533
+ fails += 1
534
+
535
+ if fails > 1:
536
+ return False, "Failed to reload systemd."
537
+
538
+ return True, f"Started job '{name}' via systemd."
539
+
540
+ def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
541
+ """
542
+ Stop a job's service.
543
+ """
544
+ job = self.get_hidden_job(name, debug=debug)
545
+ job.daemon._remove_stop_file()
546
+
547
+ status = self.get_job_status(name, debug=debug)
548
+ if status == 'paused':
549
+ return self.run_command(
550
+ ['kill', '-s', 'SIGCONT', self.get_service_name(name, debug=debug)],
551
+ debug=debug,
552
+ )
553
+
554
+ return self.run_command(
555
+ ['start', self.get_service_name(name, debug=debug)],
556
+ debug=debug,
557
+ )
558
+
559
+ def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
560
+ """
561
+ Stop a job's service.
562
+ """
563
+ job = self.get_hidden_job(name, debug=debug)
564
+ job.daemon._write_stop_file('quit')
565
+ sigint_success, sigint_msg = self.run_command(
566
+ ['kill', '-s', 'SIGINT', self.get_service_name(name, debug=debug)],
567
+ debug=debug,
568
+ )
569
+
570
+ check_timeout_interval = get_config('jobs', 'check_timeout_interval_seconds')
571
+ loop_start = time.perf_counter()
572
+ while (time.perf_counter() - loop_start) < get_config('jobs', 'timeout_seconds'):
573
+ if self.get_job_status(name, debug=debug) == 'stopped':
574
+ return True, 'Success'
575
+
576
+ time.sleep(check_timeout_interval)
577
+
578
+ return self.run_command(
579
+ ['stop', self.get_service_name(name, debug=debug)],
580
+ debug=debug,
581
+ )
582
+
583
+ def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
584
+ """
585
+ Pause a job's service.
586
+ """
587
+ job = self.get_hidden_job(name, debug=debug)
588
+ job.daemon._write_stop_file('pause')
589
+ return self.run_command(
590
+ ['kill', '-s', 'SIGSTOP', self.get_service_name(name, debug=debug)],
591
+ debug=debug,
592
+ )
593
+
594
+ def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
595
+ """
596
+ Delete a job's service.
597
+ """
598
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
599
+
600
+ _ = self.stop_job(name, debug=debug)
601
+ _ = self.run_command(
602
+ ['disable', self.get_service_name(name, debug=debug)],
603
+ debug=debug,
604
+ )
605
+
606
+ service_job_path = self.get_service_job_path(name, debug=debug)
607
+ try:
608
+ if service_job_path.exists():
609
+ shutil.rmtree(service_job_path)
610
+ except Exception as e:
611
+ warn(e)
612
+ return False, str(e)
613
+
614
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
615
+ logs_paths = [
616
+ (SYSTEMD_LOGS_RESOURCES_PATH / name)
617
+ for name in os.listdir(SYSTEMD_LOGS_RESOURCES_PATH)
618
+ if name.startswith(service_logs_path.name + '.')
619
+ ]
620
+ paths = [
621
+ self.get_service_file_path(name, debug=debug),
622
+ self.get_service_symlink_file_path(name, debug=debug),
623
+ self.get_socket_path(name, debug=debug),
624
+ self.get_result_path(name, debug=debug),
625
+ ] + logs_paths
626
+
627
+ for path in paths:
628
+ if path.exists():
629
+ try:
630
+ path.unlink()
631
+ except Exception as e:
632
+ warn(e)
633
+ return False, str(e)
634
+
635
+ job = self.get_hidden_job(name, debug=debug)
636
+ _ = job.delete()
637
+
638
+ return self.run_command(['daemon-reload'], debug=debug)
639
+
640
+ def get_logs(self, name: str, debug: bool = False) -> str:
641
+ """
642
+ Return a job's journal logs.
643
+ """
644
+ rotating_file = self.get_job_rotating_file(name, debug=debug)
645
+ return rotating_file.read()
646
+
647
+ def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]:
648
+ """
649
+ Return a job's stop time.
650
+ """
651
+ job = self.get_hidden_job(name, debug=debug)
652
+ return job.stop_time
653
+
654
+ def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
655
+ """
656
+ Return whether a job is blocking on stdin.
657
+ """
658
+ socket_path = self.get_socket_path(name, debug=debug)
659
+ blocking_path = socket_path.parent / (socket_path.name + '.block')
660
+ return blocking_path.exists()
661
+
662
+ def get_job_rotating_file(self, name: str, debug: bool = False):
663
+ """
664
+ Return a `RotatingFile` for the job's log output.
665
+ """
666
+ from meerschaum.utils.daemon import RotatingFile
667
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
668
+ return RotatingFile(service_logs_path)
669
+
670
+ async def monitor_logs_async(
671
+ self,
672
+ name: str,
673
+ *args,
674
+ debug: bool = False,
675
+ **kwargs
676
+ ):
677
+ """
678
+ Monitor a job's output.
679
+ """
680
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
681
+ job = self.get_hidden_job(name, debug=debug)
682
+ kwargs.update({
683
+ '_logs_path': SYSTEMD_LOGS_RESOURCES_PATH,
684
+ '_log': self.get_job_rotating_file(name, debug=debug),
685
+ '_stdin_file': self.get_job_stdin_file(name, debug=debug),
686
+ 'debug': debug,
687
+ })
688
+ await job.monitor_logs_async(*args, **kwargs)
689
+
690
+ def monitor_logs(self, *args, **kwargs):
691
+ """
692
+ Monitor a job's output.
693
+ """
694
+ asyncio.run(self.monitor_logs_async(*args, **kwargs))