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.
- meerschaum/__init__.py +3 -2
- meerschaum/__main__.py +0 -5
- meerschaum/_internal/arguments/__init__.py +1 -1
- meerschaum/_internal/arguments/_parse_arguments.py +56 -2
- meerschaum/_internal/arguments/_parser.py +6 -2
- meerschaum/_internal/entry.py +94 -23
- meerschaum/_internal/shell/Shell.py +112 -35
- meerschaum/actions/attach.py +12 -7
- meerschaum/actions/copy.py +68 -41
- meerschaum/actions/delete.py +75 -26
- meerschaum/actions/edit.py +3 -3
- meerschaum/actions/install.py +40 -32
- meerschaum/actions/pause.py +44 -27
- meerschaum/actions/restart.py +107 -0
- meerschaum/actions/show.py +8 -8
- meerschaum/actions/start.py +27 -46
- meerschaum/actions/stop.py +11 -4
- meerschaum/api/_events.py +10 -3
- meerschaum/api/dash/jobs.py +69 -70
- meerschaum/api/routes/_actions.py +8 -3
- meerschaum/api/routes/_jobs.py +37 -19
- meerschaum/config/_default.py +1 -1
- meerschaum/config/_paths.py +5 -0
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +5 -0
- meerschaum/connectors/Connector.py +13 -7
- meerschaum/connectors/__init__.py +21 -5
- meerschaum/connectors/api/APIConnector.py +3 -0
- meerschaum/connectors/api/_jobs.py +30 -3
- meerschaum/connectors/parse.py +10 -14
- meerschaum/core/Pipe/_bootstrap.py +16 -8
- meerschaum/jobs/_Executor.py +69 -0
- meerschaum/{utils/jobs → jobs}/_Job.py +169 -21
- meerschaum/jobs/_LocalExecutor.py +88 -0
- meerschaum/jobs/_SystemdExecutor.py +613 -0
- meerschaum/jobs/__init__.py +388 -0
- meerschaum/plugins/__init__.py +6 -6
- meerschaum/utils/daemon/Daemon.py +7 -0
- meerschaum/utils/daemon/RotatingFile.py +5 -2
- meerschaum/utils/daemon/StdinFile.py +12 -2
- meerschaum/utils/daemon/__init__.py +2 -0
- meerschaum/utils/formatting/_jobs.py +49 -25
- meerschaum/utils/misc.py +23 -5
- meerschaum/utils/packages/_packages.py +7 -4
- meerschaum/utils/process.py +9 -9
- meerschaum/utils/venv/__init__.py +2 -2
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/METADATA +14 -17
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/RECORD +54 -50
- meerschaum/utils/jobs/__init__.py +0 -245
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/WHEEL +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.0.dev3.dist-info → meerschaum-2.3.0rc2.dist-info}/top_level.txt +0 -0
- {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))
|