ominfra 0.0.0.dev151__py3-none-any.whl → 0.0.0.dev153__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/scripts/manage.py CHANGED
@@ -68,7 +68,7 @@ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalTy
68
68
  VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
69
69
  VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
70
70
 
71
- # ../../omlish/lite/asyncio/asyncio.py
71
+ # ../../omlish/asyncs/asyncio/timeouts.py
72
72
  AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
73
73
 
74
74
  # ../../omlish/lite/cached.py
@@ -103,7 +103,10 @@ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
103
103
  InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
104
104
 
105
105
  # ../../omlish/lite/subprocesses.py
106
- SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
106
+ SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
107
+
108
+ # system/packages.py
109
+ SystemPackageOrStr = ta.Union['SystemPackage', str]
107
110
 
108
111
 
109
112
  ########################################
@@ -524,6 +527,22 @@ class MainConfig:
524
527
  debug: bool = False
525
528
 
526
529
 
530
+ ########################################
531
+ # ../system/config.py
532
+
533
+
534
+ @dc.dataclass(frozen=True)
535
+ class SystemConfig:
536
+ platform: ta.Optional[str] = None
537
+
538
+
539
+ ########################################
540
+ # ../system/types.py
541
+
542
+
543
+ SystemPlatform = ta.NewType('SystemPlatform', str)
544
+
545
+
527
546
  ########################################
528
547
  # ../../pyremote.py
529
548
  """
@@ -1044,10 +1063,47 @@ class PyremoteBootstrapDriver:
1044
1063
 
1045
1064
 
1046
1065
  ########################################
1047
- # ../../../omlish/lite/asyncio/asyncio.py
1066
+ # ../../../omlish/asyncs/asyncio/channels.py
1048
1067
 
1049
1068
 
1050
- ##
1069
+ class AsyncioBytesChannelTransport(asyncio.Transport):
1070
+ def __init__(self, reader: asyncio.StreamReader) -> None:
1071
+ super().__init__()
1072
+
1073
+ self.reader = reader
1074
+ self.closed: asyncio.Future = asyncio.Future()
1075
+
1076
+ # @ta.override
1077
+ def write(self, data: bytes) -> None:
1078
+ self.reader.feed_data(data)
1079
+
1080
+ # @ta.override
1081
+ def close(self) -> None:
1082
+ self.reader.feed_eof()
1083
+ if not self.closed.done():
1084
+ self.closed.set_result(True)
1085
+
1086
+ # @ta.override
1087
+ def is_closing(self) -> bool:
1088
+ return self.closed.done()
1089
+
1090
+
1091
+ def asyncio_create_bytes_channel(
1092
+ loop: ta.Any = None,
1093
+ ) -> ta.Tuple[asyncio.StreamReader, asyncio.StreamWriter]:
1094
+ if loop is None:
1095
+ loop = asyncio.get_running_loop()
1096
+
1097
+ reader = asyncio.StreamReader()
1098
+ protocol = asyncio.StreamReaderProtocol(reader)
1099
+ transport = AsyncioBytesChannelTransport(reader)
1100
+ writer = asyncio.StreamWriter(transport, protocol, reader, loop)
1101
+
1102
+ return reader, writer
1103
+
1104
+
1105
+ ########################################
1106
+ # ../../../omlish/asyncs/asyncio/streams.py
1051
1107
 
1052
1108
 
1053
1109
  ASYNCIO_DEFAULT_BUFFER_LIMIT = 2 ** 16
@@ -1091,7 +1147,8 @@ async def asyncio_open_stream_writer(
1091
1147
  )
1092
1148
 
1093
1149
 
1094
- ##
1150
+ ########################################
1151
+ # ../../../omlish/asyncs/asyncio/timeouts.py
1095
1152
 
1096
1153
 
1097
1154
  def asyncio_maybe_timeout(
@@ -1611,30 +1668,6 @@ class Checks:
1611
1668
  check = Checks()
1612
1669
 
1613
1670
 
1614
- ########################################
1615
- # ../../../omlish/lite/deathsig.py
1616
-
1617
-
1618
- LINUX_PR_SET_PDEATHSIG = 1 # Second arg is a signal
1619
- LINUX_PR_GET_PDEATHSIG = 2 # Second arg is a ptr to return the signal
1620
-
1621
-
1622
- def set_process_deathsig(sig: int) -> bool:
1623
- if sys.platform == 'linux':
1624
- libc = ct.CDLL('libc.so.6')
1625
-
1626
- # int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
1627
- libc.prctl.restype = ct.c_int
1628
- libc.prctl.argtypes = [ct.c_int, ct.c_ulong, ct.c_ulong, ct.c_ulong, ct.c_ulong]
1629
-
1630
- libc.prctl(LINUX_PR_SET_PDEATHSIG, sig, 0, 0, 0, 0)
1631
-
1632
- return True
1633
-
1634
- else:
1635
- return False
1636
-
1637
-
1638
1671
  ########################################
1639
1672
  # ../../../omlish/lite/json.py
1640
1673
 
@@ -1880,6 +1913,30 @@ def format_num_bytes(num_bytes: int) -> str:
1880
1913
  return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
1881
1914
 
1882
1915
 
1916
+ ########################################
1917
+ # ../../../omlish/os/deathsig.py
1918
+
1919
+
1920
+ LINUX_PR_SET_PDEATHSIG = 1 # Second arg is a signal
1921
+ LINUX_PR_GET_PDEATHSIG = 2 # Second arg is a ptr to return the signal
1922
+
1923
+
1924
+ def set_process_deathsig(sig: int) -> bool:
1925
+ if sys.platform == 'linux':
1926
+ libc = ct.CDLL('libc.so.6')
1927
+
1928
+ # int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
1929
+ libc.prctl.restype = ct.c_int
1930
+ libc.prctl.argtypes = [ct.c_int, ct.c_ulong, ct.c_ulong, ct.c_ulong, ct.c_ulong]
1931
+
1932
+ libc.prctl(LINUX_PR_SET_PDEATHSIG, sig, 0, 0, 0, 0)
1933
+
1934
+ return True
1935
+
1936
+ else:
1937
+ return False
1938
+
1939
+
1883
1940
  ########################################
1884
1941
  # ../../../omdev/packaging/specifiers.py
1885
1942
  # Copyright (c) Donald Stufft and individual contributors.
@@ -2572,6 +2629,8 @@ class RemoteConfig:
2572
2629
 
2573
2630
  heartbeat_interval_s: float = 3.
2574
2631
 
2632
+ use_in_process_remote_executor: bool = False
2633
+
2575
2634
 
2576
2635
  ########################################
2577
2636
  # ../remote/payload.py
@@ -2618,6 +2677,8 @@ def get_remote_payload_src(
2618
2677
  TODO:
2619
2678
  - default command
2620
2679
  - auto match all underscores to hyphens
2680
+ - pre-run, post-run hooks
2681
+ - exitstack?
2621
2682
  """
2622
2683
 
2623
2684
 
@@ -2737,11 +2798,12 @@ class ArgparseCli:
2737
2798
 
2738
2799
  self._args, self._unknown_args = self.get_parser().parse_known_args(self._argv)
2739
2800
 
2801
+ #
2802
+
2740
2803
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
2741
2804
  super().__init_subclass__(**kwargs)
2742
2805
 
2743
2806
  ns = cls.__dict__
2744
-
2745
2807
  objs = {}
2746
2808
  mro = cls.__mro__[::-1]
2747
2809
  for bns in [bcls.__dict__ for bcls in reversed(mro)] + [ns]:
@@ -2754,24 +2816,33 @@ class ArgparseCli:
2754
2816
  elif k in objs:
2755
2817
  del [k]
2756
2818
 
2819
+ #
2820
+
2757
2821
  anns = ta.get_type_hints(_ArgparseCliAnnotationBox({
2758
2822
  **{k: v for bcls in reversed(mro) for k, v in getattr(bcls, '__annotations__', {}).items()},
2759
2823
  **ns.get('__annotations__', {}),
2760
2824
  }), globalns=ns.get('__globals__', {}))
2761
2825
 
2826
+ #
2827
+
2762
2828
  if '_parser' in ns:
2763
2829
  parser = check.isinstance(ns['_parser'], argparse.ArgumentParser)
2764
2830
  else:
2765
2831
  parser = argparse.ArgumentParser()
2766
2832
  setattr(cls, '_parser', parser)
2767
2833
 
2834
+ #
2835
+
2768
2836
  subparsers = parser.add_subparsers()
2837
+
2769
2838
  for att, obj in objs.items():
2770
2839
  if isinstance(obj, ArgparseCommand):
2771
2840
  if obj.parent is not None:
2772
2841
  raise NotImplementedError
2842
+
2773
2843
  for cn in [obj.name, *(obj.aliases or [])]:
2774
- cparser = subparsers.add_parser(cn)
2844
+ subparser = subparsers.add_parser(cn)
2845
+
2775
2846
  for arg in (obj.args or []):
2776
2847
  if (
2777
2848
  len(arg.args) == 1 and
@@ -2779,29 +2850,34 @@ class ArgparseCli:
2779
2850
  not (n := check.isinstance(arg.args[0], str)).startswith('-') and
2780
2851
  'metavar' not in arg.kwargs
2781
2852
  ):
2782
- cparser.add_argument(
2853
+ subparser.add_argument(
2783
2854
  n.replace('-', '_'),
2784
2855
  **arg.kwargs,
2785
2856
  metavar=n,
2786
2857
  )
2787
2858
  else:
2788
- cparser.add_argument(*arg.args, **arg.kwargs)
2789
- cparser.set_defaults(_cmd=obj)
2859
+ subparser.add_argument(*arg.args, **arg.kwargs)
2860
+
2861
+ subparser.set_defaults(_cmd=obj)
2790
2862
 
2791
2863
  elif isinstance(obj, ArgparseArg):
2792
2864
  if att in anns:
2793
- akwargs = _get_argparse_arg_ann_kwargs(anns[att])
2794
- obj.kwargs = {**akwargs, **obj.kwargs}
2865
+ ann_kwargs = _get_argparse_arg_ann_kwargs(anns[att])
2866
+ obj.kwargs = {**ann_kwargs, **obj.kwargs}
2867
+
2795
2868
  if not obj.dest:
2796
2869
  if 'dest' in obj.kwargs:
2797
2870
  obj.dest = obj.kwargs['dest']
2798
2871
  else:
2799
2872
  obj.dest = obj.kwargs['dest'] = att # type: ignore
2873
+
2800
2874
  parser.add_argument(*obj.args, **obj.kwargs)
2801
2875
 
2802
2876
  else:
2803
2877
  raise TypeError(obj)
2804
2878
 
2879
+ #
2880
+
2805
2881
  _parser: ta.ClassVar[argparse.ArgumentParser]
2806
2882
 
2807
2883
  @classmethod
@@ -2820,10 +2896,12 @@ class ArgparseCli:
2820
2896
  def unknown_args(self) -> ta.Sequence[str]:
2821
2897
  return self._unknown_args
2822
2898
 
2823
- def _run_cmd(self, cmd: ArgparseCommand) -> ta.Optional[int]:
2824
- return cmd.__get__(self, type(self))()
2899
+ #
2825
2900
 
2826
- def __call__(self) -> ta.Optional[int]:
2901
+ def _bind_cli_cmd(self, cmd: ArgparseCommand) -> ta.Callable:
2902
+ return cmd.__get__(self, type(self))
2903
+
2904
+ def prepare_cli_run(self) -> ta.Optional[ta.Callable]:
2827
2905
  cmd = getattr(self.args, '_cmd', None)
2828
2906
 
2829
2907
  if self._unknown_args and not (cmd is not None and cmd.accepts_unknown):
@@ -2835,12 +2913,34 @@ class ArgparseCli:
2835
2913
 
2836
2914
  if cmd is None:
2837
2915
  self.get_parser().print_help()
2916
+ return None
2917
+
2918
+ return self._bind_cli_cmd(cmd)
2919
+
2920
+ #
2921
+
2922
+ def cli_run(self) -> ta.Optional[int]:
2923
+ if (fn := self.prepare_cli_run()) is None:
2838
2924
  return 0
2839
2925
 
2840
- return self._run_cmd(cmd)
2926
+ return fn()
2927
+
2928
+ def cli_run_and_exit(self) -> ta.NoReturn:
2929
+ sys.exit(rc if isinstance(rc := self.cli_run(), int) else 0)
2930
+
2931
+ def __call__(self, *, exit: bool = False) -> ta.Optional[int]: # noqa
2932
+ if exit:
2933
+ return self.cli_run_and_exit()
2934
+ else:
2935
+ return self.cli_run()
2936
+
2937
+ #
2938
+
2939
+ async def async_cli_run(self) -> ta.Optional[int]:
2940
+ if (fn := self.prepare_cli_run()) is None:
2941
+ return 0
2841
2942
 
2842
- def call_and_exit(self) -> ta.NoReturn:
2843
- sys.exit(rc if isinstance(rc := self(), int) else 0)
2943
+ return await fn()
2844
2944
 
2845
2945
 
2846
2946
  ########################################
@@ -4565,6 +4665,8 @@ class MainBootstrap:
4565
4665
 
4566
4666
  remote_config: RemoteConfig = RemoteConfig()
4567
4667
 
4668
+ system_config: SystemConfig = SystemConfig()
4669
+
4568
4670
 
4569
4671
  ########################################
4570
4672
  # ../commands/execution.py
@@ -4614,7 +4716,7 @@ def install_command_marshaling(
4614
4716
 
4615
4717
 
4616
4718
  ########################################
4617
- # ../deploy/command.py
4719
+ # ../deploy/commands.py
4618
4720
 
4619
4721
 
4620
4722
  ##
@@ -4627,9 +4729,6 @@ class DeployCommand(Command['DeployCommand.Output']):
4627
4729
  pass
4628
4730
 
4629
4731
 
4630
- ##
4631
-
4632
-
4633
4732
  class DeployCommandExecutor(CommandExecutor[DeployCommand, DeployCommand.Output]):
4634
4733
  async def execute(self, cmd: DeployCommand) -> DeployCommand.Output:
4635
4734
  log.info('Deploying!')
@@ -4728,6 +4827,27 @@ class RemoteChannelImpl(RemoteChannel):
4728
4827
  return await self._recv_obj(ty)
4729
4828
 
4730
4829
 
4830
+ ########################################
4831
+ # ../system/commands.py
4832
+
4833
+
4834
+ ##
4835
+
4836
+
4837
+ @dc.dataclass(frozen=True)
4838
+ class CheckSystemPackageCommand(Command['CheckSystemPackageCommand.Output']):
4839
+ @dc.dataclass(frozen=True)
4840
+ class Output(Command.Output):
4841
+ pass
4842
+
4843
+
4844
+ class CheckSystemPackageCommandExecutor(CommandExecutor[CheckSystemPackageCommand, CheckSystemPackageCommand.Output]):
4845
+ async def execute(self, cmd: CheckSystemPackageCommand) -> CheckSystemPackageCommand.Output:
4846
+ log.info('Checking system package!')
4847
+
4848
+ return CheckSystemPackageCommand.Output()
4849
+
4850
+
4731
4851
  ########################################
4732
4852
  # ../../../omlish/lite/subprocesses.py
4733
4853
 
@@ -4914,6 +5034,10 @@ def subprocess_close(
4914
5034
 
4915
5035
  ########################################
4916
5036
  # ../remote/execution.py
5037
+ """
5038
+ TODO:
5039
+ - sequence all messages
5040
+ """
4917
5041
 
4918
5042
 
4919
5043
  ##
@@ -4992,38 +5116,80 @@ class _RemoteLogHandler(logging.Handler):
4992
5116
 
4993
5117
 
4994
5118
  class _RemoteCommandHandler:
5119
+ DEFAULT_PING_INTERVAL_S: float = 3.
5120
+
4995
5121
  def __init__(
4996
5122
  self,
4997
5123
  chan: RemoteChannel,
4998
5124
  executor: CommandExecutor,
4999
5125
  *,
5000
5126
  stop: ta.Optional[asyncio.Event] = None,
5127
+ ping_interval_s: float = DEFAULT_PING_INTERVAL_S,
5001
5128
  ) -> None:
5002
5129
  super().__init__()
5003
5130
 
5004
5131
  self._chan = chan
5005
5132
  self._executor = executor
5006
5133
  self._stop = stop if stop is not None else asyncio.Event()
5134
+ self._ping_interval_s = ping_interval_s
5007
5135
 
5008
5136
  self._cmds_by_seq: ta.Dict[int, _RemoteCommandHandler._Command] = {}
5009
5137
 
5138
+ self._last_ping_send: float = 0.
5139
+ self._ping_in_flight: bool = False
5140
+ self._last_ping_recv: ta.Optional[float] = None
5141
+
5142
+ def stop(self) -> None:
5143
+ self._stop.set()
5144
+
5010
5145
  @dc.dataclass(frozen=True)
5011
5146
  class _Command:
5012
5147
  req: _RemoteProtocol.CommandRequest
5013
5148
  fut: asyncio.Future
5014
5149
 
5015
5150
  async def run(self) -> None:
5151
+ log.debug('_RemoteCommandHandler loop start: %r', self)
5152
+
5016
5153
  stop_task = asyncio.create_task(self._stop.wait())
5017
5154
  recv_task: ta.Optional[asyncio.Task] = None
5018
5155
 
5019
5156
  while not self._stop.is_set():
5020
5157
  if recv_task is None:
5021
- recv_task = asyncio.create_task(_RemoteProtocol.Request.recv(self._chan))
5158
+ recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
5159
+
5160
+ if not self._ping_in_flight:
5161
+ if not self._last_ping_recv:
5162
+ ping_wait_time = 0.
5163
+ else:
5164
+ ping_wait_time = self._ping_interval_s - (time.time() - self._last_ping_recv)
5165
+ else:
5166
+ ping_wait_time = float('inf')
5167
+ wait_time = min(self._ping_interval_s, ping_wait_time)
5168
+ log.debug('_RemoteCommandHandler loop wait: %f', wait_time)
5169
+
5170
+ done, pending = await asyncio.wait(
5171
+ [
5172
+ stop_task,
5173
+ recv_task,
5174
+ ],
5175
+ return_when=asyncio.FIRST_COMPLETED,
5176
+ timeout=wait_time,
5177
+ )
5022
5178
 
5023
- done, pending = await asyncio.wait([
5024
- stop_task,
5025
- recv_task,
5026
- ], return_when=asyncio.FIRST_COMPLETED)
5179
+ #
5180
+
5181
+ if (
5182
+ (time.time() - self._last_ping_send >= self._ping_interval_s) and
5183
+ not self._ping_in_flight
5184
+ ):
5185
+ now = time.time()
5186
+ self._last_ping_send = now
5187
+ self._ping_in_flight = True
5188
+ await _RemoteProtocol.PingRequest(
5189
+ time=now,
5190
+ ).send(self._chan)
5191
+
5192
+ #
5027
5193
 
5028
5194
  if recv_task in done:
5029
5195
  msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
@@ -5037,6 +5203,20 @@ class _RemoteCommandHandler:
5037
5203
 
5038
5204
  await self._handle_message(msg)
5039
5205
 
5206
+ log.debug('_RemoteCommandHandler loop stopping: %r', self)
5207
+
5208
+ for task in [
5209
+ stop_task,
5210
+ recv_task,
5211
+ ]:
5212
+ if task is not None and not task.done():
5213
+ task.cancel()
5214
+
5215
+ for cmd in self._cmds_by_seq.values():
5216
+ cmd.fut.cancel()
5217
+
5218
+ log.debug('_RemoteCommandHandler loop exited: %r', self)
5219
+
5040
5220
  async def _handle_message(self, msg: _RemoteProtocol.Message) -> None:
5041
5221
  if isinstance(msg, _RemoteProtocol.PingRequest):
5042
5222
  log.debug('Ping: %r', msg)
@@ -5044,6 +5224,12 @@ class _RemoteCommandHandler:
5044
5224
  time=msg.time,
5045
5225
  ).send(self._chan)
5046
5226
 
5227
+ elif isinstance(msg, _RemoteProtocol.PingResponse):
5228
+ latency_s = time.time() - msg.time
5229
+ log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
5230
+ self._last_ping_recv = time.time()
5231
+ self._ping_in_flight = False
5232
+
5047
5233
  elif isinstance(msg, _RemoteProtocol.CommandRequest):
5048
5234
  fut = asyncio.create_task(self._handle_command_request(msg))
5049
5235
  self._cmds_by_seq[msg.seq] = _RemoteCommandHandler._Command(
@@ -5125,16 +5311,23 @@ class RemoteCommandExecutor(CommandExecutor):
5125
5311
  if recv_task is None:
5126
5312
  recv_task = asyncio.create_task(_RemoteProtocol.Message.recv(self._chan))
5127
5313
 
5128
- done, pending = await asyncio.wait([
5129
- stop_task,
5130
- queue_task,
5131
- recv_task,
5132
- ], return_when=asyncio.FIRST_COMPLETED)
5314
+ done, pending = await asyncio.wait(
5315
+ [
5316
+ stop_task,
5317
+ queue_task,
5318
+ recv_task,
5319
+ ],
5320
+ return_when=asyncio.FIRST_COMPLETED,
5321
+ )
5322
+
5323
+ #
5133
5324
 
5134
5325
  if queue_task in done:
5135
5326
  req = check.isinstance(queue_task.result(), RemoteCommandExecutor._Request)
5136
5327
  queue_task = None
5137
- await self._handle_request(req)
5328
+ await self._handle_queued_request(req)
5329
+
5330
+ #
5138
5331
 
5139
5332
  if recv_task in done:
5140
5333
  msg: ta.Optional[_RemoteProtocol.Message] = check.isinstance(
@@ -5164,7 +5357,7 @@ class RemoteCommandExecutor(CommandExecutor):
5164
5357
 
5165
5358
  log.debug('RemoteCommandExecutor loop exited: %r', self)
5166
5359
 
5167
- async def _handle_request(self, req: _Request) -> None:
5360
+ async def _handle_queued_request(self, req: _Request) -> None:
5168
5361
  self._reqs_by_seq[req.seq] = req
5169
5362
  await _RemoteProtocol.CommandRequest(
5170
5363
  seq=req.seq,
@@ -5178,6 +5371,10 @@ class RemoteCommandExecutor(CommandExecutor):
5178
5371
  time=msg.time,
5179
5372
  ).send(self._chan)
5180
5373
 
5374
+ elif isinstance(msg, _RemoteProtocol.PingResponse):
5375
+ latency_s = time.time() - msg.time
5376
+ log.debug('Pong: %0.2f ms %r', latency_s * 1000., msg)
5377
+
5181
5378
  elif isinstance(msg, _RemoteProtocol.LogResponse):
5182
5379
  log.info(msg.s)
5183
5380
 
@@ -5394,22 +5591,25 @@ async def asyncio_subprocess_communicate(
5394
5591
  return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
5395
5592
 
5396
5593
 
5397
- ##
5398
-
5399
-
5400
- async def _asyncio_subprocess_check_run(
5594
+ async def asyncio_subprocess_run(
5401
5595
  *args: str,
5402
5596
  input: ta.Any = None, # noqa
5403
5597
  timeout: ta.Optional[float] = None,
5598
+ check: bool = False, # noqa
5599
+ capture_output: ta.Optional[bool] = None,
5404
5600
  **kwargs: ta.Any,
5405
5601
  ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
5602
+ if capture_output:
5603
+ kwargs.setdefault('stdout', subprocess.PIPE)
5604
+ kwargs.setdefault('stderr', subprocess.PIPE)
5605
+
5406
5606
  args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
5407
5607
 
5408
5608
  proc: asyncio.subprocess.Process
5409
5609
  async with asyncio_subprocess_popen(*args, **kwargs) as proc:
5410
5610
  stdout, stderr = await asyncio_subprocess_communicate(proc, input, timeout)
5411
5611
 
5412
- if proc.returncode:
5612
+ if check and proc.returncode:
5413
5613
  raise subprocess.CalledProcessError(
5414
5614
  proc.returncode,
5415
5615
  args,
@@ -5420,6 +5620,9 @@ async def _asyncio_subprocess_check_run(
5420
5620
  return stdout, stderr
5421
5621
 
5422
5622
 
5623
+ ##
5624
+
5625
+
5423
5626
  async def asyncio_subprocess_check_call(
5424
5627
  *args: str,
5425
5628
  stdout: ta.Any = sys.stderr,
@@ -5427,11 +5630,12 @@ async def asyncio_subprocess_check_call(
5427
5630
  timeout: ta.Optional[float] = None,
5428
5631
  **kwargs: ta.Any,
5429
5632
  ) -> None:
5430
- _, _ = await _asyncio_subprocess_check_run(
5633
+ _, _ = await asyncio_subprocess_run(
5431
5634
  *args,
5432
5635
  stdout=stdout,
5433
5636
  input=input,
5434
5637
  timeout=timeout,
5638
+ check=True,
5435
5639
  **kwargs,
5436
5640
  )
5437
5641
 
@@ -5442,11 +5646,12 @@ async def asyncio_subprocess_check_output(
5442
5646
  timeout: ta.Optional[float] = None,
5443
5647
  **kwargs: ta.Any,
5444
5648
  ) -> bytes:
5445
- stdout, stderr = await _asyncio_subprocess_check_run(
5649
+ stdout, stderr = await asyncio_subprocess_run(
5446
5650
  *args,
5447
5651
  stdout=asyncio.subprocess.PIPE,
5448
5652
  input=input,
5449
5653
  timeout=timeout,
5654
+ check=True,
5450
5655
  **kwargs,
5451
5656
  )
5452
5657
 
@@ -5641,9 +5846,6 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
5641
5846
  stderr: ta.Optional[bytes] = None
5642
5847
 
5643
5848
 
5644
- ##
5645
-
5646
-
5647
5849
  class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
5648
5850
  async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
5649
5851
  proc: asyncio.subprocess.Process
@@ -5925,6 +6127,102 @@ class SubprocessRemoteSpawning(RemoteSpawning):
5925
6127
  pass
5926
6128
 
5927
6129
 
6130
+ ########################################
6131
+ # ../system/packages.py
6132
+ """
6133
+ TODO:
6134
+ - yum/rpm
6135
+ """
6136
+
6137
+
6138
+ @dc.dataclass(frozen=True)
6139
+ class SystemPackage:
6140
+ name: str
6141
+ version: ta.Optional[str] = None
6142
+
6143
+
6144
+ class SystemPackageManager(abc.ABC):
6145
+ @abc.abstractmethod
6146
+ def update(self) -> ta.Awaitable[None]:
6147
+ raise NotImplementedError
6148
+
6149
+ @abc.abstractmethod
6150
+ def upgrade(self) -> ta.Awaitable[None]:
6151
+ raise NotImplementedError
6152
+
6153
+ @abc.abstractmethod
6154
+ def install(self, *packages: SystemPackageOrStr) -> ta.Awaitable[None]:
6155
+ raise NotImplementedError
6156
+
6157
+ @abc.abstractmethod
6158
+ def query(self, *packages: SystemPackageOrStr) -> ta.Awaitable[ta.Mapping[str, SystemPackage]]:
6159
+ raise NotImplementedError
6160
+
6161
+
6162
+ class BrewSystemPackageManager(SystemPackageManager):
6163
+ async def update(self) -> None:
6164
+ await asyncio_subprocess_check_call('brew', 'update')
6165
+
6166
+ async def upgrade(self) -> None:
6167
+ await asyncio_subprocess_check_call('brew', 'upgrade')
6168
+
6169
+ async def install(self, *packages: SystemPackageOrStr) -> None:
6170
+ es: ta.List[str] = []
6171
+ for p in packages:
6172
+ if isinstance(p, SystemPackage):
6173
+ es.append(p.name + (f'@{p.version}' if p.version is not None else ''))
6174
+ else:
6175
+ es.append(p)
6176
+ await asyncio_subprocess_check_call('brew', 'install', *es)
6177
+
6178
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
6179
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
6180
+ o = await asyncio_subprocess_check_output('brew', 'info', '--json', *pns)
6181
+ j = json.loads(o.decode())
6182
+ d: ta.Dict[str, SystemPackage] = {}
6183
+ for e in j:
6184
+ if not e['installed']:
6185
+ continue
6186
+ d[e['name']] = SystemPackage(
6187
+ name=e['name'],
6188
+ version=e['installed'][0]['version'],
6189
+ )
6190
+ return d
6191
+
6192
+
6193
+ class AptSystemPackageManager(SystemPackageManager):
6194
+ _APT_ENV: ta.ClassVar[ta.Mapping[str, str]] = {
6195
+ 'DEBIAN_FRONTEND': 'noninteractive',
6196
+ }
6197
+
6198
+ async def update(self) -> None:
6199
+ await asyncio_subprocess_check_call('apt', 'update', env={**os.environ, **self._APT_ENV})
6200
+
6201
+ async def upgrade(self) -> None:
6202
+ await asyncio_subprocess_check_call('apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
6203
+
6204
+ async def install(self, *packages: SystemPackageOrStr) -> None:
6205
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
6206
+ await asyncio_subprocess_check_call('apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
6207
+
6208
+ async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
6209
+ pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
6210
+ cmd = ['dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns]
6211
+ stdout, stderr = await asyncio_subprocess_run(
6212
+ *cmd,
6213
+ capture_output=True,
6214
+ check=False,
6215
+ )
6216
+ d: ta.Dict[str, SystemPackage] = {}
6217
+ for l in check.not_none(stdout).decode('utf-8').strip().splitlines():
6218
+ n, v = l.split('=', 1)
6219
+ d[n] = SystemPackage(
6220
+ name=n,
6221
+ version=v,
6222
+ )
6223
+ return d
6224
+
6225
+
5928
6226
  ########################################
5929
6227
  # ../../../omdev/interp/providers.py
5930
6228
  """
@@ -6078,6 +6376,50 @@ class PyremoteRemoteExecutionConnector(RemoteExecutionConnector):
6078
6376
  yield rce
6079
6377
 
6080
6378
 
6379
+ ##
6380
+
6381
+
6382
+ class InProcessRemoteExecutionConnector(RemoteExecutionConnector):
6383
+ def __init__(
6384
+ self,
6385
+ *,
6386
+ msh: ObjMarshalerManager,
6387
+ local_executor: LocalCommandExecutor,
6388
+ ) -> None:
6389
+ super().__init__()
6390
+
6391
+ self._msh = msh
6392
+ self._local_executor = local_executor
6393
+
6394
+ @contextlib.asynccontextmanager
6395
+ async def connect(
6396
+ self,
6397
+ tgt: RemoteSpawning.Target,
6398
+ bs: MainBootstrap,
6399
+ ) -> ta.AsyncGenerator[RemoteCommandExecutor, None]:
6400
+ r0, w0 = asyncio_create_bytes_channel()
6401
+ r1, w1 = asyncio_create_bytes_channel()
6402
+
6403
+ remote_chan = RemoteChannelImpl(r0, w1, msh=self._msh)
6404
+ local_chan = RemoteChannelImpl(r1, w0, msh=self._msh)
6405
+
6406
+ rch = _RemoteCommandHandler(
6407
+ remote_chan,
6408
+ self._local_executor,
6409
+ )
6410
+ rch_task = asyncio.create_task(rch.run()) # noqa
6411
+ try:
6412
+ rce: RemoteCommandExecutor
6413
+ async with contextlib.aclosing(RemoteCommandExecutor(local_chan)) as rce:
6414
+ await rce.start()
6415
+
6416
+ yield rce
6417
+
6418
+ finally:
6419
+ rch.stop()
6420
+ await rch_task
6421
+
6422
+
6081
6423
  ########################################
6082
6424
  # ../../../omdev/interp/pyenv.py
6083
6425
  """
@@ -6648,13 +6990,27 @@ def bind_remote(
6648
6990
 
6649
6991
  inj.bind(SubprocessRemoteSpawning, singleton=True),
6650
6992
  inj.bind(RemoteSpawning, to_key=SubprocessRemoteSpawning),
6651
-
6652
- inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
6653
- inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
6654
6993
  ]
6655
6994
 
6995
+ #
6996
+
6997
+ if remote_config.use_in_process_remote_executor:
6998
+ lst.extend([
6999
+ inj.bind(InProcessRemoteExecutionConnector, singleton=True),
7000
+ inj.bind(RemoteExecutionConnector, to_key=InProcessRemoteExecutionConnector),
7001
+ ])
7002
+ else:
7003
+ lst.extend([
7004
+ inj.bind(PyremoteRemoteExecutionConnector, singleton=True),
7005
+ inj.bind(RemoteExecutionConnector, to_key=PyremoteRemoteExecutionConnector),
7006
+ ])
7007
+
7008
+ #
7009
+
6656
7010
  if (pf := remote_config.payload_file) is not None:
6657
- lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
7011
+ lst.append(inj.bind(pf, key=RemoteExecutionPayloadFile))
7012
+
7013
+ #
6658
7014
 
6659
7015
  return inj.as_bindings(*lst)
6660
7016
 
@@ -6776,9 +7132,6 @@ class InterpCommand(Command['InterpCommand.Output']):
6776
7132
  opts: InterpOpts
6777
7133
 
6778
7134
 
6779
- ##
6780
-
6781
-
6782
7135
  class InterpCommandExecutor(CommandExecutor[InterpCommand, InterpCommand.Output]):
6783
7136
  async def execute(self, cmd: InterpCommand) -> InterpCommand.Output:
6784
7137
  i = InterpSpecifier.parse(check.not_none(cmd.spec))
@@ -6906,6 +7259,48 @@ def bind_deploy(
6906
7259
  return inj.as_bindings(*lst)
6907
7260
 
6908
7261
 
7262
+ ########################################
7263
+ # ../system/inject.py
7264
+
7265
+
7266
+ def bind_system(
7267
+ *,
7268
+ system_config: SystemConfig,
7269
+ ) -> InjectorBindings:
7270
+ lst: ta.List[InjectorBindingOrBindings] = [
7271
+ inj.bind(system_config),
7272
+ ]
7273
+
7274
+ #
7275
+
7276
+ platform = system_config.platform or sys.platform
7277
+ lst.append(inj.bind(platform, key=SystemPlatform))
7278
+
7279
+ #
7280
+
7281
+ if platform == 'linux':
7282
+ lst.extend([
7283
+ inj.bind(AptSystemPackageManager, singleton=True),
7284
+ inj.bind(SystemPackageManager, to_key=AptSystemPackageManager),
7285
+ ])
7286
+
7287
+ elif platform == 'darwin':
7288
+ lst.extend([
7289
+ inj.bind(BrewSystemPackageManager, singleton=True),
7290
+ inj.bind(SystemPackageManager, to_key=BrewSystemPackageManager),
7291
+ ])
7292
+
7293
+ #
7294
+
7295
+ lst.extend([
7296
+ bind_command(CheckSystemPackageCommand, CheckSystemPackageCommandExecutor),
7297
+ ])
7298
+
7299
+ #
7300
+
7301
+ return inj.as_bindings(*lst)
7302
+
7303
+
6909
7304
  ########################################
6910
7305
  # ../inject.py
6911
7306
 
@@ -6917,6 +7312,7 @@ def bind_main(
6917
7312
  *,
6918
7313
  main_config: MainConfig,
6919
7314
  remote_config: RemoteConfig,
7315
+ system_config: SystemConfig,
6920
7316
  ) -> InjectorBindings:
6921
7317
  lst: ta.List[InjectorBindingOrBindings] = [
6922
7318
  inj.bind(main_config),
@@ -6925,11 +7321,15 @@ def bind_main(
6925
7321
  main_config=main_config,
6926
7322
  ),
6927
7323
 
7324
+ bind_deploy(),
7325
+
6928
7326
  bind_remote(
6929
7327
  remote_config=remote_config,
6930
7328
  ),
6931
7329
 
6932
- bind_deploy(),
7330
+ bind_system(
7331
+ system_config=system_config,
7332
+ ),
6933
7333
  ]
6934
7334
 
6935
7335
  #
@@ -6964,6 +7364,7 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
6964
7364
  injector = inj.create_injector(bind_main( # noqa
6965
7365
  main_config=bs.main_config,
6966
7366
  remote_config=bs.remote_config,
7367
+ system_config=bs.system_config,
6967
7368
  ))
6968
7369
 
6969
7370
  return injector
@@ -6975,7 +7376,7 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
6975
7376
 
6976
7377
  class MainCli(ArgparseCli):
6977
7378
  @argparse_command(
6978
- argparse_arg('--payload-file'),
7379
+ argparse_arg('--_payload-file'),
6979
7380
 
6980
7381
  argparse_arg('-s', '--shell'),
6981
7382
  argparse_arg('-q', '--shell-quote', action='store_true'),
@@ -6993,10 +7394,7 @@ class MainCli(ArgparseCli):
6993
7394
 
6994
7395
  argparse_arg('command', nargs='+'),
6995
7396
  )
6996
- def run(self) -> None:
6997
- asyncio.run(self._async_run())
6998
-
6999
- async def _async_run(self) -> None:
7397
+ async def run(self) -> None:
7000
7398
  bs = MainBootstrap(
7001
7399
  main_config=MainConfig(
7002
7400
  log_level='DEBUG' if self.args.debug else 'INFO',
@@ -7005,7 +7403,7 @@ class MainCli(ArgparseCli):
7005
7403
  ),
7006
7404
 
7007
7405
  remote_config=RemoteConfig(
7008
- payload_file=self.args.payload_file, # noqa
7406
+ payload_file=self.args._payload_file, # noqa
7009
7407
 
7010
7408
  pycharm_remote_debug=PycharmRemoteDebug(
7011
7409
  port=self.args.pycharm_debug_port,
@@ -7014,6 +7412,8 @@ class MainCli(ArgparseCli):
7014
7412
  ) if self.args.pycharm_debug_port is not None else None,
7015
7413
 
7016
7414
  timebomb_delay_s=self.args.remote_timebomb_delay_s,
7415
+
7416
+ use_in_process_remote_executor=True,
7017
7417
  ),
7018
7418
  )
7019
7419
 
@@ -7068,7 +7468,7 @@ class MainCli(ArgparseCli):
7068
7468
 
7069
7469
 
7070
7470
  def _main() -> None:
7071
- MainCli().call_and_exit()
7471
+ sys.exit(asyncio.run(MainCli().async_cli_run()))
7072
7472
 
7073
7473
 
7074
7474
  if __name__ == '__main__':