ominfra 0.0.0.dev144__py3-none-any.whl → 0.0.0.dev146__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/manage/commands/execution.py +1 -1
- ominfra/manage/commands/inject.py +7 -3
- ominfra/manage/commands/interp.py +39 -0
- ominfra/manage/commands/subprocess.py +2 -2
- ominfra/manage/deploy/command.py +4 -0
- ominfra/manage/deploy/paths.py +180 -0
- ominfra/manage/main.py +16 -9
- ominfra/manage/remote/channel.py +13 -2
- ominfra/manage/remote/execution.py +66 -9
- ominfra/scripts/journald2aws.py +73 -53
- ominfra/scripts/manage.py +2272 -293
- ominfra/scripts/supervisor.py +114 -94
- ominfra/supervisor/dispatchers.py +3 -3
- ominfra/supervisor/http.py +6 -6
- ominfra/supervisor/inject.py +12 -12
- ominfra/supervisor/io.py +2 -2
- ominfra/supervisor/spawningimpl.py +2 -2
- ominfra/supervisor/supervisor.py +2 -2
- ominfra/supervisor/types.py +2 -2
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/RECORD +25 -23
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev144.dist-info → ominfra-0.0.0.dev146.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from .base import CommandExecutor
|
|
8
8
|
CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
|
9
9
|
|
10
10
|
|
11
|
-
class
|
11
|
+
class LocalCommandExecutor(CommandExecutor):
|
12
12
|
def __init__(
|
13
13
|
self,
|
14
14
|
*,
|
@@ -18,8 +18,10 @@ from .base import CommandNameMap
|
|
18
18
|
from .base import CommandRegistration
|
19
19
|
from .base import CommandRegistrations
|
20
20
|
from .base import build_command_name_map
|
21
|
-
from .execution import CommandExecutionService
|
22
21
|
from .execution import CommandExecutorMap
|
22
|
+
from .execution import LocalCommandExecutor
|
23
|
+
from .interp import InterpCommand
|
24
|
+
from .interp import InterpCommandExecutor
|
23
25
|
from .marshal import install_command_marshaling
|
24
26
|
from .subprocess import SubprocessCommand
|
25
27
|
from .subprocess import SubprocessCommandExecutor
|
@@ -106,14 +108,16 @@ def bind_commands(
|
|
106
108
|
lst.extend([
|
107
109
|
inj.bind(provide_command_executor_map, singleton=True),
|
108
110
|
|
109
|
-
inj.bind(
|
110
|
-
inj.bind(CommandExecutor, to_key=CommandExecutionService),
|
111
|
+
inj.bind(LocalCommandExecutor, singleton=True, eager=main_config.debug),
|
111
112
|
])
|
112
113
|
|
113
114
|
#
|
114
115
|
|
116
|
+
command_cls: ta.Any
|
117
|
+
executor_cls: ta.Any
|
115
118
|
for command_cls, executor_cls in [
|
116
119
|
(SubprocessCommand, SubprocessCommandExecutor),
|
120
|
+
(InterpCommand, InterpCommandExecutor),
|
117
121
|
]:
|
118
122
|
lst.append(bind_command(command_cls, executor_cls))
|
119
123
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
import dataclasses as dc
|
3
|
+
|
4
|
+
from omdev.interp.resolvers import DEFAULT_INTERP_RESOLVER
|
5
|
+
from omdev.interp.types import InterpOpts
|
6
|
+
from omdev.interp.types import InterpSpecifier
|
7
|
+
from omlish.lite.check import check_not_none
|
8
|
+
|
9
|
+
from ..commands.base import Command
|
10
|
+
from ..commands.base import CommandExecutor
|
11
|
+
|
12
|
+
|
13
|
+
##
|
14
|
+
|
15
|
+
|
16
|
+
@dc.dataclass(frozen=True)
|
17
|
+
class InterpCommand(Command['InterpCommand.Output']):
|
18
|
+
spec: str
|
19
|
+
install: bool = False
|
20
|
+
|
21
|
+
@dc.dataclass(frozen=True)
|
22
|
+
class Output(Command.Output):
|
23
|
+
exe: str
|
24
|
+
version: str
|
25
|
+
opts: InterpOpts
|
26
|
+
|
27
|
+
|
28
|
+
##
|
29
|
+
|
30
|
+
|
31
|
+
class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
|
32
|
+
def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
|
33
|
+
i = InterpSpecifier.parse(check_not_none(cmd.spec))
|
34
|
+
o = check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
|
35
|
+
return InterpCommand.Output(
|
36
|
+
exe=o.exe,
|
37
|
+
version=str(o.version.version),
|
38
|
+
opts=o.version.opts,
|
39
|
+
)
|
@@ -5,6 +5,7 @@ import subprocess
|
|
5
5
|
import time
|
6
6
|
import typing as ta
|
7
7
|
|
8
|
+
from omlish.lite.check import check_not_isinstance
|
8
9
|
from omlish.lite.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
|
9
10
|
from omlish.lite.subprocesses import SubprocessChannelOption
|
10
11
|
from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
|
@@ -31,8 +32,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
|
|
31
32
|
timeout: ta.Optional[float] = None
|
32
33
|
|
33
34
|
def __post_init__(self) -> None:
|
34
|
-
|
35
|
-
raise TypeError(self.cmd)
|
35
|
+
check_not_isinstance(self.cmd, str)
|
36
36
|
|
37
37
|
@dc.dataclass(frozen=True)
|
38
38
|
class Output(Command.Output):
|
ominfra/manage/deploy/command.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import dataclasses as dc
|
3
3
|
|
4
|
+
from omlish.lite.logs import log
|
5
|
+
|
4
6
|
from ..commands.base import Command
|
5
7
|
from ..commands.base import CommandExecutor
|
6
8
|
|
@@ -20,4 +22,6 @@ class DeployCommand(Command['DeployCommand.Output']):
|
|
20
22
|
|
21
23
|
class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
|
22
24
|
def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
|
25
|
+
log.info('Deploying!')
|
26
|
+
|
23
27
|
return DeployCommand.Output()
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# ruff: noqa: UP006 UP007
|
2
|
+
"""
|
3
|
+
~deploy
|
4
|
+
deploy.pid (flock)
|
5
|
+
/app
|
6
|
+
/<appspec> - shallow clone
|
7
|
+
/conf
|
8
|
+
/env
|
9
|
+
<appspec>.env
|
10
|
+
/nginx
|
11
|
+
<appspec>.conf
|
12
|
+
/supervisor
|
13
|
+
<appspec>.conf
|
14
|
+
/venv
|
15
|
+
/<appspec>
|
16
|
+
|
17
|
+
?
|
18
|
+
/logs
|
19
|
+
/wrmsr--omlish--<spec>
|
20
|
+
|
21
|
+
spec = <name>--<rev>--<when>
|
22
|
+
|
23
|
+
==
|
24
|
+
|
25
|
+
for dn in [
|
26
|
+
'app',
|
27
|
+
'conf',
|
28
|
+
'conf/env',
|
29
|
+
'conf/nginx',
|
30
|
+
'conf/supervisor',
|
31
|
+
'venv',
|
32
|
+
]:
|
33
|
+
"""
|
34
|
+
import abc
|
35
|
+
import dataclasses as dc
|
36
|
+
import os.path
|
37
|
+
import typing as ta
|
38
|
+
|
39
|
+
from omlish.lite.check import check_equal
|
40
|
+
from omlish.lite.check import check_non_empty
|
41
|
+
from omlish.lite.check import check_non_empty_str
|
42
|
+
from omlish.lite.check import check_not_in
|
43
|
+
|
44
|
+
|
45
|
+
DeployPathKind = ta.Literal['dir', 'file'] # ta.TypeAlias
|
46
|
+
|
47
|
+
|
48
|
+
##
|
49
|
+
|
50
|
+
|
51
|
+
DEPLOY_PATH_SPEC_PLACEHOLDER = '@'
|
52
|
+
|
53
|
+
|
54
|
+
@dc.dataclass(frozen=True)
|
55
|
+
class DeployPathPart(abc.ABC): # noqa
|
56
|
+
@property
|
57
|
+
@abc.abstractmethod
|
58
|
+
def kind(self) -> DeployPathKind:
|
59
|
+
raise NotImplementedError
|
60
|
+
|
61
|
+
@abc.abstractmethod
|
62
|
+
def render(self) -> str:
|
63
|
+
raise NotImplementedError
|
64
|
+
|
65
|
+
|
66
|
+
#
|
67
|
+
|
68
|
+
|
69
|
+
class DeployPathDir(DeployPathPart, abc.ABC):
|
70
|
+
@property
|
71
|
+
def kind(self) -> DeployPathKind:
|
72
|
+
return 'dir'
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def parse(cls, s: str) -> 'DeployPathDir':
|
76
|
+
if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
|
77
|
+
check_equal(s, DEPLOY_PATH_SPEC_PLACEHOLDER)
|
78
|
+
return SpecDeployPathDir()
|
79
|
+
else:
|
80
|
+
return ConstDeployPathDir(s)
|
81
|
+
|
82
|
+
|
83
|
+
class DeployPathFile(DeployPathPart, abc.ABC):
|
84
|
+
@property
|
85
|
+
def kind(self) -> DeployPathKind:
|
86
|
+
return 'file'
|
87
|
+
|
88
|
+
@classmethod
|
89
|
+
def parse(cls, s: str) -> 'DeployPathFile':
|
90
|
+
if DEPLOY_PATH_SPEC_PLACEHOLDER in s:
|
91
|
+
check_equal(s[0], DEPLOY_PATH_SPEC_PLACEHOLDER)
|
92
|
+
return SpecDeployPathFile(s[1:])
|
93
|
+
else:
|
94
|
+
return ConstDeployPathFile(s)
|
95
|
+
|
96
|
+
|
97
|
+
#
|
98
|
+
|
99
|
+
|
100
|
+
@dc.dataclass(frozen=True)
|
101
|
+
class ConstDeployPathPart(DeployPathPart, abc.ABC):
|
102
|
+
name: str
|
103
|
+
|
104
|
+
def __post_init__(self) -> None:
|
105
|
+
check_non_empty_str(self.name)
|
106
|
+
check_not_in('/', self.name)
|
107
|
+
check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.name)
|
108
|
+
|
109
|
+
def render(self) -> str:
|
110
|
+
return self.name
|
111
|
+
|
112
|
+
|
113
|
+
class ConstDeployPathDir(ConstDeployPathPart, DeployPathDir):
|
114
|
+
pass
|
115
|
+
|
116
|
+
|
117
|
+
class ConstDeployPathFile(ConstDeployPathPart, DeployPathFile):
|
118
|
+
pass
|
119
|
+
|
120
|
+
|
121
|
+
#
|
122
|
+
|
123
|
+
|
124
|
+
class SpecDeployPathPart(DeployPathPart, abc.ABC):
|
125
|
+
pass
|
126
|
+
|
127
|
+
|
128
|
+
class SpecDeployPathDir(SpecDeployPathPart, DeployPathDir):
|
129
|
+
def render(self) -> str:
|
130
|
+
return DEPLOY_PATH_SPEC_PLACEHOLDER
|
131
|
+
|
132
|
+
|
133
|
+
@dc.dataclass(frozen=True)
|
134
|
+
class SpecDeployPathFile(SpecDeployPathPart, DeployPathFile):
|
135
|
+
suffix: str
|
136
|
+
|
137
|
+
def __post_init__(self) -> None:
|
138
|
+
check_non_empty_str(self.suffix)
|
139
|
+
check_not_in('/', self.suffix)
|
140
|
+
check_not_in(DEPLOY_PATH_SPEC_PLACEHOLDER, self.suffix)
|
141
|
+
|
142
|
+
def render(self) -> str:
|
143
|
+
return DEPLOY_PATH_SPEC_PLACEHOLDER + self.suffix
|
144
|
+
|
145
|
+
|
146
|
+
##
|
147
|
+
|
148
|
+
|
149
|
+
@dc.dataclass(frozen=True)
|
150
|
+
class DeployPath:
|
151
|
+
parts: ta.Sequence[DeployPathPart]
|
152
|
+
|
153
|
+
def __post_init__(self) -> None:
|
154
|
+
check_non_empty(self.parts)
|
155
|
+
for p in self.parts[:-1]:
|
156
|
+
check_equal(p.kind, 'dir')
|
157
|
+
|
158
|
+
@property
|
159
|
+
def kind(self) -> ta.Literal['file', 'dir']:
|
160
|
+
return self.parts[-1].kind
|
161
|
+
|
162
|
+
def render(self) -> str:
|
163
|
+
return os.path.join( # noqa
|
164
|
+
*[p.render() for p in self.parts],
|
165
|
+
*([''] if self.kind == 'dir' else []),
|
166
|
+
)
|
167
|
+
|
168
|
+
@classmethod
|
169
|
+
def parse(cls, s: str) -> 'DeployPath':
|
170
|
+
tail_parse: ta.Callable[[str], DeployPathPart]
|
171
|
+
if s.endswith('/'):
|
172
|
+
tail_parse = DeployPathDir.parse
|
173
|
+
s = s[:-1]
|
174
|
+
else:
|
175
|
+
tail_parse = DeployPathFile.parse
|
176
|
+
ps = check_non_empty_str(s).split('/')
|
177
|
+
return cls([
|
178
|
+
*([DeployPathDir.parse(p) for p in ps[:-1]] if len(ps) > 1 else []),
|
179
|
+
tail_parse(ps[-1]),
|
180
|
+
])
|
ominfra/manage/main.py
CHANGED
@@ -6,6 +6,7 @@ manage.py -s 'docker run -i python:3.12'
|
|
6
6
|
manage.py -s 'ssh -i /foo/bar.pem foo@bar.baz' -q --python=python3.8
|
7
7
|
"""
|
8
8
|
import contextlib
|
9
|
+
import json
|
9
10
|
import typing as ta
|
10
11
|
|
11
12
|
from omlish.lite.logs import log # noqa
|
@@ -17,9 +18,8 @@ from .bootstrap import MainBootstrap
|
|
17
18
|
from .bootstrap_ import main_bootstrap
|
18
19
|
from .commands.base import Command
|
19
20
|
from .commands.base import CommandExecutor
|
20
|
-
from .commands.
|
21
|
+
from .commands.execution import LocalCommandExecutor
|
21
22
|
from .config import MainConfig
|
22
|
-
from .deploy.command import DeployCommand
|
23
23
|
from .remote.config import RemoteConfig
|
24
24
|
from .remote.execution import RemoteExecution
|
25
25
|
from .remote.spawning import RemoteSpawning
|
@@ -77,12 +77,15 @@ def _main() -> None:
|
|
77
77
|
|
78
78
|
#
|
79
79
|
|
80
|
+
msh = injector[ObjMarshalerManager]
|
81
|
+
|
80
82
|
cmds: ta.List[Command] = []
|
83
|
+
cmd: Command
|
81
84
|
for c in args.command:
|
82
|
-
if c
|
83
|
-
|
84
|
-
|
85
|
-
|
85
|
+
if not c.startswith('{'):
|
86
|
+
c = json.dumps({c: {}})
|
87
|
+
cmd = msh.unmarshal_obj(json.loads(c), Command)
|
88
|
+
cmds.append(cmd)
|
86
89
|
|
87
90
|
#
|
88
91
|
|
@@ -90,7 +93,7 @@ def _main() -> None:
|
|
90
93
|
ce: CommandExecutor
|
91
94
|
|
92
95
|
if args.local:
|
93
|
-
ce = injector[
|
96
|
+
ce = injector[LocalCommandExecutor]
|
94
97
|
|
95
98
|
else:
|
96
99
|
tgt = RemoteSpawning.Target(
|
@@ -102,9 +105,13 @@ def _main() -> None:
|
|
102
105
|
ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
|
103
106
|
|
104
107
|
for cmd in cmds:
|
105
|
-
r = ce.try_execute(
|
108
|
+
r = ce.try_execute(
|
109
|
+
cmd,
|
110
|
+
log=log,
|
111
|
+
omit_exc_object=True,
|
112
|
+
)
|
106
113
|
|
107
|
-
print(
|
114
|
+
print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
|
108
115
|
|
109
116
|
|
110
117
|
if __name__ == '__main__':
|
ominfra/manage/remote/channel.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import json
|
3
3
|
import struct
|
4
|
+
import threading
|
4
5
|
import typing as ta
|
5
6
|
|
6
7
|
from omlish.lite.json import json_dumps_compact
|
@@ -25,10 +26,12 @@ class RemoteChannel:
|
|
25
26
|
self._output = output
|
26
27
|
self._msh = msh
|
27
28
|
|
29
|
+
self._lock = threading.RLock()
|
30
|
+
|
28
31
|
def set_marshaler(self, msh: ObjMarshalerManager) -> None:
|
29
32
|
self._msh = msh
|
30
33
|
|
31
|
-
def
|
34
|
+
def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
|
32
35
|
j = json_dumps_compact(self._msh.marshal_obj(o, ty))
|
33
36
|
d = j.encode('utf-8')
|
34
37
|
|
@@ -36,7 +39,11 @@ class RemoteChannel:
|
|
36
39
|
self._output.write(d)
|
37
40
|
self._output.flush()
|
38
41
|
|
39
|
-
def
|
42
|
+
def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
|
43
|
+
with self._lock:
|
44
|
+
return self._send_obj(o, ty)
|
45
|
+
|
46
|
+
def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
|
40
47
|
d = self._input.read(4)
|
41
48
|
if not d:
|
42
49
|
return None
|
@@ -50,3 +57,7 @@ class RemoteChannel:
|
|
50
57
|
|
51
58
|
j = json.loads(d.decode('utf-8'))
|
52
59
|
return self._msh.unmarshal_obj(j, ty)
|
60
|
+
|
61
|
+
def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
|
62
|
+
with self._lock:
|
63
|
+
return self._recv_obj(ty)
|
@@ -2,6 +2,7 @@
|
|
2
2
|
import contextlib
|
3
3
|
import dataclasses as dc
|
4
4
|
import logging
|
5
|
+
import threading
|
5
6
|
import typing as ta
|
6
7
|
|
7
8
|
from omlish.lite.cached import cached_nullary
|
@@ -20,6 +21,7 @@ from ..commands.base import CommandException
|
|
20
21
|
from ..commands.base import CommandExecutor
|
21
22
|
from ..commands.base import CommandOutputOrException
|
22
23
|
from ..commands.base import CommandOutputOrExceptionData
|
24
|
+
from ..commands.execution import LocalCommandExecutor
|
23
25
|
from .channel import RemoteChannel
|
24
26
|
from .payload import RemoteExecutionPayloadFile
|
25
27
|
from .payload import get_remote_payload_src
|
@@ -35,6 +37,32 @@ else:
|
|
35
37
|
##
|
36
38
|
|
37
39
|
|
40
|
+
class _RemoteExecutionLogHandler(logging.Handler):
|
41
|
+
def __init__(self, fn: ta.Callable[[str], None]) -> None:
|
42
|
+
super().__init__()
|
43
|
+
self._fn = fn
|
44
|
+
|
45
|
+
def emit(self, record):
|
46
|
+
msg = self.format(record)
|
47
|
+
self._fn(msg)
|
48
|
+
|
49
|
+
|
50
|
+
@dc.dataclass(frozen=True)
|
51
|
+
class _RemoteExecutionRequest:
|
52
|
+
c: Command
|
53
|
+
|
54
|
+
|
55
|
+
@dc.dataclass(frozen=True)
|
56
|
+
class _RemoteExecutionLog:
|
57
|
+
s: str
|
58
|
+
|
59
|
+
|
60
|
+
@dc.dataclass(frozen=True)
|
61
|
+
class _RemoteExecutionResponse:
|
62
|
+
r: ta.Optional[CommandOutputOrExceptionData] = None
|
63
|
+
l: ta.Optional[_RemoteExecutionLog] = None
|
64
|
+
|
65
|
+
|
38
66
|
def _remote_execution_main() -> None:
|
39
67
|
rt = pyremote_bootstrap_finalize() # noqa
|
40
68
|
|
@@ -52,20 +80,44 @@ def _remote_execution_main() -> None:
|
|
52
80
|
|
53
81
|
chan.set_marshaler(injector[ObjMarshalerManager])
|
54
82
|
|
55
|
-
|
83
|
+
#
|
84
|
+
|
85
|
+
log_lock = threading.RLock()
|
86
|
+
send_logs = False
|
87
|
+
|
88
|
+
def log_fn(s: str) -> None:
|
89
|
+
with log_lock:
|
90
|
+
if send_logs:
|
91
|
+
chan.send_obj(_RemoteExecutionResponse(l=_RemoteExecutionLog(s)))
|
92
|
+
|
93
|
+
log_handler = _RemoteExecutionLogHandler(log_fn)
|
94
|
+
logging.root.addHandler(log_handler)
|
95
|
+
|
96
|
+
#
|
97
|
+
|
98
|
+
ce = injector[LocalCommandExecutor]
|
56
99
|
|
57
100
|
while True:
|
58
|
-
|
59
|
-
if
|
101
|
+
req = chan.recv_obj(_RemoteExecutionRequest)
|
102
|
+
if req is None:
|
60
103
|
break
|
61
104
|
|
105
|
+
with log_lock:
|
106
|
+
send_logs = True
|
107
|
+
|
62
108
|
r = ce.try_execute(
|
63
|
-
|
109
|
+
req.c,
|
64
110
|
log=log,
|
65
111
|
omit_exc_object=True,
|
66
112
|
)
|
67
113
|
|
68
|
-
|
114
|
+
with log_lock:
|
115
|
+
send_logs = False
|
116
|
+
|
117
|
+
chan.send_obj(_RemoteExecutionResponse(r=CommandOutputOrExceptionData(
|
118
|
+
output=r.output,
|
119
|
+
exception=r.exception,
|
120
|
+
)))
|
69
121
|
|
70
122
|
|
71
123
|
##
|
@@ -83,12 +135,17 @@ class RemoteCommandExecutor(CommandExecutor):
|
|
83
135
|
self._chan = chan
|
84
136
|
|
85
137
|
def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
|
86
|
-
self._chan.send_obj(cmd
|
138
|
+
self._chan.send_obj(_RemoteExecutionRequest(cmd))
|
139
|
+
|
140
|
+
while True:
|
141
|
+
if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
|
142
|
+
raise EOFError
|
87
143
|
|
88
|
-
|
89
|
-
|
144
|
+
if r.l is not None:
|
145
|
+
log.info(r.l.s)
|
90
146
|
|
91
|
-
|
147
|
+
if r.r is not None:
|
148
|
+
return r.r
|
92
149
|
|
93
150
|
# @ta.override
|
94
151
|
def execute(self, cmd: Command) -> Command.Output:
|