modAL 1.4.3.dev23__py3-none-any.whl → 1.4.3.dev24__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.
- modal/_functions.py +5 -1
- modal/_utils/grpc_utils.py +10 -6
- modal/_utils/task_command_router_client.py +5 -14
- modal/cli/cluster.py +1 -4
- modal/cli/container.py +1 -3
- modal/client.pyi +2 -2
- modal/container_process.py +39 -285
- modal/container_process.pyi +11 -120
- modal/functions.pyi +6 -6
- modal/io_streams.py +69 -199
- modal/io_streams.pyi +125 -64
- modal/sandbox.py +64 -89
- modal/sandbox.pyi +28 -76
- modal/sandbox_fs.py +27 -16
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/METADATA +1 -1
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/RECORD +28 -28
- modal_proto/api_pb2.py +394 -394
- modal_proto/api_pb2.pyi +9 -5
- modal_proto/task_command_router_grpc.py +16 -0
- modal_proto/task_command_router_pb2.py +33 -13
- modal_proto/task_command_router_pb2.pyi +45 -0
- modal_proto/task_command_router_pb2_grpc.py +34 -0
- modal_proto/task_command_router_pb2_grpc.pyi +12 -0
- modal_version/__init__.py +1 -1
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/WHEEL +0 -0
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/entry_points.txt +0 -0
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/licenses/LICENSE +0 -0
- {modal-1.4.3.dev23.dist-info → modal-1.4.3.dev24.dist-info}/top_level.txt +0 -0
modal/_functions.py
CHANGED
|
@@ -549,6 +549,7 @@ class FunctionStats:
|
|
|
549
549
|
backlog: int
|
|
550
550
|
num_total_runners: int
|
|
551
551
|
num_running_inputs: int
|
|
552
|
+
input_headroom: int
|
|
552
553
|
|
|
553
554
|
|
|
554
555
|
def _parse_retries(
|
|
@@ -1851,7 +1852,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
|
1851
1852
|
retry=Retry(total_timeout=10.0),
|
|
1852
1853
|
)
|
|
1853
1854
|
return FunctionStats(
|
|
1854
|
-
backlog=resp.backlog,
|
|
1855
|
+
backlog=resp.backlog,
|
|
1856
|
+
num_total_runners=resp.num_total_tasks,
|
|
1857
|
+
num_running_inputs=resp.num_running_inputs,
|
|
1858
|
+
input_headroom=resp.input_headroom,
|
|
1855
1859
|
)
|
|
1856
1860
|
|
|
1857
1861
|
@live_method
|
modal/_utils/grpc_utils.py
CHANGED
|
@@ -403,7 +403,7 @@ async def _retry_transient_errors(
|
|
|
403
403
|
and isinstance(exc, GRPCError)
|
|
404
404
|
and (server_retry_policy := get_server_retry_policy(exc))
|
|
405
405
|
):
|
|
406
|
-
server_delay = server_retry_policy.retry_after_secs
|
|
406
|
+
server_delay = max(server_retry_policy.retry_after_secs, 0.1)
|
|
407
407
|
|
|
408
408
|
now = time.time()
|
|
409
409
|
|
|
@@ -417,6 +417,7 @@ async def _retry_transient_errors(
|
|
|
417
417
|
)
|
|
418
418
|
final_attempt = total_timeout_will_be_reached or max_throttle_will_be_reached
|
|
419
419
|
|
|
420
|
+
elapsed_time = time.monotonic() - attempt_started_at
|
|
420
421
|
with suppress_tb_frame():
|
|
421
422
|
process_exception_before_retry(
|
|
422
423
|
exc,
|
|
@@ -425,20 +426,23 @@ async def _retry_transient_errors(
|
|
|
425
426
|
n_retries,
|
|
426
427
|
server_delay,
|
|
427
428
|
idempotency_key,
|
|
428
|
-
|
|
429
|
+
elapsed_time,
|
|
429
430
|
)
|
|
430
431
|
|
|
431
|
-
now = time.time()
|
|
432
432
|
if last_server_retry_warning_time is None or (
|
|
433
433
|
now - last_server_retry_warning_time >= SERVER_RETRY_WARNING_TIME_INTERVAL
|
|
434
434
|
):
|
|
435
435
|
last_server_retry_warning_time = now
|
|
436
436
|
logger.warning(
|
|
437
|
-
f"Warning: Received {exc.status}{os.linesep}"
|
|
438
|
-
f"{
|
|
439
|
-
f"Will retry in {server_delay:0.2f} seconds."
|
|
437
|
+
f"Warning: Received {exc.status}{os.linesep}{exc.message}{os.linesep}"
|
|
438
|
+
f"for {fn.name} ({idempotency_key[:8]}). Retrying..."
|
|
440
439
|
)
|
|
441
440
|
|
|
441
|
+
logger.debug(
|
|
442
|
+
f"Server-side retry triggered with {repr(exc)} {n_throttled_retries=} {server_delay=} "
|
|
443
|
+
f"{elapsed_time=} for {fn.name} ({idempotency_key[:8]})."
|
|
444
|
+
)
|
|
445
|
+
|
|
442
446
|
n_throttled_retries += 1
|
|
443
447
|
await asyncio.sleep(server_delay)
|
|
444
448
|
continue
|
|
@@ -16,7 +16,7 @@ from grpclib import GRPCError, Status
|
|
|
16
16
|
from grpclib.exceptions import StreamTerminatedError
|
|
17
17
|
|
|
18
18
|
from modal.config import logger
|
|
19
|
-
from modal.exception import
|
|
19
|
+
from modal.exception import ExecTimeoutError
|
|
20
20
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
21
21
|
from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
22
22
|
|
|
@@ -196,21 +196,12 @@ class TaskCommandRouterClient:
|
|
|
196
196
|
return cls(server_client, task_id, url, jwt, channel, loop, jwt_refresh_lock, sandbox_id=sandbox_id)
|
|
197
197
|
|
|
198
198
|
@classmethod
|
|
199
|
-
async def
|
|
199
|
+
async def init(
|
|
200
200
|
cls,
|
|
201
201
|
server_client,
|
|
202
202
|
task_id: str,
|
|
203
|
-
) ->
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
Returns None if command router access is not enabled (FAILED_PRECONDITION).
|
|
207
|
-
"""
|
|
208
|
-
try:
|
|
209
|
-
resp = await fetch_command_router_access(server_client, task_id)
|
|
210
|
-
except ConflictError:
|
|
211
|
-
logger.debug(f"Command router access is not enabled for task {task_id}")
|
|
212
|
-
return None
|
|
213
|
-
|
|
203
|
+
) -> "TaskCommandRouterClient":
|
|
204
|
+
resp = await fetch_command_router_access(server_client, task_id)
|
|
214
205
|
logger.debug(f"Using command router access for task {task_id}")
|
|
215
206
|
return await cls._connect(server_client, task_id, resp.url, resp.jwt)
|
|
216
207
|
|
|
@@ -241,7 +232,7 @@ class TaskCommandRouterClient:
|
|
|
241
232
|
stream_stdio_retry_delay_factor: float = 2,
|
|
242
233
|
stream_stdio_max_retries: int = 10,
|
|
243
234
|
) -> None:
|
|
244
|
-
"""Callers should not use this directly. Use TaskCommandRouterClient.
|
|
235
|
+
"""Callers should not use this directly. Use TaskCommandRouterClient.init() instead."""
|
|
245
236
|
# Record the loop this instance is bound to so __del__ can safely schedule cleanup
|
|
246
237
|
# even if finalization happens from a different thread (e.g. via synchronicity).
|
|
247
238
|
self._loop = loop
|
modal/cli/cluster.py
CHANGED
|
@@ -16,7 +16,6 @@ from modal.cli.utils import display_table, env_option, is_tty
|
|
|
16
16
|
from modal.client import _Client
|
|
17
17
|
from modal.config import config
|
|
18
18
|
from modal.container_process import _ContainerProcess
|
|
19
|
-
from modal.exception import InvalidError
|
|
20
19
|
from modal.output import OutputManager
|
|
21
20
|
from modal.stream_type import StreamType
|
|
22
21
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
@@ -79,9 +78,7 @@ async def shell(cluster_id: str, rank: int = 0):
|
|
|
79
78
|
|
|
80
79
|
pty = is_tty()
|
|
81
80
|
|
|
82
|
-
command_router_client = await TaskCommandRouterClient.
|
|
83
|
-
if command_router_client is None:
|
|
84
|
-
raise InvalidError(f"Command router access is not available for container {task_id}")
|
|
81
|
+
command_router_client = await TaskCommandRouterClient.init(client, task_id)
|
|
85
82
|
|
|
86
83
|
process_id = str(uuid.uuid4())
|
|
87
84
|
|
modal/cli/container.py
CHANGED
|
@@ -278,9 +278,7 @@ async def _exec_impl(
|
|
|
278
278
|
|
|
279
279
|
client = await _Client.from_env()
|
|
280
280
|
|
|
281
|
-
command_router_client = await TaskCommandRouterClient.
|
|
282
|
-
if command_router_client is None:
|
|
283
|
-
raise InvalidError(f"Command router access is not available for container {container_id}")
|
|
281
|
+
command_router_client = await TaskCommandRouterClient.init(client, container_id)
|
|
284
282
|
|
|
285
283
|
process_id = str(uuid.uuid4())
|
|
286
284
|
|
modal/client.pyi
CHANGED
|
@@ -35,7 +35,7 @@ class _Client:
|
|
|
35
35
|
server_url: str,
|
|
36
36
|
client_type: int,
|
|
37
37
|
credentials: typing.Optional[tuple[str, str]],
|
|
38
|
-
version: str = "1.4.3.
|
|
38
|
+
version: str = "1.4.3.dev24",
|
|
39
39
|
):
|
|
40
40
|
"""mdmd:hidden
|
|
41
41
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -175,7 +175,7 @@ class Client:
|
|
|
175
175
|
server_url: str,
|
|
176
176
|
client_type: int,
|
|
177
177
|
credentials: typing.Optional[tuple[str, str]],
|
|
178
|
-
version: str = "1.4.3.
|
|
178
|
+
version: str = "1.4.3.dev24",
|
|
179
179
|
):
|
|
180
180
|
"""mdmd:hidden
|
|
181
181
|
The Modal client object is not intended to be instantiated directly by users.
|
modal/container_process.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2024
|
|
2
2
|
import asyncio
|
|
3
3
|
import platform
|
|
4
|
-
import time
|
|
5
4
|
from typing import Generic, Optional, TypeVar
|
|
6
5
|
|
|
7
6
|
from modal_proto import api_pb2
|
|
@@ -12,194 +11,17 @@ from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
|
12
11
|
from .client import _Client
|
|
13
12
|
from .config import logger
|
|
14
13
|
from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
|
|
15
|
-
from .io_streams import
|
|
14
|
+
from .io_streams import (
|
|
15
|
+
_StreamReader,
|
|
16
|
+
_StreamReaderThroughCommandRouterParams,
|
|
17
|
+
_StreamWriter,
|
|
18
|
+
_StreamWriterThroughCommandRouterParams,
|
|
19
|
+
)
|
|
16
20
|
from .stream_type import StreamType
|
|
17
21
|
|
|
18
22
|
T = TypeVar("T", str, bytes)
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
class _ContainerProcessThroughServer(Generic[T]):
|
|
22
|
-
_process_id: Optional[str] = None
|
|
23
|
-
_stdout: _StreamReader[T]
|
|
24
|
-
_stderr: _StreamReader[T]
|
|
25
|
-
_stdin: _StreamWriter
|
|
26
|
-
_exec_deadline: Optional[float] = None
|
|
27
|
-
_text: bool
|
|
28
|
-
_by_line: bool
|
|
29
|
-
_returncode: Optional[int] = None
|
|
30
|
-
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
process_id: str,
|
|
34
|
-
task_id: str,
|
|
35
|
-
client: _Client,
|
|
36
|
-
stdout: StreamType = StreamType.PIPE,
|
|
37
|
-
stderr: StreamType = StreamType.PIPE,
|
|
38
|
-
exec_deadline: Optional[float] = None,
|
|
39
|
-
text: bool = True,
|
|
40
|
-
by_line: bool = False,
|
|
41
|
-
) -> None:
|
|
42
|
-
self._process_id = process_id
|
|
43
|
-
self._client = client
|
|
44
|
-
self._exec_deadline = exec_deadline
|
|
45
|
-
self._text = text
|
|
46
|
-
self._by_line = by_line
|
|
47
|
-
self._stdout = _StreamReader[T](
|
|
48
|
-
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
49
|
-
process_id,
|
|
50
|
-
"container_process",
|
|
51
|
-
self._client,
|
|
52
|
-
stream_type=stdout,
|
|
53
|
-
text=text,
|
|
54
|
-
by_line=by_line,
|
|
55
|
-
deadline=exec_deadline,
|
|
56
|
-
task_id=task_id,
|
|
57
|
-
)
|
|
58
|
-
self._stderr = _StreamReader[T](
|
|
59
|
-
api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
60
|
-
process_id,
|
|
61
|
-
"container_process",
|
|
62
|
-
self._client,
|
|
63
|
-
stream_type=stderr,
|
|
64
|
-
text=text,
|
|
65
|
-
by_line=by_line,
|
|
66
|
-
deadline=exec_deadline,
|
|
67
|
-
task_id=task_id,
|
|
68
|
-
)
|
|
69
|
-
self._stdin = _StreamWriter(process_id, "container_process", self._client)
|
|
70
|
-
|
|
71
|
-
def __repr__(self) -> str:
|
|
72
|
-
return f"ContainerProcess(process_id={self._process_id!r})"
|
|
73
|
-
|
|
74
|
-
@property
|
|
75
|
-
def stdout(self) -> _StreamReader[T]:
|
|
76
|
-
"""StreamReader for the container process's stdout stream."""
|
|
77
|
-
return self._stdout
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def stderr(self) -> _StreamReader[T]:
|
|
81
|
-
"""StreamReader for the container process's stderr stream."""
|
|
82
|
-
return self._stderr
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def stdin(self) -> _StreamWriter:
|
|
86
|
-
"""StreamWriter for the container process's stdin stream."""
|
|
87
|
-
return self._stdin
|
|
88
|
-
|
|
89
|
-
@property
|
|
90
|
-
def returncode(self) -> int:
|
|
91
|
-
if self._returncode is None:
|
|
92
|
-
raise InvalidError(
|
|
93
|
-
"You must call wait() before accessing the returncode. "
|
|
94
|
-
"To poll for the status of a running process, use poll() instead."
|
|
95
|
-
)
|
|
96
|
-
return self._returncode
|
|
97
|
-
|
|
98
|
-
async def poll(self) -> Optional[int]:
|
|
99
|
-
"""Check if the container process has finished running.
|
|
100
|
-
|
|
101
|
-
Returns `None` if the process is still running, else returns the exit code.
|
|
102
|
-
"""
|
|
103
|
-
assert self._process_id
|
|
104
|
-
if self._returncode is not None:
|
|
105
|
-
return self._returncode
|
|
106
|
-
if self._exec_deadline and time.monotonic() >= self._exec_deadline:
|
|
107
|
-
# TODO(matt): In the future, it would be nice to raise a ContainerExecTimeoutError to make it
|
|
108
|
-
# clear to the user that their sandbox terminated due to a timeout
|
|
109
|
-
self._returncode = -1
|
|
110
|
-
return self._returncode
|
|
111
|
-
|
|
112
|
-
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=0)
|
|
113
|
-
resp = await self._client.stub.ContainerExecWait(req)
|
|
114
|
-
|
|
115
|
-
if resp.completed:
|
|
116
|
-
self._returncode = resp.exit_code
|
|
117
|
-
return self._returncode
|
|
118
|
-
|
|
119
|
-
return None
|
|
120
|
-
|
|
121
|
-
async def _wait_for_completion(self) -> int:
|
|
122
|
-
assert self._process_id
|
|
123
|
-
while True:
|
|
124
|
-
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
|
|
125
|
-
resp = await self._client.stub.ContainerExecWait(req)
|
|
126
|
-
if resp.completed:
|
|
127
|
-
return resp.exit_code
|
|
128
|
-
|
|
129
|
-
async def wait(self) -> int:
|
|
130
|
-
"""Wait for the container process to finish running. Returns the exit code."""
|
|
131
|
-
if self._returncode is not None:
|
|
132
|
-
return self._returncode
|
|
133
|
-
|
|
134
|
-
try:
|
|
135
|
-
timeout = None
|
|
136
|
-
if self._exec_deadline:
|
|
137
|
-
timeout = self._exec_deadline - time.monotonic()
|
|
138
|
-
if timeout <= 0:
|
|
139
|
-
raise TimeoutError()
|
|
140
|
-
self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
|
|
141
|
-
except (asyncio.TimeoutError, TimeoutError):
|
|
142
|
-
self._returncode = -1
|
|
143
|
-
logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
|
|
144
|
-
return self._returncode
|
|
145
|
-
|
|
146
|
-
async def attach(self):
|
|
147
|
-
"""mdmd:hidden"""
|
|
148
|
-
if platform.system() == "Windows":
|
|
149
|
-
print("interactive exec is not currently supported on Windows.") # noqa: T201
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
from .output import OutputManager
|
|
153
|
-
|
|
154
|
-
output = OutputManager.get()
|
|
155
|
-
connecting_status = output.status("Connecting...")
|
|
156
|
-
connecting_status.start()
|
|
157
|
-
on_connect = asyncio.Event()
|
|
158
|
-
|
|
159
|
-
async def _write_to_fd_loop(stream: _StreamReader):
|
|
160
|
-
# This is required to make modal shell to an existing task work,
|
|
161
|
-
# since that uses ContainerExec RPCs directly, but this is hacky.
|
|
162
|
-
#
|
|
163
|
-
# TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
|
|
164
|
-
from .io_streams import _StreamReaderThroughServer
|
|
165
|
-
|
|
166
|
-
assert isinstance(stream._impl, _StreamReaderThroughServer)
|
|
167
|
-
stream_impl = stream._impl
|
|
168
|
-
# Don't skip empty messages so we can detect when the process has booted.
|
|
169
|
-
async for chunk in stream_impl._get_logs(skip_empty_messages=False):
|
|
170
|
-
if not on_connect.is_set():
|
|
171
|
-
connecting_status.stop()
|
|
172
|
-
on_connect.set()
|
|
173
|
-
|
|
174
|
-
await write_to_fd(stream.file_descriptor, chunk)
|
|
175
|
-
|
|
176
|
-
async def _handle_input(data: bytes, message_index: int):
|
|
177
|
-
self.stdin.write(data)
|
|
178
|
-
await self.stdin.drain()
|
|
179
|
-
|
|
180
|
-
async with TaskContext() as tc:
|
|
181
|
-
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
|
182
|
-
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
# time out if we can't connect to the server fast enough
|
|
186
|
-
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
|
187
|
-
|
|
188
|
-
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
|
189
|
-
await stdout_task
|
|
190
|
-
await stderr_task
|
|
191
|
-
|
|
192
|
-
# TODO: this doesn't work right now.
|
|
193
|
-
# if exit_status != 0:
|
|
194
|
-
# raise ExecutionError(f"Process exited with status code {exit_status}")
|
|
195
|
-
|
|
196
|
-
except (asyncio.TimeoutError, TimeoutError):
|
|
197
|
-
connecting_status.stop()
|
|
198
|
-
stdout_task.cancel()
|
|
199
|
-
stderr_task.cancel()
|
|
200
|
-
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
201
|
-
|
|
202
|
-
|
|
203
25
|
async def _iter_stream_as_bytes(stream: _StreamReader[T]):
|
|
204
26
|
"""Yield raw bytes from a StreamReader regardless of text mode/backend."""
|
|
205
27
|
async for part in stream:
|
|
@@ -209,19 +31,19 @@ async def _iter_stream_as_bytes(stream: _StreamReader[T]):
|
|
|
209
31
|
yield part
|
|
210
32
|
|
|
211
33
|
|
|
212
|
-
class
|
|
213
|
-
"""
|
|
214
|
-
|
|
34
|
+
class _ContainerProcess(Generic[T]):
|
|
35
|
+
"""Represents a running process in a container.
|
|
36
|
+
|
|
37
|
+
Container processes communicate via direct communication with
|
|
215
38
|
the Modal worker where the container is running.
|
|
216
39
|
"""
|
|
217
40
|
|
|
218
41
|
def __init__(
|
|
219
42
|
self,
|
|
220
43
|
process_id: str,
|
|
44
|
+
task_id: str,
|
|
221
45
|
client: _Client,
|
|
222
46
|
command_router_client: TaskCommandRouterClient,
|
|
223
|
-
task_id: str,
|
|
224
|
-
*,
|
|
225
47
|
stdout: StreamType = StreamType.PIPE,
|
|
226
48
|
stderr: StreamType = StreamType.PIPE,
|
|
227
49
|
exec_deadline: Optional[float] = None,
|
|
@@ -236,35 +58,35 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
236
58
|
self._by_line = by_line
|
|
237
59
|
self._task_id = task_id
|
|
238
60
|
self._stdout = _StreamReader[T](
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
61
|
+
_StreamReaderThroughCommandRouterParams(
|
|
62
|
+
file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
63
|
+
task_id=self._task_id,
|
|
64
|
+
object_id=process_id,
|
|
65
|
+
command_router_client=self._command_router_client,
|
|
66
|
+
deadline=exec_deadline,
|
|
67
|
+
),
|
|
243
68
|
stream_type=stdout,
|
|
244
69
|
text=text,
|
|
245
70
|
by_line=by_line,
|
|
246
|
-
deadline=exec_deadline,
|
|
247
|
-
command_router_client=self._command_router_client,
|
|
248
|
-
task_id=self._task_id,
|
|
249
71
|
)
|
|
250
72
|
self._stderr = _StreamReader[T](
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
73
|
+
_StreamReaderThroughCommandRouterParams(
|
|
74
|
+
file_descriptor=api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
75
|
+
task_id=self._task_id,
|
|
76
|
+
object_id=process_id,
|
|
77
|
+
command_router_client=self._command_router_client,
|
|
78
|
+
deadline=exec_deadline,
|
|
79
|
+
),
|
|
255
80
|
stream_type=stderr,
|
|
256
81
|
text=text,
|
|
257
82
|
by_line=by_line,
|
|
258
|
-
deadline=exec_deadline,
|
|
259
|
-
command_router_client=self._command_router_client,
|
|
260
|
-
task_id=self._task_id,
|
|
261
83
|
)
|
|
262
84
|
self._stdin = _StreamWriter(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
85
|
+
_StreamWriterThroughCommandRouterParams(
|
|
86
|
+
task_id=self._task_id,
|
|
87
|
+
object_id=process_id,
|
|
88
|
+
command_router_client=self._command_router_client,
|
|
89
|
+
)
|
|
268
90
|
)
|
|
269
91
|
self._returncode = None
|
|
270
92
|
|
|
@@ -273,14 +95,17 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
273
95
|
|
|
274
96
|
@property
|
|
275
97
|
def stdout(self) -> _StreamReader[T]:
|
|
98
|
+
"""StreamReader for the container process's stdout stream."""
|
|
276
99
|
return self._stdout
|
|
277
100
|
|
|
278
101
|
@property
|
|
279
102
|
def stderr(self) -> _StreamReader[T]:
|
|
103
|
+
"""StreamReader for the container process's stderr stream."""
|
|
280
104
|
return self._stderr
|
|
281
105
|
|
|
282
106
|
@property
|
|
283
107
|
def stdin(self) -> _StreamWriter:
|
|
108
|
+
"""StreamWriter for the container process's stdin stream."""
|
|
284
109
|
return self._stdin
|
|
285
110
|
|
|
286
111
|
@property
|
|
@@ -293,6 +118,10 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
293
118
|
return self._returncode
|
|
294
119
|
|
|
295
120
|
async def poll(self) -> Optional[int]:
|
|
121
|
+
"""Check if the container process has finished running.
|
|
122
|
+
|
|
123
|
+
Returns `None` if the process is still running, else returns the exit code.
|
|
124
|
+
"""
|
|
296
125
|
if self._returncode is not None:
|
|
297
126
|
return self._returncode
|
|
298
127
|
try:
|
|
@@ -322,6 +151,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
322
151
|
raise
|
|
323
152
|
|
|
324
153
|
async def wait(self) -> int:
|
|
154
|
+
"""Wait for the container process to finish running. Returns the exit code."""
|
|
325
155
|
if self._returncode is not None:
|
|
326
156
|
return self._returncode
|
|
327
157
|
|
|
@@ -345,6 +175,7 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
345
175
|
return self._returncode
|
|
346
176
|
|
|
347
177
|
async def attach(self):
|
|
178
|
+
"""mdmd:hidden"""
|
|
348
179
|
if platform.system() == "Windows":
|
|
349
180
|
print("interactive exec is not currently supported on Windows.") # noqa: T201
|
|
350
181
|
return
|
|
@@ -390,81 +221,4 @@ class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
|
390
221
|
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
391
222
|
|
|
392
223
|
|
|
393
|
-
class _ContainerProcess(Generic[T]):
|
|
394
|
-
"""Represents a running process in a container."""
|
|
395
|
-
|
|
396
|
-
def __init__(
|
|
397
|
-
self,
|
|
398
|
-
process_id: str,
|
|
399
|
-
task_id: str,
|
|
400
|
-
client: _Client,
|
|
401
|
-
stdout: StreamType = StreamType.PIPE,
|
|
402
|
-
stderr: StreamType = StreamType.PIPE,
|
|
403
|
-
exec_deadline: Optional[float] = None,
|
|
404
|
-
text: bool = True,
|
|
405
|
-
by_line: bool = False,
|
|
406
|
-
command_router_client: Optional[TaskCommandRouterClient] = None,
|
|
407
|
-
) -> None:
|
|
408
|
-
if command_router_client is None:
|
|
409
|
-
self._impl = _ContainerProcessThroughServer(
|
|
410
|
-
process_id,
|
|
411
|
-
task_id,
|
|
412
|
-
client,
|
|
413
|
-
stdout=stdout,
|
|
414
|
-
stderr=stderr,
|
|
415
|
-
exec_deadline=exec_deadline,
|
|
416
|
-
text=text,
|
|
417
|
-
by_line=by_line,
|
|
418
|
-
)
|
|
419
|
-
else:
|
|
420
|
-
self._impl = _ContainerProcessThroughCommandRouter(
|
|
421
|
-
process_id,
|
|
422
|
-
client,
|
|
423
|
-
command_router_client,
|
|
424
|
-
task_id,
|
|
425
|
-
stdout=stdout,
|
|
426
|
-
stderr=stderr,
|
|
427
|
-
exec_deadline=exec_deadline,
|
|
428
|
-
text=text,
|
|
429
|
-
by_line=by_line,
|
|
430
|
-
)
|
|
431
|
-
|
|
432
|
-
def __repr__(self) -> str:
|
|
433
|
-
return self._impl.__repr__()
|
|
434
|
-
|
|
435
|
-
@property
|
|
436
|
-
def stdout(self) -> _StreamReader[T]:
|
|
437
|
-
"""StreamReader for the container process's stdout stream."""
|
|
438
|
-
return self._impl.stdout
|
|
439
|
-
|
|
440
|
-
@property
|
|
441
|
-
def stderr(self) -> _StreamReader[T]:
|
|
442
|
-
"""StreamReader for the container process's stderr stream."""
|
|
443
|
-
return self._impl.stderr
|
|
444
|
-
|
|
445
|
-
@property
|
|
446
|
-
def stdin(self) -> _StreamWriter:
|
|
447
|
-
"""StreamWriter for the container process's stdin stream."""
|
|
448
|
-
return self._impl.stdin
|
|
449
|
-
|
|
450
|
-
@property
|
|
451
|
-
def returncode(self) -> int:
|
|
452
|
-
return self._impl.returncode
|
|
453
|
-
|
|
454
|
-
async def poll(self) -> Optional[int]:
|
|
455
|
-
"""Check if the container process has finished running.
|
|
456
|
-
|
|
457
|
-
Returns `None` if the process is still running, else returns the exit code.
|
|
458
|
-
"""
|
|
459
|
-
return await self._impl.poll()
|
|
460
|
-
|
|
461
|
-
async def wait(self) -> int:
|
|
462
|
-
"""Wait for the container process to finish running. Returns the exit code."""
|
|
463
|
-
return await self._impl.wait()
|
|
464
|
-
|
|
465
|
-
async def attach(self):
|
|
466
|
-
"""mdmd:hidden"""
|
|
467
|
-
await self._impl.attach()
|
|
468
|
-
|
|
469
|
-
|
|
470
224
|
ContainerProcess = synchronize_api(_ContainerProcess)
|