ominfra 0.0.0.dev147__py3-none-any.whl → 0.0.0.dev148__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,106 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import contextlib
4
+ import typing as ta
5
+
6
+ from omlish.lite.cached import cached_nullary
7
+ from omlish.lite.marshal import ObjMarshalerManager
8
+
9
+ from ...pyremote import PyremoteBootstrapDriver
10
+ from ...pyremote import PyremoteBootstrapOptions
11
+ from ...pyremote import pyremote_build_bootstrap_cmd
12
+ from ..bootstrap import MainBootstrap
13
+ from ._main import _remote_execution_main # noqa
14
+ from .channel import RemoteChannelImpl
15
+ from .execution import RemoteCommandExecutor
16
+ from .payload import RemoteExecutionPayloadFile
17
+ from .payload import get_remote_payload_src
18
+ from .spawning import RemoteSpawning
19
+
20
+
21
+ ##
22
+
23
+
24
+ class RemoteExecutionConnector(abc.ABC):
25
+ @abc.abstractmethod
26
+ def connect(
27
+ self,
28
+ tgt: RemoteSpawning.Target,
29
+ bs: MainBootstrap,
30
+ ) -> ta.AsyncContextManager[RemoteCommandExecutor]:
31
+ raise NotImplementedError
32
+
33
+
34
+ ##
35
+
36
+
37
+ class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
38
+ def __init__(
39
+ self,
40
+ *,
41
+ spawning: RemoteSpawning,
42
+ msh: ObjMarshalerManager,
43
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
44
+ ) -> None:
45
+ super().__init__()
46
+
47
+ self._spawning = spawning
48
+ self._msh = msh
49
+ self._payload_file = payload_file
50
+
51
+ #
52
+
53
+ @cached_nullary
54
+ def _payload_src(self) -> str:
55
+ return get_remote_payload_src(file=self._payload_file)
56
+
57
+ @cached_nullary
58
+ def _remote_src(self) -> ta.Sequence[str]:
59
+ return [
60
+ self._payload_src(),
61
+ '_remote_execution_main()',
62
+ ]
63
+
64
+ @cached_nullary
65
+ def _spawn_src(self) -> str:
66
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
67
+
68
+ #
69
+
70
+ @contextlib.asynccontextmanager
71
+ async def connect(
72
+ self,
73
+ tgt: RemoteSpawning.Target,
74
+ bs: MainBootstrap,
75
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
76
+ spawn_src = self._spawn_src()
77
+ remote_src = self._remote_src()
78
+
79
+ async with self._spawning.spawn(
80
+ tgt,
81
+ spawn_src,
82
+ debug=bs.main_config.debug,
83
+ ) as proc:
84
+ res = await PyremoteBootstrapDriver( # noqa
85
+ remote_src,
86
+ PyremoteBootstrapOptions(
87
+ debug=bs.main_config.debug,
88
+ ),
89
+ ).async_run(
90
+ proc.stdout,
91
+ proc.stdin,
92
+ )
93
+
94
+ chan = RemoteChannelImpl(
95
+ proc.stdout,
96
+ proc.stdin,
97
+ msh=self._msh,
98
+ )
99
+
100
+ await chan.send_obj(bs)
101
+
102
+ rce: RemoteCommandExecutor
103
+ async with contextlib.aclosing(RemoteCommandExecutor(chan)) as rce:
104
+ await rce.start()
105
+
106
+ yield rce
@@ -1,123 +1,182 @@
1
1
  # ruff: noqa: UP006 UP007
2
- import contextlib
2
+ import abc
3
+ import asyncio
3
4
  import dataclasses as dc
5
+ import itertools
4
6
  import logging
5
- import threading
6
7
  import typing as ta
7
8
 
8
- from omlish.lite.cached import cached_nullary
9
+ from omlish.lite.check import check_isinstance
10
+ from omlish.lite.check import check_none
9
11
  from omlish.lite.check import check_not_none
12
+ from omlish.lite.check import check_state
10
13
  from omlish.lite.logs import log
11
- from omlish.lite.marshal import ObjMarshalerManager
12
- from omlish.lite.pycharm import pycharm_debug_connect
13
-
14
- from ...pyremote import PyremoteBootstrapDriver
15
- from ...pyremote import PyremoteBootstrapOptions
16
- from ...pyremote import pyremote_bootstrap_finalize
17
- from ...pyremote import pyremote_build_bootstrap_cmd
18
- from ..bootstrap import MainBootstrap
14
+
19
15
  from ..commands.base import Command
20
16
  from ..commands.base import CommandException
21
17
  from ..commands.base import CommandExecutor
22
18
  from ..commands.base import CommandOutputOrException
23
19
  from ..commands.base import CommandOutputOrExceptionData
24
- from ..commands.execution import LocalCommandExecutor
25
20
  from .channel import RemoteChannel
26
- from .payload import RemoteExecutionPayloadFile
27
- from .payload import get_remote_payload_src
28
- from .spawning import RemoteSpawning
29
21
 
30
22
 
31
- if ta.TYPE_CHECKING:
32
- from ..bootstrap_ import main_bootstrap
33
- else:
34
- main_bootstrap: ta.Any = None
23
+ T = ta.TypeVar('T')
35
24
 
36
25
 
37
26
  ##
38
27
 
39
28
 
40
- class _RemoteExecutionLogHandler(logging.Handler):
41
- def __init__(self, fn: ta.Callable[[str], None]) -> None:
42
- super().__init__()
43
- self._fn = fn
29
+ class _RemoteProtocol:
30
+ class Message(abc.ABC): # noqa
31
+ async def send(self, chan: RemoteChannel) -> None:
32
+ await chan.send_obj(self, _RemoteProtocol.Message)
44
33
 
45
- def emit(self, record):
46
- msg = self.format(record)
47
- self._fn(msg)
34
+ @classmethod
35
+ async def recv(cls: ta.Type[T], chan: RemoteChannel) -> ta.Optional[T]:
36
+ return await chan.recv_obj(cls)
48
37
 
38
+ #
49
39
 
50
- @dc.dataclass(frozen=True)
51
- class _RemoteExecutionRequest:
52
- c: Command
40
+ class Request(Message, abc.ABC): # noqa
41
+ pass
53
42
 
43
+ @dc.dataclass(frozen=True)
44
+ class CommandRequest(Request):
45
+ seq: int
46
+ cmd: Command
54
47
 
55
- @dc.dataclass(frozen=True)
56
- class _RemoteExecutionLog:
57
- s: str
48
+ @dc.dataclass(frozen=True)
49
+ class PingRequest(Request):
50
+ time: float
58
51
 
52
+ #
59
53
 
60
- @dc.dataclass(frozen=True)
61
- class _RemoteExecutionResponse:
62
- r: ta.Optional[CommandOutputOrExceptionData] = None
63
- l: ta.Optional[_RemoteExecutionLog] = None
54
+ class Response(Message, abc.ABC): # noqa
55
+ pass
64
56
 
57
+ @dc.dataclass(frozen=True)
58
+ class LogResponse(Response):
59
+ s: str
65
60
 
66
- def _remote_execution_main() -> None:
67
- rt = pyremote_bootstrap_finalize() # noqa
61
+ @dc.dataclass(frozen=True)
62
+ class CommandResponse(Response):
63
+ seq: int
64
+ res: CommandOutputOrExceptionData
68
65
 
69
- chan = RemoteChannel(
70
- rt.input,
71
- rt.output,
72
- )
66
+ @dc.dataclass(frozen=True)
67
+ class PingResponse(Response):
68
+ time: float
73
69
 
74
- bs = check_not_none(chan.recv_obj(MainBootstrap))
75
70
 
76
- if (prd := bs.remote_config.pycharm_remote_debug) is not None:
77
- pycharm_debug_connect(prd)
71
+ ##
78
72
 
79
- injector = main_bootstrap(bs)
80
73
 
81
- chan.set_marshaler(injector[ObjMarshalerManager])
74
+ class _RemoteLogHandler(logging.Handler):
75
+ def __init__(
76
+ self,
77
+ chan: RemoteChannel,
78
+ loop: ta.Any = None,
79
+ ) -> None:
80
+ super().__init__()
82
81
 
83
- #
82
+ self._chan = chan
83
+ self._loop = loop
84
84
 
85
- log_lock = threading.RLock()
86
- send_logs = False
85
+ def emit(self, record):
86
+ msg = self.format(record)
87
87
 
88
- def log_fn(s: str) -> None:
89
- with log_lock:
90
- if send_logs:
91
- chan.send_obj(_RemoteExecutionResponse(l=_RemoteExecutionLog(s)))
88
+ async def inner():
89
+ await _RemoteProtocol.LogResponse(msg).send(self._chan)
92
90
 
93
- log_handler = _RemoteExecutionLogHandler(log_fn)
94
- logging.root.addHandler(log_handler)
91
+ loop = self._loop
92
+ if loop is None:
93
+ loop = asyncio.get_running_loop()
94
+ if loop is not None:
95
+ asyncio.run_coroutine_threadsafe(inner(), loop)
95
96
 
96
- #
97
97
 
98
- ce = injector[LocalCommandExecutor]
98
+ ##
99
99
 
100
- while True:
101
- req = chan.recv_obj(_RemoteExecutionRequest)
102
- if req is None:
103
- break
104
100
 
105
- with log_lock:
106
- send_logs = True
101
+ class _RemoteCommandHandler:
102
+ def __init__(
103
+ self,
104
+ chan: RemoteChannel,
105
+ executor: CommandExecutor,
106
+ *,
107
+ stop: ta.Optional[asyncio.Event] = None,
108
+ ) -> None:
109
+ super().__init__()
107
110
 
108
- r = ce.try_execute(
109
- req.c,
111
+ self._chan = chan
112
+ self._executor = executor
113
+ self._stop = stop if stop is not None else asyncio.Event()
114
+
115
+ self._cmds_by_seq: ta.Dict[int, _RemoteCommandHandler._Command] = {}
116
+
117
+ @dc.dataclass(frozen=True)
118
+ class _Command:
119
+ req: _RemoteProtocol.CommandRequest
120
+ fut: asyncio.Future
121
+
122
+ async def run(self) -> None:
123
+ stop_task = asyncio.create_task(self._stop.wait())
124
+ recv_task: ta.Optional[asyncio.Task] = None
125
+
126
+ while not self._stop.is_set():
127
+ if recv_task is None:
128
+ recv_task = asyncio.create_task(_RemoteProtocol.Request.recv(self._chan))
129
+
130
+ done, pending = await asyncio.wait([
131
+ stop_task,
132
+ recv_task,
133
+ ], return_when=asyncio.FIRST_COMPLETED)
134
+
135
+ if recv_task in done:
136
+ msg: ta.Optional[_RemoteProtocol.Message] = check_isinstance(
137
+ recv_task.result(),
138
+ (_RemoteProtocol.Message, type(None)),
139
+ )
140
+ recv_task = None
141
+
142
+ if msg is None:
143
+ break
144
+
145
+ await self._handle_message(msg)
146
+
147
+ async def _handle_message(self, msg: _RemoteProtocol.Message) -> None:
148
+ if isinstance(msg, _RemoteProtocol.PingRequest):
149
+ log.debug('Ping: %r', msg)
150
+ await _RemoteProtocol.PingResponse(
151
+ time=msg.time,
152
+ ).send(self._chan)
153
+
154
+ elif isinstance(msg, _RemoteProtocol.CommandRequest):
155
+ fut = asyncio.create_task(self._handle_command_request(msg))
156
+ self._cmds_by_seq[msg.seq] = _RemoteCommandHandler._Command(
157
+ req=msg,
158
+ fut=fut,
159
+ )
160
+
161
+ else:
162
+ raise TypeError(msg)
163
+
164
+ async def _handle_command_request(self, req: _RemoteProtocol.CommandRequest) -> None:
165
+ res = await self._executor.try_execute(
166
+ req.cmd,
110
167
  log=log,
111
168
  omit_exc_object=True,
112
169
  )
113
170
 
114
- with log_lock:
115
- send_logs = False
171
+ await _RemoteProtocol.CommandResponse(
172
+ seq=req.seq,
173
+ res=CommandOutputOrExceptionData(
174
+ output=res.output,
175
+ exception=res.exception,
176
+ ),
177
+ ).send(self._chan)
116
178
 
117
- chan.send_obj(_RemoteExecutionResponse(r=CommandOutputOrExceptionData(
118
- output=r.output,
119
- exception=r.exception,
120
- )))
179
+ self._cmds_by_seq.pop(req.seq) # noqa
121
180
 
122
181
 
123
182
  ##
@@ -134,29 +193,129 @@ class RemoteCommandExecutor(CommandExecutor):
134
193
 
135
194
  self._chan = chan
136
195
 
137
- def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
138
- self._chan.send_obj(_RemoteExecutionRequest(cmd))
196
+ self._cmd_seq = itertools.count()
197
+ self._queue: asyncio.Queue = asyncio.Queue() # asyncio.Queue[RemoteCommandExecutor._Request]
198
+ self._stop = asyncio.Event()
199
+ self._loop_task: ta.Optional[asyncio.Task] = None
200
+ self._reqs_by_seq: ta.Dict[int, RemoteCommandExecutor._Request] = {}
201
+
202
+ #
203
+
204
+ async def start(self) -> None:
205
+ check_none(self._loop_task)
206
+ check_state(not self._stop.is_set())
207
+ self._loop_task = asyncio.create_task(self._loop())
208
+
209
+ async def aclose(self) -> None:
210
+ self._stop.set()
211
+ if self._loop_task is not None:
212
+ await self._loop_task
139
213
 
140
- while True:
141
- if (r := self._chan.recv_obj(_RemoteExecutionResponse)) is None:
142
- raise EOFError
214
+ #
215
+
216
+ @dc.dataclass(frozen=True)
217
+ class _Request:
218
+ seq: int
219
+ cmd: Command
220
+ fut: asyncio.Future
221
+
222
+ async def _loop(self) -> None:
223
+ log.debug('RemoteCommandExecutor loop start: %r', self)
224
+
225
+ stop_task = asyncio.create_task(self._stop.wait())
226
+ queue_task: ta.Optional[asyncio.Task] = None
227
+ recv_task: ta.Optional[asyncio.Task] = None
228
+
229
+ while not self._stop.is_set():
230
+ if queue_task is None:
231
+ queue_task = asyncio.create_task(self._queue.get())
232
+ if recv_task is None:
233
+ recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
234
+
235
+ done, pending = await asyncio.wait([
236
+ stop_task,
237
+ queue_task,
238
+ recv_task,
239
+ ], return_when=asyncio.FIRST_COMPLETED)
240
+
241
+ if queue_task in done:
242
+ req = check_isinstance(queue_task.result(), RemoteCommandExecutor._Request)
243
+ queue_task = None
244
+ await self._handle_request(req)
245
+
246
+ if recv_task in done:
247
+ msg: ta.Optional[_RemoteProtocol.Message] = check_isinstance(
248
+ recv_task.result(),
249
+ (_RemoteProtocol.Message, type(None)),
250
+ )
251
+ recv_task = None
252
+
253
+ if msg is None:
254
+ log.debug('RemoteCommandExecutor got eof: %r', self)
255
+ break
256
+
257
+ await self._handle_message(msg)
258
+
259
+ log.debug('RemoteCommandExecutor loop stopping: %r', self)
260
+
261
+ for task in [
262
+ stop_task,
263
+ queue_task,
264
+ recv_task,
265
+ ]:
266
+ if task is not None and not task.done():
267
+ task.cancel()
268
+
269
+ for req in self._reqs_by_seq.values():
270
+ req.fut.cancel()
271
+
272
+ log.debug('RemoteCommandExecutor loop exited: %r', self)
273
+
274
+ async def _handle_request(self, req: _Request) -> None:
275
+ self._reqs_by_seq[req.seq] = req
276
+ await _RemoteProtocol.CommandRequest(
277
+ seq=req.seq,
278
+ cmd=req.cmd,
279
+ ).send(self._chan)
280
+
281
+ async def _handle_message(self, msg: _RemoteProtocol.Message) -> None:
282
+ if isinstance(msg, _RemoteProtocol.PingRequest):
283
+ log.debug('Ping: %r', msg)
284
+ await _RemoteProtocol.PingResponse(
285
+ time=msg.time,
286
+ ).send(self._chan)
287
+
288
+ elif isinstance(msg, _RemoteProtocol.LogResponse):
289
+ log.info(msg.s)
290
+
291
+ elif isinstance(msg, _RemoteProtocol.CommandResponse):
292
+ req = self._reqs_by_seq.pop(msg.seq)
293
+ req.fut.set_result(msg.res)
294
+
295
+ else:
296
+ raise TypeError(msg)
143
297
 
144
- if r.l is not None:
145
- log.info(r.l.s)
298
+ #
146
299
 
147
- if r.r is not None:
148
- return r.r
300
+ async def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
301
+ req = RemoteCommandExecutor._Request(
302
+ seq=next(self._cmd_seq),
303
+ cmd=cmd,
304
+ fut=asyncio.Future(),
305
+ )
306
+ await self._queue.put(req)
307
+ return await req.fut
149
308
 
150
309
  # @ta.override
151
- def execute(self, cmd: Command) -> Command.Output:
152
- r = self._remote_execute(cmd)
310
+ async def execute(self, cmd: Command) -> Command.Output:
311
+ r = await self._remote_execute(cmd)
153
312
  if (e := r.exception) is not None:
154
313
  raise RemoteCommandError(e)
155
314
  else:
156
315
  return check_not_none(r.output)
157
316
 
158
317
  # @ta.override
159
- def try_execute(
318
+ async def try_execute(
160
319
  self,
161
320
  cmd: Command,
162
321
  *,
@@ -164,7 +323,7 @@ class RemoteCommandExecutor(CommandExecutor):
164
323
  omit_exc_object: bool = False,
165
324
  ) -> CommandOutputOrException:
166
325
  try:
167
- r = self._remote_execute(cmd)
326
+ r = await self._remote_execute(cmd)
168
327
 
169
328
  except Exception as e: # noqa
170
329
  if log is not None:
@@ -178,73 +337,3 @@ class RemoteCommandExecutor(CommandExecutor):
178
337
 
179
338
  else:
180
339
  return r
181
-
182
-
183
- ##
184
-
185
-
186
- class RemoteExecution:
187
- def __init__(
188
- self,
189
- *,
190
- spawning: RemoteSpawning,
191
- msh: ObjMarshalerManager,
192
- payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
193
- ) -> None:
194
- super().__init__()
195
-
196
- self._spawning = spawning
197
- self._msh = msh
198
- self._payload_file = payload_file
199
-
200
- #
201
-
202
- @cached_nullary
203
- def _payload_src(self) -> str:
204
- return get_remote_payload_src(file=self._payload_file)
205
-
206
- @cached_nullary
207
- def _remote_src(self) -> ta.Sequence[str]:
208
- return [
209
- self._payload_src(),
210
- '_remote_execution_main()',
211
- ]
212
-
213
- @cached_nullary
214
- def _spawn_src(self) -> str:
215
- return pyremote_build_bootstrap_cmd(__package__ or 'manage')
216
-
217
- #
218
-
219
- @contextlib.contextmanager
220
- def connect(
221
- self,
222
- tgt: RemoteSpawning.Target,
223
- bs: MainBootstrap,
224
- ) -> ta.Generator[RemoteCommandExecutor, None, None]:
225
- spawn_src = self._spawn_src()
226
- remote_src = self._remote_src()
227
-
228
- with self._spawning.spawn(
229
- tgt,
230
- spawn_src,
231
- ) as proc:
232
- res = PyremoteBootstrapDriver( # noqa
233
- remote_src,
234
- PyremoteBootstrapOptions(
235
- debug=bs.main_config.debug,
236
- ),
237
- ).run(
238
- proc.stdout,
239
- proc.stdin,
240
- )
241
-
242
- chan = RemoteChannel(
243
- proc.stdout,
244
- proc.stdin,
245
- msh=self._msh,
246
- )
247
-
248
- chan.send_obj(bs)
249
-
250
- yield RemoteCommandExecutor(chan)
@@ -6,9 +6,11 @@ from omlish.lite.inject import InjectorBindings
6
6
  from omlish.lite.inject import inj
7
7
 
8
8
  from .config import RemoteConfig
9
- from .execution import RemoteExecution
9
+ from .connection import PyremoteRemoteExecutionConnector
10
+ from .connection import RemoteExecutionConnector
10
11
  from .payload import RemoteExecutionPayloadFile
11
12
  from .spawning import RemoteSpawning
13
+ from .spawning import SubprocessRemoteSpawning
12
14
 
13
15
 
14
16
  def bind_remote(
@@ -18,9 +20,11 @@ def bind_remote(
18
20
  lst: ta.List[InjectorBindingOrBindings] = [
19
21
  inj.bind(remote_config),
20
22
 
21
- inj.bind(RemoteSpawning, singleton=True),
23
+ inj.bind(SubprocessRemoteSpawning, singleton=True),
24
+ inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
22
25
 
23
- inj.bind(RemoteExecution, singleton=True),
26
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
27
+ inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
24
28
  ]
25
29
 
26
30
  if (pf := remote_config.payload_file) is not None: