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.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- 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)
|