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