meerschaum 2.3.0.dev1__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 (56) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/__main__.py +0 -5
  3. meerschaum/_internal/arguments/_parse_arguments.py +10 -3
  4. meerschaum/_internal/arguments/_parser.py +6 -2
  5. meerschaum/_internal/entry.py +36 -6
  6. meerschaum/_internal/shell/Shell.py +32 -20
  7. meerschaum/actions/__init__.py +8 -6
  8. meerschaum/actions/attach.py +31 -13
  9. meerschaum/actions/copy.py +68 -41
  10. meerschaum/actions/delete.py +64 -21
  11. meerschaum/actions/edit.py +3 -3
  12. meerschaum/actions/install.py +40 -32
  13. meerschaum/actions/pause.py +44 -27
  14. meerschaum/actions/restart.py +107 -0
  15. meerschaum/actions/show.py +8 -8
  16. meerschaum/actions/start.py +26 -41
  17. meerschaum/actions/stop.py +11 -4
  18. meerschaum/api/_events.py +10 -3
  19. meerschaum/api/dash/jobs.py +69 -70
  20. meerschaum/api/routes/_actions.py +8 -3
  21. meerschaum/api/routes/_jobs.py +86 -37
  22. meerschaum/config/_default.py +1 -1
  23. meerschaum/config/_paths.py +5 -0
  24. meerschaum/config/_shell.py +1 -1
  25. meerschaum/config/_version.py +1 -1
  26. meerschaum/config/static/__init__.py +6 -1
  27. meerschaum/connectors/Connector.py +13 -7
  28. meerschaum/connectors/__init__.py +21 -5
  29. meerschaum/connectors/api/APIConnector.py +3 -0
  30. meerschaum/connectors/api/_jobs.py +108 -11
  31. meerschaum/connectors/parse.py +10 -13
  32. meerschaum/core/Pipe/_bootstrap.py +16 -8
  33. meerschaum/jobs/_Executor.py +69 -0
  34. meerschaum/{utils/jobs → jobs}/_Job.py +206 -40
  35. meerschaum/jobs/_LocalExecutor.py +88 -0
  36. meerschaum/jobs/_SystemdExecutor.py +608 -0
  37. meerschaum/jobs/__init__.py +365 -0
  38. meerschaum/plugins/__init__.py +6 -6
  39. meerschaum/utils/daemon/Daemon.py +7 -0
  40. meerschaum/utils/daemon/RotatingFile.py +5 -2
  41. meerschaum/utils/daemon/StdinFile.py +12 -2
  42. meerschaum/utils/daemon/__init__.py +2 -0
  43. meerschaum/utils/formatting/_jobs.py +52 -16
  44. meerschaum/utils/misc.py +23 -5
  45. meerschaum/utils/packages/_packages.py +7 -4
  46. meerschaum/utils/process.py +9 -9
  47. meerschaum/utils/venv/__init__.py +2 -2
  48. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/METADATA +14 -17
  49. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/RECORD +55 -51
  50. meerschaum/utils/jobs/__init__.py +0 -245
  51. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/LICENSE +0 -0
  52. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/NOTICE +0 -0
  53. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/WHEEL +0 -0
  54. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/entry_points.txt +0 -0
  55. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/top_level.txt +0 -0
  56. {meerschaum-2.3.0.dev1.dist-info → meerschaum-2.3.0rc1.dist-info}/zip-safe +0 -0
@@ -0,0 +1,608 @@
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
+ from datetime import datetime, timezone
17
+ from functools import partial
18
+
19
+ import meerschaum as mrsm
20
+ from meerschaum.jobs import Job, Executor, make_executor
21
+ from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union, Optional, Callable
22
+ from meerschaum.config.static import STATIC_CONFIG
23
+ from meerschaum.utils.warnings import warn, dprint
24
+ from meerschaum._internal.arguments._parse_arguments import parse_arguments, parse_dict_to_sysargs
25
+
26
+ JOB_METADATA_CACHE_SECONDS: int = STATIC_CONFIG['api']['jobs']['metadata_cache_seconds']
27
+
28
+
29
+ @make_executor
30
+ class SystemdExecutor(Executor):
31
+ """
32
+ Execute Meerschaum jobs via `systemd`.
33
+ """
34
+
35
+ def get_job_names(self, debug: bool = False) -> List[str]:
36
+ """
37
+ Return a list of existing jobs, including hidden ones.
38
+ """
39
+ from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
40
+ return [
41
+ service_name[len('mrsm-'):(-1 * len('.service'))]
42
+ for service_name in os.listdir(SYSTEMD_USER_RESOURCES_PATH)
43
+ if service_name.startswith('mrsm-')
44
+ ]
45
+
46
+ def get_job_exists(self, name: str, debug: bool = False) -> bool:
47
+ """
48
+ Return whether a job exists.
49
+ """
50
+ user_services = self.get_job_names(debug=debug)
51
+ if debug:
52
+ dprint(f'Existing services: {user_services}')
53
+ return name in user_services
54
+
55
+ def get_jobs(self, debug: bool = False) -> Dict[str, Job]:
56
+ """
57
+ Return a dictionary of `systemd` Jobs (excluding hidden jobs).
58
+ """
59
+ user_services = self.get_job_names(debug=debug)
60
+ jobs = {
61
+ name: Job(name, executor_keys=str(self))
62
+ for name in user_services
63
+ }
64
+ return {
65
+ name: job
66
+ for name, job in jobs.items()
67
+ if not job.hidden
68
+ }
69
+
70
+ def get_service_name(self, name: str, debug: bool = False) -> str:
71
+ """
72
+ Return a job's service name.
73
+ """
74
+ return f"mrsm-{name.replace(' ', '-')}.service"
75
+
76
+ def get_service_file_path(self, name: str, debug: bool = False) -> pathlib.Path:
77
+ """
78
+ Return the path to a Job's service file.
79
+ """
80
+ from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
81
+ return SYSTEMD_USER_RESOURCES_PATH / self.get_service_name(name, debug=debug)
82
+
83
+ def get_service_logs_path(self, name: str, debug: bool = False) -> pathlib.Path:
84
+ """
85
+ Return the path to direct service logs to.
86
+ """
87
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
88
+ return SYSTEMD_LOGS_RESOURCES_PATH / (self.get_service_name(name, debug=debug) + '.log')
89
+
90
+ def get_service_socket_path(self, name: str, debug: bool = False) -> pathlib.Path:
91
+ """
92
+ Return the path to the unit file for the socket (not the socket itself).
93
+ """
94
+ from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
95
+ return SYSTEMD_USER_RESOURCES_PATH / (
96
+ self.get_service_name(name, debug=debug).replace('.service', '.socket')
97
+ )
98
+
99
+ def get_socket_path(self, name: str, debug: bool = False) -> pathlib.Path:
100
+ """
101
+ Return the path to the FIFO file.
102
+ """
103
+ from meerschaum.config.paths import SYSTEMD_ROOT_RESOURCES_PATH
104
+ return SYSTEMD_ROOT_RESOURCES_PATH / (self.get_service_name(name, debug=debug) + '.stdin')
105
+
106
+ def get_result_path(self, name: str, debug: bool = False) -> pathlib.Path:
107
+ """
108
+ Return the path to the result file.
109
+ """
110
+ from meerschaum.config.paths import SYSTEMD_ROOT_RESOURCES_PATH
111
+ return SYSTEMD_ROOT_RESOURCES_PATH / (
112
+ self.get_service_name(name, debug=debug) + '.result.json'
113
+ )
114
+
115
+ def get_service_file_text(self, name: str, sysargs: List[str], debug: bool = False) -> str:
116
+ """
117
+ Return the contents of the unit file.
118
+ """
119
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
120
+ socket_path = self.get_socket_path(name, debug=debug)
121
+ result_path = self.get_result_path(name, debug=debug)
122
+
123
+ sysargs_str = shlex.join(sysargs)
124
+ exec_str = f'{sys.executable} -m meerschaum {sysargs_str}'
125
+ mrsm_env_var_names = set([var for var in STATIC_CONFIG['environment'].values()])
126
+ mrsm_env_vars = {
127
+ key: val
128
+ for key, val in os.environ.items()
129
+ if key in mrsm_env_var_names
130
+ }
131
+
132
+ ### Add new environment variables for the service process.
133
+ mrsm_env_vars.update({
134
+ STATIC_CONFIG['environment']['daemon_id']: name,
135
+ STATIC_CONFIG['environment']['systemd_log_path']: service_logs_path.as_posix(),
136
+ STATIC_CONFIG['environment']['systemd_result_path']: result_path.as_posix(),
137
+ STATIC_CONFIG['environment']['systemd_stdin_path']: socket_path.as_posix(),
138
+
139
+ })
140
+ environment_lines = [
141
+ f"Environment={key}={val}"
142
+ for key, val in mrsm_env_vars.items()
143
+ ]
144
+ environment_str = '\n'.join(environment_lines)
145
+ service_name = self.get_service_name(name, debug=debug)
146
+
147
+ service_text = (
148
+ "[Unit]\n"
149
+ f"Description=Run the job '{name}'\n"
150
+ "\n"
151
+ "[Service]\n"
152
+ f"ExecStart={exec_str}\n"
153
+ "KillSignal=SIGINT\n"
154
+ "TimeoutStopSpec=8\n"
155
+ "Restart=always\n"
156
+ "RestartPreventExitStatus=0\n"
157
+ f"SyslogIdentifier={service_name}\n"
158
+ f"{environment_str}\n"
159
+ "\n"
160
+ "[Install]\n"
161
+ "WantedBy=default.target\n"
162
+ )
163
+ return service_text
164
+
165
+ def get_socket_file_text(self, name: str, debug: bool = False) -> str:
166
+ """
167
+ Return the contents of the socket file.
168
+ """
169
+ service_name = self.get_service_name(name, debug=debug)
170
+ socket_path = self.get_socket_path(name, debug=debug)
171
+ socket_text = (
172
+ "[Unit]\n"
173
+ f"BindsTo={service_name}\n"
174
+ "\n"
175
+ "[Socket]\n"
176
+ f"ListenFIFO={socket_path.as_posix()}\n"
177
+ "FileDescriptorName=stdin\n"
178
+ "RemoveOnStop=true\n"
179
+ "SocketMode=0660\n"
180
+ )
181
+ return socket_text
182
+
183
+ @staticmethod
184
+ def clean_sysargs(sysargs: List[str]) -> List[str]:
185
+ """
186
+ Return a sysargs list with the executor key set to 'local'.
187
+ """
188
+ kwargs = parse_arguments(sysargs)
189
+ _ = kwargs.pop('executor_keys', None)
190
+ _ = kwargs.pop('systemd', None)
191
+ return parse_dict_to_sysargs(kwargs)
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
+ hidden_name = f'.systemd-{self.get_service_name(name, debug=debug)}'
198
+ if sysargs:
199
+ sysargs = self.clean_sysargs(sysargs)
200
+
201
+ return Job(
202
+ hidden_name,
203
+ sysargs,
204
+ executor_keys='local',
205
+ _rotating_log=self.get_job_rotating_file(name, debug=debug),
206
+ _stdin_file=self.get_job_stdin_file(name, debug=debug),
207
+ _status_hook=partial(self.get_job_status, name),
208
+ _result_hook=partial(self.get_job_result, name),
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
+ return psutil.Process(pid)
270
+
271
+ def get_job_status(self, name: str, debug: bool = False) -> str:
272
+ """
273
+ Return the job's service status.
274
+ """
275
+ output = self.run_command(
276
+ ['is-active', self.get_service_name(name, debug=debug)],
277
+ as_output=True,
278
+ debug=debug,
279
+ )
280
+
281
+ if output == 'active':
282
+ process = self.get_job_process(name, debug=debug)
283
+ if process is None:
284
+ return 'stopped'
285
+
286
+ if process.status() == 'stopped':
287
+ return 'paused'
288
+
289
+ return 'running'
290
+
291
+ return 'stopped'
292
+
293
+ def get_job_pid(self, name: str, debug: bool = False) -> Union[int, None]:
294
+ """
295
+ Return the job's service PID.
296
+ """
297
+ from meerschaum.utils.misc import is_int
298
+
299
+ output = self.run_command(
300
+ [
301
+ 'show',
302
+ self.get_service_name(name, debug=debug),
303
+ '--property=MainPID',
304
+ ],
305
+ as_output=True,
306
+ debug=debug,
307
+ )
308
+ if not output.startswith('MainPID='):
309
+ return None
310
+
311
+ pid_str = output[len('MainPID='):]
312
+ if is_int(pid_str):
313
+ return int(pid_str)
314
+
315
+ return None
316
+
317
+ def get_job_began(self, name: str, debug: bool = False) -> Union[datetime, None]:
318
+ """
319
+ Return when a job began running.
320
+ """
321
+ output = self.run_command(
322
+ [
323
+ 'show',
324
+ self.get_service_name(name, debug=debug),
325
+ '--property=ActiveEnterTimestamp'
326
+ ],
327
+ as_output=True,
328
+ debug=debug,
329
+ )
330
+ if not output.startswith('ActiveEnterTimestamp'):
331
+ return None
332
+
333
+ dateutil_parser = mrsm.attempt_import('dateutil.parser')
334
+ dt = dateutil_parser.parse(output.split('=')[-1])
335
+ return dt.astimezone(timezone.utc).isoformat()
336
+
337
+ def get_job_result(self, name: str, debug: bool = False) -> SuccessTuple:
338
+ """
339
+ Return the job's result SuccessTuple.
340
+ """
341
+ result_path = self.get_result_path(name, debug=debug)
342
+ if not result_path.exists():
343
+ return False, "No result available."
344
+
345
+ try:
346
+ with open(result_path, 'r', encoding='utf-8') as f:
347
+ result = json.load(f)
348
+ except Exception:
349
+ return False, f"Could not read result for Job '{name}'."
350
+
351
+ return tuple(result)
352
+
353
+ def get_job_sysargs(self, name: str, debug: bool = False) -> Union[List[str], None]:
354
+ """
355
+ Return the sysargs from the service file.
356
+ """
357
+ service_file_path = self.get_service_file_path(name, debug=debug)
358
+ if not service_file_path.exists():
359
+ return []
360
+
361
+ with open(service_file_path, 'r', encoding='utf-8') as f:
362
+ service_lines = f.readlines()
363
+
364
+ for line in service_lines:
365
+ if line.startswith('ExecStart='):
366
+ sysargs_str = line.split(' -m meerschaum ')[-1].split('<')[0]
367
+ return shlex.split(sysargs_str)
368
+
369
+ return []
370
+
371
+ def run_command(
372
+ self,
373
+ command_args: List[str],
374
+ as_output: bool = False,
375
+ debug: bool = False,
376
+ ) -> Union[SuccessTuple, str]:
377
+ """
378
+ Run a `systemd` command and return success.
379
+
380
+ Parameters
381
+ ----------
382
+ command_args: List[str]
383
+ The command to pass to `systemctl --user`.
384
+
385
+ as_output: bool, default False
386
+ If `True`, return the process stdout output.
387
+ Defaults to a `SuccessTuple`.
388
+
389
+ Returns
390
+ -------
391
+ A `SuccessTuple` indicating success or a str for the process output.
392
+ """
393
+ from meerschaum.utils.process import run_process
394
+
395
+ if command_args[:2] != ['systemctl', '--user']:
396
+ command_args = ['systemctl', '--user'] + command_args
397
+
398
+ if debug:
399
+ dprint(shlex.join(command_args))
400
+
401
+ proc = run_process(
402
+ command_args,
403
+ foreground=False,
404
+ as_proc=True,
405
+ capture_output=True,
406
+ text=True,
407
+ )
408
+ stdout, stderr = proc.communicate()
409
+ if debug:
410
+ dprint(f"{stdout}")
411
+
412
+ if as_output:
413
+ return stdout.strip()
414
+
415
+ command_success = proc.wait() == 0
416
+ command_msg = (
417
+ "Success"
418
+ if command_success
419
+ else f"Failed to execute command `{shlex.join(command_args)}`."
420
+ )
421
+ return command_success, command_msg
422
+
423
+ def get_job_stdin_file(self, name: str, debug: bool = False):
424
+ """
425
+ Return a `StdinFile` for the job.
426
+ """
427
+ from meerschaum.utils.daemon import StdinFile
428
+ if '_stdin_files' not in self.__dict__:
429
+ self._stdin_files: Dict[str, StdinFile] = {}
430
+
431
+ if name not in self._stdin_files:
432
+ socket_path = self.get_socket_path(name, debug=debug)
433
+ self._stdin_files[name] = StdinFile(socket_path)
434
+
435
+ return self._stdin_files[name]
436
+
437
+ def create_job(self, name: str, sysargs: List[str], debug: bool = False) -> SuccessTuple:
438
+ """
439
+ Create a job as a service to be run by `systemd`.
440
+ """
441
+
442
+ service_name = self.get_service_name(name, debug=debug)
443
+ service_file_path = self.get_service_file_path(name, debug=debug)
444
+ service_socket_path = self.get_service_socket_path(name, debug=debug)
445
+ socket_path = self.get_socket_path(name, debug=debug)
446
+ socket_stdin = self.get_job_stdin_file(name, debug=debug)
447
+ _ = socket_stdin.file_handler
448
+
449
+ job = self.get_hidden_job(name, sysargs, debug=debug)
450
+
451
+ clean_sysargs = self.clean_sysargs(sysargs)
452
+ with open(service_file_path, 'w+', encoding='utf-8') as f:
453
+ f.write(self.get_service_file_text(name, clean_sysargs, debug=debug))
454
+
455
+ commands = [
456
+ ['daemon-reload'],
457
+ ['enable', service_name],
458
+ ['start', service_name],
459
+ ]
460
+
461
+ fails = 0
462
+ for command_list in commands:
463
+ command_success, command_msg = self.run_command(command_list, debug=debug)
464
+ if not command_success:
465
+ fails += 1
466
+
467
+ if fails > 1:
468
+ return False, "Failed to reload systemd."
469
+
470
+ return True, f"Started job '{name}' via systemd."
471
+
472
+ def start_job(self, name: str, debug: bool = False) -> SuccessTuple:
473
+ """
474
+ Stop a job's service.
475
+ """
476
+ job = self.get_hidden_job(name, debug=debug)
477
+ job.daemon._remove_stop_file()
478
+
479
+ status = self.get_job_status(name, debug=debug)
480
+ if status == 'paused':
481
+ return self.run_command(
482
+ ['kill', '-s', 'SIGCONT', self.get_service_name(name, debug=debug)],
483
+ debug=debug,
484
+ )
485
+
486
+ return self.run_command(
487
+ ['start', self.get_service_name(name, debug=debug)],
488
+ debug=debug,
489
+ )
490
+
491
+ def stop_job(self, name: str, debug: bool = False) -> SuccessTuple:
492
+ """
493
+ Stop a job's service.
494
+ """
495
+ job = self.get_hidden_job(name, debug=debug)
496
+ job.daemon._write_stop_file('quit')
497
+ return self.run_command(
498
+ ['stop', self.get_service_name(name, debug=debug)],
499
+ debug=debug,
500
+ )
501
+
502
+ def pause_job(self, name: str, debug: bool = False) -> SuccessTuple:
503
+ """
504
+ Pause a job's service.
505
+ """
506
+ job = self.get_hidden_job(name, debug=debug)
507
+ job.daemon._write_stop_file('pause')
508
+ return self.run_command(
509
+ ['kill', '-s', 'SIGSTOP', self.get_service_name(name, debug=debug)],
510
+ debug=debug,
511
+ )
512
+
513
+ def delete_job(self, name: str, debug: bool = False) -> SuccessTuple:
514
+ """
515
+ Delete a job's service.
516
+ """
517
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
518
+
519
+ stop_success, stop_msg = self.stop_job(name, debug=debug)
520
+ if not stop_success:
521
+ return stop_success, stop_msg
522
+
523
+ disable_success, disable_msg = self.run_command(
524
+ ['disable', self.get_service_name(name, debug=debug)],
525
+ debug=debug,
526
+ )
527
+ if not disable_success:
528
+ return disable_success, disable_msg
529
+
530
+ service_file_path = self.get_service_file_path(name, debug=debug)
531
+ service_socket_path = self.get_service_socket_path(name, debug=debug)
532
+ socket_path = self.get_socket_path(name, debug=debug)
533
+ result_path = self.get_result_path(name, debug=debug)
534
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
535
+ logs_paths = [
536
+ (SYSTEMD_LOGS_RESOURCES_PATH / name)
537
+ for name in os.listdir(SYSTEMD_LOGS_RESOURCES_PATH)
538
+ if name.startswith(service_logs_path.name + '.')
539
+ ]
540
+ paths = [service_file_path, service_socket_path, socket_path, result_path] + logs_paths
541
+ for path in paths:
542
+ if path.exists():
543
+ try:
544
+ path.unlink()
545
+ except Exception as e:
546
+ warn(e)
547
+ return False, str(e)
548
+
549
+ job = self.get_hidden_job(name, debug=debug)
550
+ job.delete()
551
+
552
+ return self.run_command(['daemon-reload'], debug=debug)
553
+
554
+ def get_logs(self, name: str, debug: bool = False) -> str:
555
+ """
556
+ Return a job's journal logs.
557
+ """
558
+ rotating_file = self.get_job_rotating_file(name, debug=debug)
559
+ return rotating_file.read()
560
+
561
+ def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]:
562
+ """
563
+ Return a job's stop time.
564
+ """
565
+ job = self.get_hidden_job(name, debug=debug)
566
+ return job.stop_time
567
+
568
+ def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool:
569
+ """
570
+ Return whether a job is blocking on stdin.
571
+ """
572
+ socket_path = self.get_socket_path(name, debug=debug)
573
+ blocking_path = socket_path.parent / (socket_path.name + '.block')
574
+ return blocking_path.exists()
575
+
576
+ def get_job_rotating_file(self, name: str, debug: bool = False):
577
+ """
578
+ Return a `RotatingFile` for the job's log output.
579
+ """
580
+ from meerschaum.utils.daemon import RotatingFile
581
+ service_logs_path = self.get_service_logs_path(name, debug=debug)
582
+ return RotatingFile(service_logs_path)
583
+
584
+ async def monitor_logs_async(
585
+ self,
586
+ name: str,
587
+ *args,
588
+ debug: bool = False,
589
+ **kwargs
590
+ ):
591
+ """
592
+ Monitor a job's output.
593
+ """
594
+ from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
595
+ job = self.get_hidden_job(name, debug=debug)
596
+ kwargs.update({
597
+ '_logs_path': SYSTEMD_LOGS_RESOURCES_PATH,
598
+ '_log': self.get_job_rotating_file(name, debug=debug),
599
+ '_stdin_file': self.get_job_stdin_file(name, debug=debug),
600
+ 'debug': debug,
601
+ })
602
+ await job.monitor_logs_async(*args, **kwargs)
603
+
604
+ def monitor_logs(self, *args, **kwargs):
605
+ """
606
+ Monitor a job's output.
607
+ """
608
+ asyncio.run(self.monitor_logs_async(*args, **kwargs))