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.
Files changed (238) hide show
  1. loopix/__init__.py +260 -0
  2. loopix/api/__init__.py +287 -0
  3. loopix/api/client/__init__.py +8 -0
  4. loopix/api/client/api/__init__.py +1 -0
  5. loopix/api/client/api/sandboxes/__init__.py +1 -0
  6. loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
  14. loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
  15. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  16. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
  17. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  18. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  19. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
  20. loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  21. loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
  22. loopix/api/client/api/snapshots/__init__.py +1 -0
  23. loopix/api/client/api/snapshots/get_snapshots.py +202 -0
  24. loopix/api/client/api/tags/__init__.py +1 -0
  25. loopix/api/client/api/tags/delete_templates_tags.py +174 -0
  26. loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
  27. loopix/api/client/api/tags/post_templates_tags.py +176 -0
  28. loopix/api/client/api/templates/__init__.py +1 -0
  29. loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
  30. loopix/api/client/api/templates/get_templates.py +172 -0
  31. loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
  32. loopix/api/client/api/templates/get_templates_template_id.py +195 -0
  33. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
  34. loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
  35. loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  36. loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
  37. loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
  38. loopix/api/client/api/templates/post_templates.py +172 -0
  39. loopix/api/client/api/templates/post_templates_template_id.py +181 -0
  40. loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  41. loopix/api/client/api/templates/post_v2_templates.py +172 -0
  42. loopix/api/client/api/templates/post_v3_templates.py +176 -0
  43. loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  44. loopix/api/client/api/volumes/__init__.py +1 -0
  45. loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
  46. loopix/api/client/api/volumes/get_volumes.py +140 -0
  47. loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
  48. loopix/api/client/api/volumes/post_volumes.py +172 -0
  49. loopix/api/client/client.py +286 -0
  50. loopix/api/client/errors.py +16 -0
  51. loopix/api/client/models/__init__.py +185 -0
  52. loopix/api/client/models/admin_build_cancel_result.py +67 -0
  53. loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
  54. loopix/api/client/models/assign_template_tags_request.py +67 -0
  55. loopix/api/client/models/assigned_template_tags.py +68 -0
  56. loopix/api/client/models/aws_registry.py +85 -0
  57. loopix/api/client/models/aws_registry_type.py +8 -0
  58. loopix/api/client/models/build_log_entry.py +89 -0
  59. loopix/api/client/models/build_status_reason.py +95 -0
  60. loopix/api/client/models/connect_sandbox.py +59 -0
  61. loopix/api/client/models/created_access_token.py +100 -0
  62. loopix/api/client/models/created_team_api_key.py +166 -0
  63. loopix/api/client/models/delete_template_tags_request.py +67 -0
  64. loopix/api/client/models/disk_metrics.py +91 -0
  65. loopix/api/client/models/error.py +67 -0
  66. loopix/api/client/models/gcp_registry.py +69 -0
  67. loopix/api/client/models/gcp_registry_type.py +8 -0
  68. loopix/api/client/models/general_registry.py +77 -0
  69. loopix/api/client/models/general_registry_type.py +8 -0
  70. loopix/api/client/models/identifier_masking_details.py +83 -0
  71. loopix/api/client/models/listed_sandbox.py +179 -0
  72. loopix/api/client/models/log_level.py +11 -0
  73. loopix/api/client/models/logs_direction.py +9 -0
  74. loopix/api/client/models/logs_source.py +9 -0
  75. loopix/api/client/models/machine_info.py +83 -0
  76. loopix/api/client/models/max_team_metric.py +78 -0
  77. loopix/api/client/models/mcp_type_0.py +44 -0
  78. loopix/api/client/models/new_access_token.py +59 -0
  79. loopix/api/client/models/new_sandbox.py +224 -0
  80. loopix/api/client/models/new_team_api_key.py +59 -0
  81. loopix/api/client/models/new_volume.py +59 -0
  82. loopix/api/client/models/node.py +160 -0
  83. loopix/api/client/models/node_detail.py +160 -0
  84. loopix/api/client/models/node_metrics.py +122 -0
  85. loopix/api/client/models/node_status.py +12 -0
  86. loopix/api/client/models/node_status_change.py +82 -0
  87. loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  88. loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
  89. loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  90. loopix/api/client/models/resumed_sandbox.py +68 -0
  91. loopix/api/client/models/sandbox.py +145 -0
  92. loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
  93. loopix/api/client/models/sandbox_detail.py +267 -0
  94. loopix/api/client/models/sandbox_lifecycle.py +70 -0
  95. loopix/api/client/models/sandbox_log.py +70 -0
  96. loopix/api/client/models/sandbox_log_entry.py +93 -0
  97. loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
  98. loopix/api/client/models/sandbox_logs.py +91 -0
  99. loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
  100. loopix/api/client/models/sandbox_metric.py +126 -0
  101. loopix/api/client/models/sandbox_network_config.py +118 -0
  102. loopix/api/client/models/sandbox_network_config_rules.py +72 -0
  103. loopix/api/client/models/sandbox_network_rule.py +74 -0
  104. loopix/api/client/models/sandbox_network_transform.py +79 -0
  105. loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
  106. loopix/api/client/models/sandbox_network_update_config.py +114 -0
  107. loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
  108. loopix/api/client/models/sandbox_on_timeout.py +9 -0
  109. loopix/api/client/models/sandbox_pause_request.py +62 -0
  110. loopix/api/client/models/sandbox_state.py +9 -0
  111. loopix/api/client/models/sandbox_volume_mount.py +67 -0
  112. loopix/api/client/models/sandboxes_with_metrics.py +59 -0
  113. loopix/api/client/models/snapshot_info.py +70 -0
  114. loopix/api/client/models/team.py +83 -0
  115. loopix/api/client/models/team_api_key.py +158 -0
  116. loopix/api/client/models/team_metric.py +86 -0
  117. loopix/api/client/models/team_user.py +75 -0
  118. loopix/api/client/models/template.py +225 -0
  119. loopix/api/client/models/template_alias_response.py +67 -0
  120. loopix/api/client/models/template_build.py +139 -0
  121. loopix/api/client/models/template_build_file_upload.py +70 -0
  122. loopix/api/client/models/template_build_info.py +126 -0
  123. loopix/api/client/models/template_build_logs_response.py +73 -0
  124. loopix/api/client/models/template_build_request.py +115 -0
  125. loopix/api/client/models/template_build_request_v2.py +88 -0
  126. loopix/api/client/models/template_build_request_v3.py +107 -0
  127. loopix/api/client/models/template_build_start_v2.py +184 -0
  128. loopix/api/client/models/template_build_status.py +11 -0
  129. loopix/api/client/models/template_legacy.py +207 -0
  130. loopix/api/client/models/template_request_response_v3.py +99 -0
  131. loopix/api/client/models/template_step.py +91 -0
  132. loopix/api/client/models/template_tag.py +78 -0
  133. loopix/api/client/models/template_update_request.py +59 -0
  134. loopix/api/client/models/template_update_response.py +59 -0
  135. loopix/api/client/models/template_with_builds.py +156 -0
  136. loopix/api/client/models/update_team_api_key.py +59 -0
  137. loopix/api/client/models/volume.py +67 -0
  138. loopix/api/client/models/volume_and_token.py +75 -0
  139. loopix/api/client/models/volume_token.py +59 -0
  140. loopix/api/client/py.typed +1 -0
  141. loopix/api/client/types.py +54 -0
  142. loopix/api/client_async/__init__.py +74 -0
  143. loopix/api/client_sync/__init__.py +73 -0
  144. loopix/api/metadata.py +14 -0
  145. loopix/connection_config.py +309 -0
  146. loopix/envd/api.py +170 -0
  147. loopix/envd/filesystem/filesystem_connect.py +193 -0
  148. loopix/envd/filesystem/filesystem_pb2.py +80 -0
  149. loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
  150. loopix/envd/process/process_connect.py +174 -0
  151. loopix/envd/process/process_pb2.py +96 -0
  152. loopix/envd/process/process_pb2.pyi +316 -0
  153. loopix/envd/rpc.py +139 -0
  154. loopix/envd/versions.py +11 -0
  155. loopix/exceptions.py +133 -0
  156. loopix/io_utils.py +57 -0
  157. loopix/paginator.py +52 -0
  158. loopix/py.typed +0 -0
  159. loopix/sandbox/_git/__init__.py +85 -0
  160. loopix/sandbox/_git/args.py +363 -0
  161. loopix/sandbox/_git/auth.py +132 -0
  162. loopix/sandbox/_git/config.py +32 -0
  163. loopix/sandbox/_git/parse.py +222 -0
  164. loopix/sandbox/_git/types.py +149 -0
  165. loopix/sandbox/commands/command_handle.py +69 -0
  166. loopix/sandbox/commands/main.py +39 -0
  167. loopix/sandbox/filesystem/filesystem.py +337 -0
  168. loopix/sandbox/filesystem/watch_handle.py +70 -0
  169. loopix/sandbox/main.py +227 -0
  170. loopix/sandbox/mcp.py +1949 -0
  171. loopix/sandbox/network.py +8 -0
  172. loopix/sandbox/sandbox_api.py +624 -0
  173. loopix/sandbox/signature.py +47 -0
  174. loopix/sandbox/utils.py +34 -0
  175. loopix/sandbox_async/commands/command.py +396 -0
  176. loopix/sandbox_async/commands/command_handle.py +298 -0
  177. loopix/sandbox_async/commands/pty.py +257 -0
  178. loopix/sandbox_async/filesystem/filesystem.py +720 -0
  179. loopix/sandbox_async/filesystem/watch_handle.py +97 -0
  180. loopix/sandbox_async/git.py +1100 -0
  181. loopix/sandbox_async/main.py +987 -0
  182. loopix/sandbox_async/paginator.py +140 -0
  183. loopix/sandbox_async/sandbox_api.py +504 -0
  184. loopix/sandbox_async/utils.py +7 -0
  185. loopix/sandbox_domains.py +5 -0
  186. loopix/sandbox_sync/commands/command.py +420 -0
  187. loopix/sandbox_sync/commands/command_handle.py +239 -0
  188. loopix/sandbox_sync/commands/pty.py +279 -0
  189. loopix/sandbox_sync/filesystem/filesystem.py +710 -0
  190. loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
  191. loopix/sandbox_sync/git.py +1077 -0
  192. loopix/sandbox_sync/main.py +975 -0
  193. loopix/sandbox_sync/paginator.py +140 -0
  194. loopix/sandbox_sync/sandbox_api.py +491 -0
  195. loopix/template/consts.py +45 -0
  196. loopix/template/dockerfile_parser.py +286 -0
  197. loopix/template/logger.py +232 -0
  198. loopix/template/main.py +1368 -0
  199. loopix/template/readycmd.py +144 -0
  200. loopix/template/types.py +194 -0
  201. loopix/template/utils.py +426 -0
  202. loopix/template_async/build_api.py +419 -0
  203. loopix/template_async/main.py +528 -0
  204. loopix/template_sync/build_api.py +409 -0
  205. loopix/template_sync/main.py +529 -0
  206. loopix/volume/client/__init__.py +8 -0
  207. loopix/volume/client/api/__init__.py +1 -0
  208. loopix/volume/client/api/volumes/__init__.py +1 -0
  209. loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
  210. loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
  211. loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
  212. loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
  213. loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
  214. loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
  215. loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
  216. loopix/volume/client/client.py +286 -0
  217. loopix/volume/client/errors.py +16 -0
  218. loopix/volume/client/models/__init__.py +13 -0
  219. loopix/volume/client/models/error.py +67 -0
  220. loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
  221. loopix/volume/client/models/volume_entry_stat.py +145 -0
  222. loopix/volume/client/models/volume_entry_stat_type.py +11 -0
  223. loopix/volume/client/py.typed +1 -0
  224. loopix/volume/client/types.py +54 -0
  225. loopix/volume/client_async/__init__.py +88 -0
  226. loopix/volume/client_sync/__init__.py +80 -0
  227. loopix/volume/connection_config.py +145 -0
  228. loopix/volume/types.py +62 -0
  229. loopix/volume/utils.py +52 -0
  230. loopix/volume/volume_async.py +639 -0
  231. loopix/volume/volume_sync.py +639 -0
  232. loopix_connect/__init__.py +1 -0
  233. loopix_connect/client.py +534 -0
  234. loopix_connect/py.typed +0 -0
  235. loopix_sdk-2.30.0.dist-info/METADATA +98 -0
  236. loopix_sdk-2.30.0.dist-info/RECORD +238 -0
  237. loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
  238. 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
+ )