modal 1.2.1.dev13__tar.gz → 1.2.1.dev14__tar.gz
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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/PKG-INFO +1 -1
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/task_command_router_client.py +18 -4
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/cluster.py +4 -2
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/container.py +4 -2
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/client.pyi +2 -2
- modal-1.2.1.dev14/modal/container_process.py +470 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/container_process.pyi +91 -32
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/functions.pyi +6 -6
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/sandbox.py +1 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/PKG-INFO +1 -1
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_version/__init__.py +1 -1
- modal-1.2.1.dev13/modal/container_process.py +0 -204
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/LICENSE +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/README.md +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/__main__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_billing.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_clustered_functions.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_clustered_functions.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_container_entrypoint.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_functions.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_ipython.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_location.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_object.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_output.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_partial_function.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_pty.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_resolver.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_resources.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/asgi.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/container_io_manager.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/container_io_manager.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/execution_context.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/execution_context.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/gpu_memory_snapshot.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/telemetry.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_runtime/user_code_imports.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_serialization.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_traceback.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_tunnel.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_tunnel.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_type_manager.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/app_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/async_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/auth_token_manager.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/blob_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/bytes_io_segment_payload.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/deprecation.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/docker_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/function_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/git_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/grpc_testing.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/grpc_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/hash_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/http_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/jwt_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/logger.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/mount_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/name_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/package_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/pattern_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/rand_pb_testing.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/shell_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_utils/time_utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/a2wsgi_wsgi.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/cloudpickle.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_vendor/tblib.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/_watcher.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/app.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/app.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/billing.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2023.12.312.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2023.12.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2024.04.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2024.10.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/2025.06.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/PREVIEW.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/README.md +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/builder/base-images.json +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/call_graph.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/_download.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/_traceback.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/app.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/config.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/dict.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/entry_point.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/environment.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/import_refs.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/launch.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/network_file_system.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/profile.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/launch_instance_ssh.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/run_jupyter.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/run_marimo.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/programs/vscode.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/queues.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/run.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/secret.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/token.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/utils.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cli/volume.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/client.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cloud_bucket_mount.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cloud_bucket_mount.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cls.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/cls.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/config.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/dict.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/dict.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/environments.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/environments.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/exception.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/flash.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/flash.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/experimental/ipython.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_io.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_io.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/file_pattern_matcher.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/functions.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/gpu.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/image.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/image.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/io_streams.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/io_streams.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/mount.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/mount.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/network_file_system.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/network_file_system.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/object.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/object.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/output.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/parallel_map.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/parallel_map.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/partial_function.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/partial_function.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/proxy.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/proxy.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/py.typed +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/queue.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/queue.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/retries.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/runner.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/runner.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/running_app.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/sandbox.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/schedule.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/scheduler_placement.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/secret.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/secret.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/serving.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/serving.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/snapshot.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/snapshot.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/stream_type.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/token_flow.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/token_flow.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/volume.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal/volume.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/SOURCES.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/dependency_links.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/entry_points.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/requires.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal.egg-info/top_level.txt +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/gen_cli_docs.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/gen_reference_docs.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/mdmd.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_docs/mdmd/signatures.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/__init__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api.proto +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/api_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/modal_api_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/py.typed +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router.proto +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/sandbox_router_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router.proto +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2_grpc.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_proto/task_command_router_pb2_grpc.pyi +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/modal_version/__main__.py +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/pyproject.toml +0 -0
- {modal-1.2.1.dev13 → modal-1.2.1.dev14}/setup.cfg +0 -0
|
@@ -292,7 +292,9 @@ class TaskCommandRouterClient:
|
|
|
292
292
|
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
|
|
293
293
|
)
|
|
294
294
|
|
|
295
|
-
async def exec_poll(
|
|
295
|
+
async def exec_poll(
|
|
296
|
+
self, task_id: str, exec_id: str, deadline: Optional[float] = None
|
|
297
|
+
) -> sr_pb2.TaskExecPollResponse:
|
|
296
298
|
"""Poll for the exit status of an exec'd command, properly retrying on transient errors.
|
|
297
299
|
|
|
298
300
|
Args:
|
|
@@ -302,13 +304,25 @@ class TaskCommandRouterClient:
|
|
|
302
304
|
sr_pb2.TaskExecPollResponse: The exit status of the command if it has completed.
|
|
303
305
|
|
|
304
306
|
Raises:
|
|
307
|
+
ExecTimeoutError: If the deadline is exceeded.
|
|
305
308
|
Other errors: If retries are exhausted on transient errors or if there's an error
|
|
306
309
|
from the RPC itself.
|
|
307
310
|
"""
|
|
308
311
|
request = sr_pb2.TaskExecPollRequest(task_id=task_id, exec_id=exec_id)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
+
# The timeout here is really a backstop in the event of a hang contacting
|
|
313
|
+
# the command router. Poll should usually be instantaneous.
|
|
314
|
+
timeout = deadline - time.monotonic() if deadline is not None else None
|
|
315
|
+
if timeout is not None and timeout <= 0:
|
|
316
|
+
raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
|
|
317
|
+
try:
|
|
318
|
+
return await asyncio.wait_for(
|
|
319
|
+
call_with_retries_on_transient_errors(
|
|
320
|
+
lambda: self._call_with_auth_retry(self._stub.TaskExecPoll, request)
|
|
321
|
+
),
|
|
322
|
+
timeout=timeout,
|
|
323
|
+
)
|
|
324
|
+
except asyncio.TimeoutError:
|
|
325
|
+
raise ExecTimeoutError(f"Deadline exceeded while polling for exec {exec_id}")
|
|
312
326
|
|
|
313
327
|
async def exec_wait(
|
|
314
328
|
self,
|
|
@@ -83,7 +83,9 @@ async def shell(
|
|
|
83
83
|
)
|
|
84
84
|
exec_res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
|
85
85
|
if pty:
|
|
86
|
-
await _ContainerProcess(exec_res.exec_id, client).attach()
|
|
86
|
+
await _ContainerProcess(exec_res.exec_id, task_id, client).attach()
|
|
87
87
|
else:
|
|
88
88
|
# TODO: redirect stderr to its own stream?
|
|
89
|
-
await _ContainerProcess(
|
|
89
|
+
await _ContainerProcess(
|
|
90
|
+
exec_res.exec_id, task_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
91
|
+
).wait()
|
|
@@ -80,10 +80,12 @@ async def exec(
|
|
|
80
80
|
res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
|
|
81
81
|
|
|
82
82
|
if pty:
|
|
83
|
-
await _ContainerProcess(res.exec_id, client).attach()
|
|
83
|
+
await _ContainerProcess(res.exec_id, container_id, client).attach()
|
|
84
84
|
else:
|
|
85
85
|
# TODO: redirect stderr to its own stream?
|
|
86
|
-
await _ContainerProcess(
|
|
86
|
+
await _ContainerProcess(
|
|
87
|
+
res.exec_id, container_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
|
|
88
|
+
).wait()
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
@container_cli.command("stop")
|
|
@@ -33,7 +33,7 @@ class _Client:
|
|
|
33
33
|
server_url: str,
|
|
34
34
|
client_type: int,
|
|
35
35
|
credentials: typing.Optional[tuple[str, str]],
|
|
36
|
-
version: str = "1.2.1.
|
|
36
|
+
version: str = "1.2.1.dev14",
|
|
37
37
|
):
|
|
38
38
|
"""mdmd:hidden
|
|
39
39
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -164,7 +164,7 @@ class Client:
|
|
|
164
164
|
server_url: str,
|
|
165
165
|
client_type: int,
|
|
166
166
|
credentials: typing.Optional[tuple[str, str]],
|
|
167
|
-
version: str = "1.2.1.
|
|
167
|
+
version: str = "1.2.1.dev14",
|
|
168
168
|
):
|
|
169
169
|
"""mdmd:hidden
|
|
170
170
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright Modal Labs 2024
|
|
2
|
+
import asyncio
|
|
3
|
+
import platform
|
|
4
|
+
import time
|
|
5
|
+
from typing import Generic, Optional, TypeVar
|
|
6
|
+
|
|
7
|
+
from modal_proto import api_pb2
|
|
8
|
+
|
|
9
|
+
from ._utils.async_utils import TaskContext, synchronize_api
|
|
10
|
+
from ._utils.grpc_utils import retry_transient_errors
|
|
11
|
+
from ._utils.shell_utils import stream_from_stdin, write_to_fd
|
|
12
|
+
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
13
|
+
from .client import _Client
|
|
14
|
+
from .config import logger
|
|
15
|
+
from .exception import ExecTimeoutError, InteractiveTimeoutError, InvalidError
|
|
16
|
+
from .io_streams import _StreamReader, _StreamWriter
|
|
17
|
+
from .stream_type import StreamType
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T", str, bytes)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _ContainerProcessThroughServer(Generic[T]):
|
|
23
|
+
_process_id: Optional[str] = None
|
|
24
|
+
_stdout: _StreamReader[T]
|
|
25
|
+
_stderr: _StreamReader[T]
|
|
26
|
+
_stdin: _StreamWriter
|
|
27
|
+
_exec_deadline: Optional[float] = None
|
|
28
|
+
_text: bool
|
|
29
|
+
_by_line: bool
|
|
30
|
+
_returncode: Optional[int] = None
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
process_id: str,
|
|
35
|
+
task_id: str,
|
|
36
|
+
client: _Client,
|
|
37
|
+
stdout: StreamType = StreamType.PIPE,
|
|
38
|
+
stderr: StreamType = StreamType.PIPE,
|
|
39
|
+
exec_deadline: Optional[float] = None,
|
|
40
|
+
text: bool = True,
|
|
41
|
+
by_line: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._process_id = process_id
|
|
44
|
+
self._client = client
|
|
45
|
+
self._exec_deadline = exec_deadline
|
|
46
|
+
self._text = text
|
|
47
|
+
self._by_line = by_line
|
|
48
|
+
self._stdout = _StreamReader[T](
|
|
49
|
+
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
50
|
+
process_id,
|
|
51
|
+
"container_process",
|
|
52
|
+
self._client,
|
|
53
|
+
stream_type=stdout,
|
|
54
|
+
text=text,
|
|
55
|
+
by_line=by_line,
|
|
56
|
+
deadline=exec_deadline,
|
|
57
|
+
task_id=task_id,
|
|
58
|
+
)
|
|
59
|
+
self._stderr = _StreamReader[T](
|
|
60
|
+
api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
61
|
+
process_id,
|
|
62
|
+
"container_process",
|
|
63
|
+
self._client,
|
|
64
|
+
stream_type=stderr,
|
|
65
|
+
text=text,
|
|
66
|
+
by_line=by_line,
|
|
67
|
+
deadline=exec_deadline,
|
|
68
|
+
task_id=task_id,
|
|
69
|
+
)
|
|
70
|
+
self._stdin = _StreamWriter(process_id, "container_process", self._client)
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
return f"ContainerProcess(process_id={self._process_id!r})"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def stdout(self) -> _StreamReader[T]:
|
|
77
|
+
"""StreamReader for the container process's stdout stream."""
|
|
78
|
+
return self._stdout
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def stderr(self) -> _StreamReader[T]:
|
|
82
|
+
"""StreamReader for the container process's stderr stream."""
|
|
83
|
+
return self._stderr
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def stdin(self) -> _StreamWriter:
|
|
87
|
+
"""StreamWriter for the container process's stdin stream."""
|
|
88
|
+
return self._stdin
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def returncode(self) -> int:
|
|
92
|
+
if self._returncode is None:
|
|
93
|
+
raise InvalidError(
|
|
94
|
+
"You must call wait() before accessing the returncode. "
|
|
95
|
+
"To poll for the status of a running process, use poll() instead."
|
|
96
|
+
)
|
|
97
|
+
return self._returncode
|
|
98
|
+
|
|
99
|
+
async def poll(self) -> Optional[int]:
|
|
100
|
+
"""Check if the container process has finished running.
|
|
101
|
+
|
|
102
|
+
Returns `None` if the process is still running, else returns the exit code.
|
|
103
|
+
"""
|
|
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: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(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
|
+
while True:
|
|
123
|
+
req = api_pb2.ContainerExecWaitRequest(exec_id=self._process_id, timeout=10)
|
|
124
|
+
resp: api_pb2.ContainerExecWaitResponse = await retry_transient_errors(
|
|
125
|
+
self._client.stub.ContainerExecWait, req
|
|
126
|
+
)
|
|
127
|
+
if resp.completed:
|
|
128
|
+
return resp.exit_code
|
|
129
|
+
|
|
130
|
+
async def wait(self) -> int:
|
|
131
|
+
"""Wait for the container process to finish running. Returns the exit code."""
|
|
132
|
+
if self._returncode is not None:
|
|
133
|
+
return self._returncode
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
timeout = None
|
|
137
|
+
if self._exec_deadline:
|
|
138
|
+
timeout = self._exec_deadline - time.monotonic()
|
|
139
|
+
if timeout <= 0:
|
|
140
|
+
raise TimeoutError()
|
|
141
|
+
self._returncode = await asyncio.wait_for(self._wait_for_completion(), timeout=timeout)
|
|
142
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
143
|
+
self._returncode = -1
|
|
144
|
+
logger.debug(f"ContainerProcess {self._process_id} wait completed with returncode {self._returncode}")
|
|
145
|
+
return self._returncode
|
|
146
|
+
|
|
147
|
+
async def attach(self):
|
|
148
|
+
"""mdmd:hidden"""
|
|
149
|
+
if platform.system() == "Windows":
|
|
150
|
+
print("interactive exec is not currently supported on Windows.")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
from ._output import make_console
|
|
154
|
+
|
|
155
|
+
console = make_console()
|
|
156
|
+
|
|
157
|
+
connecting_status = console.status("Connecting...")
|
|
158
|
+
connecting_status.start()
|
|
159
|
+
on_connect = asyncio.Event()
|
|
160
|
+
|
|
161
|
+
async def _write_to_fd_loop(stream: _StreamReader):
|
|
162
|
+
# This is required to make modal shell to an existing task work,
|
|
163
|
+
# since that uses ContainerExec RPCs directly, but this is hacky.
|
|
164
|
+
#
|
|
165
|
+
# TODO(saltzm): Once we use the new exec path for that use case, this code can all be removed.
|
|
166
|
+
from .io_streams import _StreamReaderThroughServer
|
|
167
|
+
|
|
168
|
+
assert isinstance(stream._impl, _StreamReaderThroughServer)
|
|
169
|
+
stream_impl = stream._impl
|
|
170
|
+
# Don't skip empty messages so we can detect when the process has booted.
|
|
171
|
+
async for chunk in stream_impl._get_logs(skip_empty_messages=False):
|
|
172
|
+
if chunk is None:
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
if not on_connect.is_set():
|
|
176
|
+
connecting_status.stop()
|
|
177
|
+
on_connect.set()
|
|
178
|
+
|
|
179
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
|
180
|
+
|
|
181
|
+
async def _handle_input(data: bytes, message_index: int):
|
|
182
|
+
self.stdin.write(data)
|
|
183
|
+
await self.stdin.drain()
|
|
184
|
+
|
|
185
|
+
async with TaskContext() as tc:
|
|
186
|
+
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
|
187
|
+
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# time out if we can't connect to the server fast enough
|
|
191
|
+
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
|
192
|
+
|
|
193
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
|
194
|
+
await stdout_task
|
|
195
|
+
await stderr_task
|
|
196
|
+
|
|
197
|
+
# TODO: this doesn't work right now.
|
|
198
|
+
# if exit_status != 0:
|
|
199
|
+
# raise ExecutionError(f"Process exited with status code {exit_status}")
|
|
200
|
+
|
|
201
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
202
|
+
connecting_status.stop()
|
|
203
|
+
stdout_task.cancel()
|
|
204
|
+
stderr_task.cancel()
|
|
205
|
+
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def _iter_stream_as_bytes(stream: _StreamReader[T]):
|
|
209
|
+
"""Yield raw bytes from a StreamReader regardless of text mode/backend."""
|
|
210
|
+
async for part in stream:
|
|
211
|
+
if isinstance(part, str):
|
|
212
|
+
yield part.encode("utf-8")
|
|
213
|
+
else:
|
|
214
|
+
yield part
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class _ContainerProcessThroughCommandRouter(Generic[T]):
|
|
218
|
+
"""
|
|
219
|
+
Container process implementation that works via direct communication with
|
|
220
|
+
the Modal worker where the container is running.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
process_id: str,
|
|
226
|
+
client: _Client,
|
|
227
|
+
command_router_client: TaskCommandRouterClient,
|
|
228
|
+
task_id: str,
|
|
229
|
+
*,
|
|
230
|
+
stdout: StreamType = StreamType.PIPE,
|
|
231
|
+
stderr: StreamType = StreamType.PIPE,
|
|
232
|
+
exec_deadline: Optional[float] = None,
|
|
233
|
+
text: bool = True,
|
|
234
|
+
by_line: bool = False,
|
|
235
|
+
) -> None:
|
|
236
|
+
self._client = client
|
|
237
|
+
self._command_router_client = command_router_client
|
|
238
|
+
self._process_id = process_id
|
|
239
|
+
self._exec_deadline = exec_deadline
|
|
240
|
+
self._text = text
|
|
241
|
+
self._by_line = by_line
|
|
242
|
+
self._task_id = task_id
|
|
243
|
+
self._stdout = _StreamReader[T](
|
|
244
|
+
api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
245
|
+
process_id,
|
|
246
|
+
"container_process",
|
|
247
|
+
self._client,
|
|
248
|
+
stream_type=stdout,
|
|
249
|
+
text=text,
|
|
250
|
+
by_line=by_line,
|
|
251
|
+
deadline=exec_deadline,
|
|
252
|
+
command_router_client=self._command_router_client,
|
|
253
|
+
task_id=self._task_id,
|
|
254
|
+
)
|
|
255
|
+
self._stderr = _StreamReader[T](
|
|
256
|
+
api_pb2.FILE_DESCRIPTOR_STDERR,
|
|
257
|
+
process_id,
|
|
258
|
+
"container_process",
|
|
259
|
+
self._client,
|
|
260
|
+
stream_type=stderr,
|
|
261
|
+
text=text,
|
|
262
|
+
by_line=by_line,
|
|
263
|
+
deadline=exec_deadline,
|
|
264
|
+
command_router_client=self._command_router_client,
|
|
265
|
+
task_id=self._task_id,
|
|
266
|
+
)
|
|
267
|
+
self._stdin = _StreamWriter(
|
|
268
|
+
process_id,
|
|
269
|
+
"container_process",
|
|
270
|
+
self._client,
|
|
271
|
+
command_router_client=self._command_router_client,
|
|
272
|
+
task_id=self._task_id,
|
|
273
|
+
)
|
|
274
|
+
self._returncode = None
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def stdout(self) -> _StreamReader[T]:
|
|
278
|
+
return self._stdout
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def stderr(self) -> _StreamReader[T]:
|
|
282
|
+
return self._stderr
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def stdin(self) -> _StreamWriter:
|
|
286
|
+
return self._stdin
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def returncode(self) -> int:
|
|
290
|
+
if self._returncode is None:
|
|
291
|
+
raise InvalidError(
|
|
292
|
+
"You must call wait() before accessing the returncode. "
|
|
293
|
+
"To poll for the status of a running process, use poll() instead."
|
|
294
|
+
)
|
|
295
|
+
return self._returncode
|
|
296
|
+
|
|
297
|
+
async def poll(self) -> Optional[int]:
|
|
298
|
+
if self._returncode is not None:
|
|
299
|
+
return self._returncode
|
|
300
|
+
try:
|
|
301
|
+
resp = await self._command_router_client.exec_poll(self._task_id, self._process_id, self._exec_deadline)
|
|
302
|
+
which = resp.WhichOneof("exit_status")
|
|
303
|
+
if which is None:
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
if which == "code":
|
|
307
|
+
self._returncode = int(resp.code)
|
|
308
|
+
return self._returncode
|
|
309
|
+
elif which == "signal":
|
|
310
|
+
self._returncode = 128 + int(resp.signal)
|
|
311
|
+
return self._returncode
|
|
312
|
+
else:
|
|
313
|
+
logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
|
|
314
|
+
raise InvalidError("Unexpected exit status")
|
|
315
|
+
except ExecTimeoutError:
|
|
316
|
+
logger.debug(f"ContainerProcess poll for {self._process_id} did not complete within deadline")
|
|
317
|
+
return None
|
|
318
|
+
except Exception as e:
|
|
319
|
+
# Re-raise non-transient errors or errors resulting from exceeding retries on transient errors.
|
|
320
|
+
logger.warning(f"ContainerProcess poll for {self._process_id} failed: {e}")
|
|
321
|
+
raise
|
|
322
|
+
|
|
323
|
+
async def wait(self) -> int:
|
|
324
|
+
if self._returncode is not None:
|
|
325
|
+
return self._returncode
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
resp = await self._command_router_client.exec_wait(self._task_id, self._process_id, self._exec_deadline)
|
|
329
|
+
which = resp.WhichOneof("exit_status")
|
|
330
|
+
if which == "code":
|
|
331
|
+
self._returncode = int(resp.code)
|
|
332
|
+
elif which == "signal":
|
|
333
|
+
self._returncode = 128 + int(resp.signal)
|
|
334
|
+
else:
|
|
335
|
+
logger.debug(f"ContainerProcess {self._process_id} exited with unexpected status: {which}")
|
|
336
|
+
self._returncode = -1
|
|
337
|
+
raise InvalidError("Unexpected exit status")
|
|
338
|
+
except ExecTimeoutError:
|
|
339
|
+
logger.debug(f"ContainerProcess {self._process_id} did not complete within deadline")
|
|
340
|
+
# TODO(saltzm): This is a weird API, but customers currently may rely on it. This
|
|
341
|
+
# should be a ExecTimeoutError.
|
|
342
|
+
self._returncode = -1
|
|
343
|
+
|
|
344
|
+
return self._returncode
|
|
345
|
+
|
|
346
|
+
async def attach(self):
|
|
347
|
+
if platform.system() == "Windows":
|
|
348
|
+
print("interactive exec is not currently supported on Windows.")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
from ._output import make_console
|
|
352
|
+
|
|
353
|
+
console = make_console()
|
|
354
|
+
|
|
355
|
+
connecting_status = console.status("Connecting...")
|
|
356
|
+
connecting_status.start()
|
|
357
|
+
on_connect = asyncio.Event()
|
|
358
|
+
|
|
359
|
+
async def _write_to_fd_loop(stream: _StreamReader[T]):
|
|
360
|
+
async for chunk in _iter_stream_as_bytes(stream):
|
|
361
|
+
if chunk is None:
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
if not on_connect.is_set():
|
|
365
|
+
connecting_status.stop()
|
|
366
|
+
on_connect.set()
|
|
367
|
+
|
|
368
|
+
await write_to_fd(stream.file_descriptor, chunk)
|
|
369
|
+
|
|
370
|
+
async def _handle_input(data: bytes, message_index: int):
|
|
371
|
+
self.stdin.write(data)
|
|
372
|
+
await self.stdin.drain()
|
|
373
|
+
|
|
374
|
+
async with TaskContext() as tc:
|
|
375
|
+
stdout_task = tc.create_task(_write_to_fd_loop(self.stdout))
|
|
376
|
+
stderr_task = tc.create_task(_write_to_fd_loop(self.stderr))
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
# Time out if we can't connect fast enough.
|
|
380
|
+
await asyncio.wait_for(on_connect.wait(), timeout=60)
|
|
381
|
+
|
|
382
|
+
async with stream_from_stdin(_handle_input, use_raw_terminal=True):
|
|
383
|
+
await stdout_task
|
|
384
|
+
await stderr_task
|
|
385
|
+
|
|
386
|
+
except (asyncio.TimeoutError, TimeoutError):
|
|
387
|
+
connecting_status.stop()
|
|
388
|
+
stdout_task.cancel()
|
|
389
|
+
stderr_task.cancel()
|
|
390
|
+
raise InteractiveTimeoutError("Failed to establish connection to container. Please try again.")
|
|
391
|
+
|
|
392
|
+
|
|
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
|
+
ContainerProcess = synchronize_api(_ContainerProcess)
|