Fix download workspace zip file event loop hanging (#6722)

This commit is contained in:
diwu-sf 2025-02-19 07:51:52 -08:00 committed by GitHub
parent 81f2b08a89
commit eb5be2ab63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 20 additions and 31 deletions

View File

@ -8,7 +8,6 @@ NOTE: this will be executed inside the docker sandbox.
import argparse
import asyncio
import base64
import io
import mimetypes
import os
import shutil
@ -21,12 +20,13 @@ from zipfile import ZipFile
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import APIKeyHeader
from openhands_aci.editor.editor import OHEditor
from openhands_aci.editor.exceptions import ToolError
from openhands_aci.editor.results import ToolResult
from pydantic import BaseModel
from starlette.background import BackgroundTask
from starlette.exceptions import HTTPException as StarletteHTTPException
from uvicorn import run
@ -631,7 +631,7 @@ if __name__ == '__main__':
raise HTTPException(status_code=500, detail=str(e))
@app.get('/download_files')
async def download_file(path: str):
def download_file(path: str):
logger.debug('Downloading files')
try:
if not os.path.isabs(path):
@ -642,7 +642,7 @@ if __name__ == '__main__':
if not os.path.exists(path):
raise HTTPException(status_code=404, detail='File not found')
with tempfile.TemporaryFile() as temp_zip:
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as temp_zip:
with ZipFile(temp_zip, 'w') as zipf:
for root, _, files in os.walk(path):
for file in files:
@ -650,15 +650,11 @@ if __name__ == '__main__':
zipf.write(
file_path, arcname=os.path.relpath(file_path, path)
)
temp_zip.seek(0) # Rewind the file to the beginning after writing
content = temp_zip.read()
# Good for small to medium-sized files. For very large files, streaming directly from the
# file chunks may be more memory-efficient.
zip_stream = io.BytesIO(content)
return StreamingResponse(
content=zip_stream,
return FileResponse(
path=temp_zip.name,
media_type='application/zip',
headers={'Content-Disposition': f'attachment; filename={path}.zip'},
filename=f'{os.path.basename(path)}.zip',
background=BackgroundTask(lambda: os.unlink(temp_zip.name)),
)
except Exception as e:

View File

@ -1,4 +1,5 @@
import os
import shutil
import tempfile
import threading
from abc import abstractmethod
@ -143,12 +144,10 @@ class ActionExecutionClient(Runtime):
stream=True,
timeout=30,
) as response:
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
total_length = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk: # filter out keep-alive new chunks
total_length += len(chunk)
temp_file.write(chunk)
with tempfile.NamedTemporaryFile(
suffix='.zip', delete=False
) as temp_file:
shutil.copyfileobj(response.raw, temp_file, length=16 * 1024)
return Path(temp_file.name)
except requests.Timeout:
raise TimeoutError('Copy operation timed out')

View File

@ -3,7 +3,6 @@ import tempfile
from fastapi import (
APIRouter,
BackgroundTasks,
HTTPException,
Request,
UploadFile,
@ -12,6 +11,7 @@ from fastapi import (
from fastapi.responses import FileResponse, JSONResponse
from pathspec import PathSpec
from pathspec.patterns import GitWildMatchPattern
from starlette.background import BackgroundTask
from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import openhands_logger as logger
@ -309,31 +309,25 @@ async def save_file(request: Request):
@app.get('/zip-directory')
async def zip_current_workspace(
request: Request, conversation_id: str, background_tasks: BackgroundTasks
):
def zip_current_workspace(request: Request, conversation_id: str):
try:
logger.debug('Zipping workspace')
runtime: Runtime = request.state.conversation.runtime
path = runtime.config.workspace_mount_path_in_sandbox
try:
zip_file = await call_sync_from_async(runtime.copy_from, path)
zip_file_path = runtime.copy_from(path)
except AgentRuntimeUnavailableError as e:
logger.error(f'Error zipping workspace: {e}')
return JSONResponse(
status_code=500,
content={'error': f'Error zipping workspace: {e}'},
)
response = FileResponse(
path=zip_file,
return FileResponse(
path=zip_file_path,
filename='workspace.zip',
media_type='application/x-zip-compressed',
media_type='application/zip',
background=BackgroundTask(lambda: os.unlink(zip_file_path)),
)
# This will execute after the response is sent (So the file is not deleted before being sent)
background_tasks.add_task(zip_file.unlink)
return response
except Exception as e:
logger.error(f'Error zipping workspace: {e}')
raise HTTPException(