Class: Space::Src::Launchd::Agent
- Inherits:
-
Object
- Object
- Space::Src::Launchd::Agent
- Defined in:
- lib/space_src/launchd/agent.rb
Overview
launchctl wrapper. Holds an injected command runner (the
real default goes through Space::Src::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({argv:, 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.space-src.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>—-kkills the running instance first so the new one always starts. -
#start(plist_path) ⇒ Object
bootstrap the plist, then
enablethe service. -
#status ⇒ Object
Returns a defensive parse of
launchctl list(the machine-readable form —launchctl printis documented as "not API"). -
#stop ⇒ Object
bootout the service, then
disableit. -
#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/space_src/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/space_src/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/space_src/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/space_src/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/space_src/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/space_src/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/space_src/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/space_src/launchd/agent.rb', line 69 def uninstall r = run("bootout", "gui/#{@uid}/#{@label}") return Dry::Monads::Success("") if benign_bootout_failure?(r) r end |