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,298 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import codecs
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import (
|
|
5
|
+
Optional,
|
|
6
|
+
Callable,
|
|
7
|
+
Any,
|
|
8
|
+
AsyncGenerator,
|
|
9
|
+
List,
|
|
10
|
+
Awaitable,
|
|
11
|
+
Union,
|
|
12
|
+
Tuple,
|
|
13
|
+
Coroutine,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from loopix.envd.rpc import ahandle_rpc_exception_with_health
|
|
17
|
+
from loopix.envd.process import process_pb2
|
|
18
|
+
from loopix.exceptions import SandboxException
|
|
19
|
+
from loopix.sandbox.commands.command_handle import (
|
|
20
|
+
CommandExitException,
|
|
21
|
+
CommandResult,
|
|
22
|
+
Stderr,
|
|
23
|
+
Stdout,
|
|
24
|
+
PtyOutput,
|
|
25
|
+
)
|
|
26
|
+
from loopix.sandbox_async.utils import OutputHandler
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncCommandHandle:
|
|
30
|
+
"""
|
|
31
|
+
Command execution handle.
|
|
32
|
+
|
|
33
|
+
It provides methods for waiting for the command to finish, retrieving stdout/stderr, and killing the command.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def pid(self):
|
|
38
|
+
"""
|
|
39
|
+
Command process ID.
|
|
40
|
+
"""
|
|
41
|
+
return self._pid
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def stdout(self):
|
|
45
|
+
"""
|
|
46
|
+
Command stdout output.
|
|
47
|
+
"""
|
|
48
|
+
return "".join(self._stdout_chunks)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def stderr(self):
|
|
52
|
+
"""
|
|
53
|
+
Command stderr output.
|
|
54
|
+
"""
|
|
55
|
+
return "".join(self._stderr_chunks)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def error(self):
|
|
59
|
+
"""
|
|
60
|
+
Command execution error message.
|
|
61
|
+
"""
|
|
62
|
+
if self._result is None:
|
|
63
|
+
return None
|
|
64
|
+
return self._result.error
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def exit_code(self):
|
|
68
|
+
"""
|
|
69
|
+
Command execution exit code.
|
|
70
|
+
|
|
71
|
+
`0` if the command finished successfully.
|
|
72
|
+
|
|
73
|
+
It is `None` if the command is still running.
|
|
74
|
+
"""
|
|
75
|
+
if self._result is None:
|
|
76
|
+
return None
|
|
77
|
+
return self._result.exit_code
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
pid: int,
|
|
82
|
+
handle_kill: Callable[[], Coroutine[Any, Any, bool]],
|
|
83
|
+
events: AsyncGenerator[
|
|
84
|
+
Union[process_pb2.StartResponse, process_pb2.ConnectResponse], Any
|
|
85
|
+
],
|
|
86
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
87
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
88
|
+
on_pty: Optional[OutputHandler[PtyOutput]] = None,
|
|
89
|
+
handle_send_stdin: Optional[
|
|
90
|
+
Callable[[Union[str, bytes], Optional[float]], Coroutine[Any, Any, None]]
|
|
91
|
+
] = None,
|
|
92
|
+
handle_close_stdin: Optional[
|
|
93
|
+
Callable[[Optional[float]], Coroutine[Any, Any, None]]
|
|
94
|
+
] = None,
|
|
95
|
+
check_health: Optional[Callable[[], Awaitable[Optional[bool]]]] = None,
|
|
96
|
+
):
|
|
97
|
+
self._pid = pid
|
|
98
|
+
self._handle_kill = handle_kill
|
|
99
|
+
self._handle_send_stdin = handle_send_stdin
|
|
100
|
+
self._handle_close_stdin = handle_close_stdin
|
|
101
|
+
self._check_health = check_health
|
|
102
|
+
self._events = events
|
|
103
|
+
|
|
104
|
+
self._stdout_chunks: List[str] = []
|
|
105
|
+
self._stderr_chunks: List[str] = []
|
|
106
|
+
|
|
107
|
+
self._stdout_decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
108
|
+
self._stderr_decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
109
|
+
|
|
110
|
+
self._on_stdout = on_stdout
|
|
111
|
+
self._on_stderr = on_stderr
|
|
112
|
+
self._on_pty = on_pty
|
|
113
|
+
|
|
114
|
+
self._result: Optional[CommandResult] = None
|
|
115
|
+
self._iteration_exception: Optional[Exception] = None
|
|
116
|
+
|
|
117
|
+
self._wait = asyncio.create_task(self._handle_events())
|
|
118
|
+
|
|
119
|
+
def _flush_decoders(
|
|
120
|
+
self,
|
|
121
|
+
) -> List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]]:
|
|
122
|
+
"""
|
|
123
|
+
Flush any bytes still buffered in the stream decoders.
|
|
124
|
+
|
|
125
|
+
Incomplete trailing UTF-8 sequences are emitted as replacement
|
|
126
|
+
characters, matching the per-chunk decoding behavior.
|
|
127
|
+
"""
|
|
128
|
+
events: List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]] = []
|
|
129
|
+
out = self._stdout_decoder.decode(b"", final=True)
|
|
130
|
+
if out:
|
|
131
|
+
self._stdout_chunks.append(out)
|
|
132
|
+
events.append((out, None, None))
|
|
133
|
+
err = self._stderr_decoder.decode(b"", final=True)
|
|
134
|
+
if err:
|
|
135
|
+
self._stderr_chunks.append(err)
|
|
136
|
+
events.append((None, err, None))
|
|
137
|
+
return events
|
|
138
|
+
|
|
139
|
+
async def _iterate_events(
|
|
140
|
+
self,
|
|
141
|
+
) -> AsyncGenerator[
|
|
142
|
+
Union[
|
|
143
|
+
Tuple[Stdout, None, None],
|
|
144
|
+
Tuple[None, Stderr, None],
|
|
145
|
+
Tuple[None, None, PtyOutput],
|
|
146
|
+
],
|
|
147
|
+
None,
|
|
148
|
+
]:
|
|
149
|
+
try:
|
|
150
|
+
async for event in self._events:
|
|
151
|
+
if event.event.HasField("data"):
|
|
152
|
+
if event.event.data.stdout:
|
|
153
|
+
out = self._stdout_decoder.decode(event.event.data.stdout)
|
|
154
|
+
if out:
|
|
155
|
+
self._stdout_chunks.append(out)
|
|
156
|
+
yield out, None, None
|
|
157
|
+
if event.event.data.stderr:
|
|
158
|
+
out = self._stderr_decoder.decode(event.event.data.stderr)
|
|
159
|
+
if out:
|
|
160
|
+
self._stderr_chunks.append(out)
|
|
161
|
+
yield None, out, None
|
|
162
|
+
if event.event.data.pty:
|
|
163
|
+
yield None, None, event.event.data.pty
|
|
164
|
+
if event.event.HasField("end"):
|
|
165
|
+
# Flush trailing decoder bytes into the accumulators and
|
|
166
|
+
# record the result before yielding the flushed chunks, so a
|
|
167
|
+
# consumer that stops iterating on the first flushed chunk
|
|
168
|
+
# still observes the exit code.
|
|
169
|
+
flushed = list(self._flush_decoders())
|
|
170
|
+
self._result = CommandResult(
|
|
171
|
+
stdout="".join(self._stdout_chunks),
|
|
172
|
+
stderr="".join(self._stderr_chunks),
|
|
173
|
+
exit_code=event.event.end.exit_code,
|
|
174
|
+
error=event.event.end.error,
|
|
175
|
+
)
|
|
176
|
+
for f in flushed:
|
|
177
|
+
yield f
|
|
178
|
+
except Exception:
|
|
179
|
+
# The stream raised before an end event (e.g. disconnect or RPC
|
|
180
|
+
# failure). Flush any bytes still buffered in the decoders so
|
|
181
|
+
# incomplete trailing sequences surface as replacement characters
|
|
182
|
+
# instead of being silently dropped, then re-raise so the error is
|
|
183
|
+
# still surfaced by the consumer.
|
|
184
|
+
for flushed in self._flush_decoders():
|
|
185
|
+
yield flushed
|
|
186
|
+
raise
|
|
187
|
+
|
|
188
|
+
# If the stream closed without an end event (e.g. disconnect or a
|
|
189
|
+
# dropped connection), flush any bytes still buffered in the decoders
|
|
190
|
+
# so incomplete trailing sequences surface as replacement characters
|
|
191
|
+
# instead of being silently dropped.
|
|
192
|
+
if self._result is None:
|
|
193
|
+
for flushed in self._flush_decoders():
|
|
194
|
+
yield flushed
|
|
195
|
+
|
|
196
|
+
async def disconnect(self) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Disconnects from the command.
|
|
199
|
+
|
|
200
|
+
The command is not killed, but SDK stops receiving events from the command.
|
|
201
|
+
You can reconnect to the command using `sandbox.commands.connect` method.
|
|
202
|
+
"""
|
|
203
|
+
self._wait.cancel()
|
|
204
|
+
await asyncio.wait([self._wait])
|
|
205
|
+
try:
|
|
206
|
+
await self._events.aclose()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
async def _handle_events(self):
|
|
211
|
+
try:
|
|
212
|
+
async for stdout, stderr, pty in self._iterate_events():
|
|
213
|
+
if stdout is not None and self._on_stdout:
|
|
214
|
+
cb = self._on_stdout(stdout)
|
|
215
|
+
if inspect.isawaitable(cb):
|
|
216
|
+
await cb
|
|
217
|
+
elif stderr is not None and self._on_stderr:
|
|
218
|
+
cb = self._on_stderr(stderr)
|
|
219
|
+
if inspect.isawaitable(cb):
|
|
220
|
+
await cb
|
|
221
|
+
elif pty is not None and self._on_pty:
|
|
222
|
+
cb = self._on_pty(pty)
|
|
223
|
+
if inspect.isawaitable(cb):
|
|
224
|
+
await cb
|
|
225
|
+
except StopAsyncIteration:
|
|
226
|
+
pass
|
|
227
|
+
except Exception as e:
|
|
228
|
+
self._iteration_exception = await ahandle_rpc_exception_with_health(
|
|
229
|
+
e, self._check_health
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def wait(self) -> CommandResult:
|
|
233
|
+
"""
|
|
234
|
+
Wait for the command to finish and return the result.
|
|
235
|
+
If the command exits with a non-zero exit code, it throws a `CommandExitException`.
|
|
236
|
+
|
|
237
|
+
:return: `CommandResult` result of command execution
|
|
238
|
+
"""
|
|
239
|
+
await self._wait
|
|
240
|
+
if self._iteration_exception:
|
|
241
|
+
raise self._iteration_exception
|
|
242
|
+
|
|
243
|
+
if self._result is None:
|
|
244
|
+
raise Exception("Command ended without an end event")
|
|
245
|
+
|
|
246
|
+
if self._result.exit_code != 0:
|
|
247
|
+
raise CommandExitException(
|
|
248
|
+
stdout="".join(self._stdout_chunks),
|
|
249
|
+
stderr="".join(self._stderr_chunks),
|
|
250
|
+
exit_code=self._result.exit_code,
|
|
251
|
+
error=self._result.error,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return self._result
|
|
255
|
+
|
|
256
|
+
async def kill(self) -> bool:
|
|
257
|
+
"""
|
|
258
|
+
Kills the command.
|
|
259
|
+
|
|
260
|
+
It uses `SIGKILL` signal to kill the command
|
|
261
|
+
|
|
262
|
+
:return: `True` if the command was killed successfully, `False` if the command was not found
|
|
263
|
+
"""
|
|
264
|
+
result = await self._handle_kill()
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
async def send_stdin(
|
|
268
|
+
self,
|
|
269
|
+
data: Union[str, bytes],
|
|
270
|
+
request_timeout: Optional[float] = None,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""
|
|
273
|
+
Send data to the command stdin.
|
|
274
|
+
|
|
275
|
+
The command must have been started with `stdin=True`.
|
|
276
|
+
|
|
277
|
+
:param data: Data to send to the command
|
|
278
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
279
|
+
"""
|
|
280
|
+
if self._handle_send_stdin is None:
|
|
281
|
+
raise SandboxException(
|
|
282
|
+
"Sending stdin is not supported for this command handle."
|
|
283
|
+
)
|
|
284
|
+
await self._handle_send_stdin(data, request_timeout)
|
|
285
|
+
|
|
286
|
+
async def close_stdin(self, request_timeout: Optional[float] = None) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Close the command stdin.
|
|
289
|
+
|
|
290
|
+
This signals EOF to the command. The command must have been started with `stdin=True`.
|
|
291
|
+
|
|
292
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
293
|
+
"""
|
|
294
|
+
if self._handle_close_stdin is None:
|
|
295
|
+
raise SandboxException(
|
|
296
|
+
"Closing stdin is not supported for this command handle."
|
|
297
|
+
)
|
|
298
|
+
await self._handle_close_stdin(request_timeout)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
import loopix_connect
|
|
4
|
+
import httpcore
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from packaging.version import Version
|
|
8
|
+
from loopix.envd.process import process_connect, process_pb2
|
|
9
|
+
from loopix.connection_config import (
|
|
10
|
+
Username,
|
|
11
|
+
ConnectionConfig,
|
|
12
|
+
KEEPALIVE_PING_HEADER,
|
|
13
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
14
|
+
)
|
|
15
|
+
from loopix.exceptions import SandboxException
|
|
16
|
+
from loopix.envd.api import acheck_sandbox_health
|
|
17
|
+
from loopix.envd.rpc import authentication_header, ahandle_rpc_exception_with_health
|
|
18
|
+
from loopix.sandbox.commands.command_handle import PtySize
|
|
19
|
+
from loopix.sandbox_async.commands.command_handle import (
|
|
20
|
+
AsyncCommandHandle,
|
|
21
|
+
OutputHandler,
|
|
22
|
+
PtyOutput,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Pty:
|
|
27
|
+
"""
|
|
28
|
+
Module for interacting with PTYs (pseudo-terminals) in the sandbox.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
envd_api_url: str,
|
|
34
|
+
connection_config: ConnectionConfig,
|
|
35
|
+
pool: httpcore.AsyncConnectionPool,
|
|
36
|
+
envd_version: Version,
|
|
37
|
+
envd_api: httpx.AsyncClient,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._connection_config = connection_config
|
|
40
|
+
self._envd_version = envd_version
|
|
41
|
+
self._check_health = lambda: acheck_sandbox_health(envd_api)
|
|
42
|
+
self._rpc = process_connect.ProcessClient(
|
|
43
|
+
envd_api_url,
|
|
44
|
+
# TODO: Fix and enable compression again — the headers compression is not solved for streaming.
|
|
45
|
+
# compressor=loopix_connect.GzipCompressor,
|
|
46
|
+
async_pool=pool,
|
|
47
|
+
json=True,
|
|
48
|
+
headers=connection_config.sandbox_headers,
|
|
49
|
+
logger=connection_config.logger,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def kill(
|
|
53
|
+
self,
|
|
54
|
+
pid: int,
|
|
55
|
+
request_timeout: Optional[float] = None,
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Kill PTY.
|
|
59
|
+
|
|
60
|
+
:param pid: Process ID of the PTY
|
|
61
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
62
|
+
|
|
63
|
+
:return: `true` if the PTY was killed, `false` if the PTY was not found
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
await self._rpc.asend_signal(
|
|
67
|
+
process_pb2.SendSignalRequest(
|
|
68
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
69
|
+
signal=process_pb2.Signal.SIGNAL_SIGKILL,
|
|
70
|
+
),
|
|
71
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
72
|
+
request_timeout
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
return True
|
|
76
|
+
except Exception as e:
|
|
77
|
+
if isinstance(e, loopix_connect.ConnectException):
|
|
78
|
+
if e.status == loopix_connect.Code.not_found:
|
|
79
|
+
return False
|
|
80
|
+
raise await ahandle_rpc_exception_with_health(e, self._check_health)
|
|
81
|
+
|
|
82
|
+
async def send_stdin(
|
|
83
|
+
self,
|
|
84
|
+
pid: int,
|
|
85
|
+
data: bytes,
|
|
86
|
+
request_timeout: Optional[float] = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Send input to a PTY.
|
|
90
|
+
|
|
91
|
+
:param pid: Process ID of the PTY
|
|
92
|
+
:param data: Input data to send
|
|
93
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
await self._rpc.asend_input(
|
|
97
|
+
process_pb2.SendInputRequest(
|
|
98
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
99
|
+
input=process_pb2.ProcessInput(
|
|
100
|
+
pty=data,
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
104
|
+
request_timeout
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise await ahandle_rpc_exception_with_health(e, self._check_health)
|
|
109
|
+
|
|
110
|
+
async def create(
|
|
111
|
+
self,
|
|
112
|
+
size: PtySize,
|
|
113
|
+
on_data: OutputHandler[PtyOutput],
|
|
114
|
+
user: Optional[Username] = None,
|
|
115
|
+
cwd: Optional[str] = None,
|
|
116
|
+
envs: Optional[Dict[str, str]] = None,
|
|
117
|
+
timeout: Optional[float] = 60,
|
|
118
|
+
request_timeout: Optional[float] = None,
|
|
119
|
+
) -> AsyncCommandHandle:
|
|
120
|
+
"""
|
|
121
|
+
Start a new PTY (pseudo-terminal).
|
|
122
|
+
|
|
123
|
+
:param size: Size of the PTY
|
|
124
|
+
:param on_data: Callback to handle PTY data
|
|
125
|
+
:param user: User to use for the PTY
|
|
126
|
+
:param cwd: Working directory for the PTY
|
|
127
|
+
:param envs: Environment variables for the PTY
|
|
128
|
+
:param timeout: Timeout for the PTY in **seconds**
|
|
129
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
130
|
+
|
|
131
|
+
:return: Handle to interact with the PTY
|
|
132
|
+
"""
|
|
133
|
+
envs = dict(envs) if envs else {}
|
|
134
|
+
envs.setdefault("TERM", "xterm-256color")
|
|
135
|
+
envs.setdefault("LANG", "C.UTF-8")
|
|
136
|
+
envs.setdefault("LC_ALL", "C.UTF-8")
|
|
137
|
+
events = self._rpc.astart(
|
|
138
|
+
process_pb2.StartRequest(
|
|
139
|
+
process=process_pb2.ProcessConfig(
|
|
140
|
+
cmd="/bin/bash",
|
|
141
|
+
envs=envs,
|
|
142
|
+
args=["-i", "-l"],
|
|
143
|
+
cwd=cwd,
|
|
144
|
+
),
|
|
145
|
+
pty=process_pb2.PTY(
|
|
146
|
+
size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols)
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
headers={
|
|
150
|
+
**authentication_header(self._envd_version, user),
|
|
151
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
152
|
+
},
|
|
153
|
+
timeout=timeout,
|
|
154
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
155
|
+
request_timeout
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
start_event = await events.__anext__()
|
|
161
|
+
|
|
162
|
+
if not start_event.HasField("event"):
|
|
163
|
+
raise SandboxException(
|
|
164
|
+
f"Failed to start process: expected start event, got {start_event}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return AsyncCommandHandle(
|
|
168
|
+
pid=start_event.event.start.pid,
|
|
169
|
+
handle_kill=lambda: self.kill(start_event.event.start.pid),
|
|
170
|
+
events=events,
|
|
171
|
+
on_pty=on_data,
|
|
172
|
+
check_health=self._check_health,
|
|
173
|
+
)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
try:
|
|
176
|
+
await events.aclose()
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
raise await ahandle_rpc_exception_with_health(e, self._check_health)
|
|
180
|
+
|
|
181
|
+
async def connect(
|
|
182
|
+
self,
|
|
183
|
+
pid: int,
|
|
184
|
+
on_data: OutputHandler[PtyOutput],
|
|
185
|
+
timeout: Optional[float] = 60,
|
|
186
|
+
request_timeout: Optional[float] = None,
|
|
187
|
+
) -> AsyncCommandHandle:
|
|
188
|
+
"""
|
|
189
|
+
Connect to a running PTY.
|
|
190
|
+
|
|
191
|
+
:param pid: Process ID of the PTY to connect to. You can get the list of running PTYs using `sandbox.pty.list()`.
|
|
192
|
+
:param on_data: Callback to handle PTY data
|
|
193
|
+
:param timeout: Timeout for the PTY connection in **seconds**. Using `0` will not limit the connection time
|
|
194
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
195
|
+
|
|
196
|
+
:return: Handle to interact with the PTY
|
|
197
|
+
"""
|
|
198
|
+
events = self._rpc.aconnect(
|
|
199
|
+
process_pb2.ConnectRequest(
|
|
200
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
201
|
+
),
|
|
202
|
+
timeout=timeout,
|
|
203
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
204
|
+
request_timeout
|
|
205
|
+
),
|
|
206
|
+
headers={
|
|
207
|
+
KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
start_event = await events.__anext__()
|
|
213
|
+
|
|
214
|
+
if not start_event.HasField("event"):
|
|
215
|
+
raise SandboxException(
|
|
216
|
+
f"Failed to connect to process: expected start event, got {start_event}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return AsyncCommandHandle(
|
|
220
|
+
pid=start_event.event.start.pid,
|
|
221
|
+
handle_kill=lambda: self.kill(start_event.event.start.pid),
|
|
222
|
+
events=events,
|
|
223
|
+
on_pty=on_data,
|
|
224
|
+
check_health=self._check_health,
|
|
225
|
+
)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
try:
|
|
228
|
+
await events.aclose()
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
raise await ahandle_rpc_exception_with_health(e, self._check_health)
|
|
232
|
+
|
|
233
|
+
async def resize(
|
|
234
|
+
self,
|
|
235
|
+
pid: int,
|
|
236
|
+
size: PtySize,
|
|
237
|
+
request_timeout: Optional[float] = None,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Resize PTY.
|
|
241
|
+
Call this when the terminal window is resized and the number of columns and rows has changed.
|
|
242
|
+
|
|
243
|
+
:param pid: Process ID of the PTY
|
|
244
|
+
:param size: New size of the PTY
|
|
245
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
246
|
+
"""
|
|
247
|
+
await self._rpc.aupdate(
|
|
248
|
+
process_pb2.UpdateRequest(
|
|
249
|
+
process=process_pb2.ProcessSelector(pid=pid),
|
|
250
|
+
pty=process_pb2.PTY(
|
|
251
|
+
size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols),
|
|
252
|
+
),
|
|
253
|
+
),
|
|
254
|
+
request_timeout=self._connection_config.get_request_timeout(
|
|
255
|
+
request_timeout
|
|
256
|
+
),
|
|
257
|
+
)
|