codex round 2 (PR #48). - R1: when `herdr config check` could not run (timeout, OSError), the merged config was handed to the server as checked. A merge with a schema error makes herdr drop the whole file, restore back on - the #45 failure. The merged body is no longer used unchecked: _write_checked falls to the fixed MINIMAL config with a warning. When the check cannot run on MINIMAL either, MINIMAL is still used (with its own warning) rather than no config: returning none would leave restore on, and MINIMAL is a fixed two-line file the real-herdr test accepts. Unknown non-zero exits stay as they were - herdr exits non-zero for harmless warnings (an unknown section) that do not drop the file. - R2: _shell_in_foreground compared shell_pid and the foreground group with ==, so 0/0, False/False, ""/"" or -1/-1 closed the pane. Both must now be positive ints (bool excluded) before they are compared. - R3: the re-parse check used ==, and nan != nan, so a valid TOML nan threw away every other user setting even on Python 3.11. _same_doc compares documents recursively with nan equal to nan. Tests: R1 (timeout, OSError) and R3 (top-level, array, nested nan) in test_issue45_herdr_config.py; R2 in the new tests/test_issue45_restore_unit.py (a herdr stub: zero, false, empty, negative, null, string, true, float; missing info; a real shell closed; a busy pane kept). On the previous code 12 of them fail. Python 3.11: 67 passed; Python 3.9: 33 passed / 34 skipped (tomllib-only). Real-herdr restore tests and related files on 3.9: 197 passed / 1 skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
95 lines
4.0 KiB
Python
95 lines
4.0 KiB
Python
"""Issue #45 (B): close the restored empty shells of the session being resumed.
|
|
|
|
With restore-on-restart off (lib_py/herdr_config.py) a restarted MAM server
|
|
still brings every saved pane back - as an empty shell in its old cwd, or in
|
|
$HOME when that directory is gone. A pane keeps the `agent_session` its agent
|
|
reported before the server died. resume_session.sh calls this once, right
|
|
before it spawns, with the id it is about to resume; a pane is closed only when
|
|
all of these hold:
|
|
|
|
1. the target is an isolated MAM server (HERDR_SESSION_NAME set, not default);
|
|
2. herdr sees no live agent in it;
|
|
3. its agent_session value is that id - so a user's pane, another session's
|
|
pane and a freshly split pane (no agent_session yet) are never candidates;
|
|
4. its foreground process is the shell itself (nothing running in it).
|
|
|
|
The registry is not consulted: a session whose server died stays `running`
|
|
there, so "running" cannot tell a live pane from a restored one.
|
|
|
|
CLI: `python3 -m lib_py.herdr_restore close-restored-shells --session-id ID
|
|
[--herdr PATH]`. One stderr line per closed pane; exit 1 when herdr cannot be
|
|
queried (the caller only warns).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from typing import List, Optional
|
|
|
|
# A pane exports these; they would point the query at the caller's own server
|
|
# and workspace instead of the --session named here.
|
|
_SCOPE_ENV = ("HERDR_WORKSPACE_ID", "HERDR_PANE_ID", "HERDR_TAB_ID", "HERDR_SOCKET_PATH")
|
|
|
|
|
|
def _herdr(herdr: str, session: str, *args: str) -> dict:
|
|
env = {k: v for k, v in os.environ.items() if k not in _SCOPE_ENV}
|
|
out = subprocess.run([herdr, "--session", session, *args], env=env,
|
|
capture_output=True, text=True, timeout=30, check=True).stdout
|
|
return json.loads(out).get("result") or {}
|
|
|
|
|
|
def _shell_in_foreground(herdr: str, session: str, pane_id: str) -> bool:
|
|
info = _herdr(herdr, session, "pane", "process-info", "--pane", pane_id).get("process_info") or {}
|
|
shell, fg = info.get("shell_pid"), info.get("foreground_process_group_id")
|
|
# Only a real pid proves anything (review R2): None, 0, -1, False, "" or a
|
|
# string must leave the pane alone, however they compare.
|
|
return _is_pid(shell) and _is_pid(fg) and fg == shell
|
|
|
|
|
|
def _is_pid(v) -> bool:
|
|
return isinstance(v, int) and not isinstance(v, bool) and v > 0
|
|
|
|
|
|
def close_restored_shells(session_id: str, herdr: str = "herdr",
|
|
session: Optional[str] = None) -> List[str]:
|
|
"""Close the matching panes; return their ids."""
|
|
session = os.environ.get("HERDR_SESSION_NAME", "") if session is None else session
|
|
if not session_id or session in ("", "default"):
|
|
return []
|
|
closed = []
|
|
for p in _herdr(herdr, session, "pane", "list").get("panes") or []:
|
|
pane_id = p.get("pane_id")
|
|
if not pane_id or p.get("agent"):
|
|
continue
|
|
if (p.get("agent_session") or {}).get("value") != session_id:
|
|
continue
|
|
if not _shell_in_foreground(herdr, session, pane_id):
|
|
continue
|
|
_herdr(herdr, session, "pane", "close", pane_id)
|
|
print(f"closed restored pane {pane_id} (cwd={p.get('cwd', '')}, agent_session={session_id})",
|
|
file=sys.stderr)
|
|
closed.append(pane_id)
|
|
return closed
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
p = argparse.ArgumentParser(description="close a resumed session's restored empty shells (Issue #45)")
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
c = sub.add_parser("close-restored-shells")
|
|
c.add_argument("--session-id", required=True)
|
|
c.add_argument("--herdr", default="", help="the real herdr binary (not the MAM shim)")
|
|
args = p.parse_args(argv)
|
|
try:
|
|
close_restored_shells(args.session_id, args.herdr or "herdr")
|
|
except (OSError, subprocess.SubprocessError, ValueError) as e:
|
|
print(f"close-restored-shells: {e}", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|