ominfra 0.0.0.dev102__py3-none-any.whl → 0.0.0.dev104__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/journald2aws/cursor.py +47 -0
- ominfra/clouds/aws/journald2aws/driver.py +210 -0
- ominfra/clouds/aws/journald2aws/main.py +9 -262
- ominfra/clouds/aws/journald2aws/poster.py +89 -0
- ominfra/clouds/aws/logs.py +19 -13
- ominfra/clouds/gcp/__init__.py +0 -0
- ominfra/clouds/gcp/auth.py +48 -0
- ominfra/deploy/_executor.py +93 -30
- ominfra/deploy/poly/_main.py +26 -2
- ominfra/journald/genmessages.py +14 -2
- ominfra/journald/tailer.py +39 -33
- ominfra/pyremote/_runcommands.py +93 -30
- ominfra/scripts/journald2aws.py +496 -222
- ominfra/scripts/supervisor.py +69 -28
- ominfra/threadworkers.py +139 -0
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/RECORD +21 -16
- ominfra/threadworker.py +0 -67
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev102.dist-info → ominfra-0.0.0.dev104.dist-info}/top_level.txt +0 -0
ominfra/clouds/aws/logs.py
CHANGED
@@ -62,7 +62,7 @@ class AwsPutLogEventsResponse(AwsDataclass):
|
|
62
62
|
##
|
63
63
|
|
64
64
|
|
65
|
-
class
|
65
|
+
class AwsLogMessageBuilder:
|
66
66
|
"""
|
67
67
|
TODO:
|
68
68
|
- max_items
|
@@ -88,7 +88,7 @@ class AwsLogMessagePoster:
|
|
88
88
|
log_group_name: str,
|
89
89
|
log_stream_name: str,
|
90
90
|
region_name: str,
|
91
|
-
credentials: AwsSigner.Credentials,
|
91
|
+
credentials: ta.Optional[AwsSigner.Credentials],
|
92
92
|
|
93
93
|
url: ta.Optional[str] = None,
|
94
94
|
service_name: str = DEFAULT_SERVICE_NAME,
|
@@ -110,11 +110,16 @@ class AwsLogMessagePoster:
|
|
110
110
|
headers = {**headers, **extra_headers}
|
111
111
|
self._headers = {k: [v] for k, v in headers.items()}
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
113
|
+
signer: ta.Optional[V4AwsSigner]
|
114
|
+
if credentials is not None:
|
115
|
+
signer = V4AwsSigner(
|
116
|
+
credentials,
|
117
|
+
region_name,
|
118
|
+
service_name,
|
119
|
+
)
|
120
|
+
else:
|
121
|
+
signer = None
|
122
|
+
self._signer = signer
|
118
123
|
|
119
124
|
#
|
120
125
|
|
@@ -158,13 +163,14 @@ class AwsLogMessagePoster:
|
|
158
163
|
payload=body,
|
159
164
|
)
|
160
165
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
+
if (signer := self._signer) is not None:
|
167
|
+
sig_headers = signer.sign(
|
168
|
+
sig_req,
|
169
|
+
sign_payload=False,
|
170
|
+
)
|
171
|
+
sig_req = dc.replace(sig_req, headers={**sig_req.headers, **sig_headers})
|
166
172
|
|
167
|
-
post =
|
173
|
+
post = AwsLogMessageBuilder.Post(
|
168
174
|
url=self._url,
|
169
175
|
headers={k: check_single(v) for k, v in sig_req.headers.items()},
|
170
176
|
data=sig_req.payload,
|
File without changes
|
@@ -0,0 +1,48 @@
|
|
1
|
+
import json
|
2
|
+
import time
|
3
|
+
import typing as ta
|
4
|
+
|
5
|
+
from omlish import check
|
6
|
+
from omlish import http
|
7
|
+
from omlish.http import jwt
|
8
|
+
|
9
|
+
|
10
|
+
DEFAULT_JWT_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
|
11
|
+
|
12
|
+
|
13
|
+
def generate_gcp_jwt(
|
14
|
+
creds_dct: ta.Mapping[str, ta.Any],
|
15
|
+
*,
|
16
|
+
issued_at: int | None = None,
|
17
|
+
lifetime_s: int = 3600,
|
18
|
+
scope: str = DEFAULT_JWT_SCOPE,
|
19
|
+
) -> str:
|
20
|
+
return jwt.generate_jwt(
|
21
|
+
issuer=creds_dct['client_email'],
|
22
|
+
subject=creds_dct['client_email'],
|
23
|
+
audience=creds_dct['token_uri'],
|
24
|
+
issued_at=(issued_at := int(issued_at if issued_at is not None else time.time())),
|
25
|
+
expires_at=issued_at + lifetime_s,
|
26
|
+
scope=scope,
|
27
|
+
key=creds_dct['private_key'],
|
28
|
+
algorithm='RS256',
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def get_gcp_access_token(
|
33
|
+
creds_dct: ta.Mapping[str, ta.Any],
|
34
|
+
*,
|
35
|
+
client: http.HttpClient | None = None,
|
36
|
+
) -> str:
|
37
|
+
signed_jwt = generate_gcp_jwt(creds_dct)
|
38
|
+
resp = http.request(
|
39
|
+
creds_dct['token_uri'],
|
40
|
+
'POST',
|
41
|
+
data=jwt.build_get_token_body(signed_jwt).encode('utf-8'),
|
42
|
+
headers={
|
43
|
+
http.consts.HEADER_CONTENT_TYPE: http.consts.CONTENT_TYPE_FORM_URLENCODED,
|
44
|
+
},
|
45
|
+
client=client,
|
46
|
+
)
|
47
|
+
resp_dct = json.loads(check.not_none(resp.data).decode('utf-8'))
|
48
|
+
return resp_dct['access_token']
|
ominfra/deploy/_executor.py
CHANGED
@@ -82,7 +82,7 @@ if sys.version_info < (3, 8):
|
|
82
82
|
########################################
|
83
83
|
|
84
84
|
|
85
|
-
# ../../../../omlish/lite/
|
85
|
+
# ../../../../omlish/lite/cached.py
|
86
86
|
T = ta.TypeVar('T')
|
87
87
|
|
88
88
|
|
@@ -112,7 +112,7 @@ class HostConfig:
|
|
112
112
|
# ../../../../omlish/lite/cached.py
|
113
113
|
|
114
114
|
|
115
|
-
class
|
115
|
+
class _cached_nullary: # noqa
|
116
116
|
def __init__(self, fn):
|
117
117
|
super().__init__()
|
118
118
|
self._fn = fn
|
@@ -129,6 +129,10 @@ class cached_nullary: # noqa
|
|
129
129
|
return bound
|
130
130
|
|
131
131
|
|
132
|
+
def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
|
133
|
+
return _cached_nullary(fn)
|
134
|
+
|
135
|
+
|
132
136
|
########################################
|
133
137
|
# ../../../../omlish/lite/check.py
|
134
138
|
|
@@ -661,7 +665,7 @@ class DataclassObjMarshaler(ObjMarshaler):
|
|
661
665
|
return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
|
662
666
|
|
663
667
|
def unmarshal(self, o: ta.Any) -> ta.Any:
|
664
|
-
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if self.nonstrict or k in self.fs})
|
668
|
+
return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
|
665
669
|
|
666
670
|
|
667
671
|
@dc.dataclass(frozen=True)
|
@@ -721,7 +725,10 @@ class UuidObjMarshaler(ObjMarshaler):
|
|
721
725
|
return uuid.UUID(o)
|
722
726
|
|
723
727
|
|
724
|
-
|
728
|
+
##
|
729
|
+
|
730
|
+
|
731
|
+
_DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
|
725
732
|
**{t: NopObjMarshaler() for t in (type(None),)},
|
726
733
|
**{t: CastObjMarshaler(t) for t in (int, float, str, bool)},
|
727
734
|
**{t: Base64ObjMarshaler(t) for t in (bytes, bytearray)},
|
@@ -750,20 +757,19 @@ _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES: ta.Dict[ta.Any, type] = {
|
|
750
757
|
}
|
751
758
|
|
752
759
|
|
753
|
-
def
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
760
|
+
def _make_obj_marshaler(
|
761
|
+
ty: ta.Any,
|
762
|
+
rec: ta.Callable[[ta.Any], ObjMarshaler],
|
763
|
+
*,
|
764
|
+
nonstrict_dataclasses: bool = False,
|
765
|
+
) -> ObjMarshaler:
|
760
766
|
if isinstance(ty, type):
|
761
767
|
if abc.ABC in ty.__bases__:
|
762
768
|
impls = [ # type: ignore
|
763
769
|
PolymorphicObjMarshaler.Impl(
|
764
770
|
ity,
|
765
771
|
ity.__qualname__,
|
766
|
-
|
772
|
+
rec(ity),
|
767
773
|
)
|
768
774
|
for ity in deep_subclasses(ty)
|
769
775
|
if abc.ABC not in ity.__bases__
|
@@ -779,7 +785,8 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
779
785
|
if dc.is_dataclass(ty):
|
780
786
|
return DataclassObjMarshaler(
|
781
787
|
ty,
|
782
|
-
{f.name:
|
788
|
+
{f.name: rec(f.type) for f in dc.fields(ty)},
|
789
|
+
nonstrict=nonstrict_dataclasses,
|
783
790
|
)
|
784
791
|
|
785
792
|
if is_generic_alias(ty):
|
@@ -789,7 +796,7 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
789
796
|
pass
|
790
797
|
else:
|
791
798
|
k, v = ta.get_args(ty)
|
792
|
-
return MappingObjMarshaler(mt,
|
799
|
+
return MappingObjMarshaler(mt, rec(k), rec(v))
|
793
800
|
|
794
801
|
try:
|
795
802
|
st = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES[ta.get_origin(ty)]
|
@@ -797,33 +804,71 @@ def _make_obj_marshaler(ty: ta.Any) -> ObjMarshaler:
|
|
797
804
|
pass
|
798
805
|
else:
|
799
806
|
[e] = ta.get_args(ty)
|
800
|
-
return IterableObjMarshaler(st,
|
807
|
+
return IterableObjMarshaler(st, rec(e))
|
801
808
|
|
802
809
|
if is_union_alias(ty):
|
803
|
-
return OptionalObjMarshaler(
|
810
|
+
return OptionalObjMarshaler(rec(get_optional_alias_arg(ty)))
|
804
811
|
|
805
812
|
raise TypeError(ty)
|
806
813
|
|
807
814
|
|
808
|
-
|
809
|
-
try:
|
810
|
-
return _OBJ_MARSHALERS[ty]
|
811
|
-
except KeyError:
|
812
|
-
pass
|
815
|
+
##
|
813
816
|
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
817
|
+
|
818
|
+
_OBJ_MARSHALERS_LOCK = threading.RLock()
|
819
|
+
|
820
|
+
_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = dict(_DEFAULT_OBJ_MARSHALERS)
|
821
|
+
|
822
|
+
_OBJ_MARSHALER_PROXIES: ta.Dict[ta.Any, ProxyObjMarshaler] = {}
|
823
|
+
|
824
|
+
|
825
|
+
def register_opj_marshaler(ty: ta.Any, m: ObjMarshaler) -> None:
|
826
|
+
with _OBJ_MARSHALERS_LOCK:
|
827
|
+
if ty in _OBJ_MARSHALERS:
|
828
|
+
raise KeyError(ty)
|
823
829
|
_OBJ_MARSHALERS[ty] = m
|
830
|
+
|
831
|
+
|
832
|
+
def get_obj_marshaler(
|
833
|
+
ty: ta.Any,
|
834
|
+
*,
|
835
|
+
no_cache: bool = False,
|
836
|
+
**kwargs: ta.Any,
|
837
|
+
) -> ObjMarshaler:
|
838
|
+
with _OBJ_MARSHALERS_LOCK:
|
839
|
+
if not no_cache:
|
840
|
+
try:
|
841
|
+
return _OBJ_MARSHALERS[ty]
|
842
|
+
except KeyError:
|
843
|
+
pass
|
844
|
+
|
845
|
+
try:
|
846
|
+
return _OBJ_MARSHALER_PROXIES[ty]
|
847
|
+
except KeyError:
|
848
|
+
pass
|
849
|
+
|
850
|
+
rec = functools.partial(
|
851
|
+
get_obj_marshaler,
|
852
|
+
no_cache=no_cache,
|
853
|
+
**kwargs,
|
854
|
+
)
|
855
|
+
|
856
|
+
p = ProxyObjMarshaler()
|
857
|
+
_OBJ_MARSHALER_PROXIES[ty] = p
|
858
|
+
try:
|
859
|
+
m = _make_obj_marshaler(ty, rec, **kwargs)
|
860
|
+
finally:
|
861
|
+
del _OBJ_MARSHALER_PROXIES[ty]
|
862
|
+
p.m = m
|
863
|
+
|
864
|
+
if not no_cache:
|
865
|
+
_OBJ_MARSHALERS[ty] = m
|
824
866
|
return m
|
825
867
|
|
826
868
|
|
869
|
+
##
|
870
|
+
|
871
|
+
|
827
872
|
def marshal_obj(o: ta.Any, ty: ta.Any = None) -> ta.Any:
|
828
873
|
return get_obj_marshaler(ty if ty is not None else type(o)).marshal(o)
|
829
874
|
|
@@ -956,6 +1001,24 @@ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
|
|
956
1001
|
return out.decode().strip() if out is not None else None
|
957
1002
|
|
958
1003
|
|
1004
|
+
##
|
1005
|
+
|
1006
|
+
|
1007
|
+
def subprocess_close(
|
1008
|
+
proc: subprocess.Popen,
|
1009
|
+
timeout: ta.Optional[float] = None,
|
1010
|
+
) -> None:
|
1011
|
+
# TODO: terminate, sleep, kill
|
1012
|
+
if proc.stdout:
|
1013
|
+
proc.stdout.close()
|
1014
|
+
if proc.stderr:
|
1015
|
+
proc.stderr.close()
|
1016
|
+
if proc.stdin:
|
1017
|
+
proc.stdin.close()
|
1018
|
+
|
1019
|
+
proc.wait(timeout)
|
1020
|
+
|
1021
|
+
|
959
1022
|
########################################
|
960
1023
|
# ../base.py
|
961
1024
|
|
ominfra/deploy/poly/_main.py
CHANGED
@@ -34,8 +34,10 @@ if sys.version_info < (3, 8):
|
|
34
34
|
########################################
|
35
35
|
|
36
36
|
|
37
|
-
#
|
37
|
+
# ../../../../omlish/lite/cached.py
|
38
38
|
T = ta.TypeVar('T')
|
39
|
+
|
40
|
+
# ../base.py
|
39
41
|
ConcernT = ta.TypeVar('ConcernT')
|
40
42
|
ConfigT = ta.TypeVar('ConfigT')
|
41
43
|
SiteConcernConfigT = ta.TypeVar('SiteConcernConfigT', bound='SiteConcernConfig')
|
@@ -84,7 +86,7 @@ class DeployConfig:
|
|
84
86
|
# ../../../../omlish/lite/cached.py
|
85
87
|
|
86
88
|
|
87
|
-
class
|
89
|
+
class _cached_nullary: # noqa
|
88
90
|
def __init__(self, fn):
|
89
91
|
super().__init__()
|
90
92
|
self._fn = fn
|
@@ -101,6 +103,10 @@ class cached_nullary: # noqa
|
|
101
103
|
return bound
|
102
104
|
|
103
105
|
|
106
|
+
def cached_nullary(fn: ta.Callable[..., T]) -> ta.Callable[..., T]:
|
107
|
+
return _cached_nullary(fn)
|
108
|
+
|
109
|
+
|
104
110
|
########################################
|
105
111
|
# ../../../../omlish/lite/json.py
|
106
112
|
|
@@ -809,6 +815,24 @@ def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
|
|
809
815
|
return out.decode().strip() if out is not None else None
|
810
816
|
|
811
817
|
|
818
|
+
##
|
819
|
+
|
820
|
+
|
821
|
+
def subprocess_close(
|
822
|
+
proc: subprocess.Popen,
|
823
|
+
timeout: ta.Optional[float] = None,
|
824
|
+
) -> None:
|
825
|
+
# TODO: terminate, sleep, kill
|
826
|
+
if proc.stdout:
|
827
|
+
proc.stdout.close()
|
828
|
+
if proc.stderr:
|
829
|
+
proc.stderr.close()
|
830
|
+
if proc.stdin:
|
831
|
+
proc.stdin.close()
|
832
|
+
|
833
|
+
proc.wait(timeout)
|
834
|
+
|
835
|
+
|
812
836
|
########################################
|
813
837
|
# ../runtime.py
|
814
838
|
|
ominfra/journald/genmessages.py
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
import argparse
|
4
4
|
import datetime
|
5
5
|
import json
|
6
|
+
import os
|
6
7
|
import re
|
7
8
|
import sys
|
8
9
|
import time
|
@@ -34,6 +35,11 @@ def _main() -> None:
|
|
34
35
|
else:
|
35
36
|
start = 0
|
36
37
|
|
38
|
+
stdout_fd = sys.stdout.fileno()
|
39
|
+
out_fd = os.dup(stdout_fd)
|
40
|
+
null_fd = os.open('/dev/null', os.O_WRONLY)
|
41
|
+
os.dup2(null_fd, stdout_fd)
|
42
|
+
|
37
43
|
for i in range(start, args.n):
|
38
44
|
if args.sleep_s:
|
39
45
|
if not args.sleep_n or (i and i % args.sleep_n == 0):
|
@@ -46,8 +52,14 @@ def _main() -> None:
|
|
46
52
|
'__CURSOR': f'cursor:{i}',
|
47
53
|
'_SOURCE_REALTIME_TIMESTAMP': str(int(ts_us)),
|
48
54
|
}
|
49
|
-
|
50
|
-
|
55
|
+
|
56
|
+
buf = json.dumps(dct, indent=None, separators=(',', ':')).encode()
|
57
|
+
|
58
|
+
try:
|
59
|
+
os.write(out_fd, buf)
|
60
|
+
os.write(out_fd, b'\n')
|
61
|
+
except BrokenPipeError:
|
62
|
+
break
|
51
63
|
|
52
64
|
|
53
65
|
if __name__ == '__main__':
|
ominfra/journald/tailer.py
CHANGED
@@ -347,9 +347,10 @@ import typing as ta
|
|
347
347
|
from omlish.lite.cached import cached_nullary
|
348
348
|
from omlish.lite.check import check_not_none
|
349
349
|
from omlish.lite.logs import log
|
350
|
+
from omlish.lite.subprocesses import subprocess_close
|
350
351
|
from omlish.lite.subprocesses import subprocess_shell_wrap_exec
|
351
352
|
|
352
|
-
from ..
|
353
|
+
from ..threadworkers import ThreadWorker
|
353
354
|
from .messages import JournalctlMessage # noqa
|
354
355
|
from .messages import JournalctlMessageBuilder
|
355
356
|
|
@@ -385,7 +386,7 @@ class JournalctlTailerWorker(ThreadWorker):
|
|
385
386
|
self._read_size = read_size
|
386
387
|
self._sleep_s = sleep_s
|
387
388
|
|
388
|
-
self.
|
389
|
+
self._builder = JournalctlMessageBuilder()
|
389
390
|
|
390
391
|
self._proc: ta.Optional[subprocess.Popen] = None
|
391
392
|
|
@@ -409,45 +410,50 @@ class JournalctlTailerWorker(ThreadWorker):
|
|
409
410
|
|
410
411
|
return cmd
|
411
412
|
|
413
|
+
def _read_loop(self, stdout: ta.IO) -> None:
|
414
|
+
while stdout.readable():
|
415
|
+
self._heartbeat()
|
416
|
+
|
417
|
+
buf = stdout.read(self._read_size)
|
418
|
+
if not buf:
|
419
|
+
log.debug('Journalctl empty read')
|
420
|
+
break
|
421
|
+
|
422
|
+
log.debug('Journalctl read buffer: %r', buf)
|
423
|
+
msgs = self._builder.feed(buf)
|
424
|
+
if msgs:
|
425
|
+
while True:
|
426
|
+
try:
|
427
|
+
self._output.put(msgs, timeout=1.)
|
428
|
+
except queue.Full:
|
429
|
+
self._heartbeat()
|
430
|
+
else:
|
431
|
+
break
|
432
|
+
|
412
433
|
def _run(self) -> None:
|
413
434
|
with subprocess.Popen(
|
414
435
|
self._full_cmd(),
|
415
436
|
stdout=subprocess.PIPE,
|
416
437
|
) as self._proc:
|
417
|
-
|
438
|
+
try:
|
439
|
+
stdout = check_not_none(self._proc.stdout)
|
440
|
+
|
441
|
+
fd = stdout.fileno()
|
442
|
+
fl = fcntl.fcntl(fd, fcntl.F_GETFL)
|
443
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
418
444
|
|
419
|
-
|
420
|
-
|
421
|
-
fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
|
445
|
+
while True:
|
446
|
+
self._heartbeat()
|
422
447
|
|
423
|
-
|
424
|
-
if not self._heartbeat():
|
425
|
-
return
|
448
|
+
self._read_loop(stdout)
|
426
449
|
|
427
|
-
|
428
|
-
|
450
|
+
log.debug('Journalctl not readable')
|
451
|
+
|
452
|
+
if self._proc.poll() is not None:
|
453
|
+
log.critical('Journalctl process terminated')
|
429
454
|
return
|
430
455
|
|
431
|
-
|
432
|
-
if not buf:
|
433
|
-
log.debug('Journalctl empty read')
|
434
|
-
break
|
456
|
+
time.sleep(self._sleep_s)
|
435
457
|
|
436
|
-
|
437
|
-
|
438
|
-
if msgs:
|
439
|
-
while True:
|
440
|
-
try:
|
441
|
-
self._output.put(msgs, timeout=1.)
|
442
|
-
except queue.Full:
|
443
|
-
if not self._heartbeat():
|
444
|
-
return
|
445
|
-
else:
|
446
|
-
break
|
447
|
-
|
448
|
-
if self._proc.poll() is not None:
|
449
|
-
log.critical('Journalctl process terminated')
|
450
|
-
return
|
451
|
-
|
452
|
-
log.debug('Journalctl readable')
|
453
|
-
time.sleep(self._sleep_s)
|
458
|
+
finally:
|
459
|
+
subprocess_close(self._proc)
|