feat(registry): format version 2, migrated by the writer (Issue #71)
- lib_py/registry_format.py: a pure, idempotent migrate() drops kill_command/start_command and rebuilds attach_command on every row (stopped/terminated included) and sets the top-level registry_format key. - atomic_dump_yaml migrates an older registry under its lock, before the caller's mutation, and then rewrites the YAML as well as the DB (the YAML write is otherwise skipped when only fields change). load_state_json stays read-only. New registries are written with the current version. - Entry point for #72: `python -m lib_py.registry_format --yaml <path>` runs the migration through the writer and prints `registry-format: <from> -> <to>, rows changed: <n>`. Refs #71 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
50e2998a20
commit
9c21fa4e4f
3 files changed
+117
-1
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import hashlib
|
||||
import re
|
||||
import shlex
|
||||
|
||||
def sanitize_herdr_agent_name(name: str) -> str:
|
||||
"""
|
||||
@@ -29,3 +30,14 @@ def sanitize_herdr_agent_name(name: str) -> str:
|
||||
|
||||
return s
|
||||
|
||||
|
||||
def attach_command(server, name):
|
||||
"""The registry row's attach_command (Issue #71).
|
||||
|
||||
A native herdr command, so it works from any shell: the server is always
|
||||
named (`default` too - herdr's env-free default), and the agent by the name
|
||||
herdr registered (sanitize_herdr_agent_name). create, resume and reconcile
|
||||
all write it from here.
|
||||
"""
|
||||
return "herdr --session %s agent attach %s" % (
|
||||
shlex.quote(server or "default"), shlex.quote(sanitize_herdr_agent_name(name)))
|
||||
@@ -4,6 +4,7 @@
|
||||
import os, sys, tempfile, shutil, glob, subprocess, json, sqlite3, fcntl
|
||||
from datetime import datetime, timezone
|
||||
import yaml
|
||||
from lib_py import registry_format
|
||||
|
||||
def atomic_dump_yaml_main():
|
||||
yaml_path = os.environ['YAML_PATH']
|
||||
@@ -114,6 +115,16 @@ def atomic_dump_yaml_main():
|
||||
elif 'herdr_sessions' not in d:
|
||||
d['herdr_sessions'] = []
|
||||
|
||||
# Issue #71: an older registry is migrated here, under the lock and
|
||||
# before the caller's mutation. It changes row fields only, so the
|
||||
# YAML is rewritten whether or not the mutation changes anything.
|
||||
from_version = registry_format.stored_version(d)
|
||||
migrated_rows = 0
|
||||
force_yaml = False
|
||||
if from_version < registry_format.CURRENT:
|
||||
d, migrated_rows = registry_format.migrate(d)
|
||||
force_yaml = True
|
||||
|
||||
old_sessions = get_all_sessions_status(d)
|
||||
# a mutation that replaces a row the monitor registered for a pane it
|
||||
# created itself (create_session.sh) drops that name from old_roles
|
||||
@@ -167,7 +178,7 @@ def atomic_dump_yaml_main():
|
||||
|
||||
conn.commit()
|
||||
|
||||
if new_sessions != old_sessions:
|
||||
if force_yaml or new_sessions != old_sessions:
|
||||
if os.path.exists(yaml_path):
|
||||
try:
|
||||
shutil.copy2(yaml_path, yaml_path + '.bak')
|
||||
@@ -207,6 +218,7 @@ def atomic_dump_yaml_main():
|
||||
_flock_f.close()
|
||||
except Exception:
|
||||
pass
|
||||
return from_version, registry_format.stored_version(d), migrated_rows
|
||||
|
||||
if __name__ == '__main__':
|
||||
atomic_dump_yaml_main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Registry format version and its migration (Issue #71).
|
||||
|
||||
The session registry (agent-sessions.yaml + agent-sessions.db) carries a
|
||||
top-level `registry_format` key; it lives in the DB `state` row like every
|
||||
other top-level key. A registry without it is format 1.
|
||||
|
||||
1: rows may carry `kill_command` and `start_command`, and `attach_command`
|
||||
reads `HERDR_SESSION_NAME=<server> herdr agent attach <original name>`.
|
||||
2: no `kill_command` / `start_command`; `attach_command` is
|
||||
lib_py.agents.sanitize.attach_command(<row server>, <row name>).
|
||||
|
||||
The writer (lib_py.atomic_yaml, the only code that writes the registry)
|
||||
migrates an older registry under its lock before the caller's mutation, and
|
||||
then rewrites the YAML as well as the DB. Readers (lib_py.state) never
|
||||
migrate; they see the old rows until the first write.
|
||||
|
||||
Entry point (for #72, the update skill):
|
||||
|
||||
python -m lib_py.registry_format --yaml <agent-sessions.yaml>
|
||||
|
||||
runs that migration through the writer (same lock, YAML rewritten), prints one
|
||||
line `registry-format: <from> -> <to>, rows changed: <n>` and exits 0 when the
|
||||
registry was migrated or already current, nonzero on error. Run it with the
|
||||
skills directory on PYTHONPATH (as lib.sh does), e.g.
|
||||
`PYTHONPATH=.agents/skills python3 -m lib_py.registry_format --yaml .mam/agent-sessions.yaml`.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lib_py.agents.sanitize import attach_command
|
||||
|
||||
FORMAT_KEY = "registry_format"
|
||||
CURRENT = 2
|
||||
_DROPPED = ("kill_command", "start_command")
|
||||
|
||||
|
||||
def stored_version(d):
|
||||
"""The format a registry dict says it has (1 when it says nothing)."""
|
||||
v = d.get(FORMAT_KEY) if isinstance(d, dict) else None
|
||||
return v if isinstance(v, int) and not isinstance(v, bool) else 1
|
||||
|
||||
|
||||
def _row_server(s):
|
||||
return s.get("herdr_session") or s.get("herdr_server") or "default"
|
||||
|
||||
|
||||
def migrate(d):
|
||||
"""Return (new registry dict, rows changed). Pure and idempotent: `d` is
|
||||
not modified, and migrating the result again changes nothing. Every row is
|
||||
migrated, `stopped` and `terminated` ones included."""
|
||||
out = copy.deepcopy(d) if isinstance(d, dict) else {}
|
||||
changed = 0
|
||||
for s in out.get("herdr_sessions") or []:
|
||||
if not isinstance(s, dict) or not s.get("name"):
|
||||
continue
|
||||
before = dict(s)
|
||||
for k in _DROPPED:
|
||||
s.pop(k, None)
|
||||
s["attach_command"] = attach_command(_row_server(s), s["name"])
|
||||
if s != before:
|
||||
changed += 1
|
||||
out[FORMAT_KEY] = CURRENT
|
||||
return out, changed
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(prog="python -m lib_py.registry_format",
|
||||
description="Migrate the session registry to the current format.")
|
||||
ap.add_argument("--yaml", required=True, help="path of agent-sessions.yaml")
|
||||
a = ap.parse_args(argv)
|
||||
yaml_path = os.path.abspath(a.yaml)
|
||||
db_path = os.path.splitext(yaml_path)[0] + ".db"
|
||||
if not (os.path.exists(yaml_path) or os.path.exists(db_path)):
|
||||
print("registry-format: error: no registry at %s" % yaml_path, file=sys.stderr)
|
||||
return 1
|
||||
from lib_py.atomic_yaml import atomic_dump_yaml_main
|
||||
os.environ["YAML_PATH"] = yaml_path
|
||||
os.environ["AGENT_SESSIONS_MUTATION"] = "pass"
|
||||
try:
|
||||
frm, to, n = atomic_dump_yaml_main()
|
||||
except (Exception, SystemExit) as exc: # the writer refuses with SystemExit
|
||||
print("registry-format: error: %s" % (exc,), file=sys.stderr)
|
||||
return 1
|
||||
print("registry-format: %d -> %d, rows changed: %d" % (frm, to, n), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user