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.
ominfra/configs.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  # @omlish-lite
3
3
  import json
4
+ import os.path
4
5
  import typing as ta
5
6
 
6
7
  from omdev.toml.parser import toml_loads
@@ -14,6 +15,33 @@ T = ta.TypeVar('T')
14
15
  ConfigMapping = ta.Mapping[str, ta.Any]
15
16
 
16
17
 
18
+ def parse_config_file(
19
+ name: str,
20
+ f: ta.TextIO,
21
+ ) -> ConfigMapping:
22
+ if name.endswith('.toml'):
23
+ return toml_loads(f.read())
24
+
25
+ elif any(name.endswith(e) for e in ('.yml', '.yaml')):
26
+ yaml = __import__('yaml')
27
+ return yaml.safe_load(f)
28
+
29
+ elif name.endswith('.ini'):
30
+ import configparser
31
+ cp = configparser.ConfigParser()
32
+ cp.read_file(f)
33
+ config_dct: ta.Dict[str, ta.Any] = {}
34
+ for sec in cp.sections():
35
+ cd = config_dct
36
+ for k in sec.split('.'):
37
+ cd = cd.setdefault(k, {})
38
+ cd.update(cp.items(sec))
39
+ return config_dct
40
+
41
+ else:
42
+ return json.loads(f.read())
43
+
44
+
17
45
  def read_config_file(
18
46
  path: str,
19
47
  cls: ta.Type[T],
@@ -21,13 +49,10 @@ def read_config_file(
21
49
  prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
22
50
  ) -> T:
23
51
  with open(path) as cf:
24
- if path.endswith('.toml'):
25
- config_dct = toml_loads(cf.read())
26
- else:
27
- config_dct = json.loads(cf.read())
52
+ config_dct = parse_config_file(os.path.basename(path), cf)
28
53
 
29
54
  if prepare is not None:
30
- config_dct = prepare(config_dct) # type: ignore
55
+ config_dct = prepare(config_dct)
31
56
 
32
57
  return unmarshal_obj(config_dct, cls)
33
58
 
@@ -23,8 +23,8 @@ class Command(abc.ABC, ta.Generic[CommandOutputT]):
23
23
  pass
24
24
 
25
25
  @ta.final
26
- def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
27
- return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
26
+ async def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
27
+ return check_isinstance(await executor.execute(self), self.Output) # type: ignore[return-value]
28
28
 
29
29
 
30
30
  ##
@@ -85,10 +85,10 @@ class CommandOutputOrExceptionData(CommandOutputOrException):
85
85
 
86
86
  class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
87
87
  @abc.abstractmethod
88
- def execute(self, cmd: CommandT) -> CommandOutputT:
88
+ def execute(self, cmd: CommandT) -> ta.Awaitable[CommandOutputT]:
89
89
  raise NotImplementedError
90
90
 
91
- def try_execute(
91
+ async def try_execute(
92
92
  self,
93
93
  cmd: CommandT,
94
94
  *,
@@ -96,7 +96,7 @@ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
96
96
  omit_exc_object: bool = False,
97
97
  ) -> CommandOutputOrException[CommandOutputT]:
98
98
  try:
99
- o = self.execute(cmd)
99
+ o = await self.execute(cmd)
100
100
 
101
101
  except Exception as e: # noqa
102
102
  if log is not None:
@@ -18,6 +18,6 @@ class LocalCommandExecutor(CommandExecutor):
18
18
 
19
19
  self._command_executors = command_executors
20
20
 
21
- def execute(self, cmd: Command) -> Command.Output:
21
+ async def execute(self, cmd: Command) -> Command.Output:
22
22
  ce: CommandExecutor = self._command_executors[type(cmd)]
23
- return ce.execute(cmd)
23
+ return await ce.execute(cmd)
@@ -54,7 +54,7 @@ def bind_command(
54
54
  class _FactoryCommandExecutor(CommandExecutor):
55
55
  factory: ta.Callable[[], CommandExecutor]
56
56
 
57
- def execute(self, i: Command) -> Command.Output:
57
+ def execute(self, i: Command) -> ta.Awaitable[Command.Output]:
58
58
  return self.factory().execute(i)
59
59
 
60
60
 
@@ -29,9 +29,9 @@ class InterpCommand(Command['InterpCommand.Output']):
29
29
 
30
30
 
31
31
  class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
32
- def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
32
+ async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
33
33
  i = InterpSpecifier.parse(check_not_none(cmd.spec))
34
- o = check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
34
+ o = check_not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=cmd.install))
35
35
  return InterpCommand.Output(
36
36
  exe=o.exe,
37
37
  version=str(o.version.version),
@@ -1,11 +1,15 @@
1
- # ruff: noqa: UP006 UP007
1
+ # ruff: noqa: TC003 UP006 UP007
2
+ import asyncio.subprocess
2
3
  import dataclasses as dc
3
4
  import os
4
5
  import subprocess
5
6
  import time
6
7
  import typing as ta
7
8
 
9
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_communicate
10
+ from omlish.lite.asyncio.subprocesses import asyncio_subprocess_popen
8
11
  from omlish.lite.check import check_not_isinstance
12
+ from omlish.lite.check import check_not_none
9
13
  from omlish.lite.subprocesses import SUBPROCESS_CHANNEL_OPTION_VALUES
10
14
  from omlish.lite.subprocesses import SubprocessChannelOption
11
15
  from omlish.lite.subprocesses import subprocess_maybe_shell_wrap_exec
@@ -49,27 +53,31 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
49
53
 
50
54
 
51
55
  class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
52
- def execute(self, inp: SubprocessCommand) -> SubprocessCommand.Output:
53
- with subprocess.Popen(
54
- subprocess_maybe_shell_wrap_exec(*inp.cmd),
56
+ async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
57
+ proc: asyncio.subprocess.Process
58
+ async with asyncio_subprocess_popen(
59
+ *subprocess_maybe_shell_wrap_exec(*cmd.cmd),
55
60
 
56
- shell=inp.shell,
57
- cwd=inp.cwd,
58
- env={**os.environ, **(inp.env or {})},
61
+ shell=cmd.shell,
62
+ cwd=cmd.cwd,
63
+ env={**os.environ, **(cmd.env or {})},
59
64
 
60
- stdin=subprocess.PIPE if inp.input is not None else None,
61
- stdout=SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, inp.stdout)],
62
- stderr=SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, inp.stderr)],
65
+ stdin=subprocess.PIPE if cmd.input is not None else None,
66
+ stdout=SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, cmd.stdout)],
67
+ stderr=SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, cmd.stderr)],
68
+
69
+ timeout=cmd.timeout,
63
70
  ) as proc:
64
71
  start_time = time.time()
65
- stdout, stderr = proc.communicate(
66
- input=inp.input,
67
- timeout=inp.timeout,
72
+ stdout, stderr = await asyncio_subprocess_communicate(
73
+ proc,
74
+ input=cmd.input,
75
+ timeout=cmd.timeout,
68
76
  )
69
77
  end_time = time.time()
70
78
 
71
79
  return SubprocessCommand.Output(
72
- rc=proc.returncode,
80
+ rc=check_not_none(proc.returncode),
73
81
  pid=proc.pid,
74
82
 
75
83
  elapsed_s=end_time - start_time,
@@ -21,7 +21,7 @@ class DeployCommand(Command['DeployCommand.Output']):
21
21
 
22
22
 
23
23
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
24
- def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
24
+ async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
25
25
  log.info('Deploying!')
26
26
 
27
27
  return DeployCommand.Output()
ominfra/manage/main.py CHANGED
@@ -5,6 +5,7 @@
5
5
  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
+ import asyncio
8
9
  import contextlib
9
10
  import json
10
11
  import typing as ta
@@ -21,38 +22,14 @@ from .commands.base import CommandExecutor
21
22
  from .commands.execution import LocalCommandExecutor
22
23
  from .config import MainConfig
23
24
  from .remote.config import RemoteConfig
24
- from .remote.execution import RemoteExecution
25
+ from .remote.connection import RemoteExecutionConnector
25
26
  from .remote.spawning import RemoteSpawning
26
27
 
27
28
 
28
29
  ##
29
30
 
30
31
 
31
- def _main() -> None:
32
- import argparse
33
-
34
- parser = argparse.ArgumentParser()
35
-
36
- parser.add_argument('--_payload-file')
37
-
38
- parser.add_argument('-s', '--shell')
39
- parser.add_argument('-q', '--shell-quote', action='store_true')
40
- parser.add_argument('--python', default='python3')
41
-
42
- parser.add_argument('--pycharm-debug-port', type=int)
43
- parser.add_argument('--pycharm-debug-host')
44
- parser.add_argument('--pycharm-debug-version')
45
-
46
- parser.add_argument('--debug', action='store_true')
47
-
48
- parser.add_argument('--local', action='store_true')
49
-
50
- parser.add_argument('command', nargs='+')
51
-
52
- args = parser.parse_args()
53
-
54
- #
55
-
32
+ async def _async_main(args: ta.Any) -> None:
56
33
  bs = MainBootstrap(
57
34
  main_config=MainConfig(
58
35
  log_level='DEBUG' if args.debug else 'INFO',
@@ -65,12 +42,16 @@ def _main() -> None:
65
42
 
66
43
  pycharm_remote_debug=PycharmRemoteDebug(
67
44
  port=args.pycharm_debug_port,
68
- host=args.pycharm_debug_host,
45
+ **(dict(host=args.pycharm_debug_host) if args.pycharm_debug_host is not None else {}),
69
46
  install_version=args.pycharm_debug_version,
70
47
  ) if args.pycharm_debug_port is not None else None,
48
+
49
+ timebomb_delay_s=args.remote_timebomb_delay_s,
71
50
  ),
72
51
  )
73
52
 
53
+ #
54
+
74
55
  injector = main_bootstrap(
75
56
  bs,
76
57
  )
@@ -89,7 +70,7 @@ def _main() -> None:
89
70
 
90
71
  #
91
72
 
92
- with contextlib.ExitStack() as es:
73
+ async with contextlib.AsyncExitStack() as es:
93
74
  ce: CommandExecutor
94
75
 
95
76
  if args.local:
@@ -102,16 +83,51 @@ def _main() -> None:
102
83
  python=args.python,
103
84
  )
104
85
 
105
- ce = es.enter_context(injector[RemoteExecution].connect(tgt, bs)) # noqa
86
+ ce = await es.enter_async_context(injector[RemoteExecutionConnector].connect(tgt, bs)) # noqa
106
87
 
107
- for cmd in cmds:
108
- r = ce.try_execute(
88
+ async def run_command(cmd: Command) -> None:
89
+ res = await ce.try_execute(
109
90
  cmd,
110
91
  log=log,
111
92
  omit_exc_object=True,
112
93
  )
113
94
 
114
- print(msh.marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
95
+ print(msh.marshal_obj(res, opts=ObjMarshalOptions(raw_bytes=True)))
96
+
97
+ await asyncio.gather(*[
98
+ run_command(cmd)
99
+ for cmd in cmds
100
+ ])
101
+
102
+
103
+ def _main() -> None:
104
+ import argparse
105
+
106
+ parser = argparse.ArgumentParser()
107
+
108
+ parser.add_argument('--_payload-file')
109
+
110
+ parser.add_argument('-s', '--shell')
111
+ parser.add_argument('-q', '--shell-quote', action='store_true')
112
+ parser.add_argument('--python', default='python3')
113
+
114
+ parser.add_argument('--pycharm-debug-port', type=int)
115
+ parser.add_argument('--pycharm-debug-host')
116
+ parser.add_argument('--pycharm-debug-version')
117
+
118
+ parser.add_argument('--remote-timebomb-delay-s', type=float)
119
+
120
+ parser.add_argument('--debug', action='store_true')
121
+
122
+ parser.add_argument('--local', action='store_true')
123
+
124
+ parser.add_argument('command', nargs='+')
125
+
126
+ args = parser.parse_args()
127
+
128
+ #
129
+
130
+ asyncio.run(_async_main(args))
115
131
 
116
132
 
117
133
  if __name__ == '__main__':
@@ -0,0 +1,172 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import asyncio
3
+ import functools
4
+ import logging
5
+ import os
6
+ import signal
7
+ import threading
8
+ import time
9
+ import typing as ta
10
+
11
+ from omlish.lite.asyncio.asyncio import asyncio_open_stream_reader
12
+ from omlish.lite.asyncio.asyncio import asyncio_open_stream_writer
13
+ from omlish.lite.cached import cached_nullary
14
+ from omlish.lite.check import check_none
15
+ from omlish.lite.check import check_not_none
16
+ from omlish.lite.deathsig import set_process_deathsig
17
+ from omlish.lite.inject import Injector
18
+ from omlish.lite.logs import log
19
+ from omlish.lite.marshal import ObjMarshalerManager
20
+ from omlish.lite.pycharm import pycharm_debug_connect
21
+
22
+ from ...pyremote import pyremote_bootstrap_finalize
23
+ from ..bootstrap import MainBootstrap
24
+ from ..commands.execution import LocalCommandExecutor
25
+ from .channel import RemoteChannel
26
+ from .channel import RemoteChannelImpl
27
+ from .execution import _RemoteCommandHandler
28
+ from .execution import _RemoteLogHandler
29
+
30
+
31
+ if ta.TYPE_CHECKING:
32
+ from ..bootstrap_ import main_bootstrap
33
+ else:
34
+ main_bootstrap: ta.Any = None
35
+
36
+
37
+ ##
38
+
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
+ ##
51
+
52
+
53
+ class _RemoteExecutionMain:
54
+ def __init__(
55
+ self,
56
+ chan: RemoteChannel,
57
+ ) -> None:
58
+ super().__init__()
59
+
60
+ self._chan = chan
61
+
62
+ self.__bootstrap: ta.Optional[MainBootstrap] = None
63
+ self.__injector: ta.Optional[Injector] = None
64
+
65
+ @property
66
+ def _bootstrap(self) -> MainBootstrap:
67
+ return check_not_none(self.__bootstrap)
68
+
69
+ @property
70
+ def _injector(self) -> Injector:
71
+ return check_not_none(self.__injector)
72
+
73
+ #
74
+
75
+ def _timebomb_main(
76
+ self,
77
+ delay_s: float,
78
+ *,
79
+ sig: int = signal.SIGINT,
80
+ code: int = 1,
81
+ ) -> None:
82
+ time.sleep(delay_s)
83
+
84
+ if (pgid := os.getpgid(0)) == os.getpid():
85
+ os.killpg(pgid, sig)
86
+
87
+ os._exit(code) # noqa
88
+
89
+ @cached_nullary
90
+ def _timebomb_thread(self) -> ta.Optional[threading.Thread]:
91
+ if (tbd := self._bootstrap.remote_config.timebomb_delay_s) is None:
92
+ return None
93
+
94
+ thr = threading.Thread(
95
+ target=functools.partial(self._timebomb_main, tbd),
96
+ name=f'{self.__class__.__name__}.timebomb',
97
+ daemon=True,
98
+ )
99
+
100
+ thr.start()
101
+
102
+ log.debug('Started timebomb thread: %r', thr)
103
+
104
+ return thr
105
+
106
+ #
107
+
108
+ @cached_nullary
109
+ def _log_handler(self) -> _RemoteLogHandler:
110
+ return _RemoteLogHandler(self._chan)
111
+
112
+ #
113
+
114
+ async def _setup(self) -> None:
115
+ check_none(self.__bootstrap)
116
+ check_none(self.__injector)
117
+
118
+ # Bootstrap
119
+
120
+ self.__bootstrap = check_not_none(await self._chan.recv_obj(MainBootstrap))
121
+
122
+ if (prd := self._bootstrap.remote_config.pycharm_remote_debug) is not None:
123
+ pycharm_debug_connect(prd)
124
+
125
+ self.__injector = main_bootstrap(self._bootstrap)
126
+
127
+ self._chan.set_marshaler(self._injector[ObjMarshalerManager])
128
+
129
+ # Post-bootstrap
130
+
131
+ if self._bootstrap.remote_config.set_pgid:
132
+ if os.getpgid(0) != os.getpid():
133
+ log.debug('Setting pgid')
134
+ os.setpgid(0, 0)
135
+
136
+ if (ds := self._bootstrap.remote_config.deathsig) is not None:
137
+ log.debug('Setting deathsig: %s', ds)
138
+ set_process_deathsig(int(signal.Signals[f'SIG{ds.upper()}']))
139
+
140
+ self._timebomb_thread()
141
+
142
+ if self._bootstrap.remote_config.forward_logging:
143
+ log.debug('Installing log forwarder')
144
+ logging.root.addHandler(self._log_handler())
145
+
146
+ #
147
+
148
+ async def run(self) -> None:
149
+ await self._setup()
150
+
151
+ executor = self._injector[LocalCommandExecutor]
152
+
153
+ handler = _RemoteCommandHandler(self._chan, executor)
154
+
155
+ await handler.run()
156
+
157
+
158
+ def _remote_execution_main() -> None:
159
+ rt = pyremote_bootstrap_finalize() # noqa
160
+
161
+ async def inner() -> None:
162
+ input = await asyncio_open_stream_reader(rt.input) # noqa
163
+ output = await asyncio_open_stream_writer(rt.output)
164
+
165
+ chan = RemoteChannelImpl(
166
+ input,
167
+ output,
168
+ )
169
+
170
+ await _RemoteExecutionMain(chan).run()
171
+
172
+ asyncio.run(inner())
@@ -1,7 +1,8 @@
1
1
  # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import asyncio
2
4
  import json
3
5
  import struct
4
- import threading
5
6
  import typing as ta
6
7
 
7
8
  from omlish.lite.json import json_dumps_compact
@@ -12,11 +13,30 @@ from omlish.lite.marshal import ObjMarshalerManager
12
13
  T = ta.TypeVar('T')
13
14
 
14
15
 
15
- class RemoteChannel:
16
+ ##
17
+
18
+
19
+ class RemoteChannel(abc.ABC):
20
+ @abc.abstractmethod
21
+ def send_obj(self, o: ta.Any, ty: ta.Any = None) -> ta.Awaitable[None]:
22
+ raise NotImplementedError
23
+
24
+ @abc.abstractmethod
25
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Awaitable[ta.Optional[T]]:
26
+ raise NotImplementedError
27
+
28
+ def set_marshaler(self, msh: ObjMarshalerManager) -> None: # noqa
29
+ pass
30
+
31
+
32
+ ##
33
+
34
+
35
+ class RemoteChannelImpl(RemoteChannel):
16
36
  def __init__(
17
37
  self,
18
- input: ta.IO, # noqa
19
- output: ta.IO,
38
+ input: asyncio.StreamReader, # noqa
39
+ output: asyncio.StreamWriter,
20
40
  *,
21
41
  msh: ObjMarshalerManager = OBJ_MARSHALER_MANAGER,
22
42
  ) -> None:
@@ -26,38 +46,43 @@ class RemoteChannel:
26
46
  self._output = output
27
47
  self._msh = msh
28
48
 
29
- self._lock = threading.RLock()
49
+ self._input_lock = asyncio.Lock()
50
+ self._output_lock = asyncio.Lock()
30
51
 
31
52
  def set_marshaler(self, msh: ObjMarshalerManager) -> None:
32
53
  self._msh = msh
33
54
 
34
- def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
55
+ #
56
+
57
+ async def _send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
35
58
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
36
59
  d = j.encode('utf-8')
37
60
 
38
61
  self._output.write(struct.pack('<I', len(d)))
39
62
  self._output.write(d)
40
- self._output.flush()
63
+ await self._output.drain()
64
+
65
+ async def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
66
+ async with self._output_lock:
67
+ return await self._send_obj(o, ty)
41
68
 
42
- def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
43
- with self._lock:
44
- return self._send_obj(o, ty)
69
+ #
45
70
 
46
- def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
47
- d = self._input.read(4)
71
+ async def _recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
72
+ d = await self._input.read(4)
48
73
  if not d:
49
74
  return None
50
75
  if len(d) != 4:
51
76
  raise EOFError
52
77
 
53
78
  sz = struct.unpack('<I', d)[0]
54
- d = self._input.read(sz)
79
+ d = await self._input.read(sz)
55
80
  if len(d) != sz:
56
81
  raise EOFError
57
82
 
58
83
  j = json.loads(d.decode('utf-8'))
59
84
  return self._msh.unmarshal_obj(j, ty)
60
85
 
61
- def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
62
- with self._lock:
63
- return self._recv_obj(ty)
86
+ async def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
87
+ async with self._input_lock:
88
+ return await self._recv_obj(ty)
@@ -9,4 +9,14 @@ from omlish.lite.pycharm import PycharmRemoteDebug
9
9
  class RemoteConfig:
10
10
  payload_file: ta.Optional[str] = None
11
11
 
12
+ set_pgid: bool = True
13
+
14
+ deathsig: ta.Optional[str] = 'KILL'
15
+
12
16
  pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
17
+
18
+ forward_logging: bool = True
19
+
20
+ timebomb_delay_s: ta.Optional[float] = 60 * 60.
21
+
22
+ heartbeat_interval_s: float = 3.