Class: RepoTender::Launchd::Agent
- Inherits:
-
Object
- Object
- RepoTender::Launchd::Agent
- Defined in:
- lib/repo_tender/launchd/agent.rb
Overview
launchctl wrapper. Holds an injected command runner (the real default goes through ‘RepoTender::Shell` inside a `Sync{}` block; tests inject a `RecordingRunner` that captures argv and returns canned output — gate G2).
All public methods return ‘Dry::Monads::Result`. A non-zero `launchctl` exit surfaces as `Failure(stderr:, status:)` — the same shape `Shell.run` uses — NOT a raise. A non-zero `runner` exit propagates as `Failure` to the caller.
Domain: every operation targets ‘gui/<UID>` (the user’s per-GUI-session launchd domain — the conventional domain for user-installed LaunchAgents on macOS). The UID is resolved via ‘Process.uid` by default; tests may inject a different UID to assert the exact argv (G2).
Defined Under Namespace
Classes: ShellRunner
Constant Summary collapse
- DEFAULT_LABEL =
"io.github.jetpks.repo-tender.sync"
Instance Attribute Summary collapse
-
#label ⇒ Object
readonly
Returns the value of attribute label.
Instance Method Summary collapse
-
#initialize(runner: ShellRunner.new, uid: Process.uid, label: DEFAULT_LABEL) ⇒ Agent
constructor
A new instance of Agent.
-
#install(plist_path) ⇒ Object
‘launchctl bootstrap gui/<UID> <abs-plist-path>`.
-
#restart ⇒ Object
‘launchctl kickstart -k gui/<UID>/<label>` — `-k` kills the running instance first so the new one always starts.
-
#start(plist_path) ⇒ Object
bootstrap the plist, then ‘enable` the service.
-
#status ⇒ Object
Returns a defensive parse of ‘launchctl list` (the machine-readable form — `launchctl print` is documented as “not API”).
-
#stop ⇒ Object
bootout the service, then ‘disable` it.
-
#uninstall ⇒ Object
‘launchctl bootout gui/<UID>/<label>`.
Constructor Details
#initialize(runner: ShellRunner.new, uid: Process.uid, label: DEFAULT_LABEL) ⇒ Agent
Returns a new instance of Agent.
48 49 50 51 52 |
# File 'lib/repo_tender/launchd/agent.rb', line 48 def initialize(runner: ShellRunner.new, uid: Process.uid, label: DEFAULT_LABEL) @runner = runner @uid = uid @label = label end |
Instance Attribute Details
#label ⇒ Object (readonly)
Returns the value of attribute label.
54 55 56 |
# File 'lib/repo_tender/launchd/agent.rb', line 54 def label @label end |
Instance Method Details
#install(plist_path) ⇒ Object
‘launchctl bootstrap gui/<UID> <abs-plist-path>`
57 58 59 |
# File 'lib/repo_tender/launchd/agent.rb', line 57 def install(plist_path) run("bootstrap", "gui/#{@uid}", plist_path) end |
#restart ⇒ Object
‘launchctl kickstart -k gui/<UID>/<label>` — `-k` kills the running instance first so the new one always starts.
104 105 106 |
# File 'lib/repo_tender/launchd/agent.rb', line 104 def restart run("kickstart", "-k", "gui/#{@uid}/#{@label}") end |
#start(plist_path) ⇒ Object
bootstrap the plist, then ‘enable` the service. Both must succeed (both 0 exit) for the operation to be a `Success`; the first failure short-circuits.
78 79 80 81 82 |
# File 'lib/repo_tender/launchd/agent.rb', line 78 def start(plist_path) r1 = run("bootstrap", "gui/#{@uid}", plist_path) return r1 if r1.failure? run("enable", "gui/#{@uid}/#{@label}") end |
#status ⇒ Object
Returns a defensive parse of ‘launchctl list` (the machine-readable form — `launchctl print` is documented as “not API”). We run `launchctl list` (no service target) and search the output for our label.
The parser tolerates: empty output, a “Could not find” line, malformed rows, and PID values that are not integers. On any of those, we return Success(loaded: false) — the gate G4 “no raise on malformed” guarantee.
117 118 119 120 121 |
# File 'lib/repo_tender/launchd/agent.rb', line 117 def status result = run("list") return result if result.failure? parse_list(result.success) end |
#stop ⇒ Object
bootout the service, then ‘disable` it.
Idempotency (Slice 5 / CF5): a ‘bootout` Failure with `status == 3` (“No such process”) or matching the not-loaded stderr is treated as **already not loaded** and is not propagated — the disable step still runs so the persistent `disable` override stays in place (matching the gate’s recorded-argv assertion ‘[[bootout,…], [disable,…]]` and the “stopped” semantic). A non-benign bootout Failure (e.g. status 1 “Operation not permitted”) short- circuits as before.
96 97 98 99 100 |
# File 'lib/repo_tender/launchd/agent.rb', line 96 def stop r1 = run("bootout", "gui/#{@uid}/#{@label}") return r1 if r1.failure? && !benign_bootout_failure?(r1) run("disable", "gui/#{@uid}/#{@label}") end |
#uninstall ⇒ Object
‘launchctl bootout gui/<UID>/<label>`
Idempotency (Slice 5 / CF5): a benign bootout Failure (status 3 / “No such process” / “Could not find specified service”) is mapped to Success —uninstalling a not-loaded agent is a no-op for the bootout step. The plist removal in the CLI command layer is independent of this result.
69 70 71 72 73 |
# File 'lib/repo_tender/launchd/agent.rb', line 69 def uninstall r = run("bootout", "gui/#{@uid}/#{@label}") return Dry::Monads::Success("") if benign_bootout_failure?(r) r end |