ominfra 0.0.0.dev88__py3-none-any.whl → 0.0.0.dev90__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.
- ominfra/clouds/aws/auth.py +4 -4
- ominfra/clouds/aws/journald2aws/__init__.py +1 -0
- ominfra/clouds/aws/journald2aws/journald/__init__.py +1 -0
- ominfra/clouds/aws/journald2aws/journald/genmessages.py +54 -0
- ominfra/clouds/aws/journald2aws/{journald.py → journald/messages.py} +0 -1
- ominfra/clouds/aws/journald2aws/journald/tailer.py +108 -0
- ominfra/clouds/aws/journald2aws/main.py +317 -0
- ominfra/clouds/aws/journald2aws/threadworker.py +64 -0
- ominfra/scripts/journald2aws.py +2172 -0
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/RECORD +15 -9
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/WHEEL +1 -1
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev88.dist-info → ominfra-0.0.0.dev90.dist-info}/top_level.txt +0 -0
ominfra/clouds/aws/auth.py
CHANGED
@@ -41,8 +41,8 @@ class AwsSigner:
|
|
41
41
|
|
42
42
|
@dc.dataclass(frozen=True)
|
43
43
|
class Credentials:
|
44
|
-
|
45
|
-
|
44
|
+
access_key_id: str
|
45
|
+
secret_access_key: str = dc.field(repr=False)
|
46
46
|
|
47
47
|
@dc.dataclass(frozen=True)
|
48
48
|
class Request:
|
@@ -198,7 +198,7 @@ class V4AwsSigner(AwsSigner):
|
|
198
198
|
|
199
199
|
#
|
200
200
|
|
201
|
-
key = self._creds.
|
201
|
+
key = self._creds.secret_access_key
|
202
202
|
key_date = self._sha256_sign(f'AWS4{key}'.encode('utf-8'), req_dt[:8]) # noqa
|
203
203
|
key_region = self._sha256_sign(key_date, self._region_name)
|
204
204
|
key_service = self._sha256_sign(key_region, self._service_name)
|
@@ -208,7 +208,7 @@ class V4AwsSigner(AwsSigner):
|
|
208
208
|
#
|
209
209
|
|
210
210
|
cred_scope = '/'.join([
|
211
|
-
self._creds.
|
211
|
+
self._creds.access_key_id,
|
212
212
|
*scope_parts,
|
213
213
|
])
|
214
214
|
auth = f'{algorithm} ' + ', '.join([
|
@@ -0,0 +1 @@
|
|
1
|
+
# @omlish-lite
|
@@ -0,0 +1 @@
|
|
1
|
+
# @omlish-lite
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# @omlish-script
|
3
|
+
import argparse
|
4
|
+
import datetime
|
5
|
+
import json
|
6
|
+
import re
|
7
|
+
import sys
|
8
|
+
import time
|
9
|
+
import uuid
|
10
|
+
|
11
|
+
|
12
|
+
def _main() -> None:
|
13
|
+
parser = argparse.ArgumentParser()
|
14
|
+
|
15
|
+
parser.add_argument('n', type=int, default=10, nargs='?')
|
16
|
+
parser.add_argument('--message', default='message {i}', nargs='?')
|
17
|
+
parser.add_argument('--sleep-s', type=float, nargs='?')
|
18
|
+
parser.add_argument('--sleep-n', type=int, nargs='?')
|
19
|
+
|
20
|
+
parser.add_argument('--after-cursor', nargs='?')
|
21
|
+
|
22
|
+
# Ignored
|
23
|
+
parser.add_argument('--output', nargs='?')
|
24
|
+
parser.add_argument('--follow', action='store_true')
|
25
|
+
parser.add_argument('--show-cursor', action='store_true')
|
26
|
+
parser.add_argument('--since', nargs='?')
|
27
|
+
|
28
|
+
args = parser.parse_args()
|
29
|
+
|
30
|
+
if (ac := args.after_cursor) is not None:
|
31
|
+
if not (m := re.fullmatch(r'cursor:(?P<n>\d+)', ac)):
|
32
|
+
raise ValueError(ac)
|
33
|
+
start = int(m.groupdict()['n'])
|
34
|
+
else:
|
35
|
+
start = 0
|
36
|
+
|
37
|
+
for i in range(start, args.n):
|
38
|
+
if args.sleep_s:
|
39
|
+
if not args.sleep_n or (i and i % args.sleep_n == 0):
|
40
|
+
time.sleep(args.sleep_s)
|
41
|
+
|
42
|
+
ts_us = datetime.datetime.now(tz=datetime.timezone.utc).timestamp() * 1_000_000 # noqa
|
43
|
+
dct = {
|
44
|
+
'MESSAGE': args.message.format(i=i),
|
45
|
+
'MESSAGE_ID': uuid.uuid4().hex,
|
46
|
+
'__CURSOR': f'cursor:{i}',
|
47
|
+
'_SOURCE_REALTIME_TIMESTAMP': str(int(ts_us)),
|
48
|
+
}
|
49
|
+
print(json.dumps(dct, indent=None, separators=(',', ':')))
|
50
|
+
sys.stdout.flush()
|
51
|
+
|
52
|
+
|
53
|
+
if __name__ == '__main__':
|
54
|
+
_main()
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# ruff: noqa: UP007
|
2
|
+
import fcntl
|
3
|
+
import os.path
|
4
|
+
import queue # noqa
|
5
|
+
import subprocess
|
6
|
+
import time
|
7
|
+
import typing as ta
|
8
|
+
|
9
|
+
from omlish.lite.cached import cached_nullary
|
10
|
+
from omlish.lite.check import check_not_none
|
11
|
+
from omlish.lite.logs import log
|
12
|
+
from omlish.lite.subprocesses import subprocess_shell_wrap_exec
|
13
|
+
|
14
|
+
from ..threadworker import ThreadWorker
|
15
|
+
from .messages import JournalctlMessage # noqa
|
16
|
+
from .messages import JournalctlMessageBuilder
|
17
|
+
|
18
|
+
|
19
|
+
class JournalctlTailerWorker(ThreadWorker):
|
20
|
+
DEFAULT_CMD: ta.ClassVar[ta.Sequence[str]] = ['journalctl']
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
output, # type: queue.Queue[ta.Sequence[JournalctlMessage]]
|
25
|
+
*,
|
26
|
+
since: ta.Optional[str] = None,
|
27
|
+
after_cursor: ta.Optional[str] = None,
|
28
|
+
|
29
|
+
cmd: ta.Optional[ta.Sequence[str]] = None,
|
30
|
+
shell_wrap: bool = False,
|
31
|
+
|
32
|
+
read_size: int = 0x4000,
|
33
|
+
sleep_s: float = 1.,
|
34
|
+
|
35
|
+
**kwargs: ta.Any,
|
36
|
+
) -> None:
|
37
|
+
super().__init__(**kwargs)
|
38
|
+
|
39
|
+
self._output = output
|
40
|
+
|
41
|
+
self._since = since
|
42
|
+
self._after_cursor = after_cursor
|
43
|
+
|
44
|
+
self._cmd = cmd or self.DEFAULT_CMD
|
45
|
+
self._shell_wrap = shell_wrap
|
46
|
+
|
47
|
+
self._read_size = read_size
|
48
|
+
self._sleep_s = sleep_s
|
49
|
+
|
50
|
+
self._mb = JournalctlMessageBuilder()
|
51
|
+
|
52
|
+
self._proc: ta.Optional[subprocess.Popen] = None
|
53
|
+
|
54
|
+
@cached_nullary
|
55
|
+
def _full_cmd(self) -> ta.Sequence[str]:
|
56
|
+
cmd = [
|
57
|
+
*self._cmd,
|
58
|
+
'--output', 'json',
|
59
|
+
'--show-cursor',
|
60
|
+
'--follow',
|
61
|
+
]
|
62
|
+
|
63
|
+
if self._since is not None:
|
64
|
+
cmd.extend(['--since', self._since])
|
65
|
+
|
66
|
+
if self._after_cursor is not None:
|
67
|
+
cmd.extend(['--after-cursor', self._after_cursor])
|
68
|
+
|
69
|
+
if self._shell_wrap:
|
70
|
+
cmd = list(subprocess_shell_wrap_exec(*cmd))
|
71
|
+
|
72
|
+
return cmd
|
73
|
+
|
74
|
+
def _run(self) -> None:
|
75
|
+
with subprocess.Popen(
|
76
|
+
self._full_cmd(),
|
77
|
+
stdout=subprocess.PIPE,
|
78
|
+
) as self._proc:
|
79
|
+
stdout = check_not_none(self._proc.stdout)
|
80
|
+
|
81
|
+
fd = stdout.fileno()
|
82
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
83
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
84
|
+
|
85
|
+
while True:
|
86
|
+
if not self._heartbeat():
|
87
|
+
break
|
88
|
+
|
89
|
+
while stdout.readable():
|
90
|
+
if not self._heartbeat():
|
91
|
+
break
|
92
|
+
|
93
|
+
buf = stdout.read(self._read_size)
|
94
|
+
if not buf:
|
95
|
+
log.debug('Journalctl empty read')
|
96
|
+
break
|
97
|
+
|
98
|
+
log.debug('Journalctl read buffer: %r', buf)
|
99
|
+
msgs = self._mb.feed(buf)
|
100
|
+
if msgs:
|
101
|
+
self._output.put(msgs)
|
102
|
+
|
103
|
+
if self._proc.poll() is not None:
|
104
|
+
log.critical('Journalctl process terminated')
|
105
|
+
break
|
106
|
+
|
107
|
+
log.debug('Journalctl readable')
|
108
|
+
time.sleep(self._sleep_s)
|
@@ -0,0 +1,317 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# ruff: noqa: UP007
|
3
|
+
# @omlish-amalg ../../../scripts/journald2aws.py
|
4
|
+
"""
|
5
|
+
TODO:
|
6
|
+
- create log group
|
7
|
+
- log stats - chunk sizes etc
|
8
|
+
|
9
|
+
==
|
10
|
+
|
11
|
+
https://www.freedesktop.org/software/systemd/man/latest/journalctl.html
|
12
|
+
|
13
|
+
journalctl:
|
14
|
+
-o json
|
15
|
+
--show-cursor
|
16
|
+
|
17
|
+
--since "2012-10-30 18:17:16"
|
18
|
+
--until "2012-10-30 18:17:16"
|
19
|
+
|
20
|
+
--after-cursor <cursor>
|
21
|
+
|
22
|
+
==
|
23
|
+
|
24
|
+
https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html
|
25
|
+
|
26
|
+
==
|
27
|
+
|
28
|
+
@dc.dataclass(frozen=True)
|
29
|
+
class Journald2AwsConfig:
|
30
|
+
log_group_name: str
|
31
|
+
log_stream_name: str
|
32
|
+
|
33
|
+
aws_batch_size: int = 1_000
|
34
|
+
aws_flush_interval_s: float = 1.
|
35
|
+
"""
|
36
|
+
import argparse
|
37
|
+
import contextlib
|
38
|
+
import dataclasses as dc
|
39
|
+
import json
|
40
|
+
import os.path
|
41
|
+
import queue
|
42
|
+
import sys
|
43
|
+
import time
|
44
|
+
import typing as ta
|
45
|
+
import urllib.request
|
46
|
+
|
47
|
+
from omlish.lite.cached import cached_nullary
|
48
|
+
from omlish.lite.check import check_non_empty_str
|
49
|
+
from omlish.lite.check import check_not_none
|
50
|
+
from omlish.lite.logs import configure_standard_logging
|
51
|
+
from omlish.lite.logs import log
|
52
|
+
from omlish.lite.marshal import unmarshal_obj
|
53
|
+
from omlish.lite.pidfile import Pidfile
|
54
|
+
from omlish.lite.runtime import is_debugger_attached
|
55
|
+
|
56
|
+
from ..auth import AwsSigner
|
57
|
+
from ..logs import AwsLogMessagePoster
|
58
|
+
from ..logs import AwsPutLogEventsResponse
|
59
|
+
from .journald.messages import JournalctlMessage # noqa
|
60
|
+
from .journald.tailer import JournalctlTailerWorker
|
61
|
+
|
62
|
+
|
63
|
+
@dc.dataclass(frozen=True)
|
64
|
+
class JournalctlOpts:
|
65
|
+
after_cursor: ta.Optional[str] = None
|
66
|
+
|
67
|
+
since: ta.Optional[str] = None
|
68
|
+
until: ta.Optional[str] = None
|
69
|
+
|
70
|
+
|
71
|
+
class JournalctlToAws:
|
72
|
+
@dc.dataclass(frozen=True)
|
73
|
+
class Config:
|
74
|
+
pid_file: ta.Optional[str] = None
|
75
|
+
|
76
|
+
cursor_file: ta.Optional[str] = None
|
77
|
+
|
78
|
+
#
|
79
|
+
|
80
|
+
aws_log_group_name: str = 'omlish'
|
81
|
+
aws_log_stream_name: ta.Optional[str] = None
|
82
|
+
|
83
|
+
aws_access_key_id: ta.Optional[str] = None
|
84
|
+
aws_secret_access_key: ta.Optional[str] = dc.field(default=None, repr=False)
|
85
|
+
|
86
|
+
aws_region_name: str = 'us-west-1'
|
87
|
+
|
88
|
+
#
|
89
|
+
|
90
|
+
journalctl_cmd: ta.Optional[ta.Sequence[str]] = None
|
91
|
+
|
92
|
+
journalctl_after_cursor: ta.Optional[str] = None
|
93
|
+
journalctl_since: ta.Optional[str] = None
|
94
|
+
|
95
|
+
#
|
96
|
+
|
97
|
+
dry_run: bool = False
|
98
|
+
|
99
|
+
def __init__(self, config: Config) -> None:
|
100
|
+
super().__init__()
|
101
|
+
self._config = config
|
102
|
+
|
103
|
+
#
|
104
|
+
|
105
|
+
_es: contextlib.ExitStack
|
106
|
+
|
107
|
+
def __enter__(self) -> 'JournalctlToAws':
|
108
|
+
self._es = contextlib.ExitStack().__enter__()
|
109
|
+
return self
|
110
|
+
|
111
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
112
|
+
return self._es.__exit__(exc_type, exc_val, exc_tb)
|
113
|
+
|
114
|
+
#
|
115
|
+
|
116
|
+
@cached_nullary
|
117
|
+
def _pidfile(self) -> ta.Optional[Pidfile]:
|
118
|
+
if self._config.pid_file is None:
|
119
|
+
return None
|
120
|
+
|
121
|
+
pfp = os.path.expanduser(self._config.pid_file)
|
122
|
+
|
123
|
+
log.info('Opening pidfile %s', pfp)
|
124
|
+
|
125
|
+
pf = self._es.enter_context(Pidfile(pfp))
|
126
|
+
pf.write()
|
127
|
+
return pf
|
128
|
+
|
129
|
+
def _ensure_locked(self) -> None:
|
130
|
+
if (pf := self._pidfile()) is not None:
|
131
|
+
pf.ensure_locked()
|
132
|
+
|
133
|
+
#
|
134
|
+
|
135
|
+
def _read_cursor_file(self) -> ta.Optional[str]:
|
136
|
+
self._ensure_locked()
|
137
|
+
|
138
|
+
if not (cf := self._config.cursor_file):
|
139
|
+
return None
|
140
|
+
cf = os.path.expanduser(cf)
|
141
|
+
|
142
|
+
try:
|
143
|
+
with open(cf) as f:
|
144
|
+
return f.read().strip()
|
145
|
+
except FileNotFoundError:
|
146
|
+
return None
|
147
|
+
|
148
|
+
def _write_cursor_file(self, cursor: str) -> None:
|
149
|
+
self._ensure_locked()
|
150
|
+
|
151
|
+
if not (cf := self._config.cursor_file):
|
152
|
+
return
|
153
|
+
cf = os.path.expanduser(cf)
|
154
|
+
|
155
|
+
log.info('Writing cursor file %s : %s', cf, cursor)
|
156
|
+
with open(ncf := cf + '.next', 'w') as f:
|
157
|
+
f.write(cursor)
|
158
|
+
|
159
|
+
os.rename(ncf, cf)
|
160
|
+
|
161
|
+
#
|
162
|
+
|
163
|
+
@cached_nullary
|
164
|
+
def _aws_credentials(self) -> AwsSigner.Credentials:
|
165
|
+
return AwsSigner.Credentials(
|
166
|
+
access_key_id=check_non_empty_str(self._config.aws_access_key_id),
|
167
|
+
secret_access_key=check_non_empty_str(self._config.aws_secret_access_key),
|
168
|
+
)
|
169
|
+
|
170
|
+
@cached_nullary
|
171
|
+
def _aws_log_message_poster(self) -> AwsLogMessagePoster:
|
172
|
+
return AwsLogMessagePoster(
|
173
|
+
log_group_name=self._config.aws_log_group_name,
|
174
|
+
log_stream_name=check_non_empty_str(self._config.aws_log_stream_name),
|
175
|
+
region_name=self._config.aws_region_name,
|
176
|
+
credentials=check_not_none(self._aws_credentials()),
|
177
|
+
)
|
178
|
+
|
179
|
+
#
|
180
|
+
|
181
|
+
@cached_nullary
|
182
|
+
def _journalctl_message_queue(self): # type: () -> queue.Queue[ta.Sequence[JournalctlMessage]]
|
183
|
+
return queue.Queue()
|
184
|
+
|
185
|
+
@cached_nullary
|
186
|
+
def _journalctl_tailer_worker(self) -> JournalctlTailerWorker:
|
187
|
+
ac: ta.Optional[str] = self._config.journalctl_after_cursor
|
188
|
+
if ac is None:
|
189
|
+
ac = self._read_cursor_file()
|
190
|
+
if ac is not None:
|
191
|
+
log.info('Starting from cursor %s', ac)
|
192
|
+
|
193
|
+
if (since := self._config.journalctl_since):
|
194
|
+
log.info('Starting since %s', since)
|
195
|
+
|
196
|
+
return JournalctlTailerWorker(
|
197
|
+
self._journalctl_message_queue(),
|
198
|
+
|
199
|
+
since=since,
|
200
|
+
after_cursor=ac,
|
201
|
+
|
202
|
+
cmd=self._config.journalctl_cmd,
|
203
|
+
shell_wrap=is_debugger_attached(),
|
204
|
+
)
|
205
|
+
|
206
|
+
#
|
207
|
+
|
208
|
+
def run(self) -> None:
|
209
|
+
self._ensure_locked()
|
210
|
+
|
211
|
+
q = self._journalctl_message_queue()
|
212
|
+
jtw = self._journalctl_tailer_worker()
|
213
|
+
mp = self._aws_log_message_poster()
|
214
|
+
|
215
|
+
jtw.start()
|
216
|
+
|
217
|
+
last_cursor: ta.Optional[str] = None # noqa
|
218
|
+
while True:
|
219
|
+
if not jtw.is_alive():
|
220
|
+
log.critical('Journalctl tailer worker died')
|
221
|
+
break
|
222
|
+
|
223
|
+
msgs: ta.Sequence[JournalctlMessage] = q.get()
|
224
|
+
log.debug('%r', msgs)
|
225
|
+
|
226
|
+
cur_cursor: ta.Optional[str] = None
|
227
|
+
for m in reversed(msgs):
|
228
|
+
if m.cursor is not None:
|
229
|
+
cur_cursor = m.cursor
|
230
|
+
break
|
231
|
+
|
232
|
+
if not msgs:
|
233
|
+
log.warning('Empty queue chunk')
|
234
|
+
continue
|
235
|
+
|
236
|
+
[post] = mp.feed([mp.Message(
|
237
|
+
message=json.dumps(m.dct),
|
238
|
+
ts_ms=int(time.time() * 1000.),
|
239
|
+
) for m in msgs])
|
240
|
+
log.debug('%r', post)
|
241
|
+
|
242
|
+
if not self._config.dry_run:
|
243
|
+
with urllib.request.urlopen(urllib.request.Request( # noqa
|
244
|
+
post.url,
|
245
|
+
method='POST',
|
246
|
+
headers=dict(post.headers),
|
247
|
+
data=post.data,
|
248
|
+
)) as resp:
|
249
|
+
response = AwsPutLogEventsResponse.from_aws(json.loads(resp.read().decode('utf-8')))
|
250
|
+
log.debug('%r', response)
|
251
|
+
|
252
|
+
if cur_cursor is not None:
|
253
|
+
self._write_cursor_file(cur_cursor)
|
254
|
+
last_cursor = cur_cursor # noqa
|
255
|
+
|
256
|
+
|
257
|
+
def _main() -> None:
|
258
|
+
parser = argparse.ArgumentParser()
|
259
|
+
|
260
|
+
parser.add_argument('--config-file')
|
261
|
+
parser.add_argument('-v', '--verbose', action='store_true')
|
262
|
+
|
263
|
+
parser.add_argument('--after-cursor', nargs='?')
|
264
|
+
parser.add_argument('--since', nargs='?')
|
265
|
+
parser.add_argument('--dry-run', action='store_true')
|
266
|
+
|
267
|
+
parser.add_argument('--message', nargs='?')
|
268
|
+
parser.add_argument('--real', action='store_true')
|
269
|
+
|
270
|
+
args = parser.parse_args()
|
271
|
+
|
272
|
+
#
|
273
|
+
|
274
|
+
configure_standard_logging('DEBUG' if args.verbose else 'INFO')
|
275
|
+
|
276
|
+
#
|
277
|
+
|
278
|
+
config: JournalctlToAws.Config
|
279
|
+
if args.config_file:
|
280
|
+
with open(os.path.expanduser(args.config_file)) as cf:
|
281
|
+
config_dct = json.load(cf)
|
282
|
+
config = unmarshal_obj(config_dct, JournalctlToAws.Config)
|
283
|
+
else:
|
284
|
+
config = JournalctlToAws.Config()
|
285
|
+
|
286
|
+
#
|
287
|
+
|
288
|
+
for k in ['aws_access_key_id', 'aws_secret_access_key']:
|
289
|
+
if not getattr(config, k) and k.upper() in os.environ:
|
290
|
+
config = dc.replace(config, **{k: os.environ.get(k.upper())}) # type: ignore
|
291
|
+
|
292
|
+
#
|
293
|
+
|
294
|
+
if not args.real:
|
295
|
+
config = dc.replace(config, journalctl_cmd=[
|
296
|
+
sys.executable,
|
297
|
+
os.path.join(os.path.dirname(__file__), 'journald', 'genmessages.py'),
|
298
|
+
'--sleep-n', '2',
|
299
|
+
'--sleep-s', '.5',
|
300
|
+
*(['--message', args.message] if args.message else []),
|
301
|
+
'100000',
|
302
|
+
])
|
303
|
+
|
304
|
+
#
|
305
|
+
|
306
|
+
for a in ['after_cursor', 'since', 'dry_run']:
|
307
|
+
if (pa := getattr(args, a)):
|
308
|
+
config = dc.replace(config, **{a: pa})
|
309
|
+
|
310
|
+
#
|
311
|
+
|
312
|
+
with JournalctlToAws(config) as jta:
|
313
|
+
jta.run()
|
314
|
+
|
315
|
+
|
316
|
+
if __name__ == '__main__':
|
317
|
+
_main()
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# ruff: noqa: UP007
|
2
|
+
import abc
|
3
|
+
import threading
|
4
|
+
import time
|
5
|
+
import typing as ta
|
6
|
+
|
7
|
+
from omlish.lite.logs import log
|
8
|
+
|
9
|
+
|
10
|
+
class ThreadWorker(abc.ABC):
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
*,
|
14
|
+
stop_event: ta.Optional[threading.Event] = None,
|
15
|
+
) -> None:
|
16
|
+
super().__init__()
|
17
|
+
|
18
|
+
if stop_event is None:
|
19
|
+
stop_event = threading.Event()
|
20
|
+
self._stop_event = stop_event
|
21
|
+
|
22
|
+
self._thread: ta.Optional[threading.Thread] = None
|
23
|
+
|
24
|
+
self._last_heartbeat: ta.Optional[float] = None
|
25
|
+
|
26
|
+
#
|
27
|
+
|
28
|
+
def should_stop(self) -> bool:
|
29
|
+
return self._stop_event.is_set()
|
30
|
+
|
31
|
+
#
|
32
|
+
|
33
|
+
@property
|
34
|
+
def last_heartbeat(self) -> ta.Optional[float]:
|
35
|
+
return self._last_heartbeat
|
36
|
+
|
37
|
+
def _heartbeat(self) -> bool:
|
38
|
+
self._last_heartbeat = time.time()
|
39
|
+
|
40
|
+
if self.should_stop():
|
41
|
+
log.info('Stopping: %s', self)
|
42
|
+
return False
|
43
|
+
|
44
|
+
return True
|
45
|
+
|
46
|
+
#
|
47
|
+
|
48
|
+
def is_alive(self) -> bool:
|
49
|
+
return (thr := self._thread) is not None and thr.is_alive()
|
50
|
+
|
51
|
+
def start(self) -> None:
|
52
|
+
thr = threading.Thread(target=self._run)
|
53
|
+
self._thread = thr
|
54
|
+
thr.start()
|
55
|
+
|
56
|
+
@abc.abstractmethod
|
57
|
+
def _run(self) -> None:
|
58
|
+
raise NotImplementedError
|
59
|
+
|
60
|
+
def stop(self) -> None:
|
61
|
+
raise NotImplementedError
|
62
|
+
|
63
|
+
def cleanup(self) -> None: # noqa
|
64
|
+
pass
|