Class: RepoTender::Launchd::Plist

Inherits:
Object
  • Object
show all
Defined in:
lib/repo_tender/launchd/plist.rb

Overview

Hand-rolled launchd plist emitter. The slice forbids a plist gem (PRD §2, AGENTS.md) — this class emits an XML property list as a string and is validated offline with ‘plutil -lint`.

The plist produced here is a fixed-shape StartInterval-driven agent that:

* runs `repo-tender sync` non-interactively under the
  repo's mise-managed Ruby (so the right toolchain is in
  effect without `mise activate`, which is broken
  non-interactively);
* is classified as a Background process (lower scheduling
  + I/O priority — sync is a periodic background job);
* writes its stdout/stderr to absolute log files under
  the log dir (launchd owns the redirect, the sync process
  rotates its own log on each run — see LogRotator);
* has NO `KeepAlive` key — it is a periodic, not a daemon.

The caller is responsible for passing absolute paths. We do NOT ‘File.expand_path` here — that would mask the caller’s responsibility to pass absolute paths (the G1 / G3 gates assert that no ‘~` or `$HOME` appears in any value).

Constant Summary collapse

HEADER =

The plist’s outer XML decl + DOCTYPE — required by plutil’s lint and by launchd’s parser.

<<~XML
  <?xml version="1.0" encoding="UTF-8"?>
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  <plist version="1.0">
  <dict>
XML
"</dict>\n</plist>\n"

Class Method Summary collapse

Class Method Details

.call(label:, refresh_interval:, log_dir:, repo_root:, mise_toml:, mise_bin:, ruby_bin:, bin_path:) ⇒ String

Emit a launchd plist for a sync job.

Parameters:

  • label (String)

    The job label (must be a valid reverse-DNS string; appears as the basename of the on-disk plist).

  • refresh_interval (Integer)

    StartInterval in seconds (must be > 0).

  • log_dir (String)

    Absolute directory for the standard-out / standard-err logs.

  • repo_root (String)

    Absolute path to set as WorkingDirectory (so mise finds the repo’s mise.toml).

  • mise_toml (String)

    Absolute path to mise.toml (pinned via EnvironmentVariables.MISE_CONFIG_FILE).

  • mise_bin (String)

    Absolute path to the mise binary (ProgramArguments).

  • ruby_bin (String)

    Absolute path to the ruby to run the script under.

  • bin_path (String)

    Absolute path to the repo-tender bin script.

Returns:

  • (String)

    The full plist XML, ready to be written to disk and ‘plutil -lint`-validated.

Raises:

  • (ArgumentError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/repo_tender/launchd/plist.rb', line 49

def call(label:, refresh_interval:, log_dir:, repo_root:, mise_toml:, mise_bin:, ruby_bin:, bin_path:)
  raise ArgumentError, "label is required" if label.to_s.empty?
  raise ArgumentError, "refresh_interval must be > 0" unless refresh_interval.is_a?(Integer) && refresh_interval > 0
  %w[log_dir repo_root mise_toml mise_bin ruby_bin bin_path].each do |k|
    v = binding.local_variable_get(k)
    raise ArgumentError, "#{k} must be absolute (got #{v.inspect})" unless v.is_a?(String) && File.absolute_path?(v)
  end

  out_log = File.join(log_dir, "#{label}.out.log")
  err_log = File.join(log_dir, "#{label}.err.log")

  body = +""
  body << key("Label") << string(label) << "\n"
  body << key("ProgramArguments") << "\n" << array([
    mise_bin,
    "exec",
    "--",
    ruby_bin,
    bin_path,
    "sync"
  ])
  body << key("WorkingDirectory") << string(repo_root) << "\n"
  body << key("EnvironmentVariables") << "\n" << dict({
    "MISE_CONFIG_FILE" => mise_toml
  })
  body << key("StartInterval") << integer(refresh_interval) << "\n"
  body << key("RunAtLoad") << boolean(true) << "\n"
  body << key("ProcessType") << string("Background") << "\n"
  body << key("StandardOutPath") << string(out_log) << "\n"
  body << key("StandardErrorPath") << string(err_log) << "\n"

  HEADER + body + FOOTER
end