Repositories / agent-snapshot.git
agent-snapshot.git
Clone (read-only): git clone http://git.guha-anderson.com/git/agent-snapshot.git
@@ -179,12 +179,10 @@ The project depends on OCaml, Dune, Yojson, Camomile, and the `ocaml-git` package. Install the latter with opam (for a local checkout next to this repo, `opam install ../../homebox/ocaml-git` from the repository root). -Create the required ignore configuration before running snapshots: - -```bash -mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/agent-snapshot" -printf '[]\n' > "${XDG_CONFIG_HOME:-$HOME/.config}/agent-snapshot/ignore.json" -``` +On first use, if `ignore.json` is missing, Agent Snapshot creates the config +directory and writes that file using the default path list in the +[Configuration](#configuration) section. You can edit the file before or after +the first run. Run a command under Agent Snapshot: @@ -210,8 +208,10 @@ not rewritten by restore. ## Configuration -Snapshot runs require an ignore configuration file. If it is missing or not a -JSON array of strings, Agent Snapshot aborts before launching the traced command. +Snapshot runs read an ignore configuration file. If it is missing, Agent +Snapshot creates it with the default JSON array shown below and continues. If +the file exists but is not a JSON array of strings, Agent Snapshot aborts before +launching the traced command. The config path is: @@ -231,7 +231,9 @@ The file is a JSON list of file or directory paths: [ "$HOME/.cache", "$XDG_CONFIG_HOME/agent-snapshot/ignore.json", - "/tmp/scratch-output" + "/tmp/scratch-output", + "/usr", + "/bin" ] ```
@@ -190,11 +190,28 @@ let expand_ignore_entry (entry : string) : string = else if String.starts_with ~prefix:(xdg ^ "/") entry then concat_path (xdg_config_home_dir ()) (String.sub entry 17 (String.length entry - 17)) else entry +(** Shipped default when [ignore.json] is missing; matches the documented example plus common system trees. *) +let default_ignore_file_entries () : ignore_file_entries = + [ + "$HOME/.cache"; + "$XDG_CONFIG_HOME/agent-snapshot/ignore.json"; + "/tmp/scratch-output"; + "/usr"; + "/bin"; + ] + let load_ignore_config () : unit = ignore_config_path := best_effort_canonical (xdg_ignore_config_path ()); let json = try Json.from_file !ignore_config_path - with Sys_error _ -> failwith ("ignore config does not exist: " ^ !ignore_config_path) + with Sys_error _ -> + mkdir_p (dirname !ignore_config_path); + let text = Json.pretty_to_string ~std:true (ignore_file_entries_to_yojson (default_ignore_file_entries ())) ^ "\n" in + let oc = open_out_bin !ignore_config_path in + Fun.protect + ~finally:(fun () -> close_out_noerr oc) + (fun () -> output_string oc text); + Json.from_file !ignore_config_path in match ignore_file_entries_of_yojson json with | Ok entries -> @@ -202,6 +219,15 @@ let load_ignore_config () : unit = !ignore_config_path :: List.map (fun entry -> best_effort_canonical (expand_ignore_entry entry)) entries | Error msg -> failwith ("ignore config: " ^ msg ^ " (" ^ !ignore_config_path ^ ")") +(** Directory name for a default snapshot bundle next to [ignore.json]. *) +let timestamped_snapshot_dir_next_to_config () : string = + let tm = Unix.(localtime (gettimeofday ())) in + let name = + Printf.sprintf "%04d-%02d-%02dT%02d-%02d-%02d" + (tm.tm_year + 1900) (tm.tm_mon + 1) tm.tm_mday tm.tm_hour tm.tm_min tm.tm_sec + in + concat_path (dirname !ignore_config_path) name + let mode_of_kind (kind : Unix.file_kind) : int = match kind with | Unix.S_REG -> 0o100000 @@ -705,8 +731,13 @@ let restore_snapshot (dir : string) : unit = | _ -> ()) files -let run_snapshot (output : string) (command : string list) : int = +let run_snapshot (output : string option) (command : string list) : int = load_ignore_config (); + let output = + match output with + | Some dir -> dir + | None -> timestamped_snapshot_dir_next_to_config () + in snapshot_dir := output; reset_blob_writer (); remove_all output; @@ -722,10 +753,12 @@ open Cmdliner let output_arg = Arg.( - required + value & opt (some string) None & info [ "o"; "output" ] ~docv:"SNAPDIR" - ~doc:"Write the snapshot under this directory.") + ~doc: + "Write the snapshot under this directory. When omitted, a new timestamped directory is created next to the \ + ignore configuration file (the same directory as ignore.json).") let command_arg = Arg.( @@ -744,7 +777,7 @@ let snapshot_term : unit Term.t = Term.( const (fun output cmd -> if cmd = [] then - failwith "usage: agent-snapshot --output SNAPDIR -- command args..."; + failwith "usage: agent-snapshot [--output SNAPDIR] -- command args..."; ignore (run_snapshot output cmd : int)) $ output_arg $ command_arg) @@ -761,6 +794,7 @@ let cmd_main : unit Cmd.t = `S Manpage.s_examples; `P "$(tool) $(b,--output) /tmp/snap $(b,--) make all"; `P "$(tool) $(b,--output) /tmp/snap make all"; + `P "$(tool) $(b,--) make all"; `P "$(tool) $(b,restore) /tmp/snap"; ] in
@@ -20,24 +20,6 @@ WORKTREE = TESTDATA / "runtime_repo" PYTHON = "/usr/bin/python3" -def _prepend_opam_stublibs_to_ld_path() -> None: - """Arrow links libparquet dynamically; dune-built binaries need Opam stublibs on LD_LIBRARY_PATH.""" - try: - prefix = subprocess.run( - ["opam", "var", "prefix"], - cwd=ROOT, - capture_output=True, - text=True, - check=True, - ).stdout.strip() - except (FileNotFoundError, subprocess.CalledProcessError): - return - stublibs = str(Path(prefix) / "lib" / "stublibs") - prev = os.environ.get("LD_LIBRARY_PATH", "") - if stublibs not in prev.split(":"): - os.environ["LD_LIBRARY_PATH"] = f"{stublibs}:{prev}" if prev else stublibs - - def run(cmd, **kwargs): return subprocess.run(cmd, cwd=ROOT, text=True, check=True, **kwargs) @@ -47,7 +29,6 @@ def build_agent_snapshot(): # The tests exercise the real CLI binary instead of calling internal helper # functions. That keeps the acceptance criteria aligned with ptrace behavior, # process launch, Dune wiring, and manifest writing as users will run them. - _prepend_opam_stublibs_to_ld_path() run(["bash", "-lc", ". /scratch/arjun/ocaml/env.sh && dune build src/ocaml/agent_snapshot.exe"]) assert BIN.exists() @@ -137,17 +118,21 @@ def capture(tmp_path: Path, *command: str) -> Snapshot: return Snapshot(out) -def test_missing_ignore_config_aborts_at_startup(tmp_path, ignore_config): +def test_missing_ignore_config_creates_defaults(tmp_path, ignore_config): ignore_config.unlink() - result = subprocess.run( - [str(BIN), "--output", str(tmp_path / "snapshot"), "--", PYTHON, "test_programs/read_clean.py"], - cwd=ROOT, - text=True, - capture_output=True, - ) - - assert result.returncode != 0 - assert "ignore" in result.stderr + assert not ignore_config.exists() + out = tmp_path / "snapshot" + run([str(BIN), "--output", str(out), "--", PYTHON, "test_programs/read_clean.py"]) + + assert ignore_config.exists() + data = json.loads(ignore_config.read_text()) + assert data == [ + "$HOME/.cache", + "$XDG_CONFIG_HOME/agent-snapshot/ignore.json", + "/tmp/scratch-output", + "/usr", + "/bin", + ] def test_ignore_config_suppresses_files_directories_and_itself(tmp_path, ignore_config):