fix(terminal): guard os.getcwd() against a deleted CWD
`os.getcwd()` raises FileNotFoundError when the process's working directory was removed out from under it (e.g. a scratch workspace cleaned up mid-session), crashing terminal env setup. Extract a `_safe_getcwd()` helper that falls back to TERMINAL_CWD, then the user's home, on FileNotFoundError, and route all three `os.getcwd()` call sites in terminal_tool.py through it (local default_cwd, the Docker cwd-passthrough source, and the debug-config print) so the same crash can't resurface at a sibling site. Adds unit tests for the real-cwd path and both fallback branches. Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
@ -218,3 +218,27 @@ def test_registering_non_cwd_override_leaves_live_env_cwd_untouched(monkeypatch)
|
|||||||
terminal_tool.register_task_env_overrides(task_id, {"modal_image": "custom:latest"})
|
terminal_tool.register_task_env_overrides(task_id, {"modal_image": "custom:latest"})
|
||||||
|
|
||||||
assert fake_env.cwd == "/workspace/keep"
|
assert fake_env.cwd == "/workspace/keep"
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_getcwd_returns_real_cwd(monkeypatch):
|
||||||
|
monkeypatch.setattr(terminal_tool.os, "getcwd", lambda: "/home/user/project")
|
||||||
|
assert terminal_tool._safe_getcwd() == "/home/user/project"
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_getcwd_falls_back_to_terminal_cwd_when_cwd_deleted(monkeypatch):
|
||||||
|
def _boom():
|
||||||
|
raise FileNotFoundError("[Errno 2] No such file or directory")
|
||||||
|
|
||||||
|
monkeypatch.setattr(terminal_tool.os, "getcwd", _boom)
|
||||||
|
monkeypatch.setenv("TERMINAL_CWD", "/srv/work")
|
||||||
|
assert terminal_tool._safe_getcwd() == "/srv/work"
|
||||||
|
|
||||||
|
|
||||||
|
def test_safe_getcwd_falls_back_to_home_when_no_terminal_cwd(monkeypatch):
|
||||||
|
def _boom():
|
||||||
|
raise FileNotFoundError()
|
||||||
|
|
||||||
|
monkeypatch.setattr(terminal_tool.os, "getcwd", _boom)
|
||||||
|
monkeypatch.delenv("TERMINAL_CWD", raising=False)
|
||||||
|
monkeypatch.setattr(terminal_tool.os.path, "expanduser", lambda p: "/home/me")
|
||||||
|
assert terminal_tool._safe_getcwd() == "/home/me"
|
||||||
|
|||||||
@ -1030,6 +1030,20 @@ def _parse_env_var(name: str, default: str, converter=int, type_label: str = "in
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_getcwd() -> str:
|
||||||
|
"""Return the current working directory, tolerating a deleted CWD.
|
||||||
|
|
||||||
|
``os.getcwd()`` raises FileNotFoundError when the process's working
|
||||||
|
directory has been removed out from under it (e.g. a scratch workspace
|
||||||
|
that was cleaned up mid-session). Fall back to TERMINAL_CWD, then the
|
||||||
|
user's home directory, so terminal setup never crashes on a stale CWD.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return os.getcwd()
|
||||||
|
except FileNotFoundError:
|
||||||
|
return os.getenv("TERMINAL_CWD") or os.path.expanduser("~")
|
||||||
|
|
||||||
|
|
||||||
def _get_env_config() -> Dict[str, Any]:
|
def _get_env_config() -> Dict[str, Any]:
|
||||||
"""Get terminal environment configuration from environment variables."""
|
"""Get terminal environment configuration from environment variables."""
|
||||||
# Default image with Python and Node.js for maximum compatibility
|
# Default image with Python and Node.js for maximum compatibility
|
||||||
@ -1042,7 +1056,7 @@ def _get_env_config() -> Dict[str, Any]:
|
|||||||
# remote home, and everything else starts in the backend's default
|
# remote home, and everything else starts in the backend's default
|
||||||
# root-like cwd.
|
# root-like cwd.
|
||||||
if env_type == "local":
|
if env_type == "local":
|
||||||
default_cwd = os.getcwd()
|
default_cwd = _safe_getcwd()
|
||||||
elif env_type == "ssh":
|
elif env_type == "ssh":
|
||||||
default_cwd = "~"
|
default_cwd = "~"
|
||||||
else:
|
else:
|
||||||
@ -1058,7 +1072,7 @@ def _get_env_config() -> Dict[str, Any]:
|
|||||||
host_cwd = None
|
host_cwd = None
|
||||||
host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
|
host_prefixes = ("/Users/", "/home/", "C:\\", "C:/")
|
||||||
if env_type == "docker" and mount_docker_cwd:
|
if env_type == "docker" and mount_docker_cwd:
|
||||||
docker_cwd_source = os.getenv("TERMINAL_CWD") or os.getcwd()
|
docker_cwd_source = os.getenv("TERMINAL_CWD") or _safe_getcwd()
|
||||||
candidate = os.path.abspath(os.path.expanduser(docker_cwd_source))
|
candidate = os.path.abspath(os.path.expanduser(docker_cwd_source))
|
||||||
if (
|
if (
|
||||||
any(candidate.startswith(p) for p in host_prefixes)
|
any(candidate.startswith(p) for p in host_prefixes)
|
||||||
@ -2516,7 +2530,7 @@ if __name__ == "__main__":
|
|||||||
print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}")
|
print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}")
|
||||||
print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}")
|
print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}")
|
||||||
print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}")
|
print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}")
|
||||||
print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', os.getcwd())}")
|
print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', _safe_getcwd())}")
|
||||||
from hermes_constants import display_hermes_home as _dhh
|
from hermes_constants import display_hermes_home as _dhh
|
||||||
print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{_dhh()}/sandboxes')}")
|
print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{_dhh()}/sandboxes')}")
|
||||||
print(f" TERMINAL_TIMEOUT: {os.getenv('TERMINAL_TIMEOUT', '60')}")
|
print(f" TERMINAL_TIMEOUT: {os.getenv('TERMINAL_TIMEOUT', '60')}")
|
||||||
|
|||||||
Reference in New Issue
Block a user