Add kill_modal script to manage Modal applications and better handling of file and terminal tools
- Introduced a new script, `kill_modal.sh`, to facilitate stopping running Modal apps, including the ability to stop all apps or specific swe-rex sandboxes. - Enhanced user experience with clear usage instructions and feedback during the stopping process. - Improved error handling to ensure smooth execution even if some apps fail to stop.
This commit is contained in:
parent
1b7bc299f3
commit
f23856df8e
3 changed files with 141 additions and 99 deletions
34
scripts/kill_modal.sh
Executable file
34
scripts/kill_modal.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Kill all running Modal apps (sandboxes, deployments, etc.)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/kill_modal.sh # Stop swe-rex (the sandbox app)
|
||||||
|
# bash scripts/kill_modal.sh --all # Stop ALL Modal apps
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
echo "Fetching Modal app list..."
|
||||||
|
APP_LIST=$(modal app list 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--all" ]]; then
|
||||||
|
echo "Stopping ALL Modal apps..."
|
||||||
|
echo "$APP_LIST" | grep -oE 'ap-[A-Za-z0-9]+' | sort -u | while read app_id; do
|
||||||
|
echo " Stopping $app_id"
|
||||||
|
modal app stop "$app_id" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Stopping swe-rex sandboxes..."
|
||||||
|
APPS=$(echo "$APP_LIST" | grep 'swe-rex' | grep -oE 'ap-[A-Za-z0-9]+' || true)
|
||||||
|
if [[ -z "$APPS" ]]; then
|
||||||
|
echo " No swe-rex apps found."
|
||||||
|
else
|
||||||
|
echo "$APPS" | while read app_id; do
|
||||||
|
echo " Stopping $app_id"
|
||||||
|
modal app stop "$app_id" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Current swe-rex status:"
|
||||||
|
modal app list 2>/dev/null | grep -E 'State|swe-rex' || echo " (none)"
|
||||||
|
|
@ -30,62 +30,63 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations:
|
||||||
if task_id in _file_ops_cache:
|
if task_id in _file_ops_cache:
|
||||||
return _file_ops_cache[task_id]
|
return _file_ops_cache[task_id]
|
||||||
|
|
||||||
# Check if we need to create a new environment
|
# Check if we need to create a new environment.
|
||||||
|
# Uses the same per-task creation locks as terminal_tool to prevent
|
||||||
|
# duplicate sandbox creation from concurrent tool calls.
|
||||||
|
from tools.terminal_tool import _creation_locks, _creation_locks_lock
|
||||||
|
|
||||||
needs_creation = False
|
needs_creation = False
|
||||||
with _env_lock:
|
with _env_lock:
|
||||||
if task_id not in _active_environments:
|
if task_id not in _active_environments:
|
||||||
needs_creation = True
|
needs_creation = True
|
||||||
|
|
||||||
# Create environment OUTSIDE locks so we don't block other rollouts
|
|
||||||
# during slow Modal/Docker startup (~10s)
|
|
||||||
if needs_creation:
|
if needs_creation:
|
||||||
from tools.terminal_tool import _task_env_overrides
|
# Per-task lock: only one thread creates the sandbox, others wait
|
||||||
|
with _creation_locks_lock:
|
||||||
|
if task_id not in _creation_locks:
|
||||||
|
_creation_locks[task_id] = __import__("threading").Lock()
|
||||||
|
task_lock = _creation_locks[task_id]
|
||||||
|
|
||||||
config = _get_env_config()
|
with task_lock:
|
||||||
env_type = config["env_type"]
|
# Double-check after acquiring the per-task lock
|
||||||
|
with _env_lock:
|
||||||
|
if task_id in _active_environments:
|
||||||
|
needs_creation = False
|
||||||
|
|
||||||
# Check per-task overrides (set by environments like TerminalBench2Env)
|
if needs_creation:
|
||||||
overrides = _task_env_overrides.get(task_id, {})
|
from tools.terminal_tool import _task_env_overrides
|
||||||
|
|
||||||
if env_type == "docker":
|
config = _get_env_config()
|
||||||
image = overrides.get("docker_image") or config["docker_image"]
|
env_type = config["env_type"]
|
||||||
elif env_type == "singularity":
|
overrides = _task_env_overrides.get(task_id, {})
|
||||||
image = overrides.get("singularity_image") or config["singularity_image"]
|
|
||||||
elif env_type == "modal":
|
|
||||||
image = overrides.get("modal_image") or config["modal_image"]
|
|
||||||
else:
|
|
||||||
image = ""
|
|
||||||
|
|
||||||
cwd = overrides.get("cwd") or config["cwd"]
|
if env_type == "docker":
|
||||||
_check_disk_usage_warning()
|
image = overrides.get("docker_image") or config["docker_image"]
|
||||||
if not os.getenv("HERMES_QUIET"):
|
elif env_type == "singularity":
|
||||||
print(f"[FileTools] Creating new {env_type} environment for task {task_id[:8]}...", flush=True)
|
image = overrides.get("singularity_image") or config["singularity_image"]
|
||||||
|
elif env_type == "modal":
|
||||||
|
image = overrides.get("modal_image") or config["modal_image"]
|
||||||
|
else:
|
||||||
|
image = ""
|
||||||
|
|
||||||
new_env = _create_environment(
|
cwd = overrides.get("cwd") or config["cwd"]
|
||||||
env_type=env_type,
|
if not os.getenv("HERMES_QUIET"):
|
||||||
image=image,
|
print(f"[FileTools] Creating new {env_type} environment for task {task_id[:8]}...", flush=True)
|
||||||
cwd=cwd,
|
|
||||||
timeout=config["timeout"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Store under lock (brief) -- do NOT call _start_cleanup_thread inside
|
new_env = _create_environment(
|
||||||
# the lock because it also acquires _env_lock (non-reentrant = deadlock)
|
env_type=env_type,
|
||||||
created = False
|
image=image,
|
||||||
with _env_lock:
|
cwd=cwd,
|
||||||
if task_id not in _active_environments:
|
timeout=config["timeout"],
|
||||||
_active_environments[task_id] = new_env
|
)
|
||||||
created = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if hasattr(new_env, 'stop'):
|
|
||||||
new_env.stop()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if created:
|
with _env_lock:
|
||||||
_start_cleanup_thread()
|
_active_environments[task_id] = new_env
|
||||||
if not os.getenv("HERMES_QUIET"):
|
_last_activity[task_id] = __import__("time").time()
|
||||||
print(f"[FileTools] {env_type} environment ready for task {task_id[:8]}", flush=True)
|
|
||||||
|
_start_cleanup_thread()
|
||||||
|
if not os.getenv("HERMES_QUIET"):
|
||||||
|
print(f"[FileTools] {env_type} environment ready for task {task_id[:8]}", flush=True)
|
||||||
|
|
||||||
# Now get the environment and build file_ops
|
# Now get the environment and build file_ops
|
||||||
with _env_lock:
|
with _env_lock:
|
||||||
|
|
|
||||||
|
|
@ -1132,6 +1132,8 @@ _active_environments: Dict[str, Any] = {}
|
||||||
_task_workdirs: Dict[str, str] = {} # Maps task_id to working directory
|
_task_workdirs: Dict[str, str] = {} # Maps task_id to working directory
|
||||||
_last_activity: Dict[str, float] = {}
|
_last_activity: Dict[str, float] = {}
|
||||||
_env_lock = threading.Lock()
|
_env_lock = threading.Lock()
|
||||||
|
_creation_locks: Dict[str, threading.Lock] = {} # Per-task locks for sandbox creation
|
||||||
|
_creation_locks_lock = threading.Lock() # Protects _creation_locks dict itself
|
||||||
_cleanup_thread = None
|
_cleanup_thread = None
|
||||||
_cleanup_running = False
|
_cleanup_running = False
|
||||||
|
|
||||||
|
|
@ -1515,64 +1517,69 @@ def terminal_tool(
|
||||||
# Start cleanup thread
|
# Start cleanup thread
|
||||||
_start_cleanup_thread()
|
_start_cleanup_thread()
|
||||||
|
|
||||||
# Get or create environment
|
# Get or create environment.
|
||||||
# Check under lock, but create OUTSIDE lock so we don't block
|
# Use a per-task creation lock so concurrent tool calls for the same
|
||||||
# other concurrent rollouts during slow Modal/Docker startup
|
# task_id wait for the first one to finish creating the sandbox,
|
||||||
needs_creation = False
|
# instead of each creating their own (wasting Modal resources).
|
||||||
with _env_lock:
|
with _env_lock:
|
||||||
if effective_task_id not in _active_environments:
|
if effective_task_id in _active_environments:
|
||||||
needs_creation = True
|
|
||||||
else:
|
|
||||||
_last_activity[effective_task_id] = time.time()
|
_last_activity[effective_task_id] = time.time()
|
||||||
env = _active_environments[effective_task_id]
|
env = _active_environments[effective_task_id]
|
||||||
|
needs_creation = False
|
||||||
|
else:
|
||||||
|
needs_creation = True
|
||||||
|
|
||||||
if needs_creation:
|
if needs_creation:
|
||||||
# Disk usage warning only relevant for local/singularity backends
|
# Per-task lock: only one thread creates the sandbox, others wait
|
||||||
if env_type in ("singularity", "local"):
|
with _creation_locks_lock:
|
||||||
_check_disk_usage_warning()
|
if effective_task_id not in _creation_locks:
|
||||||
if not os.getenv("HERMES_QUIET"):
|
_creation_locks[effective_task_id] = threading.Lock()
|
||||||
print(f"[Terminal] Creating new {env_type} environment for task {effective_task_id[:8]}...", flush=True)
|
task_lock = _creation_locks[effective_task_id]
|
||||||
try:
|
|
||||||
ssh_config = None
|
|
||||||
if env_type == "ssh":
|
|
||||||
ssh_config = {
|
|
||||||
"host": config.get("ssh_host", ""),
|
|
||||||
"user": config.get("ssh_user", ""),
|
|
||||||
"port": config.get("ssh_port", 22),
|
|
||||||
"key": config.get("ssh_key", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
new_env = _create_environment(
|
with task_lock:
|
||||||
env_type=env_type,
|
# Double-check after acquiring the per-task lock
|
||||||
image=image,
|
with _env_lock:
|
||||||
cwd=cwd,
|
if effective_task_id in _active_environments:
|
||||||
timeout=effective_timeout,
|
_last_activity[effective_task_id] = time.time()
|
||||||
ssh_config=ssh_config
|
env = _active_environments[effective_task_id]
|
||||||
)
|
needs_creation = False
|
||||||
except ImportError as e:
|
|
||||||
return json.dumps({
|
|
||||||
"output": "",
|
|
||||||
"exit_code": -1,
|
|
||||||
"error": f"Terminal tool disabled: mini-swe-agent not available ({e})",
|
|
||||||
"status": "disabled"
|
|
||||||
}, ensure_ascii=False)
|
|
||||||
|
|
||||||
# Store under lock (brief)
|
if needs_creation:
|
||||||
with _env_lock:
|
if env_type in ("singularity", "local"):
|
||||||
if effective_task_id not in _active_environments:
|
_check_disk_usage_warning()
|
||||||
_active_environments[effective_task_id] = new_env
|
if not os.getenv("HERMES_QUIET"):
|
||||||
else:
|
print(f"[Terminal] Creating new {env_type} environment for task {effective_task_id[:8]}...", flush=True)
|
||||||
# Another thread created it while we were building -- clean up ours
|
|
||||||
try:
|
try:
|
||||||
if hasattr(new_env, 'stop'):
|
ssh_config = None
|
||||||
new_env.stop()
|
if env_type == "ssh":
|
||||||
except Exception:
|
ssh_config = {
|
||||||
pass
|
"host": config.get("ssh_host", ""),
|
||||||
|
"user": config.get("ssh_user", ""),
|
||||||
|
"port": config.get("ssh_port", 22),
|
||||||
|
"key": config.get("ssh_key", ""),
|
||||||
|
}
|
||||||
|
|
||||||
_last_activity[effective_task_id] = time.time()
|
new_env = _create_environment(
|
||||||
env = _active_environments[effective_task_id]
|
env_type=env_type,
|
||||||
if not os.getenv("HERMES_QUIET"):
|
image=image,
|
||||||
print(f"[Terminal] {env_type} environment ready for task {effective_task_id[:8]}", flush=True)
|
cwd=cwd,
|
||||||
|
timeout=effective_timeout,
|
||||||
|
ssh_config=ssh_config
|
||||||
|
)
|
||||||
|
except ImportError as e:
|
||||||
|
return json.dumps({
|
||||||
|
"output": "",
|
||||||
|
"exit_code": -1,
|
||||||
|
"error": f"Terminal tool disabled: mini-swe-agent not available ({e})",
|
||||||
|
"status": "disabled"
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
with _env_lock:
|
||||||
|
_active_environments[effective_task_id] = new_env
|
||||||
|
_last_activity[effective_task_id] = time.time()
|
||||||
|
env = new_env
|
||||||
|
if not os.getenv("HERMES_QUIET"):
|
||||||
|
print(f"[Terminal] {env_type} environment ready for task {effective_task_id[:8]}", flush=True)
|
||||||
|
|
||||||
# Check for dangerous commands (only for local/ssh in interactive modes)
|
# Check for dangerous commands (only for local/ssh in interactive modes)
|
||||||
# Skip check if force=True (user has confirmed they want to run it)
|
# Skip check if force=True (user has confirmed they want to run it)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue