loopix-sdk 2.30.0__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.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Callable, Dict, List, Literal, Optional, Union, overload
|
|
3
|
+
|
|
4
|
+
import loopix_connect
|
|
5
|
+
import httpx
|
|
6
|
+
from packaging.version import Version
|
|
7
|
+
from loopix.api import make_logging_event_hooks
|
|
8
|
+
from loopix.api.client_sync import get_envd_transport
|
|
9
|
+
from loopix.connection_config import (
|
|
10
|
+
ConnectionConfig,
|
|
11
|
+
Username,
|
|
12
|
+
KEEPALIVE_PING_HEADER,
|
|
13
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
14
|
+
)
|
|
15
|
+
from loopix.envd.process import process_connect, process_pb2
|
|
16
|
+
from loopix.envd.api import check_sandbox_health
|
|
17
|
+
from loopix.envd.rpc import authentication_header, handle_rpc_exception_with_health
|
|
18
|
+
from loopix.envd.versions import ENVD_COMMANDS_STDIN, ENVD_ENVD_CLOSE
|
|
19
|
+
from loopix.exceptions import SandboxException
|
|
20
|
+
from loopix.sandbox.commands.main import ProcessInfo
|
|
21
|
+
from loopix.sandbox.commands.command_handle import CommandResult
|
|
22
|
+
from loopix.sandbox_sync.commands.command_handle import CommandHandle
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Commands:
|
|
26
|
+
"""
|
|
27
|
+
Module for executing commands in the sandbox.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
envd_api_url: str,
|
|
33
|
+
connection_config: ConnectionConfig,
|
|
34
|
+
envd_version: Version,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._envd_api_url = envd_api_url
|
|
37
|
+
self._connection_config = connection_config
|
|
38
|
+
self._envd_version = envd_version
|
|
39
|
+
self._thread_local = threading.local()
|
|
40
|
+
|
|
41
|
+
def _create_envd_api(self) -> httpx.Client:
|
|
42
|
+
transport = get_envd_transport(self._connection_config)
|
|
43
|
+
return httpx.Client(
|
|
44
|
+
base_url=self._envd_api_url,
|
|
45
|
+
transport=transport,
|
|
46
|
+
headers=self._connection_config.sandbox_headers,
|
|
47
|
+
event_hooks=make_logging_event_hooks(self._connection_config.logger),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _create_rpc(self) -> process_connect.ProcessClient:
|
|
51
|
+
transport = get_envd_transport(self._connection_config)
|
|
52
|
+
return process_connect.ProcessClient(
|
|
53
|
+
self._envd_api_url,
|
|
54
|
+
# TODO: Fix and enable compression again — the headers compression is not solved for streaming.
|
|
55
|
+
# compressor=loopix_connect.GzipCompressor,
|
|
56
|
+
pool=transport.pool,
|
|
57
|
+
json=True,
|
|
58
|
+
headers=self._connection_config.sandbox_headers,
|
|
59
|
+
logger=self._connection_config.logger,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def _envd_api(self) -> httpx.Client:
|
|
64
|
+
envd_api = getattr(self._thread_local, "envd_api", None)
|
|
65
|
+
if envd_api is None:
|
|
66
|
+
envd_api = self._create_envd_api()
|
|
67
|
+
self._thread_local.envd_api = envd_api
|
|
68
|
+
return envd_api
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def _rpc(self) -> process_connect.ProcessClient:
|
|
72
|
+
rpc = getattr(self._thread_local, "rpc", None)
|
|
73
|
+
if rpc is None:
|
|
74
|
+
rpc = self._create_rpc()
|
|
75
|
+
self._thread_local.rpc = rpc
|
|
76
|
+
return rpc
|
|
77
|
+
|
|
78
|
+
def _check_health(self) -> Optional[bool]:
|
|
79
|
+
return check_sandbox_health(self._envd_api)
|
|
80
|
+
|
|
81
|
+
def list(
|
|
82
|
+
self,
|
|
83
|
+
request_timeout: Optional[float] = None,
|
|
84
|
+
) -> List[ProcessInfo]:
|
|
85
|
+
"""
|
|
86
|
+
Lists all running commands and PTY sessions.
|
|
87
|
+
|
|
88
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
89
|
+
|
|
90
|
+
:return: List of running commands and PTY sessions
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
res = self._rpc.list(
|
|
94
|
+
process_pb2.ListRequest(),
|
|
95
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
96
|
+
request_timeout
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
return [
|
|
100
|
+
ProcessInfo(
|
|
101
|
+
pid=p.pid,
|
|
102
|
+
tag=p.tag if p.HasField("tag") else None,
|
|
103
|
+
cmd=p.config.cmd,
|
|
104
|
+
args=list(p.config.args),
|
|
105
|
+
envs=dict(p.config.envs),
|
|
106
|
+
cwd=p.config.cwd if p.config.HasField("cwd") else None,
|
|
107
|
+
)
|
|
108
|
+
for p in res.processes
|
|
109
|
+
]
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
112
|
+
|
|
113
|
+
def kill(
|
|
114
|
+
self,
|
|
115
|
+
pid: int,
|
|
116
|
+
request_timeout: Optional[float] = None,
|
|
117
|
+
) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Kill a running command specified by its process ID.
|
|
120
|
+
It uses `SIGKILL` signal to kill the command.
|
|
121
|
+
|
|
122
|
+
:param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()`
|
|
123
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
124
|
+
|
|
125
|
+
:return: `True` if the command was killed, `False` if the command was not found
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
self._rpc.send_signal(
|
|
129
|
+
process_pb2.SendSignalRequest(
|
|
130
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
131
|
+
signal=process_pb2.Signal.SIGNAL_SIGKILL,
|
|
132
|
+
),
|
|
133
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
134
|
+
request_timeout
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
return True
|
|
138
|
+
except Exception as e:
|
|
139
|
+
if isinstance(e, loopix_connect.ConnectException):
|
|
140
|
+
if e.status == loopix_connect.Code.not_found:
|
|
141
|
+
return False
|
|
142
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
143
|
+
|
|
144
|
+
def send_stdin(
|
|
145
|
+
self,
|
|
146
|
+
pid: int,
|
|
147
|
+
data: Union[str, bytes],
|
|
148
|
+
request_timeout: Optional[float] = None,
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
Send data to command stdin.
|
|
152
|
+
|
|
153
|
+
:param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
|
|
154
|
+
:param data: Data to send to the command
|
|
155
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
156
|
+
"""
|
|
157
|
+
try:
|
|
158
|
+
self._rpc.send_input(
|
|
159
|
+
process_pb2.SendInputRequest(
|
|
160
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
161
|
+
input=process_pb2.ProcessInput(
|
|
162
|
+
stdin=data.encode() if isinstance(data, str) else data,
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
166
|
+
request_timeout
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
171
|
+
|
|
172
|
+
def close_stdin(
|
|
173
|
+
self,
|
|
174
|
+
pid: int,
|
|
175
|
+
request_timeout: Optional[float] = None,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Close the command stdin.
|
|
179
|
+
|
|
180
|
+
This signals EOF to the command. The command must have been started with `stdin=True`.
|
|
181
|
+
|
|
182
|
+
:param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
|
|
183
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
184
|
+
"""
|
|
185
|
+
if self._envd_version < ENVD_ENVD_CLOSE:
|
|
186
|
+
raise SandboxException(
|
|
187
|
+
f"Sandbox envd version {self._envd_version} doesn't support closing stdin. "
|
|
188
|
+
f"Please rebuild your template to pick up the latest sandbox version."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
self._rpc.close_stdin(
|
|
193
|
+
process_pb2.CloseStdinRequest(
|
|
194
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
195
|
+
),
|
|
196
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
197
|
+
request_timeout
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
202
|
+
|
|
203
|
+
@overload
|
|
204
|
+
def run(
|
|
205
|
+
self,
|
|
206
|
+
cmd: str,
|
|
207
|
+
background: Union[Literal[False], None] = None,
|
|
208
|
+
envs: Optional[Dict[str, str]] = None,
|
|
209
|
+
user: Optional[Username] = None,
|
|
210
|
+
cwd: Optional[str] = None,
|
|
211
|
+
on_stdout: Optional[Callable[[str], None]] = None,
|
|
212
|
+
on_stderr: Optional[Callable[[str], None]] = None,
|
|
213
|
+
stdin: Optional[bool] = None,
|
|
214
|
+
timeout: Optional[float] = 60,
|
|
215
|
+
request_timeout: Optional[float] = None,
|
|
216
|
+
) -> CommandResult:
|
|
217
|
+
"""
|
|
218
|
+
Start a new command and wait until it finishes executing.
|
|
219
|
+
|
|
220
|
+
:param cmd: Command to execute
|
|
221
|
+
:param background: **`False` if the command should be executed in the foreground**, `True` if the command should be executed in the background
|
|
222
|
+
:param envs: Environment variables used for the command
|
|
223
|
+
:param user: User to run the command as
|
|
224
|
+
:param cwd: Working directory to run the command
|
|
225
|
+
:param on_stdout: Callback for command stdout output
|
|
226
|
+
:param on_stderr: Callback for command stderr output
|
|
227
|
+
:param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
|
|
228
|
+
:param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
|
|
229
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
230
|
+
|
|
231
|
+
:return: `CommandResult` result of the command execution
|
|
232
|
+
"""
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
@overload
|
|
236
|
+
def run(
|
|
237
|
+
self,
|
|
238
|
+
cmd: str,
|
|
239
|
+
background: Literal[True],
|
|
240
|
+
envs: Optional[Dict[str, str]] = None,
|
|
241
|
+
user: Optional[Username] = None,
|
|
242
|
+
cwd: Optional[str] = None,
|
|
243
|
+
on_stdout: None = None,
|
|
244
|
+
on_stderr: None = None,
|
|
245
|
+
stdin: Optional[bool] = None,
|
|
246
|
+
timeout: Optional[float] = 60,
|
|
247
|
+
request_timeout: Optional[float] = None,
|
|
248
|
+
) -> CommandHandle:
|
|
249
|
+
"""
|
|
250
|
+
Start a new command and return a handle to interact with it.
|
|
251
|
+
|
|
252
|
+
:param cmd: Command to execute
|
|
253
|
+
:param background: `False` if the command should be executed in the foreground, **`True` if the command should be executed in the background**
|
|
254
|
+
:param envs: Environment variables used for the command
|
|
255
|
+
:param user: User to run the command as
|
|
256
|
+
:param cwd: Working directory to run the command
|
|
257
|
+
:param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
|
|
258
|
+
:param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
|
|
259
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
260
|
+
|
|
261
|
+
:return: `CommandHandle` handle to interact with the running command
|
|
262
|
+
"""
|
|
263
|
+
...
|
|
264
|
+
|
|
265
|
+
def run(
|
|
266
|
+
self,
|
|
267
|
+
cmd: str,
|
|
268
|
+
background: Union[bool, None] = None,
|
|
269
|
+
envs: Optional[Dict[str, str]] = None,
|
|
270
|
+
user: Optional[Username] = None,
|
|
271
|
+
cwd: Optional[str] = None,
|
|
272
|
+
on_stdout: Optional[Callable[[str], None]] = None,
|
|
273
|
+
on_stderr: Optional[Callable[[str], None]] = None,
|
|
274
|
+
stdin: Optional[bool] = None,
|
|
275
|
+
timeout: Optional[float] = 60,
|
|
276
|
+
request_timeout: Optional[float] = None,
|
|
277
|
+
):
|
|
278
|
+
# Check version for stdin support
|
|
279
|
+
if stdin is False and self._envd_version < ENVD_COMMANDS_STDIN:
|
|
280
|
+
raise SandboxException(
|
|
281
|
+
f"Sandbox envd version {self._envd_version} can't specify stdin, it's always turned on. "
|
|
282
|
+
f"Please rebuild your template if you need this feature."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Default to `False`
|
|
286
|
+
stdin = stdin or False
|
|
287
|
+
|
|
288
|
+
proc = self._start(
|
|
289
|
+
cmd,
|
|
290
|
+
envs,
|
|
291
|
+
user,
|
|
292
|
+
cwd,
|
|
293
|
+
stdin,
|
|
294
|
+
timeout,
|
|
295
|
+
request_timeout,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
proc
|
|
300
|
+
if background
|
|
301
|
+
else proc.wait(
|
|
302
|
+
on_stdout=on_stdout,
|
|
303
|
+
on_stderr=on_stderr,
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def _start(
|
|
308
|
+
self,
|
|
309
|
+
cmd: str,
|
|
310
|
+
envs: Optional[Dict[str, str]],
|
|
311
|
+
user: Optional[Username],
|
|
312
|
+
cwd: Optional[str],
|
|
313
|
+
stdin: bool,
|
|
314
|
+
timeout: Optional[float],
|
|
315
|
+
request_timeout: Optional[float],
|
|
316
|
+
):
|
|
317
|
+
events = self._rpc.start(
|
|
318
|
+
process_pb2.StartRequest(
|
|
319
|
+
process=process_pb2.ProcessConfig(
|
|
320
|
+
cmd="/bin/bash",
|
|
321
|
+
envs=envs,
|
|
322
|
+
args=["-l", "-c", cmd],
|
|
323
|
+
cwd=cwd,
|
|
324
|
+
),
|
|
325
|
+
stdin=stdin,
|
|
326
|
+
),
|
|
327
|
+
headers={
|
|
328
|
+
**authentication_header(self._envd_version, user),
|
|
329
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
330
|
+
},
|
|
331
|
+
timeout=timeout,
|
|
332
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
333
|
+
request_timeout
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
start_event = events.__next__()
|
|
339
|
+
|
|
340
|
+
if not start_event.HasField("event"):
|
|
341
|
+
raise SandboxException(
|
|
342
|
+
f"Failed to start process: expected start event, got {start_event}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
pid = start_event.event.start.pid
|
|
346
|
+
return CommandHandle(
|
|
347
|
+
pid=pid,
|
|
348
|
+
handle_kill=lambda: self.kill(pid),
|
|
349
|
+
events=events,
|
|
350
|
+
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
|
|
351
|
+
pid, data, request_timeout
|
|
352
|
+
),
|
|
353
|
+
handle_close_stdin=lambda request_timeout=None: self.close_stdin(
|
|
354
|
+
pid, request_timeout
|
|
355
|
+
),
|
|
356
|
+
check_health=self._check_health,
|
|
357
|
+
)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
try:
|
|
360
|
+
events.close()
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
364
|
+
|
|
365
|
+
def connect(
|
|
366
|
+
self,
|
|
367
|
+
pid: int,
|
|
368
|
+
timeout: Optional[float] = 60,
|
|
369
|
+
request_timeout: Optional[float] = None,
|
|
370
|
+
):
|
|
371
|
+
"""
|
|
372
|
+
Connects to a running command.
|
|
373
|
+
You can use `CommandHandle.wait()` to wait for the command to finish and get execution results.
|
|
374
|
+
|
|
375
|
+
:param pid: Process ID of the command to connect to. You can get the list of processes using `sandbox.commands.list()`
|
|
376
|
+
:param timeout: Timeout for the connection in **seconds**. Using `0` will not limit the connection time
|
|
377
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
378
|
+
|
|
379
|
+
:return: `CommandHandle` handle to interact with the running command
|
|
380
|
+
"""
|
|
381
|
+
events = self._rpc.connect(
|
|
382
|
+
process_pb2.ConnectRequest(
|
|
383
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
384
|
+
),
|
|
385
|
+
headers={
|
|
386
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
387
|
+
},
|
|
388
|
+
timeout=timeout,
|
|
389
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
390
|
+
request_timeout
|
|
391
|
+
),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
start_event = events.__next__()
|
|
396
|
+
|
|
397
|
+
if not start_event.HasField("event"):
|
|
398
|
+
raise SandboxException(
|
|
399
|
+
f"Failed to connect to process: expected start event, got {start_event}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
pid = start_event.event.start.pid
|
|
403
|
+
return CommandHandle(
|
|
404
|
+
pid=pid,
|
|
405
|
+
handle_kill=lambda: self.kill(pid),
|
|
406
|
+
events=events,
|
|
407
|
+
handle_send_stdin=lambda data, request_timeout=None: self.send_stdin(
|
|
408
|
+
pid, data, request_timeout
|
|
409
|
+
),
|
|
410
|
+
handle_close_stdin=lambda request_timeout=None: self.close_stdin(
|
|
411
|
+
pid, request_timeout
|
|
412
|
+
),
|
|
413
|
+
check_health=self._check_health,
|
|
414
|
+
)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
try:
|
|
417
|
+
events.close()
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Callable, Any, Generator, List, Union, Tuple
|
|
4
|
+
|
|
5
|
+
from loopix.envd.rpc import handle_rpc_exception_with_health
|
|
6
|
+
from loopix.envd.process import process_pb2
|
|
7
|
+
from loopix.exceptions import SandboxException
|
|
8
|
+
from loopix.sandbox.commands.command_handle import (
|
|
9
|
+
CommandExitException,
|
|
10
|
+
CommandResult,
|
|
11
|
+
Stderr,
|
|
12
|
+
Stdout,
|
|
13
|
+
PtyOutput,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CommandHandle:
|
|
18
|
+
"""
|
|
19
|
+
Command execution handle.
|
|
20
|
+
|
|
21
|
+
It provides methods for waiting for the command to finish, retrieving stdout/stderr, and killing the command.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def pid(self):
|
|
26
|
+
"""
|
|
27
|
+
Command process ID.
|
|
28
|
+
"""
|
|
29
|
+
return self._pid
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
pid: int,
|
|
34
|
+
handle_kill: Callable[[], bool],
|
|
35
|
+
events: Generator[
|
|
36
|
+
Union[process_pb2.StartResponse, process_pb2.ConnectResponse], Any, None
|
|
37
|
+
],
|
|
38
|
+
handle_send_stdin: Optional[
|
|
39
|
+
Callable[[Union[str, bytes], Optional[float]], None]
|
|
40
|
+
] = None,
|
|
41
|
+
handle_close_stdin: Optional[Callable[[Optional[float]], None]] = None,
|
|
42
|
+
check_health: Optional[Callable[[], Optional[bool]]] = None,
|
|
43
|
+
):
|
|
44
|
+
self._pid = pid
|
|
45
|
+
self._handle_kill = handle_kill
|
|
46
|
+
self._handle_send_stdin = handle_send_stdin
|
|
47
|
+
self._handle_close_stdin = handle_close_stdin
|
|
48
|
+
self._check_health = check_health
|
|
49
|
+
self._events = events
|
|
50
|
+
|
|
51
|
+
self._stdout_chunks: List[str] = []
|
|
52
|
+
self._stderr_chunks: List[str] = []
|
|
53
|
+
|
|
54
|
+
self._stdout_decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
55
|
+
self._stderr_decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
56
|
+
|
|
57
|
+
self._result: Optional[CommandResult] = None
|
|
58
|
+
self._iteration_exception: Optional[Exception] = None
|
|
59
|
+
|
|
60
|
+
def __iter__(self):
|
|
61
|
+
"""
|
|
62
|
+
Iterate over the command output.
|
|
63
|
+
|
|
64
|
+
:return: Generator of command outputs
|
|
65
|
+
"""
|
|
66
|
+
return self._handle_events()
|
|
67
|
+
|
|
68
|
+
def _flush_decoders(
|
|
69
|
+
self,
|
|
70
|
+
) -> List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]]:
|
|
71
|
+
"""
|
|
72
|
+
Flush any bytes still buffered in the stream decoders.
|
|
73
|
+
|
|
74
|
+
Incomplete trailing UTF-8 sequences are emitted as replacement
|
|
75
|
+
characters, matching the per-chunk decoding behavior.
|
|
76
|
+
"""
|
|
77
|
+
events: List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]] = []
|
|
78
|
+
out = self._stdout_decoder.decode(b"", final=True)
|
|
79
|
+
if out:
|
|
80
|
+
self._stdout_chunks.append(out)
|
|
81
|
+
events.append((out, None, None))
|
|
82
|
+
err = self._stderr_decoder.decode(b"", final=True)
|
|
83
|
+
if err:
|
|
84
|
+
self._stderr_chunks.append(err)
|
|
85
|
+
events.append((None, err, None))
|
|
86
|
+
return events
|
|
87
|
+
|
|
88
|
+
def _handle_events(
|
|
89
|
+
self,
|
|
90
|
+
) -> Generator[
|
|
91
|
+
Union[
|
|
92
|
+
Tuple[Stdout, None, None],
|
|
93
|
+
Tuple[None, Stderr, None],
|
|
94
|
+
Tuple[None, None, PtyOutput],
|
|
95
|
+
],
|
|
96
|
+
None,
|
|
97
|
+
None,
|
|
98
|
+
]:
|
|
99
|
+
try:
|
|
100
|
+
for event in self._events:
|
|
101
|
+
if event.event.HasField("data"):
|
|
102
|
+
if event.event.data.stdout:
|
|
103
|
+
out = self._stdout_decoder.decode(event.event.data.stdout)
|
|
104
|
+
if out:
|
|
105
|
+
self._stdout_chunks.append(out)
|
|
106
|
+
yield out, None, None
|
|
107
|
+
if event.event.data.stderr:
|
|
108
|
+
out = self._stderr_decoder.decode(event.event.data.stderr)
|
|
109
|
+
if out:
|
|
110
|
+
self._stderr_chunks.append(out)
|
|
111
|
+
yield None, out, None
|
|
112
|
+
if event.event.data.pty:
|
|
113
|
+
yield None, None, event.event.data.pty
|
|
114
|
+
if event.event.HasField("end"):
|
|
115
|
+
# Flush trailing decoder bytes into the accumulators and
|
|
116
|
+
# record the result before yielding the flushed chunks, so a
|
|
117
|
+
# consumer that stops iterating on the first flushed chunk
|
|
118
|
+
# still observes the exit code.
|
|
119
|
+
flushed = list(self._flush_decoders())
|
|
120
|
+
self._result = CommandResult(
|
|
121
|
+
stdout="".join(self._stdout_chunks),
|
|
122
|
+
stderr="".join(self._stderr_chunks),
|
|
123
|
+
exit_code=event.event.end.exit_code,
|
|
124
|
+
error=event.event.end.error,
|
|
125
|
+
)
|
|
126
|
+
yield from flushed
|
|
127
|
+
|
|
128
|
+
# If the stream closed without an end event (e.g. disconnect or a
|
|
129
|
+
# dropped connection), flush any bytes still buffered in the
|
|
130
|
+
# decoders so incomplete trailing sequences surface as replacement
|
|
131
|
+
# characters instead of being silently dropped.
|
|
132
|
+
if self._result is None:
|
|
133
|
+
yield from self._flush_decoders()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
# The stream raised before an end event (e.g. disconnect or RPC
|
|
136
|
+
# failure). Flush any bytes still buffered in the decoders so
|
|
137
|
+
# incomplete trailing sequences surface as replacement characters
|
|
138
|
+
# instead of being silently dropped, then surface the error.
|
|
139
|
+
yield from self._flush_decoders()
|
|
140
|
+
raise handle_rpc_exception_with_health(e, self._check_health)
|
|
141
|
+
|
|
142
|
+
def disconnect(self) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Disconnect from the command.
|
|
145
|
+
|
|
146
|
+
The command is not killed, but SDK stops receiving events from the command.
|
|
147
|
+
You can reconnect to the command using `sandbox.commands.connect` method.
|
|
148
|
+
"""
|
|
149
|
+
self._events.close()
|
|
150
|
+
|
|
151
|
+
def wait(
|
|
152
|
+
self,
|
|
153
|
+
on_pty: Optional[Callable[[PtyOutput], None]] = None,
|
|
154
|
+
on_stdout: Optional[Callable[[str], None]] = None,
|
|
155
|
+
on_stderr: Optional[Callable[[str], None]] = None,
|
|
156
|
+
) -> CommandResult:
|
|
157
|
+
"""
|
|
158
|
+
Wait for the command to finish and returns the result.
|
|
159
|
+
If the command exits with a non-zero exit code, it throws a `CommandExitException`.
|
|
160
|
+
|
|
161
|
+
:param on_pty: Callback for pty output
|
|
162
|
+
:param on_stdout: Callback for stdout output
|
|
163
|
+
:param on_stderr: Callback for stderr output
|
|
164
|
+
|
|
165
|
+
:return: `CommandResult` result of command execution
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
for stdout, stderr, pty in self:
|
|
169
|
+
if stdout is not None and on_stdout:
|
|
170
|
+
on_stdout(stdout)
|
|
171
|
+
elif stderr is not None and on_stderr:
|
|
172
|
+
on_stderr(stderr)
|
|
173
|
+
elif pty is not None and on_pty:
|
|
174
|
+
on_pty(pty)
|
|
175
|
+
except StopIteration:
|
|
176
|
+
pass
|
|
177
|
+
except Exception as e:
|
|
178
|
+
self._iteration_exception = handle_rpc_exception_with_health(
|
|
179
|
+
e, self._check_health
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if self._iteration_exception:
|
|
183
|
+
raise self._iteration_exception
|
|
184
|
+
|
|
185
|
+
if self._result is None:
|
|
186
|
+
raise Exception("Command ended without an end event")
|
|
187
|
+
|
|
188
|
+
if self._result.exit_code != 0:
|
|
189
|
+
raise CommandExitException(
|
|
190
|
+
stdout="".join(self._stdout_chunks),
|
|
191
|
+
stderr="".join(self._stderr_chunks),
|
|
192
|
+
exit_code=self._result.exit_code,
|
|
193
|
+
error=self._result.error,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return self._result
|
|
197
|
+
|
|
198
|
+
def kill(self) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Kills the command.
|
|
201
|
+
|
|
202
|
+
It uses `SIGKILL` signal to kill the command.
|
|
203
|
+
|
|
204
|
+
:return: Whether the command was killed successfully
|
|
205
|
+
"""
|
|
206
|
+
return self._handle_kill()
|
|
207
|
+
|
|
208
|
+
def send_stdin(
|
|
209
|
+
self,
|
|
210
|
+
data: Union[str, bytes],
|
|
211
|
+
request_timeout: Optional[float] = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Send data to the command stdin.
|
|
215
|
+
|
|
216
|
+
The command must have been started with `stdin=True`.
|
|
217
|
+
|
|
218
|
+
:param data: Data to send to the command
|
|
219
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
220
|
+
"""
|
|
221
|
+
if self._handle_send_stdin is None:
|
|
222
|
+
raise SandboxException(
|
|
223
|
+
"Sending stdin is not supported for this command handle."
|
|
224
|
+
)
|
|
225
|
+
self._handle_send_stdin(data, request_timeout)
|
|
226
|
+
|
|
227
|
+
def close_stdin(self, request_timeout: Optional[float] = None) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Close the command stdin.
|
|
230
|
+
|
|
231
|
+
This signals EOF to the command. The command must have been started with `stdin=True`.
|
|
232
|
+
|
|
233
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
234
|
+
"""
|
|
235
|
+
if self._handle_close_stdin is None:
|
|
236
|
+
raise SandboxException(
|
|
237
|
+
"Closing stdin is not supported for this command handle."
|
|
238
|
+
)
|
|
239
|
+
self._handle_close_stdin(request_timeout)
|