meerschaum 2.3.0rc1__py3-none-any.whl → 2.3.0rc3__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/_internal/arguments/__init__.py +1 -1
- meerschaum/_internal/arguments/_parse_arguments.py +56 -2
- meerschaum/_internal/entry.py +63 -22
- meerschaum/_internal/shell/Shell.py +80 -15
- meerschaum/actions/delete.py +17 -7
- meerschaum/actions/start.py +2 -6
- meerschaum/api/dash/callbacks/jobs.py +36 -44
- meerschaum/api/dash/jobs.py +24 -15
- meerschaum/api/routes/_actions.py +6 -5
- meerschaum/api/routes/_jobs.py +19 -1
- meerschaum/config/_jobs.py +1 -1
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +2 -0
- meerschaum/connectors/api/APIConnector.py +1 -0
- meerschaum/connectors/api/_jobs.py +13 -2
- meerschaum/connectors/parse.py +0 -1
- meerschaum/jobs/_Job.py +24 -7
- meerschaum/jobs/_SystemdExecutor.py +118 -49
- meerschaum/jobs/__init__.py +41 -12
- meerschaum/utils/daemon/Daemon.py +14 -0
- meerschaum/utils/daemon/_names.py +1 -1
- meerschaum/utils/formatting/_jobs.py +2 -14
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/METADATA +1 -1
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/RECORD +30 -30
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/WHEEL +0 -0
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.0rc1.dist-info → meerschaum-2.3.0rc3.dist-info}/zip-safe +0 -0
meerschaum/api/dash/jobs.py
CHANGED
@@ -15,12 +15,12 @@ from meerschaum.api.dash.users import is_session_authenticated
|
|
15
15
|
from meerschaum.api import CHECK_UPDATE
|
16
16
|
dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
|
17
17
|
html, dcc = import_html(), import_dcc()
|
18
|
-
dateutil_parser = attempt_import('dateutil.parser', check_update=CHECK_UPDATE)
|
19
18
|
from meerschaum.jobs import (
|
20
19
|
get_jobs,
|
21
20
|
get_running_jobs,
|
22
21
|
get_paused_jobs,
|
23
22
|
get_stopped_jobs,
|
23
|
+
get_executor_keys_from_context,
|
24
24
|
Job,
|
25
25
|
)
|
26
26
|
from meerschaum.config import get_config
|
@@ -33,20 +33,21 @@ STATUS_EMOJI: Dict[str, str] = {
|
|
33
33
|
'dne': get_config('formatting', 'emoji', 'failure')
|
34
34
|
}
|
35
35
|
|
36
|
+
EXECUTOR_KEYS: str = get_executor_keys_from_context()
|
37
|
+
|
36
38
|
def get_jobs_cards(state: WebState):
|
37
39
|
"""
|
38
40
|
Build cards and alerts lists for jobs.
|
39
41
|
"""
|
40
|
-
jobs = get_jobs(include_hidden=False)
|
42
|
+
jobs = get_jobs(executor_keys=EXECUTOR_KEYS, include_hidden=False)
|
41
43
|
session_id = state['session-store.data'].get('session-id', None)
|
42
44
|
is_authenticated = is_session_authenticated(session_id)
|
43
45
|
|
44
46
|
cards = []
|
45
47
|
|
46
48
|
for name, job in jobs.items():
|
47
|
-
d = job.daemon
|
48
49
|
footer_children = html.Div(
|
49
|
-
build_process_timestamps_children(
|
50
|
+
build_process_timestamps_children(job),
|
50
51
|
id = {'type': 'process-timestamps-div', 'index': name},
|
51
52
|
)
|
52
53
|
follow_logs_button = dbc.DropdownMenuItem(
|
@@ -62,7 +63,7 @@ def get_jobs_cards(state: WebState):
|
|
62
63
|
)
|
63
64
|
header_children = [
|
64
65
|
html.Div(
|
65
|
-
build_status_children(
|
66
|
+
build_status_children(job),
|
66
67
|
id={'type': 'manage-job-status-div', 'index': name},
|
67
68
|
style={'float': 'left'},
|
68
69
|
),
|
@@ -83,15 +84,16 @@ def get_jobs_cards(state: WebState):
|
|
83
84
|
html.H4(html.B(name), className="card-title"),
|
84
85
|
html.Div(
|
85
86
|
html.P(
|
86
|
-
|
87
|
+
job.label,
|
87
88
|
className="card-text job-card-text",
|
88
89
|
style={"word-wrap": "break-word"},
|
90
|
+
id={'type': 'job-label-p', 'index': name},
|
89
91
|
),
|
90
92
|
style={"white-space": "pre-wrap"},
|
91
93
|
),
|
92
94
|
html.Div(
|
93
95
|
(
|
94
|
-
build_manage_job_buttons_div_children(
|
96
|
+
build_manage_job_buttons_div_children(job)
|
95
97
|
if is_authenticated
|
96
98
|
else []
|
97
99
|
),
|
@@ -179,13 +181,14 @@ def build_manage_job_buttons(job: Job):
|
|
179
181
|
},
|
180
182
|
)
|
181
183
|
buttons = []
|
182
|
-
|
184
|
+
status = job.status
|
185
|
+
if status in ('stopped', 'paused'):
|
183
186
|
buttons.append(start_button)
|
184
|
-
if
|
187
|
+
if status == 'stopped':
|
185
188
|
buttons.append(delete_button)
|
186
|
-
if
|
189
|
+
if status in ('running',):
|
187
190
|
buttons.append(pause_button)
|
188
|
-
if
|
191
|
+
if status in ('running', 'paused'):
|
189
192
|
buttons.append(stop_button)
|
190
193
|
|
191
194
|
return buttons
|
@@ -193,7 +196,7 @@ def build_manage_job_buttons(job: Job):
|
|
193
196
|
|
194
197
|
def build_status_children(job: Job) -> List[html.P]:
|
195
198
|
"""
|
196
|
-
Return the status HTML component for this
|
199
|
+
Return the status HTML component for this Job.
|
197
200
|
"""
|
198
201
|
if job is None:
|
199
202
|
return STATUS_EMOJI['dne']
|
@@ -217,10 +220,16 @@ def build_process_timestamps_children(job: Job) -> List[dbc.Row]:
|
|
217
220
|
return []
|
218
221
|
|
219
222
|
children = []
|
220
|
-
for timestamp_key,
|
221
|
-
|
223
|
+
for timestamp_key, timestamp in sorted_dict(
|
224
|
+
{
|
225
|
+
'began': job.began,
|
226
|
+
'paused': job.paused,
|
227
|
+
'ended': job.ended,
|
228
|
+
}
|
222
229
|
).items():
|
223
|
-
timestamp
|
230
|
+
if timestamp is None:
|
231
|
+
continue
|
232
|
+
|
224
233
|
timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M UTC')
|
225
234
|
children.append(
|
226
235
|
dbc.Row(
|
@@ -26,6 +26,7 @@ from meerschaum.actions import actions
|
|
26
26
|
import meerschaum.core
|
27
27
|
from meerschaum.config import get_config
|
28
28
|
from meerschaum._internal.arguments._parse_arguments import parse_dict_to_sysargs, parse_arguments
|
29
|
+
from meerschaum.api.routes._jobs import clean_sysargs
|
29
30
|
|
30
31
|
actions_endpoint = endpoints['actions']
|
31
32
|
|
@@ -115,11 +116,11 @@ async def do_action_websocket(websocket: WebSocket):
|
|
115
116
|
if not auth_success:
|
116
117
|
await websocket.close()
|
117
118
|
|
118
|
-
sysargs = await websocket.receive_json()
|
119
|
-
kwargs = parse_arguments(sysargs)
|
120
|
-
_ = kwargs.pop('executor_keys', None)
|
121
|
-
_ = kwargs.pop('shell', None)
|
122
|
-
sysargs = parse_dict_to_sysargs(kwargs)
|
119
|
+
sysargs = clean_sysargs(await websocket.receive_json())
|
120
|
+
# kwargs = parse_arguments(sysargs)
|
121
|
+
# _ = kwargs.pop('executor_keys', None)
|
122
|
+
# _ = kwargs.pop('shell', None)
|
123
|
+
# sysargs = parse_dict_to_sysargs(kwargs)
|
123
124
|
|
124
125
|
job = Job(
|
125
126
|
job_name,
|
meerschaum/api/routes/_jobs.py
CHANGED
@@ -107,6 +107,24 @@ def get_job(
|
|
107
107
|
}
|
108
108
|
|
109
109
|
|
110
|
+
def clean_sysargs(sysargs: List[str]) -> List[str]:
|
111
|
+
"""
|
112
|
+
Remove the executor flag or leading `api {label}` action.
|
113
|
+
"""
|
114
|
+
clean_sysargs = []
|
115
|
+
executor_flag = False
|
116
|
+
for arg in sysargs:
|
117
|
+
if arg in ('-e', '--executor', 'api'):
|
118
|
+
executor_flag = True
|
119
|
+
continue
|
120
|
+
if executor_flag:
|
121
|
+
executor_flag = False
|
122
|
+
continue
|
123
|
+
|
124
|
+
clean_sysargs.append(arg)
|
125
|
+
return clean_sysargs
|
126
|
+
|
127
|
+
|
110
128
|
@app.post(endpoints['jobs'] + '/{name}', tags=['Jobs'])
|
111
129
|
def create_job(
|
112
130
|
name: str,
|
@@ -118,7 +136,7 @@ def create_job(
|
|
118
136
|
"""
|
119
137
|
Create and start a new job.
|
120
138
|
"""
|
121
|
-
job = Job(name, sysargs, executor_keys=EXECUTOR_KEYS)
|
139
|
+
job = Job(name, clean_sysargs(sysargs), executor_keys=EXECUTOR_KEYS)
|
122
140
|
if job.exists():
|
123
141
|
raise fastapi.HTTPException(
|
124
142
|
status_code=409,
|
meerschaum/config/_jobs.py
CHANGED
meerschaum/config/_version.py
CHANGED
@@ -88,6 +88,8 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
88
88
|
),
|
89
89
|
'underscore_standin': '<UNDERSCORE>', ### Temporary replacement for parsing.
|
90
90
|
'failure_key': '_argparse_exception',
|
91
|
+
'and_key': '+',
|
92
|
+
'escaped_and_key': '++',
|
91
93
|
},
|
92
94
|
'urls': {
|
93
95
|
'get-pip.py': 'https://bootstrap.pypa.io/get-pip.py',
|
@@ -112,7 +112,7 @@ def get_job_status(self, name: str, debug: bool = False) -> str:
|
|
112
112
|
metadata = self.get_job_metadata(name, debug=debug)
|
113
113
|
return metadata.get('status', 'stopped')
|
114
114
|
|
115
|
-
def get_job_began(self, name: str, debug: bool = False) -> str:
|
115
|
+
def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
|
116
116
|
"""
|
117
117
|
Return a job's `began` timestamp, if it exists.
|
118
118
|
"""
|
@@ -123,7 +123,7 @@ def get_job_began(self, name: str, debug: bool = False) -> str:
|
|
123
123
|
|
124
124
|
return began_str
|
125
125
|
|
126
|
-
def get_job_ended(self, name: str, debug: bool = False) -> str:
|
126
|
+
def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
|
127
127
|
"""
|
128
128
|
Return a job's `ended` timestamp, if it exists.
|
129
129
|
"""
|
@@ -134,6 +134,17 @@ def get_job_ended(self, name: str, debug: bool = False) -> str:
|
|
134
134
|
|
135
135
|
return ended_str
|
136
136
|
|
137
|
+
def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
|
138
|
+
"""
|
139
|
+
Return a job's `paused` timestamp, if it exists.
|
140
|
+
"""
|
141
|
+
properties = self.get_job_properties(name, debug=debug)
|
142
|
+
paused_str = properties.get('daemon', {}).get('paused', None)
|
143
|
+
if paused_str is None:
|
144
|
+
return None
|
145
|
+
|
146
|
+
return paused_str
|
147
|
+
|
137
148
|
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
138
149
|
"""
|
139
150
|
Return whether a job exists.
|
meerschaum/connectors/parse.py
CHANGED
@@ -120,7 +120,6 @@ def parse_repo_keys(keys: Optional[str] = None, **kw):
|
|
120
120
|
def parse_executor_keys(keys: Optional[str] = None, **kw):
|
121
121
|
"""Parse the executor keys into an APIConnector or string."""
|
122
122
|
from meerschaum.jobs import get_executor_keys_from_context
|
123
|
-
from meerschaum.config import get_config
|
124
123
|
if keys is None:
|
125
124
|
keys = get_executor_keys_from_context()
|
126
125
|
|
meerschaum/jobs/_Job.py
CHANGED
@@ -91,6 +91,14 @@ class Job:
|
|
91
91
|
if isinstance(sysargs, str):
|
92
92
|
sysargs = shlex.split(sysargs)
|
93
93
|
|
94
|
+
and_key = STATIC_CONFIG['system']['arguments']['and_key']
|
95
|
+
escaped_and_key = STATIC_CONFIG['system']['arguments']['escaped_and_key']
|
96
|
+
if sysargs:
|
97
|
+
sysargs = [
|
98
|
+
(arg if arg != escaped_and_key else and_key)
|
99
|
+
for arg in sysargs
|
100
|
+
]
|
101
|
+
|
94
102
|
### NOTE: 'local' and 'systemd' executors are being coalesced.
|
95
103
|
if executor_keys is None:
|
96
104
|
from meerschaum.jobs import get_executor_keys_from_context
|
@@ -732,7 +740,7 @@ class Job:
|
|
732
740
|
The datetime when the job began running.
|
733
741
|
"""
|
734
742
|
if self.executor is not None:
|
735
|
-
began_str = self.executor.get_job_began(name)
|
743
|
+
began_str = self.executor.get_job_began(self.name)
|
736
744
|
if began_str is None:
|
737
745
|
return None
|
738
746
|
return (
|
@@ -754,6 +762,8 @@ class Job:
|
|
754
762
|
"""
|
755
763
|
if self.executor is not None:
|
756
764
|
ended_str = self.executor.get_job_ended(self.name)
|
765
|
+
if ended_str is None:
|
766
|
+
return None
|
757
767
|
return (
|
758
768
|
datetime.fromisoformat(ended_str)
|
759
769
|
.astimezone(timezone.utc)
|
@@ -771,6 +781,16 @@ class Job:
|
|
771
781
|
"""
|
772
782
|
The datetime when the job was suspended while running.
|
773
783
|
"""
|
784
|
+
if self.executor is not None:
|
785
|
+
paused_str = self.executor.get_job_paused(self.name)
|
786
|
+
if paused_str is None:
|
787
|
+
return None
|
788
|
+
return (
|
789
|
+
datetime.fromisoformat(paused_str)
|
790
|
+
.astimezone(timezone.utc)
|
791
|
+
.replace(tzinfo=None)
|
792
|
+
)
|
793
|
+
|
774
794
|
paused_str = self.daemon.properties.get('process', {}).get('paused', None)
|
775
795
|
if paused_str is None:
|
776
796
|
return None
|
@@ -788,11 +808,8 @@ class Job:
|
|
788
808
|
if not self.daemon.stop_path.exists():
|
789
809
|
return None
|
790
810
|
|
791
|
-
|
792
|
-
|
793
|
-
stop_data = json.load(f)
|
794
|
-
except Exception as e:
|
795
|
-
warn(f"Failed to read stop file for {self}:\n{e}")
|
811
|
+
stop_data = self.daemon._read_stop_file()
|
812
|
+
if not stop_data:
|
796
813
|
return None
|
797
814
|
|
798
815
|
stop_time_str = stop_data.get('stop_time', None)
|
@@ -831,7 +848,7 @@ class Job:
|
|
831
848
|
"""
|
832
849
|
Return the job's Daemon label (joined sysargs).
|
833
850
|
"""
|
834
|
-
return shlex.join(self.sysargs)
|
851
|
+
return shlex.join(self.sysargs).replace(' + ', '\n+ ')
|
835
852
|
|
836
853
|
def __str__(self) -> str:
|
837
854
|
sysargs = self.sysargs
|
@@ -19,9 +19,10 @@ from functools import partial
|
|
19
19
|
import meerschaum as mrsm
|
20
20
|
from meerschaum.jobs import Job, Executor, make_executor
|
21
21
|
from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union, Optional, Callable
|
22
|
+
from meerschaum.config import get_config
|
22
23
|
from meerschaum.config.static import STATIC_CONFIG
|
23
24
|
from meerschaum.utils.warnings import warn, dprint
|
24
|
-
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
25
|
+
from meerschaum._internal.arguments._parse_arguments import parse_arguments
|
25
26
|
|
26
27
|
JOB_METADATA_CACHE_SECONDS: int = STATIC_CONFIG['api']['jobs']['metadata_cache_seconds']
|
27
28
|
|
@@ -36,11 +37,11 @@ class SystemdExecutor(Executor):
|
|
36
37
|
"""
|
37
38
|
Return a list of existing jobs, including hidden ones.
|
38
39
|
"""
|
39
|
-
from meerschaum.config.paths import
|
40
|
+
from meerschaum.config.paths import SYSTEMD_ROOT_RESOURCES_PATH
|
40
41
|
return [
|
41
42
|
service_name[len('mrsm-'):(-1 * len('.service'))]
|
42
|
-
for service_name in os.listdir(
|
43
|
-
if service_name.startswith('mrsm-')
|
43
|
+
for service_name in os.listdir(SYSTEMD_ROOT_RESOURCES_PATH)
|
44
|
+
if service_name.startswith('mrsm-') and service_name.endswith('.service')
|
44
45
|
]
|
45
46
|
|
46
47
|
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
@@ -51,10 +52,10 @@ class SystemdExecutor(Executor):
|
|
51
52
|
if debug:
|
52
53
|
dprint(f'Existing services: {user_services}')
|
53
54
|
return name in user_services
|
54
|
-
|
55
|
+
|
55
56
|
def get_jobs(self, debug: bool = False) -> Dict[str, Job]:
|
56
57
|
"""
|
57
|
-
Return a dictionary of `systemd` Jobs (
|
58
|
+
Return a dictionary of `systemd` Jobs (including hidden jobs).
|
58
59
|
"""
|
59
60
|
user_services = self.get_job_names(debug=debug)
|
60
61
|
jobs = {
|
@@ -64,7 +65,6 @@ class SystemdExecutor(Executor):
|
|
64
65
|
return {
|
65
66
|
name: job
|
66
67
|
for name, job in jobs.items()
|
67
|
-
if not job.hidden
|
68
68
|
}
|
69
69
|
|
70
70
|
def get_service_name(self, name: str, debug: bool = False) -> str:
|
@@ -73,13 +73,20 @@ class SystemdExecutor(Executor):
|
|
73
73
|
"""
|
74
74
|
return f"mrsm-{name.replace(' ', '-')}.service"
|
75
75
|
|
76
|
-
def
|
76
|
+
def get_service_symlink_file_path(self, name: str, debug: bool = False) -> pathlib.Path:
|
77
77
|
"""
|
78
|
-
Return the path to
|
78
|
+
Return the path to where to create the service symlink.
|
79
79
|
"""
|
80
80
|
from meerschaum.config.paths import SYSTEMD_USER_RESOURCES_PATH
|
81
81
|
return SYSTEMD_USER_RESOURCES_PATH / self.get_service_name(name, debug=debug)
|
82
82
|
|
83
|
+
def get_service_file_path(self, name: str, debug: bool = False) -> pathlib.Path:
|
84
|
+
"""
|
85
|
+
Return the path to a Job's service file.
|
86
|
+
"""
|
87
|
+
from meerschaum.config.paths import SYSTEMD_ROOT_RESOURCES_PATH
|
88
|
+
return SYSTEMD_ROOT_RESOURCES_PATH / self.get_service_name(name, debug=debug)
|
89
|
+
|
83
90
|
def get_service_logs_path(self, name: str, debug: bool = False) -> pathlib.Path:
|
84
91
|
"""
|
85
92
|
Return the path to direct service logs to.
|
@@ -135,7 +142,8 @@ class SystemdExecutor(Executor):
|
|
135
142
|
STATIC_CONFIG['environment']['systemd_log_path']: service_logs_path.as_posix(),
|
136
143
|
STATIC_CONFIG['environment']['systemd_result_path']: result_path.as_posix(),
|
137
144
|
STATIC_CONFIG['environment']['systemd_stdin_path']: socket_path.as_posix(),
|
138
|
-
|
145
|
+
'LINES': get_config('jobs', 'terminal', 'lines'),
|
146
|
+
'COLUMNS': get_config('jobs', 'terminal', 'columns'),
|
139
147
|
})
|
140
148
|
environment_lines = [
|
141
149
|
f"Environment={key}={val}"
|
@@ -150,8 +158,7 @@ class SystemdExecutor(Executor):
|
|
150
158
|
"\n"
|
151
159
|
"[Service]\n"
|
152
160
|
f"ExecStart={exec_str}\n"
|
153
|
-
"KillSignal=
|
154
|
-
"TimeoutStopSpec=8\n"
|
161
|
+
"KillSignal=SIGTERM\n"
|
155
162
|
"Restart=always\n"
|
156
163
|
"RestartPreventExitStatus=0\n"
|
157
164
|
f"SyslogIdentifier={service_name}\n"
|
@@ -180,23 +187,11 @@ class SystemdExecutor(Executor):
|
|
180
187
|
)
|
181
188
|
return socket_text
|
182
189
|
|
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
190
|
def get_hidden_job(self, name: str, sysargs: Optional[List[str]] = None, debug: bool = False):
|
194
191
|
"""
|
195
192
|
Return the hidden "sister" job to store a job's parameters.
|
196
193
|
"""
|
197
194
|
hidden_name = f'.systemd-{self.get_service_name(name, debug=debug)}'
|
198
|
-
if sysargs:
|
199
|
-
sysargs = self.clean_sysargs(sysargs)
|
200
195
|
|
201
196
|
return Job(
|
202
197
|
hidden_name,
|
@@ -266,7 +261,10 @@ class SystemdExecutor(Executor):
|
|
266
261
|
return None
|
267
262
|
|
268
263
|
psutil = mrsm.attempt_import('psutil')
|
269
|
-
|
264
|
+
try:
|
265
|
+
return psutil.Process(pid)
|
266
|
+
except Exception:
|
267
|
+
return None
|
270
268
|
|
271
269
|
def get_job_status(self, name: str, debug: bool = False) -> str:
|
272
270
|
"""
|
@@ -278,13 +276,19 @@ class SystemdExecutor(Executor):
|
|
278
276
|
debug=debug,
|
279
277
|
)
|
280
278
|
|
279
|
+
if output == 'activating':
|
280
|
+
return 'running'
|
281
|
+
|
281
282
|
if output == 'active':
|
282
283
|
process = self.get_job_process(name, debug=debug)
|
283
284
|
if process is None:
|
284
285
|
return 'stopped'
|
285
286
|
|
286
|
-
|
287
|
-
|
287
|
+
try:
|
288
|
+
if process.status() == 'stopped':
|
289
|
+
return 'paused'
|
290
|
+
except Exception:
|
291
|
+
return 'stopped'
|
288
292
|
|
289
293
|
return 'running'
|
290
294
|
|
@@ -309,12 +313,15 @@ class SystemdExecutor(Executor):
|
|
309
313
|
return None
|
310
314
|
|
311
315
|
pid_str = output[len('MainPID='):]
|
316
|
+
if pid_str == '0':
|
317
|
+
return None
|
318
|
+
|
312
319
|
if is_int(pid_str):
|
313
320
|
return int(pid_str)
|
314
321
|
|
315
322
|
return None
|
316
323
|
|
317
|
-
def get_job_began(self, name: str, debug: bool = False) -> Union[
|
324
|
+
def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]:
|
318
325
|
"""
|
319
326
|
Return when a job began running.
|
320
327
|
"""
|
@@ -330,10 +337,62 @@ class SystemdExecutor(Executor):
|
|
330
337
|
if not output.startswith('ActiveEnterTimestamp'):
|
331
338
|
return None
|
332
339
|
|
340
|
+
dt_str = output.split('=')[-1]
|
341
|
+
if not dt_str:
|
342
|
+
return None
|
343
|
+
|
333
344
|
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
334
|
-
|
345
|
+
try:
|
346
|
+
dt = dateutil_parser.parse(dt_str)
|
347
|
+
except Exception as e:
|
348
|
+
warn(f"Cannot parse '{output}' as a datetime:\n{e}")
|
349
|
+
return None
|
350
|
+
|
335
351
|
return dt.astimezone(timezone.utc).isoformat()
|
336
352
|
|
353
|
+
def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]:
|
354
|
+
"""
|
355
|
+
Return when a job began running.
|
356
|
+
"""
|
357
|
+
output = self.run_command(
|
358
|
+
[
|
359
|
+
'show',
|
360
|
+
self.get_service_name(name, debug=debug),
|
361
|
+
'--property=InactiveEnterTimestamp'
|
362
|
+
],
|
363
|
+
as_output=True,
|
364
|
+
debug=debug,
|
365
|
+
)
|
366
|
+
if not output.startswith('InactiveEnterTimestamp'):
|
367
|
+
return None
|
368
|
+
|
369
|
+
dt_str = output.split('=')[-1]
|
370
|
+
if not dt_str:
|
371
|
+
return None
|
372
|
+
|
373
|
+
dateutil_parser = mrsm.attempt_import('dateutil.parser')
|
374
|
+
|
375
|
+
try:
|
376
|
+
dt = dateutil_parser.parse(dt_str)
|
377
|
+
except Exception as e:
|
378
|
+
warn(f"Cannot parse '{output}' as a datetime:\n{e}")
|
379
|
+
return None
|
380
|
+
return dt.astimezone(timezone.utc).isoformat()
|
381
|
+
|
382
|
+
def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]:
|
383
|
+
"""
|
384
|
+
Return when a job was paused.
|
385
|
+
"""
|
386
|
+
job = self.get_hidden_job(name, debug=debug)
|
387
|
+
if self.get_job_status(name, debug=debug) != 'paused':
|
388
|
+
return None
|
389
|
+
|
390
|
+
stop_time = job.stop_time
|
391
|
+
if stop_time is None:
|
392
|
+
return None
|
393
|
+
|
394
|
+
return stop_time.isoformat()
|
395
|
+
|
337
396
|
def get_job_result(self, name: str, debug: bool = False) -> SuccessTuple:
|
338
397
|
"""
|
339
398
|
Return the job's result SuccessTuple.
|
@@ -438,19 +497,19 @@ class SystemdExecutor(Executor):
|
|
438
497
|
"""
|
439
498
|
Create a job as a service to be run by `systemd`.
|
440
499
|
"""
|
441
|
-
|
500
|
+
from meerschaum.utils.misc import make_symlink
|
442
501
|
service_name = self.get_service_name(name, debug=debug)
|
443
502
|
service_file_path = self.get_service_file_path(name, debug=debug)
|
444
|
-
|
445
|
-
socket_path = self.get_socket_path(name, debug=debug)
|
503
|
+
service_symlink_file_path = self.get_service_symlink_file_path(name, debug=debug)
|
446
504
|
socket_stdin = self.get_job_stdin_file(name, debug=debug)
|
447
505
|
_ = socket_stdin.file_handler
|
448
506
|
|
449
|
-
job = self.get_hidden_job(name, sysargs, debug=debug)
|
450
|
-
|
451
|
-
clean_sysargs = self.clean_sysargs(sysargs)
|
452
507
|
with open(service_file_path, 'w+', encoding='utf-8') as f:
|
453
|
-
f.write(self.get_service_file_text(name,
|
508
|
+
f.write(self.get_service_file_text(name, sysargs, debug=debug))
|
509
|
+
|
510
|
+
symlink_success, symlink_msg = make_symlink(service_file_path, service_symlink_file_path)
|
511
|
+
if not symlink_success:
|
512
|
+
return symlink_success, symlink_msg
|
454
513
|
|
455
514
|
commands = [
|
456
515
|
['daemon-reload'],
|
@@ -494,6 +553,19 @@ class SystemdExecutor(Executor):
|
|
494
553
|
"""
|
495
554
|
job = self.get_hidden_job(name, debug=debug)
|
496
555
|
job.daemon._write_stop_file('quit')
|
556
|
+
sigint_success, sigint_msg = self.run_command(
|
557
|
+
['kill', '-s', 'SIGINT', self.get_service_name(name, debug=debug)],
|
558
|
+
debug=debug,
|
559
|
+
)
|
560
|
+
|
561
|
+
check_timeout_interval = get_config('jobs', 'check_timeout_interval_seconds')
|
562
|
+
loop_start = time.perf_counter()
|
563
|
+
while (time.perf_counter() - loop_start) < get_config('jobs', 'timeout_seconds'):
|
564
|
+
if self.get_job_status(name, debug=debug) == 'stopped':
|
565
|
+
return True, 'Success'
|
566
|
+
|
567
|
+
time.sleep(check_timeout_interval)
|
568
|
+
|
497
569
|
return self.run_command(
|
498
570
|
['stop', self.get_service_name(name, debug=debug)],
|
499
571
|
debug=debug,
|
@@ -516,28 +588,25 @@ class SystemdExecutor(Executor):
|
|
516
588
|
"""
|
517
589
|
from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
|
518
590
|
|
519
|
-
|
520
|
-
|
521
|
-
return stop_success, stop_msg
|
522
|
-
|
523
|
-
disable_success, disable_msg = self.run_command(
|
591
|
+
_ = self.stop_job(name, debug=debug)
|
592
|
+
_ = self.run_command(
|
524
593
|
['disable', self.get_service_name(name, debug=debug)],
|
525
594
|
debug=debug,
|
526
595
|
)
|
527
|
-
if not disable_success:
|
528
|
-
return disable_success, disable_msg
|
529
596
|
|
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
597
|
service_logs_path = self.get_service_logs_path(name, debug=debug)
|
535
598
|
logs_paths = [
|
536
599
|
(SYSTEMD_LOGS_RESOURCES_PATH / name)
|
537
600
|
for name in os.listdir(SYSTEMD_LOGS_RESOURCES_PATH)
|
538
601
|
if name.startswith(service_logs_path.name + '.')
|
539
602
|
]
|
540
|
-
paths = [
|
603
|
+
paths = [
|
604
|
+
self.get_service_file_path(name, debug=debug),
|
605
|
+
self.get_service_symlink_file_path(name, debug=debug),
|
606
|
+
self.get_socket_path(name, debug=debug),
|
607
|
+
self.get_result_path(name, debug=debug),
|
608
|
+
] + logs_paths
|
609
|
+
|
541
610
|
for path in paths:
|
542
611
|
if path.exists():
|
543
612
|
try:
|
@@ -547,7 +616,7 @@ class SystemdExecutor(Executor):
|
|
547
616
|
return False, str(e)
|
548
617
|
|
549
618
|
job = self.get_hidden_job(name, debug=debug)
|
550
|
-
job.delete()
|
619
|
+
_ = job.delete()
|
551
620
|
|
552
621
|
return self.run_command(['daemon-reload'], debug=debug)
|
553
622
|
|