moru 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.
Files changed (152) hide show
  1. moru/__init__.py +174 -0
  2. moru/api/__init__.py +164 -0
  3. moru/api/client/__init__.py +8 -0
  4. moru/api/client/api/__init__.py +1 -0
  5. moru/api/client/api/sandboxes/__init__.py +1 -0
  6. moru/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. moru/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. moru/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. moru/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
  12. moru/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
  13. moru/api/client/api/sandboxes/post_sandboxes.py +172 -0
  14. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  15. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  16. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
  17. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
  18. moru/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
  19. moru/api/client/api/templates/__init__.py +1 -0
  20. moru/api/client/api/templates/delete_templates_template_id.py +157 -0
  21. moru/api/client/api/templates/get_templates.py +172 -0
  22. moru/api/client/api/templates/get_templates_template_id.py +195 -0
  23. moru/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +217 -0
  24. moru/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
  25. moru/api/client/api/templates/patch_templates_template_id.py +183 -0
  26. moru/api/client/api/templates/post_templates.py +172 -0
  27. moru/api/client/api/templates/post_templates_template_id.py +181 -0
  28. moru/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
  29. moru/api/client/api/templates/post_v2_templates.py +172 -0
  30. moru/api/client/api/templates/post_v3_templates.py +172 -0
  31. moru/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
  32. moru/api/client/client.py +286 -0
  33. moru/api/client/errors.py +16 -0
  34. moru/api/client/models/__init__.py +123 -0
  35. moru/api/client/models/aws_registry.py +85 -0
  36. moru/api/client/models/aws_registry_type.py +8 -0
  37. moru/api/client/models/build_log_entry.py +89 -0
  38. moru/api/client/models/build_status_reason.py +95 -0
  39. moru/api/client/models/connect_sandbox.py +59 -0
  40. moru/api/client/models/created_access_token.py +100 -0
  41. moru/api/client/models/created_team_api_key.py +166 -0
  42. moru/api/client/models/disk_metrics.py +91 -0
  43. moru/api/client/models/error.py +67 -0
  44. moru/api/client/models/gcp_registry.py +69 -0
  45. moru/api/client/models/gcp_registry_type.py +8 -0
  46. moru/api/client/models/general_registry.py +77 -0
  47. moru/api/client/models/general_registry_type.py +8 -0
  48. moru/api/client/models/identifier_masking_details.py +83 -0
  49. moru/api/client/models/listed_sandbox.py +154 -0
  50. moru/api/client/models/log_level.py +11 -0
  51. moru/api/client/models/max_team_metric.py +78 -0
  52. moru/api/client/models/mcp_type_0.py +44 -0
  53. moru/api/client/models/new_access_token.py +59 -0
  54. moru/api/client/models/new_sandbox.py +172 -0
  55. moru/api/client/models/new_team_api_key.py +59 -0
  56. moru/api/client/models/node.py +155 -0
  57. moru/api/client/models/node_detail.py +165 -0
  58. moru/api/client/models/node_metrics.py +122 -0
  59. moru/api/client/models/node_status.py +11 -0
  60. moru/api/client/models/node_status_change.py +79 -0
  61. moru/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  62. moru/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  63. moru/api/client/models/resumed_sandbox.py +68 -0
  64. moru/api/client/models/sandbox.py +145 -0
  65. moru/api/client/models/sandbox_detail.py +183 -0
  66. moru/api/client/models/sandbox_log.py +70 -0
  67. moru/api/client/models/sandbox_log_entry.py +93 -0
  68. moru/api/client/models/sandbox_log_entry_fields.py +44 -0
  69. moru/api/client/models/sandbox_logs.py +91 -0
  70. moru/api/client/models/sandbox_metric.py +118 -0
  71. moru/api/client/models/sandbox_network_config.py +92 -0
  72. moru/api/client/models/sandbox_state.py +9 -0
  73. moru/api/client/models/sandboxes_with_metrics.py +59 -0
  74. moru/api/client/models/team.py +83 -0
  75. moru/api/client/models/team_api_key.py +158 -0
  76. moru/api/client/models/team_metric.py +86 -0
  77. moru/api/client/models/team_user.py +68 -0
  78. moru/api/client/models/template.py +217 -0
  79. moru/api/client/models/template_build.py +139 -0
  80. moru/api/client/models/template_build_file_upload.py +70 -0
  81. moru/api/client/models/template_build_info.py +126 -0
  82. moru/api/client/models/template_build_request.py +115 -0
  83. moru/api/client/models/template_build_request_v2.py +88 -0
  84. moru/api/client/models/template_build_request_v3.py +88 -0
  85. moru/api/client/models/template_build_start_v2.py +184 -0
  86. moru/api/client/models/template_build_status.py +11 -0
  87. moru/api/client/models/template_legacy.py +207 -0
  88. moru/api/client/models/template_request_response_v3.py +83 -0
  89. moru/api/client/models/template_step.py +91 -0
  90. moru/api/client/models/template_update_request.py +59 -0
  91. moru/api/client/models/template_with_builds.py +148 -0
  92. moru/api/client/models/update_team_api_key.py +59 -0
  93. moru/api/client/py.typed +1 -0
  94. moru/api/client/types.py +54 -0
  95. moru/api/client_async/__init__.py +50 -0
  96. moru/api/client_sync/__init__.py +52 -0
  97. moru/api/metadata.py +14 -0
  98. moru/connection_config.py +217 -0
  99. moru/envd/api.py +59 -0
  100. moru/envd/filesystem/filesystem_connect.py +193 -0
  101. moru/envd/filesystem/filesystem_pb2.py +76 -0
  102. moru/envd/filesystem/filesystem_pb2.pyi +233 -0
  103. moru/envd/process/process_connect.py +155 -0
  104. moru/envd/process/process_pb2.py +92 -0
  105. moru/envd/process/process_pb2.pyi +304 -0
  106. moru/envd/rpc.py +61 -0
  107. moru/envd/versions.py +6 -0
  108. moru/exceptions.py +95 -0
  109. moru/sandbox/commands/command_handle.py +69 -0
  110. moru/sandbox/commands/main.py +39 -0
  111. moru/sandbox/filesystem/filesystem.py +94 -0
  112. moru/sandbox/filesystem/watch_handle.py +60 -0
  113. moru/sandbox/main.py +210 -0
  114. moru/sandbox/mcp.py +1120 -0
  115. moru/sandbox/network.py +8 -0
  116. moru/sandbox/sandbox_api.py +210 -0
  117. moru/sandbox/signature.py +45 -0
  118. moru/sandbox/utils.py +34 -0
  119. moru/sandbox_async/commands/command.py +336 -0
  120. moru/sandbox_async/commands/command_handle.py +196 -0
  121. moru/sandbox_async/commands/pty.py +240 -0
  122. moru/sandbox_async/filesystem/filesystem.py +531 -0
  123. moru/sandbox_async/filesystem/watch_handle.py +62 -0
  124. moru/sandbox_async/main.py +734 -0
  125. moru/sandbox_async/paginator.py +69 -0
  126. moru/sandbox_async/sandbox_api.py +325 -0
  127. moru/sandbox_async/utils.py +7 -0
  128. moru/sandbox_sync/commands/command.py +328 -0
  129. moru/sandbox_sync/commands/command_handle.py +150 -0
  130. moru/sandbox_sync/commands/pty.py +230 -0
  131. moru/sandbox_sync/filesystem/filesystem.py +518 -0
  132. moru/sandbox_sync/filesystem/watch_handle.py +69 -0
  133. moru/sandbox_sync/main.py +726 -0
  134. moru/sandbox_sync/paginator.py +69 -0
  135. moru/sandbox_sync/sandbox_api.py +308 -0
  136. moru/template/consts.py +30 -0
  137. moru/template/dockerfile_parser.py +275 -0
  138. moru/template/logger.py +232 -0
  139. moru/template/main.py +1360 -0
  140. moru/template/readycmd.py +138 -0
  141. moru/template/types.py +105 -0
  142. moru/template/utils.py +320 -0
  143. moru/template_async/build_api.py +202 -0
  144. moru/template_async/main.py +366 -0
  145. moru/template_sync/build_api.py +199 -0
  146. moru/template_sync/main.py +371 -0
  147. moru-0.1.0.dist-info/METADATA +63 -0
  148. moru-0.1.0.dist-info/RECORD +152 -0
  149. moru-0.1.0.dist-info/WHEEL +4 -0
  150. moru-0.1.0.dist-info/licenses/LICENSE +9 -0
  151. moru_connect/__init__.py +1 -0
  152. moru_connect/client.py +493 -0
@@ -0,0 +1,328 @@
1
+ from typing import Callable, Dict, List, Literal, Optional, Union, overload
2
+
3
+ import moru_connect
4
+ import httpcore
5
+ from packaging.version import Version
6
+ from moru.connection_config import (
7
+ ConnectionConfig,
8
+ Username,
9
+ KEEPALIVE_PING_HEADER,
10
+ KEEPALIVE_PING_INTERVAL_SEC,
11
+ )
12
+ from moru.envd.process import process_connect, process_pb2
13
+ from moru.envd.rpc import authentication_header, handle_rpc_exception
14
+ from moru.envd.versions import ENVD_COMMANDS_STDIN
15
+ from moru.exceptions import SandboxException
16
+ from moru.sandbox.commands.main import ProcessInfo
17
+ from moru.sandbox.commands.command_handle import CommandResult
18
+ from moru.sandbox_sync.commands.command_handle import CommandHandle
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: httpcore.ConnectionPool,
31
+ envd_version: Version,
32
+ ) -> None:
33
+ self._connection_config = connection_config
34
+ self._envd_version = envd_version
35
+ self._rpc = process_connect.ProcessClient(
36
+ envd_api_url,
37
+ # TODO: Fix and enable compression again — the headers compression is not solved for streaming.
38
+ # compressor=moru_connect.GzipCompressor,
39
+ pool=pool,
40
+ json=True,
41
+ headers=connection_config.sandbox_headers,
42
+ )
43
+
44
+ def list(
45
+ self,
46
+ request_timeout: Optional[float] = None,
47
+ ) -> List[ProcessInfo]:
48
+ """
49
+ Lists all running commands and PTY sessions.
50
+
51
+ :param request_timeout: Timeout for the request in **seconds**
52
+
53
+ :return: List of running commands and PTY sessions
54
+ """
55
+ try:
56
+ res = self._rpc.list(
57
+ process_pb2.ListRequest(),
58
+ request_timeout=self._connection_config.get_request_timeout(
59
+ request_timeout
60
+ ),
61
+ )
62
+ return [
63
+ ProcessInfo(
64
+ pid=p.pid,
65
+ tag=p.tag,
66
+ cmd=p.config.cmd,
67
+ args=list(p.config.args),
68
+ envs=dict(p.config.envs),
69
+ cwd=p.config.cwd,
70
+ )
71
+ for p in res.processes
72
+ ]
73
+ except Exception as e:
74
+ raise handle_rpc_exception(e)
75
+
76
+ def kill(
77
+ self,
78
+ pid: int,
79
+ request_timeout: Optional[float] = None,
80
+ ) -> bool:
81
+ """
82
+ Kills a running command specified by its process ID.
83
+ It uses `SIGKILL` signal to kill the command.
84
+
85
+ :param pid: Process ID of the command. You can get the list of processes using `sandbox.commands.list()`
86
+ :param request_timeout: Timeout for the request in **seconds**
87
+
88
+ :return: `True` if the command was killed, `False` if the command was not found
89
+ """
90
+ try:
91
+ self._rpc.send_signal(
92
+ process_pb2.SendSignalRequest(
93
+ process=process_pb2.ProcessSelector(pid=pid),
94
+ signal=process_pb2.Signal.SIGNAL_SIGKILL,
95
+ ),
96
+ request_timeout=self._connection_config.get_request_timeout(
97
+ request_timeout
98
+ ),
99
+ )
100
+ return True
101
+ except Exception as e:
102
+ if isinstance(e, moru_connect.ConnectException):
103
+ if e.status == moru_connect.Code.not_found:
104
+ return False
105
+ raise handle_rpc_exception(e)
106
+
107
+ def send_stdin(
108
+ self,
109
+ pid: int,
110
+ data: str,
111
+ request_timeout: Optional[float] = None,
112
+ ):
113
+ """
114
+ Send data to command stdin.
115
+
116
+ :param pid Process ID of the command. You can get the list of processes using `sandbox.commands.list()`.
117
+ :param data: Data to send to the command
118
+ :param request_timeout: Timeout for the request in **seconds**
119
+ """
120
+ try:
121
+ self._rpc.send_input(
122
+ process_pb2.SendInputRequest(
123
+ process=process_pb2.ProcessSelector(pid=pid),
124
+ input=process_pb2.ProcessInput(
125
+ stdin=data.encode(),
126
+ ),
127
+ ),
128
+ request_timeout=self._connection_config.get_request_timeout(
129
+ request_timeout
130
+ ),
131
+ )
132
+ except Exception as e:
133
+ raise handle_rpc_exception(e)
134
+
135
+ @overload
136
+ def run(
137
+ self,
138
+ cmd: str,
139
+ background: Union[Literal[False], None] = None,
140
+ envs: Optional[Dict[str, str]] = None,
141
+ user: Optional[Username] = None,
142
+ cwd: Optional[str] = None,
143
+ on_stdout: Optional[Callable[[str], None]] = None,
144
+ on_stderr: Optional[Callable[[str], None]] = None,
145
+ stdin: Optional[bool] = None,
146
+ timeout: Optional[float] = 60,
147
+ request_timeout: Optional[float] = None,
148
+ ) -> CommandResult:
149
+ """
150
+ Start a new command and wait until it finishes executing.
151
+
152
+ :param cmd: Command to execute
153
+ :param background: **`False` if the command should be executed in the foreground**, `True` if the command should be executed in the background
154
+ :param envs: Environment variables used for the command
155
+ :param user: User to run the command as
156
+ :param cwd: Working directory to run the command
157
+ :param on_stdout: Callback for command stdout output
158
+ :param on_stderr: Callback for command stderr output
159
+ :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
160
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
161
+ :param request_timeout: Timeout for the request in **seconds**
162
+
163
+ :return: `CommandResult` result of the command execution
164
+ """
165
+ ...
166
+
167
+ @overload
168
+ def run(
169
+ self,
170
+ cmd: str,
171
+ background: Literal[True],
172
+ envs: Optional[Dict[str, str]] = None,
173
+ user: Optional[Username] = None,
174
+ cwd: Optional[str] = None,
175
+ on_stdout: None = None,
176
+ on_stderr: None = None,
177
+ stdin: Optional[bool] = None,
178
+ timeout: Optional[float] = 60,
179
+ request_timeout: Optional[float] = None,
180
+ ) -> CommandHandle:
181
+ """
182
+ Start a new command and return a handle to interact with it.
183
+
184
+ :param cmd: Command to execute
185
+ :param background: `False` if the command should be executed in the foreground, **`True` if the command should be executed in the background**
186
+ :param envs: Environment variables used for the command
187
+ :param user: User to run the command as
188
+ :param cwd: Working directory to run the command
189
+ :param stdin: If `True`, the command will have a stdin stream that you can send data to using `sandbox.commands.send_stdin()`
190
+ :param timeout: Timeout for the command connection in **seconds**. Using `0` will not limit the command connection time
191
+ :param request_timeout: Timeout for the request in **seconds**
192
+
193
+ :return: `CommandHandle` handle to interact with the running command
194
+ """
195
+ ...
196
+
197
+ def run(
198
+ self,
199
+ cmd: str,
200
+ background: Union[bool, None] = None,
201
+ envs: Optional[Dict[str, str]] = None,
202
+ user: Optional[Username] = None,
203
+ cwd: Optional[str] = None,
204
+ on_stdout: Optional[Callable[[str], None]] = None,
205
+ on_stderr: Optional[Callable[[str], None]] = None,
206
+ stdin: Optional[bool] = None,
207
+ timeout: Optional[float] = 60,
208
+ request_timeout: Optional[float] = None,
209
+ ):
210
+ # Check version for stdin support
211
+ if stdin is False and self._envd_version < ENVD_COMMANDS_STDIN:
212
+ raise SandboxException(
213
+ f"Sandbox envd version {self._envd_version} can't specify stdin, it's always turned on. "
214
+ f"Please rebuild your template if you need this feature."
215
+ )
216
+
217
+ # Default to `False`
218
+ stdin = stdin or False
219
+
220
+ proc = self._start(
221
+ cmd,
222
+ envs,
223
+ user,
224
+ cwd,
225
+ stdin,
226
+ timeout,
227
+ request_timeout,
228
+ )
229
+
230
+ return (
231
+ proc
232
+ if background
233
+ else proc.wait(
234
+ on_stdout=on_stdout,
235
+ on_stderr=on_stderr,
236
+ )
237
+ )
238
+
239
+ def _start(
240
+ self,
241
+ cmd: str,
242
+ envs: Optional[Dict[str, str]],
243
+ user: Username,
244
+ cwd: Optional[str],
245
+ stdin: bool,
246
+ timeout: Optional[float],
247
+ request_timeout: Optional[float],
248
+ ):
249
+ events = self._rpc.start(
250
+ process_pb2.StartRequest(
251
+ process=process_pb2.ProcessConfig(
252
+ cmd="/bin/bash",
253
+ envs=envs,
254
+ args=["-l", "-c", cmd],
255
+ cwd=cwd,
256
+ ),
257
+ stdin=stdin,
258
+ ),
259
+ headers={
260
+ **authentication_header(self._envd_version, user),
261
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
262
+ },
263
+ timeout=timeout,
264
+ request_timeout=self._connection_config.get_request_timeout(
265
+ request_timeout
266
+ ),
267
+ )
268
+
269
+ try:
270
+ start_event = events.__next__()
271
+
272
+ if not start_event.HasField("event"):
273
+ raise SandboxException(
274
+ f"Failed to start process: expected start event, got {start_event}"
275
+ )
276
+
277
+ return CommandHandle(
278
+ pid=start_event.event.start.pid,
279
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
280
+ events=events,
281
+ )
282
+ except Exception as e:
283
+ raise handle_rpc_exception(e)
284
+
285
+ def connect(
286
+ self,
287
+ pid: int,
288
+ timeout: Optional[float] = 60,
289
+ request_timeout: Optional[float] = None,
290
+ ):
291
+ """
292
+ Connects to a running command.
293
+ You can use `CommandHandle.wait()` to wait for the command to finish and get execution results.
294
+
295
+ :param pid: Process ID of the command to connect to. You can get the list of processes using `sandbox.commands.list()`
296
+ :param timeout: Timeout for the connection in **seconds**. Using `0` will not limit the connection time
297
+ :param request_timeout: Timeout for the request in **seconds**
298
+
299
+ :return: `CommandHandle` handle to interact with the running command
300
+ """
301
+ events = self._rpc.connect(
302
+ process_pb2.ConnectRequest(
303
+ process=process_pb2.ProcessSelector(pid=pid),
304
+ ),
305
+ headers={
306
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
307
+ },
308
+ timeout=timeout,
309
+ request_timeout=self._connection_config.get_request_timeout(
310
+ request_timeout
311
+ ),
312
+ )
313
+
314
+ try:
315
+ start_event = events.__next__()
316
+
317
+ if not start_event.HasField("event"):
318
+ raise SandboxException(
319
+ f"Failed to connect to process: expected start event, got {start_event}"
320
+ )
321
+
322
+ return CommandHandle(
323
+ pid=start_event.event.start.pid,
324
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
325
+ events=events,
326
+ )
327
+ except Exception as e:
328
+ raise handle_rpc_exception(e)
@@ -0,0 +1,150 @@
1
+ from typing import Optional, Callable, Any, Generator, Union, Tuple
2
+
3
+ from moru.envd.rpc import handle_rpc_exception
4
+ from moru.envd.process import process_pb2
5
+ from moru.sandbox.commands.command_handle import (
6
+ CommandExitException,
7
+ CommandResult,
8
+ Stderr,
9
+ Stdout,
10
+ PtyOutput,
11
+ )
12
+
13
+
14
+ class CommandHandle:
15
+ """
16
+ Command execution handle.
17
+
18
+ It provides methods for waiting for the command to finish, retrieving stdout/stderr, and killing the command.
19
+ """
20
+
21
+ @property
22
+ def pid(self):
23
+ """
24
+ Command process ID.
25
+ """
26
+ return self._pid
27
+
28
+ def __init__(
29
+ self,
30
+ pid: int,
31
+ handle_kill: Callable[[], bool],
32
+ events: Generator[
33
+ Union[process_pb2.StartResponse, process_pb2.ConnectResponse], Any, None
34
+ ],
35
+ ):
36
+ self._pid = pid
37
+ self._handle_kill = handle_kill
38
+ self._events = events
39
+
40
+ self._stdout: str = ""
41
+ self._stderr: str = ""
42
+
43
+ self._result: Optional[CommandResult] = None
44
+ self._iteration_exception: Optional[Exception] = None
45
+
46
+ def __iter__(self):
47
+ """
48
+ Iterate over the command output.
49
+
50
+ :return: Generator of command outputs
51
+ """
52
+ return self._handle_events()
53
+
54
+ def _handle_events(
55
+ self,
56
+ ) -> Generator[
57
+ Union[
58
+ Tuple[Stdout, None, None],
59
+ Tuple[None, Stderr, None],
60
+ Tuple[None, None, PtyOutput],
61
+ ],
62
+ None,
63
+ None,
64
+ ]:
65
+ try:
66
+ for event in self._events:
67
+ if event.event.HasField("data"):
68
+ if event.event.data.stdout:
69
+ out = event.event.data.stdout.decode("utf-8", "replace")
70
+ self._stdout += out
71
+ yield out, None, None
72
+ if event.event.data.stderr:
73
+ out = event.event.data.stderr.decode("utf-8", "replace")
74
+ self._stderr += out
75
+ yield None, out, None
76
+ if event.event.data.pty:
77
+ yield None, None, event.event.data.pty
78
+ if event.event.HasField("end"):
79
+ self._result = CommandResult(
80
+ stdout=self._stdout,
81
+ stderr=self._stderr,
82
+ exit_code=event.event.end.exit_code,
83
+ error=event.event.end.error,
84
+ )
85
+ except Exception as e:
86
+ raise handle_rpc_exception(e)
87
+
88
+ def disconnect(self) -> None:
89
+ """
90
+ Disconnect from the command.
91
+
92
+ The command is not killed, but SDK stops receiving events from the command.
93
+ You can reconnect to the command using `sandbox.commands.connect` method.
94
+ """
95
+ self._events.close()
96
+
97
+ def wait(
98
+ self,
99
+ on_pty: Optional[Callable[[PtyOutput], None]] = None,
100
+ on_stdout: Optional[Callable[[str], None]] = None,
101
+ on_stderr: Optional[Callable[[str], None]] = None,
102
+ ) -> CommandResult:
103
+ """
104
+ Wait for the command to finish and returns the result.
105
+ If the command exits with a non-zero exit code, it throws a `CommandExitException`.
106
+
107
+ :param on_pty: Callback for pty output
108
+ :param on_stdout: Callback for stdout output
109
+ :param on_stderr: Callback for stderr output
110
+
111
+ :return: `CommandResult` result of command execution
112
+ """
113
+ try:
114
+ for stdout, stderr, pty in self:
115
+ if stdout is not None and on_stdout:
116
+ on_stdout(stdout)
117
+ elif stderr is not None and on_stderr:
118
+ on_stderr(stderr)
119
+ elif pty is not None and on_pty:
120
+ on_pty(pty)
121
+ except StopIteration:
122
+ pass
123
+ except Exception as e:
124
+ self._iteration_exception = handle_rpc_exception(e)
125
+
126
+ if self._iteration_exception:
127
+ raise self._iteration_exception
128
+
129
+ if self._result is None:
130
+ raise Exception("Command ended without an end event")
131
+
132
+ if self._result.exit_code != 0:
133
+ raise CommandExitException(
134
+ stdout=self._stdout,
135
+ stderr=self._stderr,
136
+ exit_code=self._result.exit_code,
137
+ error=self._result.error,
138
+ )
139
+
140
+ return self._result
141
+
142
+ def kill(self) -> bool:
143
+ """
144
+ Kills the command.
145
+
146
+ It uses `SIGKILL` signal to kill the command.
147
+
148
+ :return: Whether the command was killed successfully
149
+ """
150
+ return self._handle_kill()
@@ -0,0 +1,230 @@
1
+ import moru_connect
2
+ import httpcore
3
+
4
+ from typing import Dict, Optional
5
+
6
+ from packaging.version import Version
7
+ from moru.envd.process import process_connect, process_pb2
8
+ from moru.connection_config import (
9
+ Username,
10
+ ConnectionConfig,
11
+ KEEPALIVE_PING_HEADER,
12
+ KEEPALIVE_PING_INTERVAL_SEC,
13
+ )
14
+ from moru.exceptions import SandboxException
15
+ from moru.envd.rpc import authentication_header, handle_rpc_exception
16
+ from moru.sandbox.commands.command_handle import PtySize
17
+ from moru.sandbox_sync.commands.command_handle import CommandHandle
18
+
19
+
20
+ class Pty:
21
+ """
22
+ Module for interacting with PTYs (pseudo-terminals) in the sandbox.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ envd_api_url: str,
28
+ connection_config: ConnectionConfig,
29
+ pool: httpcore.ConnectionPool,
30
+ envd_version: Version,
31
+ ) -> None:
32
+ self._connection_config = connection_config
33
+ self._envd_version = envd_version
34
+ self._rpc = process_connect.ProcessClient(
35
+ envd_api_url,
36
+ # TODO: Fix and enable compression again — the headers compression is not solved for streaming.
37
+ # compressor=moru_connect.GzipCompressor,
38
+ pool=pool,
39
+ json=True,
40
+ headers=connection_config.sandbox_headers,
41
+ )
42
+
43
+ def kill(
44
+ self,
45
+ pid: int,
46
+ request_timeout: Optional[float] = None,
47
+ ) -> bool:
48
+ """
49
+ Kill PTY.
50
+
51
+ :param pid: Process ID of the PTY
52
+ :param request_timeout: Timeout for the request in **seconds**
53
+
54
+ :return: `true` if the PTY was killed, `false` if the PTY was not found
55
+ """
56
+ try:
57
+ self._rpc.send_signal(
58
+ process_pb2.SendSignalRequest(
59
+ process=process_pb2.ProcessSelector(pid=pid),
60
+ signal=process_pb2.Signal.SIGNAL_SIGKILL,
61
+ ),
62
+ request_timeout=self._connection_config.get_request_timeout(
63
+ request_timeout
64
+ ),
65
+ )
66
+ return True
67
+ except Exception as e:
68
+ if isinstance(e, moru_connect.ConnectException):
69
+ if e.status == moru_connect.Code.not_found:
70
+ return False
71
+ raise handle_rpc_exception(e)
72
+
73
+ 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
+ self._rpc.send_input(
88
+ process_pb2.SendInputRequest(
89
+ process=process_pb2.ProcessSelector(pid=pid),
90
+ input=process_pb2.ProcessInput(
91
+ pty=data,
92
+ ),
93
+ ),
94
+ request_timeout=self._connection_config.get_request_timeout(
95
+ request_timeout
96
+ ),
97
+ )
98
+ except Exception as e:
99
+ raise handle_rpc_exception(e)
100
+
101
+ def create(
102
+ self,
103
+ size: PtySize,
104
+ user: Optional[Username] = None,
105
+ cwd: Optional[str] = None,
106
+ envs: Optional[Dict[str, str]] = None,
107
+ timeout: Optional[float] = 60,
108
+ request_timeout: Optional[float] = None,
109
+ ) -> CommandHandle:
110
+ """
111
+ Start a new PTY (pseudo-terminal).
112
+
113
+ :param size: Size of the PTY
114
+ :param user: User to use for the PTY
115
+ :param cwd: Working directory for the PTY
116
+ :param envs: Environment variables for the PTY
117
+ :param timeout: Timeout for the PTY in **seconds**
118
+ :param request_timeout: Timeout for the request in **seconds**
119
+
120
+ :return: Handle to interact with the PTY
121
+ """
122
+ envs = envs or {}
123
+ envs["TERM"] = "xterm-256color"
124
+ events = self._rpc.start(
125
+ process_pb2.StartRequest(
126
+ process=process_pb2.ProcessConfig(
127
+ cmd="/bin/bash",
128
+ envs=envs,
129
+ args=["-i", "-l"],
130
+ cwd=cwd,
131
+ ),
132
+ pty=process_pb2.PTY(
133
+ size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols)
134
+ ),
135
+ ),
136
+ headers={
137
+ **authentication_header(self._envd_version, user),
138
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
139
+ },
140
+ timeout=timeout,
141
+ request_timeout=self._connection_config.get_request_timeout(
142
+ request_timeout
143
+ ),
144
+ )
145
+
146
+ try:
147
+ start_event = events.__next__()
148
+
149
+ if not start_event.HasField("event"):
150
+ raise SandboxException(
151
+ f"Failed to start process: expected start event, got {start_event}"
152
+ )
153
+
154
+ return CommandHandle(
155
+ pid=start_event.event.start.pid,
156
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
157
+ events=events,
158
+ )
159
+ except Exception as e:
160
+ raise handle_rpc_exception(e)
161
+
162
+ def connect(
163
+ self,
164
+ pid: int,
165
+ timeout: Optional[float] = 60,
166
+ request_timeout: Optional[float] = None,
167
+ ) -> CommandHandle:
168
+ """
169
+ Connect to a running PTY.
170
+
171
+ :param pid: Process ID of the PTY to connect to. You can get the list of running PTYs using `sandbox.pty.list()`.
172
+ :param timeout: Timeout for the PTY connection in **seconds**. Using `0` will not limit the connection time
173
+ :param request_timeout: Timeout for the request in **seconds**
174
+
175
+ :return: Handle to interact with the PTY
176
+ """
177
+ events = self._rpc.connect(
178
+ process_pb2.ConnectRequest(
179
+ process=process_pb2.ProcessSelector(pid=pid),
180
+ ),
181
+ headers={
182
+ KEEPALIVE_PING_HEADER: str(KEEPALIVE_PING_INTERVAL_SEC),
183
+ },
184
+ timeout=timeout,
185
+ request_timeout=self._connection_config.get_request_timeout(
186
+ request_timeout
187
+ ),
188
+ )
189
+
190
+ try:
191
+ start_event = events.__next__()
192
+
193
+ if not start_event.HasField("event"):
194
+ raise SandboxException(
195
+ f"Failed to connect to process: expected start event, got {start_event}"
196
+ )
197
+
198
+ return CommandHandle(
199
+ pid=start_event.event.start.pid,
200
+ handle_kill=lambda: self.kill(start_event.event.start.pid),
201
+ events=events,
202
+ )
203
+ except Exception as e:
204
+ raise handle_rpc_exception(e)
205
+
206
+ def resize(
207
+ self,
208
+ pid: int,
209
+ size: PtySize,
210
+ request_timeout: Optional[float] = None,
211
+ ) -> None:
212
+ """
213
+ Resize PTY.
214
+ Call this when the terminal window is resized and the number of columns and rows has changed.
215
+
216
+ :param pid: Process ID of the PTY
217
+ :param size: New size of the PTY
218
+ :param request_timeout: Timeout for the request in **seconds**s
219
+ """
220
+ self._rpc.update(
221
+ process_pb2.UpdateRequest(
222
+ process=process_pb2.ProcessSelector(pid=pid),
223
+ pty=process_pb2.PTY(
224
+ size=process_pb2.PTY.Size(rows=size.rows, cols=size.cols),
225
+ ),
226
+ ),
227
+ request_timeout=self._connection_config.get_request_timeout(
228
+ request_timeout
229
+ ),
230
+ )