feature: enhance alias sandbox performance when running code (#91)
This commit is contained in:
208
.github/workflows/build_alias_sandbox_image.yml
vendored
Normal file
208
.github/workflows/build_alias_sandbox_image.yml
vendored
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
name: Manual Build Alias Sandbox Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: "Docker architecture platform (used for multi-arch builds)"
|
||||||
|
required: true
|
||||||
|
default: "linux/amd64"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
single_arch:
|
||||||
|
description: "Single architecture build (forces amd64, no multi-arch manifest merge)"
|
||||||
|
required: true
|
||||||
|
default: "false"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
tag:
|
||||||
|
description: "Custom image tag (default: current date)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest ]
|
||||||
|
python-version: [ '3.10' ]
|
||||||
|
environment: prod
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to DockerHub
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ env.DOCKER_USERNAME }}
|
||||||
|
password: ${{ env.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Log in to Aliyun ACR
|
||||||
|
env:
|
||||||
|
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
|
||||||
|
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
|
||||||
|
if: ${{ env.ALIYUN_ACR_USERNAME != '' && env.ALIYUN_ACR_PASSWORD != '' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: agentscope-registry.ap-southeast-1.cr.aliyuncs.com
|
||||||
|
username: ${{ env.ALIYUN_ACR_USERNAME }}
|
||||||
|
password: ${{ env.ALIYUN_ACR_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pip-
|
||||||
|
|
||||||
|
- name: Update setuptools
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install setuptools==78.1.1 wheel==0.45.1
|
||||||
|
|
||||||
|
- name: Set PYTHONPATH
|
||||||
|
run: |
|
||||||
|
echo "PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/src" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
export PIP_DEFAULT_TIMEOUT=300
|
||||||
|
pip install agentscope-runtime loguru
|
||||||
|
|
||||||
|
- name: Run build script for all types
|
||||||
|
working-directory: alias/src/alias/runtime/alias_sandbox
|
||||||
|
env:
|
||||||
|
AUTO_BUILD: "true"
|
||||||
|
run: |
|
||||||
|
TAG_INPUT="${{ github.event.inputs.tag }}"
|
||||||
|
if [ -z "$TAG_INPUT" ]; then
|
||||||
|
TAG_INPUT=$(date +%Y%m%d)
|
||||||
|
fi
|
||||||
|
PLATFORM_INPUT="${{ github.event.inputs.platform }}"
|
||||||
|
SINGLE="${{ github.event.inputs.single_arch }}"
|
||||||
|
if [ "$SINGLE" = "true" ]; then
|
||||||
|
PLATFORM_INPUT="linux/amd64"
|
||||||
|
fi
|
||||||
|
IFS=',' read -ra TYPES <<< "alias"
|
||||||
|
for TYPE in "${TYPES[@]}"; do
|
||||||
|
runtime-sandbox-builder "$TYPE" --platform "$PLATFORM_INPUT" --dockerfile_path Dockerfile --extension alias_sandbox.py
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Tag & Push Images
|
||||||
|
working-directory: alias/src/alias/runtime/alias_sandbox
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
|
||||||
|
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
TAG_INPUT="${{ github.event.inputs.tag }}"
|
||||||
|
if [ -z "$TAG_INPUT" ]; then
|
||||||
|
TAG_INPUT=$(date +%Y%m%d)
|
||||||
|
fi
|
||||||
|
SINGLE="${{ github.event.inputs.single_arch }}"
|
||||||
|
ARCH_TAG="${{ github.event.inputs.platform }}"
|
||||||
|
if [ "$SINGLE" = "true" ]; then
|
||||||
|
ARCH_TAG="linux/amd64"
|
||||||
|
fi
|
||||||
|
IFS=',' read -ra TYPES <<< "alias"
|
||||||
|
for TYPE in "${TYPES[@]}"; do
|
||||||
|
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
|
||||||
|
IMAGE_NAME="${IMAGE_BASE%%:*}"
|
||||||
|
if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
|
||||||
|
if [ "$SINGLE" = "true" ]; then
|
||||||
|
docker tag "$IMAGE_BASE" "docker.io/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
docker push "docker.io/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
else
|
||||||
|
ARCH_SUFFIX=$(echo "$ARCH_TAG" | tr '/' '-')
|
||||||
|
docker tag "$IMAGE_BASE" "docker.io/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
|
||||||
|
docker push "docker.io/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "$ALIYUN_ACR_USERNAME" ] && [ -n "$ALIYUN_ACR_PASSWORD" ]; then
|
||||||
|
REGISTRY="agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
|
||||||
|
if [ "$SINGLE" = "true" ]; then
|
||||||
|
docker tag "$IMAGE_BASE" "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
else
|
||||||
|
ARCH_SUFFIX=$(echo "$ARCH_TAG" | tr '/' '-')
|
||||||
|
docker tag "$IMAGE_BASE" "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
|
||||||
|
docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_INPUT}-${ARCH_SUFFIX}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create Multi-arch Manifest for DockerHub
|
||||||
|
working-directory: alias/src/alias/runtime/alias_sandbox
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
if: ${{ github.event.inputs.single_arch == 'false' && env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
|
||||||
|
run: |
|
||||||
|
TAG_INPUT="${{ github.event.inputs.tag }}"
|
||||||
|
if [ -z "$TAG_INPUT" ]; then
|
||||||
|
TAG_INPUT=$(date +%Y%m%d)
|
||||||
|
fi
|
||||||
|
IFS=',' read -ra TYPES <<< "alias"
|
||||||
|
for TYPE in "${TYPES[@]}"; do
|
||||||
|
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
|
||||||
|
IMAGE_NAME="${IMAGE_BASE%%:*}"
|
||||||
|
AMD_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}-linux-amd64"
|
||||||
|
ARM_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}-linux-arm64"
|
||||||
|
COMMON_TAG="docker.io/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
if docker manifest inspect "$AMD_TAG" >/dev/null 2>&1 && docker manifest inspect "$ARM_TAG" >/dev/null 2>&1; then
|
||||||
|
docker manifest create "$COMMON_TAG" --amend "$AMD_TAG" --amend "$ARM_TAG"
|
||||||
|
docker manifest push "$COMMON_TAG"
|
||||||
|
else
|
||||||
|
echo "Missing architecture image in DockerHub, skipping manifest creation."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create Multi-arch Manifest for Aliyun ACR
|
||||||
|
working-directory: alias/src/alias/runtime/alias_sandbox
|
||||||
|
env:
|
||||||
|
ALIYUN_ACR_USERNAME: ${{ secrets.ALIYUN_ACR_USERNAME }}
|
||||||
|
ALIYUN_ACR_PASSWORD: ${{ secrets.ALIYUN_ACR_PASSWORD }}
|
||||||
|
if: ${{ github.event.inputs.single_arch == 'false' && env.ALIYUN_ACR_USERNAME != '' && env.ALIYUN_ACR_PASSWORD != '' }}
|
||||||
|
run: |
|
||||||
|
TAG_INPUT="${{ github.event.inputs.tag }}"
|
||||||
|
if [ -z "$TAG_INPUT" ]; then
|
||||||
|
TAG_INPUT=$(date +%Y%m%d)
|
||||||
|
fi
|
||||||
|
REG="agentscope-registry.ap-southeast-1.cr.aliyuncs.com"
|
||||||
|
IFS=',' read -ra TYPES <<< "alias"
|
||||||
|
for TYPE in "${TYPES[@]}"; do
|
||||||
|
IMAGE_BASE=$(python -c "import alias_sandbox; from agentscope_runtime.sandbox.registry import SandboxRegistry; print(SandboxRegistry.get_image_by_type('$TYPE'))")
|
||||||
|
IMAGE_NAME="${IMAGE_BASE%%:*}"
|
||||||
|
AMD_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}-linux-amd64"
|
||||||
|
ARM_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}-linux-arm64"
|
||||||
|
COMMON_TAG="${REG}/${IMAGE_NAME}:${TAG_INPUT}"
|
||||||
|
if docker manifest inspect "$AMD_TAG" >/dev/null 2>&1 && docker manifest inspect "$ARM_TAG" >/dev/null 2>&1; then
|
||||||
|
docker manifest create "$COMMON_TAG" --amend "$AMD_TAG" --amend "$ARM_TAG"
|
||||||
|
docker manifest push "$COMMON_TAG"
|
||||||
|
else
|
||||||
|
echo "Missing architecture image in Aliyun ACR, skipping manifest creation."
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import io
|
import io
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import redirect_stderr, redirect_stdout
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ async def run_ipython_cell(
|
|||||||
stdout_buf = io.StringIO()
|
stdout_buf = io.StringIO()
|
||||||
stderr_buf = io.StringIO()
|
stderr_buf = io.StringIO()
|
||||||
|
|
||||||
|
def thread_target():
|
||||||
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
with redirect_stdout(stdout_buf), redirect_stderr(stderr_buf):
|
||||||
preprocessing_exc_tuple = None
|
preprocessing_exc_tuple = None
|
||||||
try:
|
try:
|
||||||
@@ -55,16 +56,22 @@ async def run_ipython_cell(
|
|||||||
if transformed_cell is None:
|
if transformed_cell is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="IPython cell transformation failed: "
|
detail=(
|
||||||
"transformed_cell is None.",
|
"IPython cell transformation failed: "
|
||||||
|
"transformed_cell is None."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await ipy.run_cell_async(
|
asyncio.run(
|
||||||
|
ipy.run_cell_async(
|
||||||
code,
|
code,
|
||||||
transformed_cell=transformed_cell,
|
transformed_cell=transformed_cell,
|
||||||
preprocessing_exc_tuple=preprocessing_exc_tuple,
|
preprocessing_exc_tuple=preprocessing_exc_tuple,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await asyncio.to_thread(thread_target)
|
||||||
|
|
||||||
stdout_content = stdout_buf.getvalue()
|
stdout_content = stdout_buf.getvalue()
|
||||||
stderr_content = stderr_buf.getvalue()
|
stderr_content = stderr_buf.getvalue()
|
||||||
|
|
||||||
@@ -128,16 +135,16 @@ async def run_shell_command(
|
|||||||
if not command:
|
if not command:
|
||||||
raise HTTPException(status_code=400, detail="Command is required.")
|
raise HTTPException(status_code=400, detail="Command is required.")
|
||||||
|
|
||||||
result = subprocess.run(
|
proc = await asyncio.create_subprocess_shell(
|
||||||
command,
|
command,
|
||||||
shell=True,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
)
|
||||||
stdout_content = result.stdout
|
|
||||||
stderr_content = result.stderr
|
stdout_bytes, stderr_bytes = await proc.communicate()
|
||||||
|
|
||||||
|
stdout_content = stdout_bytes.decode()
|
||||||
|
stderr_content = stderr_bytes.decode()
|
||||||
|
|
||||||
content_list = []
|
content_list = []
|
||||||
|
|
||||||
@@ -161,7 +168,7 @@ async def run_shell_command(
|
|||||||
content_list.append(
|
content_list.append(
|
||||||
TextContent(
|
TextContent(
|
||||||
type="text",
|
type="text",
|
||||||
text=str(result.returncode),
|
text=str(proc.returncode),
|
||||||
description="returncode",
|
description="returncode",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -173,7 +180,7 @@ async def run_shell_command(
|
|||||||
+ "\n"
|
+ "\n"
|
||||||
+ stderr_content
|
+ stderr_content
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ str(result.returncode),
|
+ str(proc.returncode),
|
||||||
description="output",
|
description="output",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -107,12 +107,31 @@
|
|||||||
|
|
||||||
function getSandboxIdFromPath() {
|
function getSandboxIdFromPath() {
|
||||||
const pathParts = window.location.pathname.split('/');
|
const pathParts = window.location.pathname.split('/');
|
||||||
if (pathParts.length >= 3 && pathParts[1] === 'desktop') {
|
// Find 'desktop' anywhere in the path to support arbitrary URL prefixes
|
||||||
return pathParts[2];
|
// Supports: /desktop/{sandbox_id}/... or /custom/prefix/desktop/{sandbox_id}/...
|
||||||
|
const desktopIndex = pathParts.indexOf('desktop');
|
||||||
|
if (desktopIndex !== -1 && pathParts.length > desktopIndex + 1) {
|
||||||
|
// Return null if sandbox_id is empty (e.g., /desktop/ or /desktop//)
|
||||||
|
return pathParts[desktopIndex + 1] || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWebSocketPath(sandbox_id) {
|
||||||
|
// Extract the path prefix up to and including /desktop/{sandbox_id}
|
||||||
|
// This preserves any custom URL prefix for reverse proxy deployments
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
// Use non-greedy matching (.*?) to find the FIRST occurrence of /desktop/
|
||||||
|
// This ensures consistency with getSandboxIdFromPath() which uses indexOf()
|
||||||
|
const desktopMatch = currentPath.match(/(.*?)\/desktop\/([^\/]+)/);
|
||||||
|
if (desktopMatch) {
|
||||||
|
// Use the passed sandbox_id parameter for consistency
|
||||||
|
return desktopMatch[1] + '/desktop/' + sandbox_id;
|
||||||
|
}
|
||||||
|
// Fallback to simple path
|
||||||
|
return '/desktop/' + sandbox_id;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('sendCtrlAltDelButton')
|
document.getElementById('sendCtrlAltDelButton')
|
||||||
.onclick = sendCtrlAltDel;
|
.onclick = sendCtrlAltDel;
|
||||||
|
|
||||||
@@ -141,7 +160,7 @@
|
|||||||
url += ':' + port;
|
url += ':' + port;
|
||||||
}
|
}
|
||||||
|
|
||||||
url += '/desktop/' + sandbox_id;
|
url += getWebSocketPath(sandbox_id);
|
||||||
|
|
||||||
if (path && path !== 'websockify') {
|
if (path && path !== 'websockify') {
|
||||||
url += '?path=' + encodeURIComponent(path);
|
url += '?path=' + encodeURIComponent(path);
|
||||||
|
|||||||
Reference in New Issue
Block a user