playground-ls-cli 4.14.1.dev8__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.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,514 @@
1
+ import io
2
+ import logging
3
+ import os
4
+ import re
5
+ import select
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ import time
10
+ from collections.abc import Callable
11
+ from functools import lru_cache
12
+ from queue import Queue
13
+ from typing import Any, AnyStr
14
+
15
+ from localstack_cli import config
16
+
17
+ # TODO: remove imports from here (need to update any client code that imports these from utils.common)
18
+ from localstack_cli.utils.platform import is_linux, is_mac_os, is_windows # noqa
19
+
20
+ from .sync import retry
21
+ from .threads import FuncThread, start_worker_thread
22
+
23
+ LOG = logging.getLogger(__name__)
24
+
25
+
26
+ def run(
27
+ cmd: str | list[str],
28
+ print_error=True,
29
+ asynchronous=False,
30
+ stdin=False,
31
+ stderr=subprocess.STDOUT,
32
+ outfile=None,
33
+ env_vars: dict[AnyStr, AnyStr] | None = None,
34
+ inherit_cwd=False,
35
+ inherit_env=True,
36
+ tty=False,
37
+ shell=True,
38
+ cwd: str = None,
39
+ ) -> str | subprocess.Popen:
40
+ LOG.debug("Executing command: %s", cmd)
41
+ env_dict = os.environ.copy() if inherit_env else {}
42
+ if env_vars:
43
+ env_dict.update(env_vars)
44
+ env_dict = {k: to_str(str(v)) for k, v in env_dict.items()}
45
+
46
+ if isinstance(cmd, list):
47
+ # See docs of subprocess.Popen(...):
48
+ # "On POSIX with shell=True, the shell defaults to /bin/sh. If args is a string,
49
+ # the string specifies the command to execute through the shell. [...] If args is
50
+ # a sequence, the first item specifies the command string, and any additional
51
+ # items will be treated as additional arguments to the shell itself."
52
+ # Hence, we should *disable* shell mode here to be on the safe side, to prevent
53
+ # arguments in the cmd list from leaking into arguments to the shell itself. This will
54
+ # effectively allow us to call run(..) with both - str and list - as cmd argument, although
55
+ # over time we should move from "cmd: Union[str, List[str]]" to "cmd: List[str]" only.
56
+ shell = False
57
+
58
+ if tty:
59
+ asynchronous = True
60
+ stdin = True
61
+
62
+ try:
63
+ if inherit_cwd and not cwd:
64
+ cwd = os.getcwd()
65
+ if not asynchronous:
66
+ if stdin:
67
+ return subprocess.check_output(
68
+ cmd, shell=shell, stderr=stderr, env=env_dict, stdin=subprocess.PIPE, cwd=cwd
69
+ )
70
+ output = subprocess.check_output(cmd, shell=shell, stderr=stderr, env=env_dict, cwd=cwd)
71
+ return output.decode(config.DEFAULT_ENCODING)
72
+
73
+ stdin_arg = subprocess.PIPE if stdin else None
74
+ stdout_arg = open(outfile, "ab") if isinstance(outfile, str) else outfile
75
+ stderr_arg = stderr
76
+ if tty:
77
+ # Note: leave the "pty" import here (not supported in Windows)
78
+ import pty
79
+
80
+ master_fd, slave_fd = pty.openpty()
81
+ stdin_arg = slave_fd
82
+ stdout_arg = stderr_arg = None
83
+
84
+ # start the actual sub process
85
+ kwargs = {}
86
+ if is_linux() or is_mac_os():
87
+ kwargs["start_new_session"] = True
88
+ process = subprocess.Popen(
89
+ cmd,
90
+ shell=shell,
91
+ stdin=stdin_arg,
92
+ bufsize=-1,
93
+ stderr=stderr_arg,
94
+ stdout=stdout_arg,
95
+ env=env_dict,
96
+ cwd=cwd,
97
+ **kwargs,
98
+ )
99
+
100
+ if tty:
101
+ # based on: https://stackoverflow.com/questions/41542960
102
+ def pipe_streams(*args):
103
+ while process.poll() is None:
104
+ r, w, e = select.select([sys.stdin, master_fd], [], [])
105
+ if sys.stdin in r:
106
+ d = os.read(sys.stdin.fileno(), 10240)
107
+ os.write(master_fd, d)
108
+ elif master_fd in r:
109
+ o = os.read(master_fd, 10240)
110
+ if o:
111
+ os.write(sys.stdout.fileno(), o)
112
+
113
+ FuncThread(pipe_streams, name="pipe-streams").start()
114
+
115
+ return process
116
+ except subprocess.CalledProcessError as e:
117
+ if print_error:
118
+ print(f"ERROR: '{cmd}': exit code {e.returncode}; output: {e.output}")
119
+ sys.stdout.flush()
120
+ raise e
121
+
122
+
123
+ def run_for_max_seconds(max_secs, _function, *args, **kwargs):
124
+ """Run the given function for a maximum of `max_secs` seconds - continue running
125
+ in a background thread if the function does not finish in time."""
126
+
127
+ def _worker(*_args):
128
+ try:
129
+ fn_result = _function(*args, **kwargs)
130
+ except Exception as e:
131
+ fn_result = e
132
+
133
+ fn_result = True if fn_result is None else fn_result
134
+ q.put(fn_result)
135
+ return fn_result
136
+
137
+ start = time.time()
138
+ q = Queue()
139
+ start_worker_thread(_worker)
140
+ for i in range(max_secs * 2):
141
+ result = None
142
+ try:
143
+ result = q.get_nowait()
144
+ except Exception:
145
+ pass
146
+ if result is not None:
147
+ if isinstance(result, Exception):
148
+ raise result
149
+ return result
150
+ if time.time() - start >= max_secs:
151
+ return
152
+ time.sleep(0.5)
153
+
154
+
155
+ def run_interactive(command: list[str]):
156
+ """
157
+ Run an interactive command in a subprocess. This blocks the current thread and attaches sys.stdin to
158
+ the process. Copied from https://stackoverflow.com/a/43012138/804840
159
+
160
+ :param command: the command to pass to subprocess.Popen
161
+ """
162
+ subprocess.check_call(command)
163
+
164
+
165
+ def is_command_available(cmd: str) -> bool:
166
+ try:
167
+ run(["which", cmd], print_error=False)
168
+ return True
169
+ except Exception:
170
+ return False
171
+
172
+
173
+ def kill_process_tree(parent_pid):
174
+ # Note: Do NOT import "psutil" at the root scope
175
+ import psutil
176
+
177
+ parent_pid = getattr(parent_pid, "pid", None) or parent_pid
178
+ parent = psutil.Process(parent_pid)
179
+ for child in parent.children(recursive=True):
180
+ try:
181
+ child.kill()
182
+ except Exception:
183
+ pass
184
+ parent.kill()
185
+
186
+
187
+ def wait_for_process_to_be_killed(pid: int, sleep: float = None, retries: int = None):
188
+ import psutil
189
+
190
+ def _check_pid():
191
+ assert not psutil.pid_exists(pid)
192
+
193
+ retry(_check_pid, sleep=sleep, retries=retries)
194
+
195
+
196
+ def is_root() -> bool:
197
+ return get_os_user() == "root"
198
+
199
+
200
+ @lru_cache
201
+ def get_os_user() -> str:
202
+ # using getpass.getuser() seems to be reporting a different/invalid user in Docker/macOS
203
+ return run("whoami").strip()
204
+
205
+
206
+ def to_str(obj: str | bytes, errors="strict"):
207
+ return obj.decode(config.DEFAULT_ENCODING, errors) if isinstance(obj, bytes) else obj
208
+
209
+
210
+ class ShellCommandThread(FuncThread):
211
+ """Helper class to run a shell command in a background thread."""
212
+
213
+ def __init__(
214
+ self,
215
+ cmd: str | list[str],
216
+ params: Any = None,
217
+ outfile: str | int = None,
218
+ env_vars: dict[str, str] = None,
219
+ stdin: bool = False,
220
+ auto_restart: bool = False,
221
+ quiet: bool = True,
222
+ inherit_cwd: bool = False,
223
+ inherit_env: bool = True,
224
+ log_listener: Callable = None,
225
+ stop_listener: Callable = None,
226
+ strip_color: bool = False,
227
+ name: str | None = None,
228
+ cwd: str | None = None,
229
+ ):
230
+ params = params if params is not None else {}
231
+ env_vars = env_vars if env_vars is not None else {}
232
+ self.stopped = False
233
+ self.cmd = cmd
234
+ self.process = None
235
+ self.outfile = outfile
236
+ self.stdin = stdin
237
+ self.env_vars = env_vars
238
+ self.inherit_cwd = inherit_cwd
239
+ self.inherit_env = inherit_env
240
+ self.auto_restart = auto_restart
241
+ self.log_listener = log_listener
242
+ self.stop_listener = stop_listener
243
+ self.strip_color = strip_color
244
+ self.started = threading.Event()
245
+ self.cwd = cwd
246
+ FuncThread.__init__(
247
+ self, self.run_cmd, params, quiet=quiet, name=(name or "shell-cmd-thread")
248
+ )
249
+
250
+ def run_cmd(self, params):
251
+ while True:
252
+ self.do_run_cmd()
253
+ try:
254
+ from localstack_cli.runtime import events
255
+ infra_stopping = events.infra_stopping.is_set()
256
+ except ImportError:
257
+ infra_stopping = False
258
+
259
+ if (
260
+ infra_stopping # FIXME: this is the wrong level of abstraction
261
+ or not self.auto_restart
262
+ or not self.process
263
+ or self.process.returncode == 0
264
+ ):
265
+ return self.process.returncode if self.process else None
266
+ LOG.info(
267
+ "Restarting process (received exit code %s): %s", self.process.returncode, self.cmd
268
+ )
269
+
270
+ def do_run_cmd(self):
271
+ def convert_line(line):
272
+ line = to_str(line or "")
273
+ if self.strip_color:
274
+ # strip color codes
275
+ line = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", line)
276
+ return f"{line.strip()}\r\n"
277
+
278
+ def filter_line(line):
279
+ """Return True if this line should be filtered, i.e., not printed"""
280
+ return "(Press CTRL+C to quit)" in line
281
+
282
+ outfile = self.outfile or os.devnull
283
+ if self.log_listener and outfile == os.devnull:
284
+ outfile = subprocess.PIPE
285
+ try:
286
+ self.process = run(
287
+ self.cmd,
288
+ asynchronous=True,
289
+ stdin=self.stdin,
290
+ outfile=outfile,
291
+ env_vars=self.env_vars,
292
+ inherit_cwd=self.inherit_cwd,
293
+ inherit_env=self.inherit_env,
294
+ cwd=self.cwd,
295
+ )
296
+ self.started.set()
297
+ if outfile:
298
+ if outfile == subprocess.PIPE:
299
+ # get stdout/stderr from child process and write to parent output
300
+ streams = (
301
+ (self.process.stdout, sys.stdout),
302
+ (self.process.stderr, sys.stderr),
303
+ )
304
+ for instream, outstream in streams:
305
+ if not instream:
306
+ continue
307
+ for line in iter(instream.readline, None):
308
+ # `line` should contain a newline at the end as we're iterating,
309
+ # hence we can safely break the loop if `line` is None or empty string
310
+ if line in [None, "", b""]:
311
+ break
312
+ if not (line and line.strip()) and self.is_killed():
313
+ break
314
+ line = convert_line(line)
315
+ if filter_line(line):
316
+ continue
317
+ if self.log_listener:
318
+ self.log_listener(line, stream=instream)
319
+ if self.outfile not in [None, os.devnull]:
320
+ outstream.write(line)
321
+ outstream.flush()
322
+ if self.process:
323
+ self.process.wait()
324
+ else:
325
+ self.process.communicate()
326
+ except Exception as e:
327
+ self.result_future.set_exception(e)
328
+ if self.process and not self.quiet:
329
+ LOG.warning('Shell command error "%s": %s', e, self.cmd)
330
+ if self.process and not self.quiet and self.process.returncode != 0:
331
+ LOG.warning('Shell command exit code "%s": %s', self.process.returncode, self.cmd)
332
+
333
+ def is_killed(self):
334
+ try:
335
+ from localstack_cli.runtime import events
336
+ infra_stopping = events.infra_stopping.is_set()
337
+ except ImportError:
338
+ infra_stopping = False
339
+
340
+ if not self.process:
341
+ return True
342
+ if infra_stopping: # FIXME
343
+ return True
344
+ # Note: Do NOT import "psutil" at the root scope, as this leads
345
+ # to problems when importing this file from our test Lambdas in Docker
346
+ # (Error: libc.musl-x86_64.so.1: cannot open shared object file)
347
+ import psutil
348
+
349
+ return not psutil.pid_exists(self.process.pid)
350
+
351
+ def stop(self, quiet=False):
352
+ if self.stopped:
353
+ return
354
+ if not self.process:
355
+ LOG.warning("No process found for command '%s'", self.cmd)
356
+ return
357
+
358
+ parent_pid = self.process.pid
359
+ try:
360
+ kill_process_tree(parent_pid)
361
+ self.process = None
362
+ except Exception as e:
363
+ if not quiet:
364
+ LOG.warning("Unable to kill process with pid %s: %s", parent_pid, e)
365
+ try:
366
+ self.stop_listener and self.stop_listener(self)
367
+ except Exception as e:
368
+ if not quiet:
369
+ LOG.warning("Unable to run stop handler for shell command thread %s: %s", self, e)
370
+ self.stopped = True
371
+
372
+
373
+ class CaptureOutput:
374
+ """A context manager that captures stdout/stderr of the current thread. Use it as follows:
375
+
376
+ with CaptureOutput() as c:
377
+ ...
378
+ print(c.stdout(), c.stderr())
379
+ """
380
+
381
+ orig_stdout = sys.stdout
382
+ orig_stderr = sys.stderr
383
+ orig___stdout = sys.__stdout__
384
+ orig___stderr = sys.__stderr__
385
+ CONTEXTS_BY_THREAD = {}
386
+
387
+ class LogStreamIO(io.StringIO):
388
+ def write(self, s):
389
+ if isinstance(s, str) and hasattr(s, "decode"):
390
+ s = s.decode("unicode-escape")
391
+ return super(CaptureOutput.LogStreamIO, self).write(s)
392
+
393
+ def __init__(self):
394
+ self._stdout = self.LogStreamIO()
395
+ self._stderr = self.LogStreamIO()
396
+
397
+ def __enter__(self):
398
+ # Note: import werkzeug here (not at top of file) to allow dependency pruning
399
+ from werkzeug.local import LocalProxy
400
+
401
+ ident = self._ident()
402
+ if ident not in self.CONTEXTS_BY_THREAD:
403
+ self.CONTEXTS_BY_THREAD[ident] = self
404
+ self._set(
405
+ LocalProxy(self._proxy(sys.stdout, "stdout")),
406
+ LocalProxy(self._proxy(sys.stderr, "stderr")),
407
+ LocalProxy(self._proxy(sys.__stdout__, "stdout")),
408
+ LocalProxy(self._proxy(sys.__stderr__, "stderr")),
409
+ )
410
+ return self
411
+
412
+ def __exit__(self, type, value, traceback):
413
+ ident = self._ident()
414
+ removed = self.CONTEXTS_BY_THREAD.pop(ident, None)
415
+ if not self.CONTEXTS_BY_THREAD:
416
+ # reset pointers
417
+ self._set(
418
+ self.orig_stdout,
419
+ self.orig_stderr,
420
+ self.orig___stdout,
421
+ self.orig___stderr,
422
+ )
423
+ # get value from streams
424
+ removed._stdout.flush()
425
+ removed._stderr.flush()
426
+ out = removed._stdout.getvalue()
427
+ err = removed._stderr.getvalue()
428
+ # close handles
429
+ removed._stdout.close()
430
+ removed._stderr.close()
431
+ removed._stdout = out
432
+ removed._stderr = err
433
+
434
+ def _set(self, out, err, __out, __err):
435
+ sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__ = (
436
+ out,
437
+ err,
438
+ __out,
439
+ __err,
440
+ )
441
+
442
+ def _proxy(self, original_stream, type):
443
+ def proxy():
444
+ ident = self._ident()
445
+ ctx = self.CONTEXTS_BY_THREAD.get(ident)
446
+ if ctx:
447
+ return ctx._stdout if type == "stdout" else ctx._stderr
448
+ return original_stream
449
+
450
+ return proxy
451
+
452
+ def _ident(self):
453
+ return threading.current_thread().ident
454
+
455
+ def stdout(self):
456
+ return self._stream_value(self._stdout)
457
+
458
+ def stderr(self):
459
+ return self._stream_value(self._stderr)
460
+
461
+ def _stream_value(self, stream):
462
+ return stream.getvalue() if hasattr(stream, "getvalue") else stream
463
+
464
+
465
+ class CaptureOutputProcess:
466
+ """A context manager that captures stdout/stderr of the current process.
467
+
468
+ Basically a lightweight version of CaptureOutput without tracking internal thread mapping
469
+
470
+ Use it as follows:
471
+
472
+ with CaptureOutput() as c:
473
+ ...
474
+ print(c.stdout(), c.stderr())
475
+ """
476
+
477
+ class LogStreamIO(io.StringIO):
478
+ def write(self, s):
479
+ if isinstance(s, str) and hasattr(s, "decode"):
480
+ s = s.decode("unicode-escape")
481
+ return super(CaptureOutputProcess.LogStreamIO, self).write(s)
482
+
483
+ def __init__(self):
484
+ self.orig_stdout = sys.stdout
485
+ self._stdout = self.LogStreamIO()
486
+ self.orig_stderr = sys.stderr
487
+ self._stderr = self.LogStreamIO()
488
+ self.stdout_value = None
489
+ self.stderr_value = None
490
+
491
+ def __enter__(self):
492
+ sys.stdout = self._stdout
493
+ sys.stderr = self._stderr
494
+ return self
495
+
496
+ def __exit__(self, type, value, traceback):
497
+ self._stdout.flush()
498
+ self._stderr.flush()
499
+
500
+ self.stdout_value = self._stdout.getvalue()
501
+ self.stderr_value = self._stderr.getvalue()
502
+
503
+ # close handles
504
+ self._stdout.close()
505
+ self._stderr.close()
506
+
507
+ sys.stdout = self.orig_stdout
508
+ sys.stderr = self.orig_stderr
509
+
510
+ def stdout(self):
511
+ return self.stdout_value
512
+
513
+ def stderr(self):
514
+ return self.stderr_value
File without changes
@@ -0,0 +1,108 @@
1
+ import logging
2
+ import select
3
+ import socket
4
+ from collections.abc import Callable
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ from localstack_cli.utils.serving import Server
8
+
9
+ LOG = logging.getLogger(__name__)
10
+
11
+
12
+ class TCPProxy(Server):
13
+ """
14
+ Server based TCP proxy abstraction.
15
+ This uses a ThreadPoolExecutor, so the maximum number of parallel connections is limited.
16
+ """
17
+
18
+ _target_address: str
19
+ _target_port: int
20
+ _handler: Callable[[bytes], tuple[bytes, bytes]] | None
21
+ _buffer_size: int
22
+ _thread_pool: ThreadPoolExecutor
23
+ _server_socket: socket.socket | None
24
+
25
+ def __init__(
26
+ self,
27
+ target_address: str,
28
+ target_port: int,
29
+ port: int,
30
+ host: str,
31
+ handler: Callable[[bytes], tuple[bytes, bytes]] = None,
32
+ ) -> None:
33
+ super().__init__(port, host)
34
+ self._target_address = target_address
35
+ self._target_port = target_port
36
+ self._handler = handler
37
+ self._buffer_size = 1024
38
+ # thread pool limited to 64 workers for now - can be increased or made configurable if this should not suffice
39
+ # for certain use cases
40
+ self._thread_pool = ThreadPoolExecutor(thread_name_prefix="tcp-proxy", max_workers=64)
41
+ self._server_socket = None
42
+
43
+ def _handle_request(self, s_src: socket.socket):
44
+ try:
45
+ s_dst = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
46
+ with s_src as s_src, s_dst as s_dst:
47
+ s_dst.connect((self._target_address, self._target_port))
48
+
49
+ sockets = [s_src, s_dst]
50
+ while not self._stopped.is_set():
51
+ s_read, _, _ = select.select(sockets, [], [], 1)
52
+
53
+ for s in s_read:
54
+ data = s.recv(self._buffer_size)
55
+ if not data:
56
+ return
57
+
58
+ if s == s_src:
59
+ forward, response = data, None
60
+ if self._handler:
61
+ forward, response = self._handler(data)
62
+ if forward is not None:
63
+ s_dst.sendall(forward)
64
+ elif response is not None:
65
+ s_src.sendall(response)
66
+ return
67
+ elif s == s_dst:
68
+ s_src.sendall(data)
69
+ except Exception as e:
70
+ LOG.error(
71
+ "Error while handling request from %s to %s:%s: %s",
72
+ s_src.getpeername(),
73
+ self._target_address,
74
+ self._target_port,
75
+ e,
76
+ )
77
+
78
+ def do_run(self):
79
+ self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
80
+ self._server_socket.bind((self.host, self.port))
81
+ self._server_socket.listen(1)
82
+ self._server_socket.settimeout(10)
83
+ LOG.debug(
84
+ "Starting TCP proxy bound on %s:%s forwarding to %s:%s",
85
+ self.host,
86
+ self.port,
87
+ self._target_address,
88
+ self._target_port,
89
+ )
90
+
91
+ with self._server_socket:
92
+ while not self._stopped.is_set():
93
+ try:
94
+ src_socket, _ = self._server_socket.accept()
95
+ self._thread_pool.submit(self._handle_request, src_socket)
96
+ except TimeoutError:
97
+ pass
98
+ except OSError as e:
99
+ # avoid creating an error message if OSError is thrown due to socket closing
100
+ if not self._stopped.is_set():
101
+ LOG.warning("Error during during TCPProxy socket accept: %s", e)
102
+
103
+ def do_shutdown(self):
104
+ if self._server_socket:
105
+ self._server_socket.shutdown(socket.SHUT_RDWR)
106
+ self._server_socket.close()
107
+ self._thread_pool.shutdown(cancel_futures=True)
108
+ LOG.debug("Shut down TCPProxy on %s:%s", self.host, self.port)