refactor: deduplicate execute/cleanup, merge init, clean up helpers
- Merge _init_persistent_shell + _start_persistent_shell into single method - Move execute() dispatcher and cleanup() into PersistentShellMixin so LocalEnvironment and SSHEnvironment inherit them - Remove broad except Exception wrappers from _execute_oneshot in both backends - Replace try/except with os.path.exists checks in local _read_temp_files and _cleanup_temp_files - Remove redundant bash -c from SSH oneshot (SSH already runs in a shell) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7be314c456
commit
9f36483bf4
3 changed files with 195 additions and 203 deletions
|
|
@ -247,10 +247,10 @@ class LocalEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
def _read_temp_files(self, *paths: str) -> list[str]:
|
def _read_temp_files(self, *paths: str) -> list[str]:
|
||||||
results = []
|
results = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
if os.path.exists(path):
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
results.append(f.read())
|
results.append(f.read())
|
||||||
except OSError:
|
else:
|
||||||
results.append("")
|
results.append("")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
@ -262,15 +262,13 @@ class LocalEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
["pkill", "-P", str(self._shell_pid)],
|
["pkill", "-P", str(self._shell_pid)],
|
||||||
capture_output=True, timeout=5,
|
capture_output=True, timeout=5,
|
||||||
)
|
)
|
||||||
except (subprocess.TimeoutExpired, OSError, FileNotFoundError):
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _cleanup_temp_files(self):
|
def _cleanup_temp_files(self):
|
||||||
for f in glob.glob(f"{self._temp_prefix}-*"):
|
for f in glob.glob(f"{self._temp_prefix}-*"):
|
||||||
try:
|
if os.path.exists(f):
|
||||||
os.remove(f)
|
os.remove(f)
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _execute_oneshot(self, command: str, cwd: str = "", *,
|
def _execute_oneshot(self, command: str, cwd: str = "", *,
|
||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
|
|
@ -286,7 +284,6 @@ class LocalEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
else:
|
else:
|
||||||
effective_stdin = stdin_data
|
effective_stdin = stdin_data
|
||||||
|
|
||||||
try:
|
|
||||||
user_shell = _find_bash()
|
user_shell = _find_bash()
|
||||||
fenced_cmd = (
|
fenced_cmd = (
|
||||||
f"printf '{_OUTPUT_FENCE}';"
|
f"printf '{_OUTPUT_FENCE}';"
|
||||||
|
|
@ -371,21 +368,3 @@ class LocalEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
reader.join(timeout=5)
|
reader.join(timeout=5)
|
||||||
output = _extract_fenced_output("".join(_output_chunks))
|
output = _extract_fenced_output("".join(_output_chunks))
|
||||||
return {"output": output, "returncode": proc.returncode}
|
return {"output": output, "returncode": proc.returncode}
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"output": f"Execution error: {str(e)}", "returncode": 1}
|
|
||||||
|
|
||||||
def execute(self, command: str, cwd: str = "", *,
|
|
||||||
timeout: int | None = None,
|
|
||||||
stdin_data: str | None = None) -> dict:
|
|
||||||
if self.persistent:
|
|
||||||
return self._execute_persistent(
|
|
||||||
command, cwd, timeout=timeout, stdin_data=stdin_data,
|
|
||||||
)
|
|
||||||
return self._execute_oneshot(
|
|
||||||
command, cwd, timeout=timeout, stdin_data=stdin_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
if self.persistent:
|
|
||||||
self._cleanup_persistent_shell()
|
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,11 @@ class PersistentShellMixin:
|
||||||
"""Mixin that adds persistent shell capability to any BaseEnvironment.
|
"""Mixin that adds persistent shell capability to any BaseEnvironment.
|
||||||
|
|
||||||
Subclasses must implement ``_spawn_shell_process()``, ``_read_temp_files()``,
|
Subclasses must implement ``_spawn_shell_process()``, ``_read_temp_files()``,
|
||||||
``_kill_shell_children()``, and ``_execute_oneshot()`` (stdin fallback).
|
``_kill_shell_children()``, ``_execute_oneshot()``, and ``_cleanup_temp_files()``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
persistent: bool
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def _spawn_shell_process(self) -> subprocess.Popen: ...
|
def _spawn_shell_process(self) -> subprocess.Popen: ...
|
||||||
|
|
||||||
|
|
@ -43,15 +45,16 @@ class PersistentShellMixin:
|
||||||
def _temp_prefix(self) -> str:
|
def _temp_prefix(self) -> str:
|
||||||
return f"/tmp/hermes-persistent-{self._session_id}"
|
return f"/tmp/hermes-persistent-{self._session_id}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Lifecycle
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _init_persistent_shell(self):
|
def _init_persistent_shell(self):
|
||||||
self._shell_lock = threading.Lock()
|
self._shell_lock = threading.Lock()
|
||||||
self._session_id: str = ""
|
|
||||||
self._shell_proc: subprocess.Popen | None = None
|
self._shell_proc: subprocess.Popen | None = None
|
||||||
self._shell_alive: bool = False
|
self._shell_alive: bool = False
|
||||||
self._shell_pid: int | None = None
|
self._shell_pid: int | None = None
|
||||||
self._start_persistent_shell()
|
|
||||||
|
|
||||||
def _start_persistent_shell(self):
|
|
||||||
self._session_id = uuid.uuid4().hex[:12]
|
self._session_id = uuid.uuid4().hex[:12]
|
||||||
p = self._temp_prefix
|
p = self._temp_prefix
|
||||||
self._pshell_stdout = f"{p}-stdout"
|
self._pshell_stdout = f"{p}-stdout"
|
||||||
|
|
@ -98,6 +101,52 @@ class PersistentShellMixin:
|
||||||
if reported_cwd:
|
if reported_cwd:
|
||||||
self.cwd = reported_cwd
|
self.cwd = reported_cwd
|
||||||
|
|
||||||
|
def _cleanup_persistent_shell(self):
|
||||||
|
if self._shell_proc is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._session_id:
|
||||||
|
self._cleanup_temp_files()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._shell_proc.stdin.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self._shell_proc.terminate()
|
||||||
|
self._shell_proc.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self._shell_proc.kill()
|
||||||
|
|
||||||
|
self._shell_alive = False
|
||||||
|
self._shell_proc = None
|
||||||
|
|
||||||
|
if hasattr(self, "_drain_thread") and self._drain_thread.is_alive():
|
||||||
|
self._drain_thread.join(timeout=1.0)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# execute() / cleanup() — shared dispatcher, subclasses inherit
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def execute(self, command: str, cwd: str = "", *,
|
||||||
|
timeout: int | None = None,
|
||||||
|
stdin_data: str | None = None) -> dict:
|
||||||
|
if self.persistent:
|
||||||
|
return self._execute_persistent(
|
||||||
|
command, cwd, timeout=timeout, stdin_data=stdin_data,
|
||||||
|
)
|
||||||
|
return self._execute_oneshot(
|
||||||
|
command, cwd, timeout=timeout, stdin_data=stdin_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
if self.persistent:
|
||||||
|
self._cleanup_persistent_shell()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Shell I/O
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _drain_shell_output(self):
|
def _drain_shell_output(self):
|
||||||
try:
|
try:
|
||||||
for _ in self._shell_proc.stdout:
|
for _ in self._shell_proc.stdout:
|
||||||
|
|
@ -130,12 +179,16 @@ class PersistentShellMixin:
|
||||||
exit_code = 1
|
exit_code = 1
|
||||||
return output, exit_code, cwd.strip()
|
return output, exit_code, cwd.strip()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Execution
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _execute_persistent(self, command: str, cwd: str, *,
|
def _execute_persistent(self, command: str, cwd: str, *,
|
||||||
timeout: int | None = None,
|
timeout: int | None = None,
|
||||||
stdin_data: str | None = None) -> dict:
|
stdin_data: str | None = None) -> dict:
|
||||||
if not self._shell_alive:
|
if not self._shell_alive:
|
||||||
logger.info("Persistent shell died, restarting...")
|
logger.info("Persistent shell died, restarting...")
|
||||||
self._start_persistent_shell()
|
self._init_persistent_shell()
|
||||||
|
|
||||||
exec_command, sudo_stdin = self._prepare_command(command)
|
exec_command, sudo_stdin = self._prepare_command(command)
|
||||||
effective_timeout = timeout or self.timeout
|
effective_timeout = timeout or self.timeout
|
||||||
|
|
@ -216,27 +269,3 @@ class PersistentShellMixin:
|
||||||
if stderr.strip():
|
if stderr.strip():
|
||||||
parts.append(stderr.rstrip("\n"))
|
parts.append(stderr.rstrip("\n"))
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
def _cleanup_persistent_shell(self):
|
|
||||||
if self._shell_proc is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._session_id:
|
|
||||||
self._cleanup_temp_files()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._shell_proc.stdin.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
self._shell_proc.terminate()
|
|
||||||
self._shell_proc.wait(timeout=3)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
self._shell_proc.kill()
|
|
||||||
|
|
||||||
self._shell_alive = False
|
|
||||||
self._shell_proc = None
|
|
||||||
|
|
||||||
if hasattr(self, "_drain_thread") and self._drain_thread.is_alive():
|
|
||||||
self._drain_thread.join(timeout=1.0)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,11 +130,11 @@ class SSHEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _cleanup_temp_files(self):
|
def _cleanup_temp_files(self):
|
||||||
try:
|
|
||||||
cmd = self._build_ssh_command()
|
cmd = self._build_ssh_command()
|
||||||
cmd.append(f"rm -f {self._temp_prefix}-*")
|
cmd.append(f"rm -f {self._temp_prefix}-*")
|
||||||
|
try:
|
||||||
subprocess.run(cmd, capture_output=True, timeout=5)
|
subprocess.run(cmd, capture_output=True, timeout=5)
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _execute_oneshot(self, command: str, cwd: str = "", *,
|
def _execute_oneshot(self, command: str, cwd: str = "", *,
|
||||||
|
|
@ -155,7 +155,6 @@ class SSHEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
cmd = self._build_ssh_command()
|
cmd = self._build_ssh_command()
|
||||||
cmd.append(wrapped)
|
cmd.append(wrapped)
|
||||||
|
|
||||||
try:
|
|
||||||
kwargs = self._build_run_kwargs(timeout, effective_stdin)
|
kwargs = self._build_run_kwargs(timeout, effective_stdin)
|
||||||
kwargs.pop("timeout", None)
|
kwargs.pop("timeout", None)
|
||||||
_output_chunks = []
|
_output_chunks = []
|
||||||
|
|
@ -171,7 +170,7 @@ class SSHEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
try:
|
try:
|
||||||
proc.stdin.write(effective_stdin)
|
proc.stdin.write(effective_stdin)
|
||||||
proc.stdin.close()
|
proc.stdin.close()
|
||||||
except Exception:
|
except (BrokenPipeError, OSError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _drain():
|
def _drain():
|
||||||
|
|
@ -206,23 +205,8 @@ class SSHEnvironment(PersistentShellMixin, BaseEnvironment):
|
||||||
reader.join(timeout=5)
|
reader.join(timeout=5)
|
||||||
return {"output": "".join(_output_chunks), "returncode": proc.returncode}
|
return {"output": "".join(_output_chunks), "returncode": proc.returncode}
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"output": f"SSH execution error: {str(e)}", "returncode": 1}
|
|
||||||
|
|
||||||
def execute(self, command: str, cwd: str = "", *,
|
|
||||||
timeout: int | None = None,
|
|
||||||
stdin_data: str | None = None) -> dict:
|
|
||||||
if self.persistent:
|
|
||||||
return self._execute_persistent(
|
|
||||||
command, cwd, timeout=timeout, stdin_data=stdin_data
|
|
||||||
)
|
|
||||||
return self._execute_oneshot(
|
|
||||||
command, cwd, timeout=timeout, stdin_data=stdin_data
|
|
||||||
)
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
if self.persistent:
|
super().cleanup()
|
||||||
self._cleanup_persistent_shell()
|
|
||||||
if self.control_socket.exists():
|
if self.control_socket.exists():
|
||||||
try:
|
try:
|
||||||
cmd = ["ssh", "-o", f"ControlPath={self.control_socket}",
|
cmd = ["ssh", "-o", f"ControlPath={self.control_socket}",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue