mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
refactor(sandbox): extract connection helpers and move run_command to helper module
- Add helpers.py with connection management utilities:
- with_connection: context manager for connection lifecycle
- submit_command: execute command and return CommandFuture
- execute: run command with auto connection, raise on failure
- try_execute: run command with auto connection, return result
- Add CommandExecutionError to exec.py for typed error handling
with access to exit_code, stderr, and full result
- Remove run_command method from VirtualEnvironment base class
(now available as submit_command helper)
- Update all call sites to use new helper functions:
- sandbox/session.py
- sandbox/storage/archive_storage.py
- sandbox/bash/bash_tool.py
- workflow/nodes/command/node.py
- Add comprehensive unit tests for helpers with connection reuse
This commit is contained in:
@ -1,3 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.virtual_environment.__base.entities import CommandResult
|
||||
|
||||
|
||||
class ArchNotSupportedError(Exception):
|
||||
"""Exception raised when the architecture is not supported."""
|
||||
|
||||
@ -20,3 +28,19 @@ class SandboxConfigValidationError(ValueError):
|
||||
"""Exception raised when sandbox configuration validation fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CommandExecutionError(Exception):
|
||||
"""Raised when a command execution fails."""
|
||||
|
||||
def __init__(self, message: str, result: CommandResult):
|
||||
super().__init__(message)
|
||||
self.result = result
|
||||
|
||||
@property
|
||||
def exit_code(self) -> int | None:
|
||||
return self.result.exit_code
|
||||
|
||||
@property
|
||||
def stderr(self) -> bytes:
|
||||
return self.result.stderr
|
||||
|
||||
149
api/core/virtual_environment/__base/helpers.py
Normal file
149
api/core/virtual_environment/__base/helpers.py
Normal file
@ -0,0 +1,149 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import Generator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
|
||||
from core.virtual_environment.__base.command_future import CommandFuture
|
||||
from core.virtual_environment.__base.entities import CommandResult, ConnectionHandle
|
||||
from core.virtual_environment.__base.exec import CommandExecutionError
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
|
||||
@contextmanager
|
||||
def with_connection(env: VirtualEnvironment) -> Generator[ConnectionHandle, None, None]:
|
||||
"""Context manager for VirtualEnvironment connection lifecycle.
|
||||
|
||||
Automatically establishes and releases connection handles.
|
||||
|
||||
Usage:
|
||||
with with_connection(env) as conn:
|
||||
future = run_command(env, conn, ["echo", "hello"])
|
||||
result = future.result(timeout=10)
|
||||
"""
|
||||
connection_handle = env.establish_connection()
|
||||
try:
|
||||
yield connection_handle
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
env.release_connection(connection_handle)
|
||||
|
||||
|
||||
def submit_command(
|
||||
env: VirtualEnvironment,
|
||||
connection: ConnectionHandle,
|
||||
command: list[str],
|
||||
environments: Mapping[str, str] | None = None,
|
||||
*,
|
||||
cwd: str | None = None,
|
||||
) -> CommandFuture:
|
||||
"""Execute a command and return a Future for the result.
|
||||
|
||||
High-level interface that handles IO draining internally.
|
||||
For streaming output, use env.execute_command() instead.
|
||||
|
||||
Args:
|
||||
env: The virtual environment to execute the command in.
|
||||
connection: The connection handle.
|
||||
command: Command as list of strings.
|
||||
environments: Environment variables.
|
||||
cwd: Working directory for the command. If None, uses the provider's default.
|
||||
|
||||
Returns:
|
||||
CommandFuture that can be used to get result with timeout or cancel.
|
||||
|
||||
Example:
|
||||
with with_connection(env) as conn:
|
||||
result = run_command(env, conn, ["ls", "-la"]).result(timeout=30)
|
||||
"""
|
||||
pid, stdin_transport, stdout_transport, stderr_transport = env.execute_command(
|
||||
connection, command, environments, cwd
|
||||
)
|
||||
|
||||
return CommandFuture(
|
||||
pid=pid,
|
||||
stdin_transport=stdin_transport,
|
||||
stdout_transport=stdout_transport,
|
||||
stderr_transport=stderr_transport,
|
||||
poll_status=partial(env.get_command_status, connection, pid),
|
||||
)
|
||||
|
||||
|
||||
def _execute_with_connection(
|
||||
env: VirtualEnvironment,
|
||||
conn: ConnectionHandle,
|
||||
command: list[str],
|
||||
timeout: float | None,
|
||||
cwd: str | None,
|
||||
) -> CommandResult:
|
||||
"""Internal helper to execute command with given connection."""
|
||||
future = submit_command(env, conn, command, cwd=cwd)
|
||||
return future.result(timeout=timeout)
|
||||
|
||||
|
||||
def execute(
|
||||
env: VirtualEnvironment,
|
||||
command: list[str],
|
||||
*,
|
||||
timeout: float | None = 30,
|
||||
cwd: str | None = None,
|
||||
error_message: str = "Command failed",
|
||||
connection: ConnectionHandle | None = None,
|
||||
) -> CommandResult:
|
||||
"""Execute a command with automatic connection management.
|
||||
|
||||
Raises CommandExecutionError if the command fails (non-zero exit code).
|
||||
|
||||
Args:
|
||||
env: The virtual environment to execute the command in.
|
||||
command: The command to execute as a list of strings.
|
||||
timeout: Maximum time to wait for the command to complete (seconds).
|
||||
cwd: Working directory for the command.
|
||||
error_message: Custom error message prefix for failures.
|
||||
connection: Optional connection handle to reuse. If None, creates and releases a new connection.
|
||||
|
||||
Returns:
|
||||
CommandResult on success.
|
||||
|
||||
Raises:
|
||||
CommandExecutionError: If the command fails.
|
||||
"""
|
||||
if connection is not None:
|
||||
result = _execute_with_connection(env, connection, command, timeout, cwd)
|
||||
else:
|
||||
with with_connection(env) as conn:
|
||||
result = _execute_with_connection(env, conn, command, timeout, cwd)
|
||||
|
||||
if result.is_error:
|
||||
raise CommandExecutionError(f"{error_message}: {result.error_message}", result)
|
||||
return result
|
||||
|
||||
|
||||
def try_execute(
|
||||
env: VirtualEnvironment,
|
||||
command: list[str],
|
||||
*,
|
||||
timeout: float | None = 30,
|
||||
cwd: str | None = None,
|
||||
connection: ConnectionHandle | None = None,
|
||||
) -> CommandResult:
|
||||
"""Execute a command with automatic connection management.
|
||||
|
||||
Does not raise on failure - returns the result for caller to handle.
|
||||
|
||||
Args:
|
||||
env: The virtual environment to execute the command in.
|
||||
command: The command to execute as a list of strings.
|
||||
timeout: Maximum time to wait for the command to complete (seconds).
|
||||
cwd: Working directory for the command.
|
||||
connection: Optional connection handle to reuse. If None, creates and releases a new connection.
|
||||
|
||||
Returns:
|
||||
CommandResult containing stdout, stderr, and exit_code.
|
||||
"""
|
||||
if connection is not None:
|
||||
return _execute_with_connection(env, connection, command, timeout, cwd)
|
||||
|
||||
with with_connection(env) as conn:
|
||||
return _execute_with_connection(env, conn, command, timeout, cwd)
|
||||
@ -1,10 +1,8 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping, Sequence
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
from core.virtual_environment.__base.command_future import CommandFuture
|
||||
from core.virtual_environment.__base.entities import CommandStatus, ConnectionHandle, FileState, Metadata
|
||||
from core.virtual_environment.channel.transport import TransportReadCloser, TransportWriteCloser
|
||||
|
||||
@ -176,40 +174,3 @@ class VirtualEnvironment(ABC):
|
||||
Returns:
|
||||
CommandStatus: The status of the command execution.
|
||||
"""
|
||||
|
||||
def run_command(
|
||||
self,
|
||||
connection_handle: ConnectionHandle,
|
||||
command: list[str],
|
||||
environments: Mapping[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
) -> CommandFuture:
|
||||
"""
|
||||
Execute a command and return a Future for the result.
|
||||
|
||||
High-level interface that handles IO draining internally.
|
||||
For streaming output, use execute_command() instead.
|
||||
|
||||
Args:
|
||||
connection_handle: The connection handle.
|
||||
command: Command as list of strings.
|
||||
environments: Environment variables.
|
||||
cwd: Working directory for the command. If None, uses the provider's default.
|
||||
|
||||
Returns:
|
||||
CommandFuture that can be used to get result with timeout or cancel.
|
||||
|
||||
Example:
|
||||
result = env.run_command(handle, ["ls", "-la"]).result(timeout=30)
|
||||
"""
|
||||
pid, stdin_transport, stdout_transport, stderr_transport = self.execute_command(
|
||||
connection_handle, command, environments, cwd
|
||||
)
|
||||
|
||||
return CommandFuture(
|
||||
pid=pid,
|
||||
stdin_transport=stdin_transport,
|
||||
stdout_transport=stdout_transport,
|
||||
stderr_transport=stderr_transport,
|
||||
poll_status=partial(self.get_command_status, connection_handle, pid),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user