ominfra 0.0.0.dev102__py3-none-any.whl → 0.0.0.dev104__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|