scalebox-sdk 0.1.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.
- scalebox/__init__.py +80 -0
- scalebox/api/__init__.py +128 -0
- scalebox/api/client/__init__.py +8 -0
- scalebox/api/client/api/__init__.py +1 -0
- scalebox/api/client/api/sandboxes/__init__.py +0 -0
- scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
- scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
- scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
- scalebox/api/client/client.py +288 -0
- scalebox/api/client/errors.py +16 -0
- scalebox/api/client/models/__init__.py +81 -0
- scalebox/api/client/models/build_log_entry.py +79 -0
- scalebox/api/client/models/created_access_token.py +100 -0
- scalebox/api/client/models/created_team_api_key.py +166 -0
- scalebox/api/client/models/error.py +67 -0
- scalebox/api/client/models/identifier_masking_details.py +83 -0
- scalebox/api/client/models/listed_sandbox.py +138 -0
- scalebox/api/client/models/log_level.py +11 -0
- scalebox/api/client/models/new_access_token.py +59 -0
- scalebox/api/client/models/new_sandbox.py +125 -0
- scalebox/api/client/models/new_team_api_key.py +59 -0
- scalebox/api/client/models/node.py +154 -0
- scalebox/api/client/models/node_detail.py +152 -0
- scalebox/api/client/models/node_status.py +11 -0
- scalebox/api/client/models/node_status_change.py +61 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- scalebox/api/client/models/resumed_sandbox.py +68 -0
- scalebox/api/client/models/sandbox.py +125 -0
- scalebox/api/client/models/sandbox_detail.py +178 -0
- scalebox/api/client/models/sandbox_log.py +70 -0
- scalebox/api/client/models/sandbox_logs.py +73 -0
- scalebox/api/client/models/sandbox_metric.py +110 -0
- scalebox/api/client/models/sandbox_state.py +9 -0
- scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
- scalebox/api/client/models/team.py +83 -0
- scalebox/api/client/models/team_api_key.py +158 -0
- scalebox/api/client/models/team_user.py +68 -0
- scalebox/api/client/models/template.py +179 -0
- scalebox/api/client/models/template_build.py +117 -0
- scalebox/api/client/models/template_build_file_upload.py +70 -0
- scalebox/api/client/models/template_build_request.py +115 -0
- scalebox/api/client/models/template_build_request_v2.py +88 -0
- scalebox/api/client/models/template_build_start_v2.py +114 -0
- scalebox/api/client/models/template_build_status.py +11 -0
- scalebox/api/client/models/template_step.py +91 -0
- scalebox/api/client/models/template_update_request.py +59 -0
- scalebox/api/client/models/update_team_api_key.py +59 -0
- scalebox/api/client/py.typed +1 -0
- scalebox/api/client/types.py +46 -0
- scalebox/api/metadata.py +19 -0
- scalebox/cli.py +125 -0
- scalebox/client/__init__.py +0 -0
- scalebox/client/aclient.py +57 -0
- scalebox/client/api.proto +460 -0
- scalebox/client/buf.gen.yaml +8 -0
- scalebox/client/client.py +102 -0
- scalebox/client/requirements.txt +5 -0
- scalebox/code_interpreter/__init__.py +12 -0
- scalebox/code_interpreter/charts.py +230 -0
- scalebox/code_interpreter/code_interpreter_async.py +369 -0
- scalebox/code_interpreter/code_interpreter_sync.py +317 -0
- scalebox/code_interpreter/constants.py +3 -0
- scalebox/code_interpreter/exceptions.py +13 -0
- scalebox/code_interpreter/models.py +485 -0
- scalebox/connection_config.py +92 -0
- scalebox/csx_connect/__init__.py +1 -0
- scalebox/csx_connect/client.py +485 -0
- scalebox/csx_desktop/__init__.py +0 -0
- scalebox/csx_desktop/main.py +651 -0
- scalebox/exceptions.py +83 -0
- scalebox/generated/__init__.py +0 -0
- scalebox/generated/api.py +61 -0
- scalebox/generated/api_pb2.py +203 -0
- scalebox/generated/api_pb2.pyi +956 -0
- scalebox/generated/api_pb2_connect.py +1456 -0
- scalebox/generated/rpc.py +50 -0
- scalebox/generated/versions.py +3 -0
- scalebox/requirements.txt +36 -0
- scalebox/sandbox/__init__.py +0 -0
- scalebox/sandbox/commands/__init__.py +0 -0
- scalebox/sandbox/commands/command_handle.py +69 -0
- scalebox/sandbox/commands/main.py +39 -0
- scalebox/sandbox/filesystem/__init__.py +0 -0
- scalebox/sandbox/filesystem/filesystem.py +95 -0
- scalebox/sandbox/filesystem/watch_handle.py +60 -0
- scalebox/sandbox/main.py +139 -0
- scalebox/sandbox/sandbox_api.py +91 -0
- scalebox/sandbox/signature.py +40 -0
- scalebox/sandbox/utils.py +34 -0
- scalebox/sandbox_async/__init__.py +1 -0
- scalebox/sandbox_async/commands/command.py +307 -0
- scalebox/sandbox_async/commands/command_handle.py +187 -0
- scalebox/sandbox_async/commands/pty.py +187 -0
- scalebox/sandbox_async/filesystem/filesystem.py +557 -0
- scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
- scalebox/sandbox_async/main.py +646 -0
- scalebox/sandbox_async/sandbox_api.py +365 -0
- scalebox/sandbox_async/utils.py +7 -0
- scalebox/sandbox_sync/__init__.py +2 -0
- scalebox/sandbox_sync/commands/__init__.py +0 -0
- scalebox/sandbox_sync/commands/command.py +300 -0
- scalebox/sandbox_sync/commands/command_handle.py +150 -0
- scalebox/sandbox_sync/commands/pty.py +181 -0
- scalebox/sandbox_sync/filesystem/__init__.py +0 -0
- scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
- scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
- scalebox/sandbox_sync/main.py +790 -0
- scalebox/sandbox_sync/sandbox_api.py +356 -0
- scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
- scalebox/test/README.md +329 -0
- scalebox/test/__init__.py +0 -0
- scalebox/test/aclient.py +72 -0
- scalebox/test/code_interpreter_centext.py +21 -0
- scalebox/test/code_interpreter_centext_sync.py +21 -0
- scalebox/test/code_interpreter_test.py +34 -0
- scalebox/test/code_interpreter_test_sync.py +34 -0
- scalebox/test/run_all_validation_tests.py +334 -0
- scalebox/test/run_code_interpreter_tests.sh +67 -0
- scalebox/test/run_tests.sh +230 -0
- scalebox/test/test_basic.py +78 -0
- scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
- scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
- scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
- scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
- scalebox/test/test_e2b_first.py +11 -0
- scalebox/test/test_sandbox_async_comprehensive.py +738 -0
- scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
- scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
- scalebox/test/test_sandbox_usage_examples.py +987 -0
- scalebox/test/testacreate.py +24 -0
- scalebox/test/testagetinfo.py +18 -0
- scalebox/test/testcodeinterpreter_async.py +508 -0
- scalebox/test/testcodeinterpreter_sync.py +239 -0
- scalebox/test/testcomputeuse.py +243 -0
- scalebox/test/testnovnc.py +12 -0
- scalebox/test/testsandbox_async.py +118 -0
- scalebox/test/testsandbox_sync.py +38 -0
- scalebox/utils/__init__.py +0 -0
- scalebox/utils/httpcoreclient.py +297 -0
- scalebox/utils/httpxclient.py +403 -0
- scalebox/version.py +16 -0
- scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
- scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
- scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
- scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- scalebox_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from typing import Dict, List, Literal, Optional, Union, overload
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from ... import csx_connect
|
|
6
|
+
from ...connection_config import (
|
|
7
|
+
KEEPALIVE_PING_HEADER,
|
|
8
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
9
|
+
ConnectionConfig,
|
|
10
|
+
Username,
|
|
11
|
+
)
|
|
12
|
+
from ...exceptions import SandboxException
|
|
13
|
+
from ...generated import api_pb2, api_pb2_connect
|
|
14
|
+
from ...generated.rpc import handle_rpc_exception
|
|
15
|
+
from ...sandbox.commands.command_handle import CommandResult
|
|
16
|
+
from ...sandbox.commands.main import ProcessInfo
|
|
17
|
+
from ...sandbox_async.commands.command_handle import AsyncCommandHandle, Stderr, Stdout
|
|
18
|
+
from ...sandbox_async.utils import OutputHandler
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Commands:
|
|
22
|
+
"""
|
|
23
|
+
Module for executing commands in the sandbox.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
envd_api_url: str,
|
|
29
|
+
connection_config: ConnectionConfig,
|
|
30
|
+
pool: aiohttp.ClientSession,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._connection_config = connection_config
|
|
33
|
+
self._rpc = api_pb2_connect.AsyncProcessClient(
|
|
34
|
+
envd_api_url,
|
|
35
|
+
http_client=pool
|
|
36
|
+
)
|
|
37
|
+
self._headers=connection_config.headers
|
|
38
|
+
self._pool = pool
|
|
39
|
+
|
|
40
|
+
async def list(
|
|
41
|
+
self,
|
|
42
|
+
request_timeout: Optional[float] = None,
|
|
43
|
+
) -> List[ProcessInfo]:
|
|
44
|
+
"""
|
|
45
|
+
Lists all running commands and PTY sessions.
|
|
46
|
+
|
|
47
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
48
|
+
|
|
49
|
+
:return: List of running commands and PTY sessions
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
res = await self._rpc.list(
|
|
53
|
+
api_pb2.ListRequest(),
|
|
54
|
+
self._headers,
|
|
55
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
56
|
+
request_timeout
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
return [
|
|
60
|
+
ProcessInfo(
|
|
61
|
+
pid=p.pid,
|
|
62
|
+
tag=p.tag,
|
|
63
|
+
cmd=p.config.cmd,
|
|
64
|
+
args=list(p.config.args),
|
|
65
|
+
envs=dict(p.config.envs),
|
|
66
|
+
cwd=p.config.cwd,
|
|
67
|
+
)
|
|
68
|
+
for p in res.processes
|
|
69
|
+
]
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise handle_rpc_exception(e)
|
|
72
|
+
|
|
73
|
+
async def kill(
|
|
74
|
+
self,
|
|
75
|
+
pid: int,
|
|
76
|
+
request_timeout: Optional[float] = None,
|
|
77
|
+
) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Kill a running command specified by its process ID.
|
|
80
|
+
It uses `SIGKILL` signal to kill the command.
|
|
81
|
+
|
|
82
|
+
:param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()`
|
|
83
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
84
|
+
|
|
85
|
+
:return: `True` if the command was killed, `False` if the command was not found
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
await self._rpc.send_signal(
|
|
89
|
+
api_pb2.SendSignalRequest(
|
|
90
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
91
|
+
signal=api_pb2.Signal.SIGNAL_SIGKILL,
|
|
92
|
+
),
|
|
93
|
+
self._headers,
|
|
94
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
95
|
+
request_timeout
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
return True
|
|
99
|
+
except Exception as e:
|
|
100
|
+
if "not found" in str(e):
|
|
101
|
+
return False
|
|
102
|
+
raise handle_rpc_exception(e)
|
|
103
|
+
|
|
104
|
+
async def send_stdin(
|
|
105
|
+
self,
|
|
106
|
+
pid: int,
|
|
107
|
+
data: str,
|
|
108
|
+
request_timeout: Optional[float] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Send data to command stdin.
|
|
112
|
+
|
|
113
|
+
:param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
|
|
114
|
+
:param data: Data to send to the command
|
|
115
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
await self._rpc.send_input(
|
|
119
|
+
api_pb2.SendInputRequest(
|
|
120
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
121
|
+
input=api_pb2.ProcessInput(
|
|
122
|
+
stdin=data.encode(),
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
self._headers,
|
|
126
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
127
|
+
request_timeout
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(e)
|
|
132
|
+
raise handle_rpc_exception(e)
|
|
133
|
+
|
|
134
|
+
@overload
|
|
135
|
+
async def run(
|
|
136
|
+
self,
|
|
137
|
+
cmd: str,
|
|
138
|
+
background: Union[Literal[False], None] = None,
|
|
139
|
+
envs: Optional[Dict[str, str]] = None,
|
|
140
|
+
user: Username = "user",
|
|
141
|
+
cwd: Optional[str] = None,
|
|
142
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
143
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
144
|
+
timeout: Optional[float] = 60,
|
|
145
|
+
request_timeout: Optional[float] = None,
|
|
146
|
+
) -> CommandResult:
|
|
147
|
+
"""
|
|
148
|
+
Start a new command and wait until it finishes executing.
|
|
149
|
+
|
|
150
|
+
:param cmd: Command to execute
|
|
151
|
+
:param background: **`False` if the command should be executed in the foreground**, `True` if the command should be executed in the background
|
|
152
|
+
:param envs: Environment variables used for the command
|
|
153
|
+
:param user: User to run the command as
|
|
154
|
+
:param cwd: Working directory to run the command
|
|
155
|
+
:param on_stdout: Callback for command stdout output
|
|
156
|
+
:param on_stderr: Callback for command stderr output
|
|
157
|
+
:param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
|
|
158
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
159
|
+
|
|
160
|
+
:return: `CommandResult` result of the command execution
|
|
161
|
+
"""
|
|
162
|
+
...
|
|
163
|
+
|
|
164
|
+
@overload
|
|
165
|
+
async def run(
|
|
166
|
+
self,
|
|
167
|
+
cmd: str,
|
|
168
|
+
background: Literal[True],
|
|
169
|
+
envs: Optional[Dict[str, str]] = None,
|
|
170
|
+
user: Username = "user",
|
|
171
|
+
cwd: Optional[str] = None,
|
|
172
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
173
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
174
|
+
timeout: Optional[float] = 60,
|
|
175
|
+
request_timeout: Optional[float] = None,
|
|
176
|
+
) -> AsyncCommandHandle:
|
|
177
|
+
"""
|
|
178
|
+
Start a new command and return a handle to interact with it.
|
|
179
|
+
|
|
180
|
+
:param cmd: Command to execute
|
|
181
|
+
:param background: `False` if the command should be executed in the foreground, **`True` if the command should be executed in the background**
|
|
182
|
+
:param envs: Environment variables used for the command
|
|
183
|
+
:param user: User to run the command as
|
|
184
|
+
:param cwd: Working directory to run the command
|
|
185
|
+
:param on_stdout: Callback for command stdout output
|
|
186
|
+
:param on_stderr: Callback for command stderr output
|
|
187
|
+
:param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
|
|
188
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
189
|
+
|
|
190
|
+
:return: `AsyncCommandHandle` handle to interact with the running command
|
|
191
|
+
"""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
async def run(
|
|
195
|
+
self,
|
|
196
|
+
cmd: str,
|
|
197
|
+
background: Union[bool, None] = None,
|
|
198
|
+
envs: Optional[Dict[str, str]] = None,
|
|
199
|
+
user: Username = "user",
|
|
200
|
+
cwd: Optional[str] = None,
|
|
201
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
202
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
203
|
+
timeout: Optional[float] = 60,
|
|
204
|
+
request_timeout: Optional[float] = None,
|
|
205
|
+
):
|
|
206
|
+
proc = await self._start(
|
|
207
|
+
cmd,
|
|
208
|
+
envs,
|
|
209
|
+
user,
|
|
210
|
+
cwd,
|
|
211
|
+
timeout,
|
|
212
|
+
request_timeout,
|
|
213
|
+
on_stdout=on_stdout,
|
|
214
|
+
on_stderr=on_stderr,
|
|
215
|
+
)
|
|
216
|
+
return proc if background else await proc.wait()
|
|
217
|
+
|
|
218
|
+
async def _start(
|
|
219
|
+
self,
|
|
220
|
+
cmd: str,
|
|
221
|
+
envs: Optional[Dict[str, str]] = None,
|
|
222
|
+
user: Username = "user",
|
|
223
|
+
cwd: Optional[str] = None,
|
|
224
|
+
timeout: Optional[float] = 60,
|
|
225
|
+
request_timeout: Optional[float] = None,
|
|
226
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
227
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
228
|
+
) -> AsyncCommandHandle:
|
|
229
|
+
try:
|
|
230
|
+
events = self._rpc.start(
|
|
231
|
+
api_pb2.StartRequest(
|
|
232
|
+
process=api_pb2.ProcessConfig(
|
|
233
|
+
cmd="/bin/bash",
|
|
234
|
+
envs=envs,
|
|
235
|
+
args=["-l", "-c", cmd],
|
|
236
|
+
cwd=cwd,
|
|
237
|
+
),
|
|
238
|
+
),
|
|
239
|
+
self._headers,
|
|
240
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
241
|
+
request_timeout
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
start_event = await events.__anext__()
|
|
245
|
+
|
|
246
|
+
if not start_event.HasField("event"):
|
|
247
|
+
raise SandboxException(
|
|
248
|
+
f"Failed to start process: expected start event, got {start_event}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return AsyncCommandHandle(
|
|
252
|
+
pid=start_event.event.start.pid,
|
|
253
|
+
handle_kill=lambda: self.kill(start_event.event.start.pid),
|
|
254
|
+
events=events,
|
|
255
|
+
on_stdout=on_stdout,
|
|
256
|
+
on_stderr=on_stderr,
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
raise handle_rpc_exception(e)
|
|
260
|
+
|
|
261
|
+
async def connect(
|
|
262
|
+
self,
|
|
263
|
+
pid: int,
|
|
264
|
+
timeout: Optional[float] = 60,
|
|
265
|
+
request_timeout: Optional[float] = None,
|
|
266
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
267
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
268
|
+
) -> AsyncCommandHandle:
|
|
269
|
+
"""
|
|
270
|
+
Connects to a running command.
|
|
271
|
+
You can use `AsyncCommandHandle.wait()` to wait for the command to finish and get execution results.
|
|
272
|
+
|
|
273
|
+
:param pid: Process ID of the command to connect to. You can get the list of processes using `sandbox.commands.list()`
|
|
274
|
+
:param request_timeout: Request timeout in **seconds**
|
|
275
|
+
:param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
|
|
276
|
+
:param on_stdout: Callback for command stdout output
|
|
277
|
+
:param on_stderr: Callback for command stderr output
|
|
278
|
+
|
|
279
|
+
:return: `AsyncCommandHandle` handle to interact with the running command
|
|
280
|
+
"""
|
|
281
|
+
events = self._rpc.connect(
|
|
282
|
+
api_pb2.ConnectRequest(
|
|
283
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
284
|
+
),
|
|
285
|
+
self._headers,
|
|
286
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
287
|
+
request_timeout
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
start_event = await events.__anext__()
|
|
293
|
+
|
|
294
|
+
if not start_event.HasField("event"):
|
|
295
|
+
raise SandboxException(
|
|
296
|
+
f"Failed to connect to process: expected start event, got {start_event}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return AsyncCommandHandle(
|
|
300
|
+
pid=start_event.event.start.pid,
|
|
301
|
+
handle_kill=lambda: self.kill(start_event.event.start.pid),
|
|
302
|
+
events=events,
|
|
303
|
+
on_stdout=on_stdout,
|
|
304
|
+
on_stderr=on_stderr,
|
|
305
|
+
)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
raise handle_rpc_exception(e)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, AsyncIterator, Callable, Coroutine, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
from ...generated import api_pb2
|
|
6
|
+
from ...generated.rpc import handle_rpc_exception
|
|
7
|
+
from ...sandbox.commands.command_handle import (
|
|
8
|
+
CommandExitException,
|
|
9
|
+
CommandResult,
|
|
10
|
+
PtyOutput,
|
|
11
|
+
Stderr,
|
|
12
|
+
Stdout,
|
|
13
|
+
)
|
|
14
|
+
from ...sandbox_async.utils import OutputHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AsyncCommandHandle:
|
|
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
|
+
@property
|
|
32
|
+
def stdout(self):
|
|
33
|
+
"""
|
|
34
|
+
Command stdout output.
|
|
35
|
+
"""
|
|
36
|
+
return self._stdout
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def stderr(self):
|
|
40
|
+
"""
|
|
41
|
+
Command stderr output.
|
|
42
|
+
"""
|
|
43
|
+
return self._stderr
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def error(self):
|
|
47
|
+
"""
|
|
48
|
+
Command execution error message.
|
|
49
|
+
"""
|
|
50
|
+
if self._result is None:
|
|
51
|
+
return None
|
|
52
|
+
return self._result.error
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def exit_code(self):
|
|
56
|
+
"""
|
|
57
|
+
Command execution exit code.
|
|
58
|
+
|
|
59
|
+
`0` if the command finished successfully.
|
|
60
|
+
|
|
61
|
+
It is `None` if the command is still running.
|
|
62
|
+
"""
|
|
63
|
+
if self._result is None:
|
|
64
|
+
return None
|
|
65
|
+
return self._result.exit_code
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
pid: int,
|
|
70
|
+
handle_kill: Callable[[], Coroutine[Any, Any, bool]],
|
|
71
|
+
events: AsyncIterator[
|
|
72
|
+
Union[api_pb2.StartResponse, api_pb2.ConnectResponse]
|
|
73
|
+
],
|
|
74
|
+
on_stdout: Optional[OutputHandler[Stdout]] = None,
|
|
75
|
+
on_stderr: Optional[OutputHandler[Stderr]] = None,
|
|
76
|
+
on_pty: Optional[OutputHandler[PtyOutput]] = None,
|
|
77
|
+
):
|
|
78
|
+
self._pid = pid
|
|
79
|
+
self._handle_kill = handle_kill
|
|
80
|
+
self._events = events
|
|
81
|
+
|
|
82
|
+
self._stdout: str = ""
|
|
83
|
+
self._stderr: str = ""
|
|
84
|
+
|
|
85
|
+
self._on_stdout = on_stdout
|
|
86
|
+
self._on_stderr = on_stderr
|
|
87
|
+
self._on_pty = on_pty
|
|
88
|
+
|
|
89
|
+
self._result: Optional[CommandResult] = None
|
|
90
|
+
self._iteration_exception: Optional[Exception] = None
|
|
91
|
+
|
|
92
|
+
self._wait = asyncio.create_task(self._handle_events())
|
|
93
|
+
|
|
94
|
+
async def _iterate_events(
|
|
95
|
+
self,
|
|
96
|
+
) -> AsyncIterator[
|
|
97
|
+
Union[
|
|
98
|
+
Tuple[Stdout, None, None],
|
|
99
|
+
Tuple[None, Stderr, None],
|
|
100
|
+
Tuple[None, None, PtyOutput],
|
|
101
|
+
],
|
|
102
|
+
]:
|
|
103
|
+
async for event in self._events:
|
|
104
|
+
if event.event.HasField("data"):
|
|
105
|
+
if event.event.data.stdout:
|
|
106
|
+
out = event.event.data.stdout.decode("utf-8", "replace")
|
|
107
|
+
self._stdout += out
|
|
108
|
+
yield out, None, None
|
|
109
|
+
if event.event.data.stderr:
|
|
110
|
+
out = event.event.data.stderr.decode("utf-8", "replace")
|
|
111
|
+
self._stderr += out
|
|
112
|
+
yield None, out, None
|
|
113
|
+
if event.event.data.pty:
|
|
114
|
+
yield None, None, event.event.data.pty
|
|
115
|
+
if event.event.HasField("end"):
|
|
116
|
+
self._result = CommandResult(
|
|
117
|
+
stdout=self._stdout,
|
|
118
|
+
stderr=self._stderr,
|
|
119
|
+
exit_code=event.event.end.exit_code,
|
|
120
|
+
error=event.event.end.error,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def disconnect(self) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Disconnects from the command.
|
|
126
|
+
|
|
127
|
+
The command is not killed, but SDK stops receiving events from the command.
|
|
128
|
+
You can reconnect to the command using `sandbox.commands.connect` method.
|
|
129
|
+
"""
|
|
130
|
+
self._wait.cancel()
|
|
131
|
+
# BUG: In Python 3.8 closing async generator can throw RuntimeError.
|
|
132
|
+
# await self._events.aclose()
|
|
133
|
+
|
|
134
|
+
async def _handle_events(self):
|
|
135
|
+
try:
|
|
136
|
+
async for stdout, stderr, pty in self._iterate_events():
|
|
137
|
+
if stdout is not None and self._on_stdout:
|
|
138
|
+
cb = self._on_stdout(stdout)
|
|
139
|
+
if inspect.isawaitable(cb):
|
|
140
|
+
await cb
|
|
141
|
+
elif stderr is not None and self._on_stderr:
|
|
142
|
+
cb = self._on_stderr(stderr)
|
|
143
|
+
if inspect.isawaitable(cb):
|
|
144
|
+
await cb
|
|
145
|
+
elif pty is not None and self._on_pty:
|
|
146
|
+
cb = self._on_pty(pty)
|
|
147
|
+
if inspect.isawaitable(cb):
|
|
148
|
+
await cb
|
|
149
|
+
except StopAsyncIteration:
|
|
150
|
+
pass
|
|
151
|
+
except Exception as e:
|
|
152
|
+
self._iteration_exception = handle_rpc_exception(e)
|
|
153
|
+
|
|
154
|
+
async def wait(self) -> CommandResult:
|
|
155
|
+
"""
|
|
156
|
+
Wait for the command to finish and return the result.
|
|
157
|
+
If the command exits with a non-zero exit code, it throws a `CommandExitException`.
|
|
158
|
+
|
|
159
|
+
:return: `CommandResult` result of command execution
|
|
160
|
+
"""
|
|
161
|
+
await self._wait
|
|
162
|
+
if self._iteration_exception:
|
|
163
|
+
raise self._iteration_exception
|
|
164
|
+
|
|
165
|
+
if self._result is None:
|
|
166
|
+
raise Exception("Command ended without an end event")
|
|
167
|
+
|
|
168
|
+
if self._result.exit_code != 0:
|
|
169
|
+
raise CommandExitException(
|
|
170
|
+
stdout=self._stdout,
|
|
171
|
+
stderr=self._stderr,
|
|
172
|
+
exit_code=self._result.exit_code,
|
|
173
|
+
error=self._result.error,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return self._result
|
|
177
|
+
|
|
178
|
+
async def kill(self) -> bool:
|
|
179
|
+
"""
|
|
180
|
+
Kills the command.
|
|
181
|
+
|
|
182
|
+
It uses `SIGKILL` signal to kill the command
|
|
183
|
+
|
|
184
|
+
:return: `True` if the command was killed successfully, `False` if the command was not found
|
|
185
|
+
"""
|
|
186
|
+
result = await self._handle_kill()
|
|
187
|
+
return result
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
|
|
5
|
+
from ... import csx_connect
|
|
6
|
+
from ...connection_config import (
|
|
7
|
+
KEEPALIVE_PING_HEADER,
|
|
8
|
+
KEEPALIVE_PING_INTERVAL_SEC,
|
|
9
|
+
ConnectionConfig,
|
|
10
|
+
Username,
|
|
11
|
+
)
|
|
12
|
+
from ...exceptions import SandboxException
|
|
13
|
+
from ...generated import api_pb2, api_pb2_connect
|
|
14
|
+
from ...generated.rpc import authentication_header, handle_rpc_exception
|
|
15
|
+
from ...sandbox.commands.command_handle import PtySize
|
|
16
|
+
from ...sandbox_async.commands.command_handle import (
|
|
17
|
+
AsyncCommandHandle,
|
|
18
|
+
OutputHandler,
|
|
19
|
+
PtyOutput,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Pty:
|
|
24
|
+
"""
|
|
25
|
+
Module for interacting with PTYs (pseudo-terminals) in the sandbox.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
envd_api_url: str,
|
|
31
|
+
connection_config: ConnectionConfig,
|
|
32
|
+
pool: aiohttp.ClientSession,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._connection_config = connection_config
|
|
35
|
+
self._rpc = api_pb2_connect.AsyncProcessClient(
|
|
36
|
+
envd_api_url,
|
|
37
|
+
pool
|
|
38
|
+
)
|
|
39
|
+
self._headers = connection_config.headers
|
|
40
|
+
self._pool = pool
|
|
41
|
+
|
|
42
|
+
async def kill(
|
|
43
|
+
self,
|
|
44
|
+
pid: int,
|
|
45
|
+
request_timeout: Optional[float] = None,
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Kill PTY.
|
|
49
|
+
|
|
50
|
+
:param pid: Process ID of the PTY
|
|
51
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
52
|
+
|
|
53
|
+
:return: `true` if the PTY was killed, `false` if the PTY was not found
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
await self._rpc.send_signal(
|
|
57
|
+
api_pb2.SendSignalRequest(
|
|
58
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
59
|
+
signal=api_pb2.Signal.SIGNAL_SIGKILL,
|
|
60
|
+
),
|
|
61
|
+
self._headers,
|
|
62
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
63
|
+
request_timeout
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
return True
|
|
67
|
+
except Exception as e:
|
|
68
|
+
if isinstance(e, csx_connect.ConnectException):
|
|
69
|
+
if e.status == csx_connect.Code.not_found:
|
|
70
|
+
return False
|
|
71
|
+
raise handle_rpc_exception(e)
|
|
72
|
+
|
|
73
|
+
async def send_stdin(
|
|
74
|
+
self,
|
|
75
|
+
pid: int,
|
|
76
|
+
data: bytes,
|
|
77
|
+
request_timeout: Optional[float] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Send input to a PTY.
|
|
81
|
+
|
|
82
|
+
:param pid: Process ID of the PTY
|
|
83
|
+
:param data: Input data to send
|
|
84
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
await self._rpc.send_input(
|
|
88
|
+
api_pb2.SendInputRequest(
|
|
89
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
90
|
+
input=api_pb2.ProcessInput(
|
|
91
|
+
pty=data,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
self._headers,
|
|
95
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
96
|
+
request_timeout
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise handle_rpc_exception(e)
|
|
101
|
+
|
|
102
|
+
async def create(
|
|
103
|
+
self,
|
|
104
|
+
size: PtySize,
|
|
105
|
+
on_data: OutputHandler[PtyOutput],
|
|
106
|
+
user: Username = "user",
|
|
107
|
+
cwd: Optional[str] = None,
|
|
108
|
+
envs: Optional[Dict[str, str]] = None,
|
|
109
|
+
timeout: Optional[float] = 60,
|
|
110
|
+
request_timeout: Optional[float] = None,
|
|
111
|
+
) -> AsyncCommandHandle:
|
|
112
|
+
"""
|
|
113
|
+
Start a new PTY (pseudo-terminal).
|
|
114
|
+
|
|
115
|
+
:param size: Size of the PTY
|
|
116
|
+
:param on_data: Callback to handle PTY data
|
|
117
|
+
:param user: User to use for the PTY
|
|
118
|
+
:param cwd: Working directory for the PTY
|
|
119
|
+
:param envs: Environment variables for the PTY
|
|
120
|
+
:param timeout: Timeout for the PTY in **seconds**
|
|
121
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
122
|
+
|
|
123
|
+
:return: Handle to interact with the PTY
|
|
124
|
+
"""
|
|
125
|
+
envs = envs or {}
|
|
126
|
+
envs["TERM"] = "xterm-256color"
|
|
127
|
+
events = self._rpc.start(
|
|
128
|
+
api_pb2.StartRequest(
|
|
129
|
+
process=api_pb2.ProcessConfig(
|
|
130
|
+
cmd="/bin/bash",
|
|
131
|
+
envs=envs,
|
|
132
|
+
args=["-i", "-l"],
|
|
133
|
+
cwd=cwd,
|
|
134
|
+
),
|
|
135
|
+
pty=api_pb2.PTY(
|
|
136
|
+
size=api_pb2.PTY.Size(rows=size.rows, cols=size.cols)
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
self._headers,
|
|
140
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
141
|
+
request_timeout
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
start_event = await events.__anext__()
|
|
147
|
+
|
|
148
|
+
if not start_event.HasField("event"):
|
|
149
|
+
raise SandboxException(
|
|
150
|
+
f"Failed to start process: expected start event, got {start_event}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return AsyncCommandHandle(
|
|
154
|
+
pid=start_event.event.start.pid,
|
|
155
|
+
handle_kill=lambda: self.kill(start_event.event.start.pid),
|
|
156
|
+
events=events,
|
|
157
|
+
on_pty=on_data,
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise handle_rpc_exception(e)
|
|
161
|
+
|
|
162
|
+
async def resize(
|
|
163
|
+
self,
|
|
164
|
+
pid: int,
|
|
165
|
+
size: PtySize,
|
|
166
|
+
request_timeout: Optional[float] = None,
|
|
167
|
+
):
|
|
168
|
+
"""
|
|
169
|
+
Resize PTY.
|
|
170
|
+
Call this when the terminal window is resized and the number of columns and rows has changed.
|
|
171
|
+
|
|
172
|
+
:param pid: Process ID of the PTY
|
|
173
|
+
:param size: New size of the PTY
|
|
174
|
+
:param request_timeout: Timeout for the request in **seconds**
|
|
175
|
+
"""
|
|
176
|
+
await self._rpc.update(
|
|
177
|
+
api_pb2.UpdateRequest(
|
|
178
|
+
process=api_pb2.ProcessSelector(pid=pid),
|
|
179
|
+
pty=api_pb2.PTY(
|
|
180
|
+
size=api_pb2.PTY.Size(rows=size.rows, cols=size.cols),
|
|
181
|
+
),
|
|
182
|
+
),
|
|
183
|
+
self._headers,
|
|
184
|
+
timeout_seconds=self._connection_config.get_request_timeout(
|
|
185
|
+
request_timeout
|
|
186
|
+
),
|
|
187
|
+
)
|