mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
feat(sandbox): draft storage
This commit is contained in:
@ -33,6 +33,8 @@ class SandboxConfigValidationError(ValueError):
|
||||
class CommandExecutionError(Exception):
|
||||
"""Raised when a command execution fails."""
|
||||
|
||||
result: CommandResult
|
||||
|
||||
def __init__(self, message: str, result: CommandResult):
|
||||
super().__init__(message)
|
||||
self.result = result
|
||||
@ -44,3 +46,19 @@ class CommandExecutionError(Exception):
|
||||
@property
|
||||
def stderr(self) -> bytes:
|
||||
return self.result.stderr
|
||||
|
||||
|
||||
class PipelineExecutionError(CommandExecutionError):
|
||||
"""Raised when a pipeline command fails in strict mode."""
|
||||
|
||||
index: int
|
||||
command: list[str]
|
||||
results: list[CommandResult]
|
||||
|
||||
def __init__(
|
||||
self, message: str, result: CommandResult, *, index: int, command: list[str], results: list[CommandResult]
|
||||
):
|
||||
super().__init__(message, result)
|
||||
self.index = index
|
||||
self.command = command
|
||||
self.results = results
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import shlex
|
||||
from collections.abc import Generator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
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.exec import CommandExecutionError, PipelineExecutionError
|
||||
from core.virtual_environment.__base.virtual_environment import VirtualEnvironment
|
||||
|
||||
_PIPE_SENTINEL = "__DIFY_PIPE__"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def with_connection(env: VirtualEnvironment) -> Generator[ConnectionHandle, None, None]:
|
||||
@ -147,3 +151,127 @@ def try_execute(
|
||||
|
||||
with with_connection(env) as conn:
|
||||
return _execute_with_connection(env, conn, command, timeout, cwd)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _PipelineStep:
|
||||
argv: list[str]
|
||||
error_message: str = "Command failed"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandPipeline:
|
||||
"""Batch multiple commands into a single shell execution (Redis pipeline style).
|
||||
|
||||
Example:
|
||||
results = pipeline(env).add(["echo", "hi"]).add(["ls"]).execute()
|
||||
# Strict mode: raise on first failure
|
||||
pipeline(env).add(["mkdir", "/x"], error_message="mkdir failed").execute(raise_on_error=True)
|
||||
"""
|
||||
|
||||
env: VirtualEnvironment
|
||||
connection: ConnectionHandle | None = None
|
||||
cwd: str | None = None
|
||||
environments: Mapping[str, str] | None = None
|
||||
|
||||
_steps: list[_PipelineStep] = field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
|
||||
|
||||
def add(self, command: list[str], *, error_message: str = "Command failed") -> CommandPipeline:
|
||||
self._steps.append(_PipelineStep(argv=command, error_message=error_message))
|
||||
return self
|
||||
|
||||
def execute(self, *, timeout: float | None = 30, raise_on_error: bool = False) -> list[CommandResult]:
|
||||
if not self._steps:
|
||||
return []
|
||||
|
||||
script = self._build_script(fail_fast=raise_on_error)
|
||||
batch_cmd = ["sh", "-lc", script]
|
||||
|
||||
if self.connection is not None:
|
||||
batch_result = try_execute(self.env, batch_cmd, timeout=timeout, cwd=self.cwd, connection=self.connection)
|
||||
else:
|
||||
with with_connection(self.env) as conn:
|
||||
batch_result = try_execute(self.env, batch_cmd, timeout=timeout, cwd=self.cwd, connection=conn)
|
||||
|
||||
results = self._parse_results(batch_result.stdout, batch_result.pid)
|
||||
|
||||
if raise_on_error:
|
||||
for i, r in enumerate(iterable=results):
|
||||
if r.is_error:
|
||||
step = self._steps[i]
|
||||
raise PipelineExecutionError(
|
||||
f"{step.error_message}: {r.error_message}",
|
||||
r,
|
||||
index=i,
|
||||
command=step.argv,
|
||||
results=results,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def _build_script(self, *, fail_fast: bool = False) -> str:
|
||||
lines = [
|
||||
"run() {",
|
||||
' i="$1"; shift',
|
||||
' out="$(mktemp)"; err="$(mktemp)"',
|
||||
' ("$@") >"$out" 2>"$err"; ec="$?"',
|
||||
' os="$(wc -c <"$out" | tr -d \' \')"',
|
||||
' es="$(wc -c <"$err" | tr -d \' \')"',
|
||||
f' printf \'{_PIPE_SENTINEL} %s %s %s %s\\n\' "$i" "$ec" "$os" "$es"',
|
||||
' cat "$out"',
|
||||
' cat "$err"',
|
||||
' rm -f "$out" "$err"',
|
||||
' return "$ec"',
|
||||
"}",
|
||||
]
|
||||
suffix = " || exit $?" if fail_fast else ""
|
||||
for i, step in enumerate(self._steps):
|
||||
quoted = " ".join(shlex.quote(arg) for arg in step.argv)
|
||||
lines.append(f"run {i} {quoted}{suffix}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _parse_results(stdout: bytes, pid: str) -> list[CommandResult]:
|
||||
results: list[CommandResult] = []
|
||||
pos = 0
|
||||
sentinel = _PIPE_SENTINEL.encode() + b" "
|
||||
|
||||
while pos < len(stdout):
|
||||
nl = stdout.find(b"\n", pos)
|
||||
if nl == -1:
|
||||
break
|
||||
header = stdout[pos : nl + 1]
|
||||
pos = nl + 1
|
||||
|
||||
if not header.startswith(sentinel):
|
||||
raise ValueError("Malformed pipeline output: missing sentinel")
|
||||
|
||||
parts = header.decode().strip().split(" ")
|
||||
_, idx, ec, os_len, es_len = parts
|
||||
out_len, err_len = int(os_len), int(es_len)
|
||||
|
||||
out_bytes = stdout[pos : pos + out_len]
|
||||
pos += out_len
|
||||
err_bytes = stdout[pos : pos + err_len]
|
||||
pos += err_len
|
||||
|
||||
results.append(
|
||||
CommandResult(
|
||||
stdout=out_bytes,
|
||||
stderr=err_bytes,
|
||||
exit_code=int(ec),
|
||||
pid=f"{pid}:{idx}",
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def pipeline(
|
||||
env: VirtualEnvironment,
|
||||
connection: ConnectionHandle | None = None,
|
||||
*,
|
||||
cwd: str | None = None,
|
||||
environments: Mapping[str, str] | None = None,
|
||||
) -> CommandPipeline:
|
||||
return CommandPipeline(env=env, connection=connection, cwd=cwd, environments=environments)
|
||||
|
||||
Reference in New Issue
Block a user