ominfra 0.0.0.dev142__py3-none-any.whl → 0.0.0.dev143__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,193 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import contextlib
3
+ import dataclasses as dc
4
+ import logging
5
+ import typing as ta
6
+
7
+ from omlish.lite.cached import cached_nullary
8
+ from omlish.lite.check import check_not_none
9
+ from omlish.lite.logs import log
10
+ from omlish.lite.marshal import ObjMarshalerManager
11
+ from omlish.lite.pycharm import pycharm_debug_connect
12
+
13
+ from ...pyremote import PyremoteBootstrapDriver
14
+ from ...pyremote import PyremoteBootstrapOptions
15
+ from ...pyremote import pyremote_bootstrap_finalize
16
+ from ...pyremote import pyremote_build_bootstrap_cmd
17
+ from ..bootstrap import MainBootstrap
18
+ from ..commands.base import Command
19
+ from ..commands.base import CommandException
20
+ from ..commands.base import CommandExecutor
21
+ from ..commands.base import CommandOutputOrException
22
+ from ..commands.base import CommandOutputOrExceptionData
23
+ from .channel import RemoteChannel
24
+ from .payload import RemoteExecutionPayloadFile
25
+ from .payload import get_remote_payload_src
26
+ from .spawning import RemoteSpawning
27
+
28
+
29
+ if ta.TYPE_CHECKING:
30
+ from ..bootstrap_ import main_bootstrap
31
+ else:
32
+ main_bootstrap: ta.Any = None
33
+
34
+
35
+ ##
36
+
37
+
38
+ def _remote_execution_main() -> None:
39
+ rt = pyremote_bootstrap_finalize() # noqa
40
+
41
+ chan = RemoteChannel(
42
+ rt.input,
43
+ rt.output,
44
+ )
45
+
46
+ bs = check_not_none(chan.recv_obj(MainBootstrap))
47
+
48
+ if (prd := bs.remote_config.pycharm_remote_debug) is not None:
49
+ pycharm_debug_connect(prd)
50
+
51
+ injector = main_bootstrap(bs)
52
+
53
+ chan.set_marshaler(injector[ObjMarshalerManager])
54
+
55
+ ce = injector[CommandExecutor]
56
+
57
+ while True:
58
+ i = chan.recv_obj(Command)
59
+ if i is None:
60
+ break
61
+
62
+ r = ce.try_execute(
63
+ i,
64
+ log=log,
65
+ omit_exc_object=True,
66
+ )
67
+
68
+ chan.send_obj(r)
69
+
70
+
71
+ ##
72
+
73
+
74
+ @dc.dataclass()
75
+ class RemoteCommandError(Exception):
76
+ e: CommandException
77
+
78
+
79
+ class RemoteCommandExecutor(CommandExecutor):
80
+ def __init__(self, chan: RemoteChannel) -> None:
81
+ super().__init__()
82
+
83
+ self._chan = chan
84
+
85
+ def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
86
+ self._chan.send_obj(cmd, Command)
87
+
88
+ if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
89
+ raise EOFError
90
+
91
+ return r
92
+
93
+ # @ta.override
94
+ def execute(self, cmd: Command) -> Command.Output:
95
+ r = self._remote_execute(cmd)
96
+ if (e := r.exception) is not None:
97
+ raise RemoteCommandError(e)
98
+ else:
99
+ return check_not_none(r.output)
100
+
101
+ # @ta.override
102
+ def try_execute(
103
+ self,
104
+ cmd: Command,
105
+ *,
106
+ log: ta.Optional[logging.Logger] = None,
107
+ omit_exc_object: bool = False,
108
+ ) -> CommandOutputOrException:
109
+ try:
110
+ r = self._remote_execute(cmd)
111
+
112
+ except Exception as e: # noqa
113
+ if log is not None:
114
+ log.exception('Exception executing remote command: %r', type(cmd))
115
+
116
+ return CommandOutputOrExceptionData(exception=CommandException.of(
117
+ e,
118
+ omit_exc_object=omit_exc_object,
119
+ cmd=cmd,
120
+ ))
121
+
122
+ else:
123
+ return r
124
+
125
+
126
+ ##
127
+
128
+
129
+ class RemoteExecution:
130
+ def __init__(
131
+ self,
132
+ *,
133
+ spawning: RemoteSpawning,
134
+ msh: ObjMarshalerManager,
135
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
136
+ ) -> None:
137
+ super().__init__()
138
+
139
+ self._spawning = spawning
140
+ self._msh = msh
141
+ self._payload_file = payload_file
142
+
143
+ #
144
+
145
+ @cached_nullary
146
+ def _payload_src(self) -> str:
147
+ return get_remote_payload_src(file=self._payload_file)
148
+
149
+ @cached_nullary
150
+ def _remote_src(self) -> ta.Sequence[str]:
151
+ return [
152
+ self._payload_src(),
153
+ '_remote_execution_main()',
154
+ ]
155
+
156
+ @cached_nullary
157
+ def _spawn_src(self) -> str:
158
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
159
+
160
+ #
161
+
162
+ @contextlib.contextmanager
163
+ def connect(
164
+ self,
165
+ tgt: RemoteSpawning.Target,
166
+ bs: MainBootstrap,
167
+ ) -> ta.Generator[RemoteCommandExecutor, None, None]:
168
+ spawn_src = self._spawn_src()
169
+ remote_src = self._remote_src()
170
+
171
+ with self._spawning.spawn(
172
+ tgt,
173
+ spawn_src,
174
+ ) as proc:
175
+ res = PyremoteBootstrapDriver( # noqa
176
+ remote_src,
177
+ PyremoteBootstrapOptions(
178
+ debug=bs.main_config.debug,
179
+ ),
180
+ ).run(
181
+ proc.stdout,
182
+ proc.stdin,
183
+ )
184
+
185
+ chan = RemoteChannel(
186
+ proc.stdout,
187
+ proc.stdin,
188
+ msh=self._msh,
189
+ )
190
+
191
+ chan.send_obj(bs)
192
+
193
+ yield RemoteCommandExecutor(chan)
@@ -0,0 +1,29 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.inject import InjectorBindingOrBindings
5
+ from omlish.lite.inject import InjectorBindings
6
+ from omlish.lite.inject import inj
7
+
8
+ from .config import RemoteConfig
9
+ from .execution import RemoteExecution
10
+ from .payload import RemoteExecutionPayloadFile
11
+ from .spawning import RemoteSpawning
12
+
13
+
14
+ def bind_remote(
15
+ *,
16
+ remote_config: RemoteConfig,
17
+ ) -> InjectorBindings:
18
+ lst: ta.List[InjectorBindingOrBindings] = [
19
+ inj.bind(remote_config),
20
+
21
+ inj.bind(RemoteSpawning, singleton=True),
22
+
23
+ inj.bind(RemoteExecution, singleton=True),
24
+ ]
25
+
26
+ if (pf := remote_config.payload_file) is not None:
27
+ lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
28
+
29
+ return inj.as_bindings(*lst)
@@ -6,6 +6,9 @@ import typing as ta
6
6
  from omlish.lite.cached import cached_nullary
7
7
 
8
8
 
9
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
10
+
11
+
9
12
  @cached_nullary
10
13
  def _get_self_src() -> str:
11
14
  return inspect.getsource(sys.modules[__name__])
@@ -23,7 +26,10 @@ def _is_self_amalg() -> bool:
23
26
  return _is_src_amalg(_get_self_src())
24
27
 
25
28
 
26
- def get_payload_src(*, file: ta.Optional[str]) -> str:
29
+ def get_remote_payload_src(
30
+ *,
31
+ file: ta.Optional[RemoteExecutionPayloadFile],
32
+ ) -> str:
27
33
  if file is not None:
28
34
  with open(file) as f:
29
35
  return f.read()
@@ -11,25 +11,16 @@ from omlish.lite.subprocesses import SubprocessChannelOption
11
11
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
12
12
 
13
13
 
14
- class PySpawner:
15
- DEFAULT_PYTHON = 'python3'
14
+ class RemoteSpawning:
15
+ @dc.dataclass(frozen=True)
16
+ class Target:
17
+ shell: ta.Optional[str] = None
18
+ shell_quote: bool = False
16
19
 
17
- def __init__(
18
- self,
19
- src: str,
20
- *,
21
- shell: ta.Optional[str] = None,
22
- shell_quote: bool = False,
23
- python: str = DEFAULT_PYTHON,
24
- stderr: ta.Optional[SubprocessChannelOption] = None,
25
- ) -> None:
26
- super().__init__()
27
-
28
- self._src = src
29
- self._shell = shell
30
- self._shell_quote = shell_quote
31
- self._python = python
32
- self._stderr = stderr
20
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
21
+ python: str = DEFAULT_PYTHON
22
+
23
+ stderr: ta.Optional[str] = None # SubprocessChannelOption
33
24
 
34
25
  #
35
26
 
@@ -37,20 +28,24 @@ class PySpawner:
37
28
  cmd: ta.Sequence[str]
38
29
  shell: bool
39
30
 
40
- def _prepare_cmd(self) -> _PreparedCmd:
41
- if self._shell is not None:
42
- sh_src = f'{self._python} -c {shlex.quote(self._src)}'
43
- if self._shell_quote:
31
+ def _prepare_cmd(
32
+ self,
33
+ tgt: Target,
34
+ src: str,
35
+ ) -> _PreparedCmd:
36
+ if tgt.shell is not None:
37
+ sh_src = f'{tgt.python} -c {shlex.quote(src)}'
38
+ if tgt.shell_quote:
44
39
  sh_src = shlex.quote(sh_src)
45
- sh_cmd = f'{self._shell} {sh_src}'
46
- return PySpawner._PreparedCmd(
40
+ sh_cmd = f'{tgt.shell} {sh_src}'
41
+ return RemoteSpawning._PreparedCmd(
47
42
  cmd=[sh_cmd],
48
43
  shell=True,
49
44
  )
50
45
 
51
46
  else:
52
- return PySpawner._PreparedCmd(
53
- cmd=[self._python, '-c', self._src],
47
+ return RemoteSpawning._PreparedCmd(
48
+ cmd=[tgt.python, '-c', src],
54
49
  shell=False,
55
50
  )
56
51
 
@@ -65,23 +60,28 @@ class PySpawner:
65
60
  @contextlib.contextmanager
66
61
  def spawn(
67
62
  self,
63
+ tgt: Target,
64
+ src: str,
68
65
  *,
69
66
  timeout: ta.Optional[float] = None,
70
67
  ) -> ta.Generator[Spawned, None, None]:
71
- pc = self._prepare_cmd()
68
+ pc = self._prepare_cmd(tgt, src)
72
69
 
73
70
  with subprocess.Popen(
74
71
  subprocess_maybe_shell_wrap_exec(*pc.cmd),
75
72
  shell=pc.shell,
76
73
  stdin=subprocess.PIPE,
77
74
  stdout=subprocess.PIPE,
78
- stderr=SUBPROCESS_CHANNEL_OPTION_VALUES[self._stderr] if self._stderr is not None else None,
75
+ stderr=(
76
+ SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, tgt.stderr)]
77
+ if tgt.stderr is not None else None
78
+ ),
79
79
  ) as proc:
80
80
  stdin = check_not_none(proc.stdin)
81
81
  stdout = check_not_none(proc.stdout)
82
82
 
83
83
  try:
84
- yield PySpawner.Spawned(
84
+ yield RemoteSpawning.Spawned(
85
85
  stdin=stdin,
86
86
  stdout=stdout,
87
87
  stderr=proc.stderr,
ominfra/pyremote.py CHANGED
@@ -2,6 +2,9 @@
2
2
  # @omlish-lite
3
3
  """
4
4
  Basically this: https://mitogen.networkgenomics.com/howitworks.html
5
+
6
+ TODO:
7
+ - log: ta.Optional[logging.Logger] = None + log.debug's
5
8
  """
6
9
  import base64
7
10
  import dataclasses as dc
@@ -23,6 +26,9 @@ import zlib
23
26
  class PyremoteBootstrapOptions:
24
27
  debug: bool = False
25
28
 
29
+ DEFAULT_MAIN_NAME_OVERRIDE: ta.ClassVar[str] = '__pyremote__'
30
+ main_name_override: ta.Optional[str] = DEFAULT_MAIN_NAME_OVERRIDE
31
+
26
32
 
27
33
  ##
28
34
 
@@ -339,6 +345,10 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
339
345
  os.dup2(nfd := os.open('/dev/null', os.O_WRONLY), 1)
340
346
  os.close(nfd)
341
347
 
348
+ if (mn := options.main_name_override) is not None:
349
+ # Inspections like typing.get_type_hints need an entry in sys.modules.
350
+ sys.modules[mn] = sys.modules['__main__']
351
+
342
352
  # Write fourth ack
343
353
  output.write(_PYREMOTE_BOOTSTRAP_ACK3)
344
354
 
@@ -357,14 +367,41 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
357
367
 
358
368
 
359
369
  class PyremoteBootstrapDriver:
360
- def __init__(self, main_src: str, options: PyremoteBootstrapOptions = PyremoteBootstrapOptions()) -> None:
370
+ def __init__(
371
+ self,
372
+ main_src: ta.Union[str, ta.Sequence[str]],
373
+ options: PyremoteBootstrapOptions = PyremoteBootstrapOptions(),
374
+ ) -> None:
361
375
  super().__init__()
362
376
 
363
377
  self._main_src = main_src
364
- self._main_z = zlib.compress(main_src.encode('utf-8'))
365
-
366
378
  self._options = options
379
+
380
+ self._prepared_main_src = self._prepare_main_src(main_src, options)
381
+ self._main_z = zlib.compress(self._prepared_main_src.encode('utf-8'))
382
+
367
383
  self._options_json = json.dumps(dc.asdict(options), indent=None, separators=(',', ':')).encode('utf-8') # noqa
384
+ #
385
+
386
+ @classmethod
387
+ def _prepare_main_src(
388
+ cls,
389
+ main_src: ta.Union[str, ta.Sequence[str]],
390
+ options: PyremoteBootstrapOptions,
391
+ ) -> str:
392
+ parts: ta.List[str]
393
+ if isinstance(main_src, str):
394
+ parts = [main_src]
395
+ else:
396
+ parts = list(main_src)
397
+
398
+ if (mn := options.main_name_override) is not None:
399
+ parts.insert(0, f'__name__ = {mn!r}')
400
+
401
+ if len(parts) == 1:
402
+ return parts[0]
403
+ else:
404
+ return '\n\n'.join(parts)
368
405
 
369
406
  #
370
407