ominfra 0.0.0.dev147__py3-none-any.whl → 0.0.0.dev148__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.
@@ -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: