ominfra 0.0.0.dev151__py3-none-any.whl → 0.0.0.dev153__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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__':