locd 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +108 -0
- data/.gitmodules +9 -0
- data/.qb-options.yml +4 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/.yardopts +7 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +72 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/config/default.yml +24 -0
- data/doc/files/design/domains_and_labels.md +117 -0
- data/doc/files/topics/agents.md +16 -0
- data/doc/files/topics/launchd.md +15 -0
- data/doc/files/topics/plists.md +39 -0
- data/doc/include/plist.md +3 -0
- data/exe/locd +24 -0
- data/lib/locd.rb +75 -0
- data/lib/locd/agent.rb +1186 -0
- data/lib/locd/agent/job.rb +142 -0
- data/lib/locd/agent/proxy.rb +111 -0
- data/lib/locd/agent/rotate_logs.rb +82 -0
- data/lib/locd/agent/site.rb +174 -0
- data/lib/locd/agent/system.rb +270 -0
- data/lib/locd/cli.rb +4 -0
- data/lib/locd/cli/command.rb +9 -0
- data/lib/locd/cli/command/agent.rb +310 -0
- data/lib/locd/cli/command/base.rb +243 -0
- data/lib/locd/cli/command/job.rb +110 -0
- data/lib/locd/cli/command/main.rb +201 -0
- data/lib/locd/cli/command/proxy.rb +177 -0
- data/lib/locd/cli/command/rotate_logs.rb +152 -0
- data/lib/locd/cli/command/site.rb +47 -0
- data/lib/locd/cli/table.rb +157 -0
- data/lib/locd/config.rb +237 -0
- data/lib/locd/config/base.rb +93 -0
- data/lib/locd/errors.rb +65 -0
- data/lib/locd/label.rb +61 -0
- data/lib/locd/launchctl.rb +209 -0
- data/lib/locd/logging.rb +360 -0
- data/lib/locd/newsyslog.rb +402 -0
- data/lib/locd/pattern.rb +193 -0
- data/lib/locd/proxy.rb +272 -0
- data/lib/locd/proxymachine.rb +34 -0
- data/lib/locd/util.rb +49 -0
- data/lib/locd/version.rb +26 -0
- data/locd.gemspec +66 -0
- metadata +262 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
Property Lists (`*.plist` Files)
|
2
|
+
==============================================================================
|
3
|
+
|
4
|
+
Property lists are an Apple-specific serialization format:
|
5
|
+
|
6
|
+
<https://en.wikipedia.org/wiki/Property_list>
|
7
|
+
|
8
|
+
They come in several formats - XML, binary and a funky "old" / NeXTSTEP custom encoding.
|
9
|
+
|
10
|
+
We care about them because {file:doc/topics/launchd.md}'s {file:doc/topics/agents.md} are defined in plist files.
|
11
|
+
|
12
|
+
All the agent plists I've seen are in XML format, and that's what Loc'd writes and expects to find when reading.
|
13
|
+
|
14
|
+
For more info on launchd plists check out:
|
15
|
+
|
16
|
+
<https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man5/launchd.plist.5.html>
|
17
|
+
|
18
|
+
|
19
|
+
plists in Ruby
|
20
|
+
------------------------------------------------------------------------------
|
21
|
+
|
22
|
+
Loc'd uses the [plist][] gem to read and write agent plists:
|
23
|
+
|
24
|
+
1. [Plist.parse_xml][] to parse agent plist XML files to Ruby hashes.
|
25
|
+
|
26
|
+
2. [Plist::Emit.dump][] to encode Ruby hashes into plist XML for writing.
|
27
|
+
|
28
|
+
When you see a `plist` in Loc'd it should be a Ruby hash that was read from an agent plist file and/or created or mutated to be written to an agent plist file (plists are a general data format, but the top-level object in agent plists should always be a `dict`, which is mapped to a Ruby hash).
|
29
|
+
|
30
|
+
The keys of these hashes should all be strings. For details on the possible Ruby value types see
|
31
|
+
|
32
|
+
<http://www.rubydoc.info/gems/plist#Generation>
|
33
|
+
|
34
|
+
|
35
|
+
<!-- References & Further Reading: -->
|
36
|
+
|
37
|
+
[plist]: https://rubygems.org/gems/plist
|
38
|
+
|
39
|
+
[Plist.parse_xml]: http://www.rubydoc.info/gems/plist/Plist.parse_xml
|
data/exe/locd
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'locd'
|
6
|
+
|
7
|
+
# Setup logging *before* including {Locd::CLI} so that we can get
|
8
|
+
# trace/debug output from that setup if enabled via ENV vars
|
9
|
+
Locd::Logging.setup dest: $stderr, sync: true
|
10
|
+
|
11
|
+
# Logger for this file
|
12
|
+
logger = SemanticLogger[__FILE__]
|
13
|
+
|
14
|
+
require 'locd/cli'
|
15
|
+
|
16
|
+
begin
|
17
|
+
Locd::CLI::Command::Main.start
|
18
|
+
rescue Exception => e
|
19
|
+
if e.instance_of?(SystemExit)
|
20
|
+
raise
|
21
|
+
else
|
22
|
+
logger.fatal e
|
23
|
+
end
|
24
|
+
end
|
data/lib/locd.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
# Requirements
|
5
|
+
# =======================================================================
|
6
|
+
|
7
|
+
# Stdlib
|
8
|
+
# -----------------------------------------------------------------------
|
9
|
+
require 'pp'
|
10
|
+
require 'pathname'
|
11
|
+
require 'shellwords'
|
12
|
+
|
13
|
+
# Deps
|
14
|
+
# -----------------------------------------------------------------------
|
15
|
+
require 'nrser'
|
16
|
+
require 'semantic_logger'
|
17
|
+
require 'cmds'
|
18
|
+
|
19
|
+
# Project / Package
|
20
|
+
# -----------------------------------------------------------------------
|
21
|
+
require 'locd/version'
|
22
|
+
require 'locd/errors'
|
23
|
+
require 'locd/config'
|
24
|
+
require 'locd/util'
|
25
|
+
require 'locd/logging'
|
26
|
+
require 'locd/label'
|
27
|
+
require 'locd/pattern'
|
28
|
+
require 'locd/launchctl'
|
29
|
+
require 'locd/newsyslog'
|
30
|
+
require 'locd/agent'
|
31
|
+
require 'locd/proxy'
|
32
|
+
|
33
|
+
|
34
|
+
# Refinements
|
35
|
+
# =======================================================================
|
36
|
+
|
37
|
+
using NRSER
|
38
|
+
|
39
|
+
|
40
|
+
# Definitions
|
41
|
+
# =======================================================================
|
42
|
+
|
43
|
+
module Locd
|
44
|
+
|
45
|
+
# Constants
|
46
|
+
# ======================================================================
|
47
|
+
|
48
|
+
# {Regexp} to match HTTP "Host" header line.
|
49
|
+
#
|
50
|
+
# @return [Regexp]
|
51
|
+
#
|
52
|
+
HOST_RE = /^Host\:\ /i
|
53
|
+
|
54
|
+
|
55
|
+
ROTATE_LOGS_LABEL = 'com.nrser.locd.rotate-logs'
|
56
|
+
|
57
|
+
|
58
|
+
# Mixins
|
59
|
+
# ============================================================================
|
60
|
+
|
61
|
+
# Add {.logger} and {#logger} methods
|
62
|
+
include SemanticLogger::Loggable
|
63
|
+
|
64
|
+
|
65
|
+
# Module Methods
|
66
|
+
# ======================================================================
|
67
|
+
|
68
|
+
# @return [Locd::Config]
|
69
|
+
# The configuration.
|
70
|
+
#
|
71
|
+
def self.config
|
72
|
+
@config ||= Locd::Config.new
|
73
|
+
end
|
74
|
+
|
75
|
+
end # module Locd
|
data/lib/locd/agent.rb
ADDED
@@ -0,0 +1,1186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Requirements
|
4
|
+
# =======================================================================
|
5
|
+
|
6
|
+
# Stdlib
|
7
|
+
# -----------------------------------------------------------------------
|
8
|
+
|
9
|
+
# Deps
|
10
|
+
# -----------------------------------------------------------------------
|
11
|
+
require 'plist'
|
12
|
+
require 'fileutils'
|
13
|
+
|
14
|
+
# Project / Package
|
15
|
+
# -----------------------------------------------------------------------
|
16
|
+
|
17
|
+
|
18
|
+
# Refinements
|
19
|
+
# =======================================================================
|
20
|
+
|
21
|
+
using NRSER
|
22
|
+
using NRSER::Types
|
23
|
+
|
24
|
+
|
25
|
+
# Definitions
|
26
|
+
# =======================================================================
|
27
|
+
|
28
|
+
|
29
|
+
# Represents a backend agent that the proxy can route to.
|
30
|
+
#
|
31
|
+
# Agents are managed by `launchd`, macOS's system service manager, and
|
32
|
+
# created on-demand by generating "property list" files (`.plist`, XML format)
|
33
|
+
# and installing them in the user's `~/Library/LaunchAgents` directory.
|
34
|
+
#
|
35
|
+
# From there they can be managed directly with macOS's `launchctl` utility,
|
36
|
+
# with the `lunchy` gem, etc.
|
37
|
+
#
|
38
|
+
class Locd::Agent
|
39
|
+
|
40
|
+
# Constants
|
41
|
+
# ==========================================================================
|
42
|
+
|
43
|
+
# Extra plist key we store Loc'd config under (this is where the port is
|
44
|
+
# stored). We also use it's presence to detect that a plist is Loc'd.
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
CONFIG_KEY = 'locd_config'
|
49
|
+
|
50
|
+
|
51
|
+
# Attribute / method names that {#to_h} uses.
|
52
|
+
#
|
53
|
+
# @return [Hamster::SortedSet<Symbol>]
|
54
|
+
#
|
55
|
+
TO_H_NAMES = Hamster::SortedSet[:label, :path, :plist, :status]
|
56
|
+
|
57
|
+
|
58
|
+
# Mixins
|
59
|
+
# ==========================================================================
|
60
|
+
|
61
|
+
# Add {.logger} and {#logger} methods
|
62
|
+
include SemanticLogger::Loggable
|
63
|
+
|
64
|
+
# Make agents sortable by label
|
65
|
+
include Comparable
|
66
|
+
|
67
|
+
|
68
|
+
# Class Methods
|
69
|
+
# ==========================================================================
|
70
|
+
|
71
|
+
# Test if the parse of a property list is for a Loc'd agent by seeing if
|
72
|
+
# it has {CONFIG_KEY} as a key.
|
73
|
+
#
|
74
|
+
# @param [Hash<String, Object>] plist
|
75
|
+
# {include:file:doc/include/plist.md}
|
76
|
+
#
|
77
|
+
# @return [Boolean]
|
78
|
+
# `true` if the plist looks like it's from Loc'd.
|
79
|
+
#
|
80
|
+
def self.plist? plist
|
81
|
+
plist.key? CONFIG_KEY
|
82
|
+
end # .plist?
|
83
|
+
|
84
|
+
|
85
|
+
# @!group Computing Paths
|
86
|
+
# --------------------------------------------------------------------------
|
87
|
+
|
88
|
+
# Absolute path to `launchd` plist directory for the current user, which is
|
89
|
+
# an expansion of `~/Library/LaunchAgents`.
|
90
|
+
#
|
91
|
+
# @return [Pathname]
|
92
|
+
#
|
93
|
+
def self.user_plist_abs_dir
|
94
|
+
Pathname.new( '~/Library/LaunchAgents' ).expand_path
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# Absolute path for the plist file given it's label, equivalent to expanding
|
99
|
+
# `~/Library/LaunchAgents/<label>.plist`.
|
100
|
+
#
|
101
|
+
# @param [String] label
|
102
|
+
# The agent's label.
|
103
|
+
#
|
104
|
+
# @return [Pathname]
|
105
|
+
#
|
106
|
+
def self.plist_abs_path label
|
107
|
+
user_plist_abs_dir / "#{ label }.plist"
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Default path for a {Locd::Agent} output file.
|
112
|
+
#
|
113
|
+
# @param [Pathname] workdir
|
114
|
+
# The working directory the agent will run in.
|
115
|
+
#
|
116
|
+
# @param [String] label
|
117
|
+
# The agent's label.
|
118
|
+
#
|
119
|
+
# @return [Pathname]
|
120
|
+
# If we could find a `tmp` directory walking up from `workdir`.
|
121
|
+
#
|
122
|
+
# @return [nil]
|
123
|
+
# If we could not find a `tmp` directory walking
|
124
|
+
#
|
125
|
+
def self.default_log_path workdir, label
|
126
|
+
tmp_dir = workdir.find_up 'tmp', test: :directory?, result: :path
|
127
|
+
|
128
|
+
if tmp_dir.nil?
|
129
|
+
logger.warn "Unable to find `tmp` dir, output will not be redirected",
|
130
|
+
workdir: workdir,
|
131
|
+
label: label,
|
132
|
+
stream: stream
|
133
|
+
return nil
|
134
|
+
end
|
135
|
+
|
136
|
+
unless tmp_dir.writable?
|
137
|
+
logger.warn \
|
138
|
+
"Found `tmp` dir, but not writable. Output will not be redirected",
|
139
|
+
workdir: workdir,
|
140
|
+
label: label,
|
141
|
+
stream: stream
|
142
|
+
return nil
|
143
|
+
end
|
144
|
+
|
145
|
+
tmp_dir / 'locd' / "#{ label }.log"
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Get the path to log `STDOUT` and `STDERR` to given the option value and other
|
150
|
+
# options we need to figure it out if that doesn't suffice.
|
151
|
+
#
|
152
|
+
# Note that we might not figure it out at all (returning `nil`), and that
|
153
|
+
# needs to be ok too (though it's not expected to happen all too often).
|
154
|
+
#
|
155
|
+
# @param [nil | String | Pathname] log_path:
|
156
|
+
# The value the user provided (if any).
|
157
|
+
#
|
158
|
+
# 1. `nil` - {.default_log_path} is called and it's result returned.
|
159
|
+
#
|
160
|
+
# 2. `String | Pathname` - Value is expanded against `workdir` and the
|
161
|
+
# resulting absolute path is returned (which of course may not exist).
|
162
|
+
#
|
163
|
+
# This means that absolute, home-relative (`~/` paths) and
|
164
|
+
# `workdir`-relative paths should all produce the expected results.
|
165
|
+
#
|
166
|
+
# @return [Pathname]
|
167
|
+
# Absolute path where the agent will try to log output.
|
168
|
+
#
|
169
|
+
# @return [nil]
|
170
|
+
# If the user doesn't supply a value and {.default_log_path} returns
|
171
|
+
# `nil`. The agent will not log output in this case.
|
172
|
+
#
|
173
|
+
def self.resolve_log_path log_path:, workdir:, label:
|
174
|
+
if log_path.nil?
|
175
|
+
default_log_path workdir, label
|
176
|
+
else
|
177
|
+
log_path.to_pn.expand_path workdir
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# @!endgroup Computing Paths
|
182
|
+
|
183
|
+
|
184
|
+
# @!group Instantiating
|
185
|
+
# --------------------------------------------------------------------------
|
186
|
+
|
187
|
+
# Get the agent class that a plist should be instantiated as.
|
188
|
+
#
|
189
|
+
# @param [Hash<String, Object>] plist
|
190
|
+
# {include:file:doc/include/plist.md}
|
191
|
+
#
|
192
|
+
# @return [Class<Locd::Agent>]
|
193
|
+
# If the plist is for an agent of the current Loc'd configuration, the
|
194
|
+
# appropriate class to instantiate it as.
|
195
|
+
#
|
196
|
+
# Plists for user sites and jobs will always return their subclass.
|
197
|
+
#
|
198
|
+
# Plists for system agents will only return their class when they are
|
199
|
+
# for the current configuration's label namespace.
|
200
|
+
#
|
201
|
+
# @return [nil]
|
202
|
+
# If the plist is not for an agent of the current Loc'd configuration
|
203
|
+
# (and should be ignored by Loc'd).
|
204
|
+
#
|
205
|
+
def self.class_for plist
|
206
|
+
# 1. See if it's a Loc'd system agent plist
|
207
|
+
if system_class = Locd::Agent::System.class_for( plist )
|
208
|
+
return system_class
|
209
|
+
end
|
210
|
+
|
211
|
+
# 2. See if it's a Loc'd user agent plist
|
212
|
+
if user_class = [
|
213
|
+
Locd::Agent::Site,
|
214
|
+
Locd::Agent::Job,
|
215
|
+
].find_bounded( max: 1 ) { |cls| cls.plist? plist }.first
|
216
|
+
return user_class
|
217
|
+
end
|
218
|
+
|
219
|
+
# 3. Return a vanilla agent if it's a plist for one
|
220
|
+
#
|
221
|
+
# Really, not sure when this would happen...
|
222
|
+
#
|
223
|
+
if Locd::Agent.plist?( plist )
|
224
|
+
return Locd::Agent
|
225
|
+
end
|
226
|
+
|
227
|
+
# Nada
|
228
|
+
nil
|
229
|
+
end # .class_for
|
230
|
+
|
231
|
+
|
232
|
+
# Instantiate a {Locd::Agent} from the path to it's `.plist` file.
|
233
|
+
#
|
234
|
+
# @param [Pathname | String] plist_path
|
235
|
+
# Path to the `.plist` file.
|
236
|
+
#
|
237
|
+
# @return [Locd::Agent]
|
238
|
+
#
|
239
|
+
def self.from_path plist_path
|
240
|
+
plist = Plist.parse_xml plist_path.to_s
|
241
|
+
|
242
|
+
class_for( plist ).new plist: plist, path: plist_path
|
243
|
+
end
|
244
|
+
|
245
|
+
# @!endgroup
|
246
|
+
|
247
|
+
|
248
|
+
# @!group Querying
|
249
|
+
# --------------------------------------------------------------------------
|
250
|
+
|
251
|
+
# All installed Loc'd agents.
|
252
|
+
#
|
253
|
+
# @return [Hamster::Hash<String, Locd::Agent>]
|
254
|
+
# Map of all Loc'd agents by label.
|
255
|
+
#
|
256
|
+
def self.plists
|
257
|
+
Pathname.glob( user_plist_abs_dir / '*.plist' ).
|
258
|
+
map { |path|
|
259
|
+
begin
|
260
|
+
[path, Plist.parse_xml( path.to_s )]
|
261
|
+
rescue Exception => error
|
262
|
+
logger.trace "{Plist.parse_xml} failed to parse plist",
|
263
|
+
path: path.to_s,
|
264
|
+
error: error,
|
265
|
+
backtrace: error.backtrace
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
}.
|
269
|
+
compact.
|
270
|
+
select { |path, plist| plist? plist }.
|
271
|
+
thru( &Hamster::Hash.method( :new ) )
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
# All {Locd::Agent} that are instances of `self`.
|
276
|
+
#
|
277
|
+
# So, when invoked through {Locd::Agent.all} returns all agents.
|
278
|
+
#
|
279
|
+
# When invoked from a {Locd::Agent} subclass, returns all agents that are
|
280
|
+
# instances of *that subclass* - {Locd::Agent::Site.all} returns all
|
281
|
+
# {Locd::Agent} that are {Locd::Agent::Site} instances.
|
282
|
+
#
|
283
|
+
# @return [Hamster::Hash<String, Locd::Agent>]
|
284
|
+
# Map of agent {#label} to instance.
|
285
|
+
#
|
286
|
+
def self.all
|
287
|
+
# If we're being invoked through {Locd::Agent} itself actually find and
|
288
|
+
# load everyone
|
289
|
+
if self == Locd::Agent
|
290
|
+
plists.each_pair.map { |path, plist|
|
291
|
+
begin
|
292
|
+
agent_class = class_for plist
|
293
|
+
|
294
|
+
if agent_class.nil?
|
295
|
+
nil
|
296
|
+
else
|
297
|
+
agent = agent_class.new path: path, plist: plist
|
298
|
+
[agent.label, agent]
|
299
|
+
end
|
300
|
+
|
301
|
+
rescue Exception => error
|
302
|
+
logger.error "Failed to parse Loc'd Agent plist",
|
303
|
+
path: path,
|
304
|
+
error: error,
|
305
|
+
backtrace: error.backtrace
|
306
|
+
|
307
|
+
nil
|
308
|
+
end
|
309
|
+
}.
|
310
|
+
compact.
|
311
|
+
thru( &Hamster::Hash.method( :new ) )
|
312
|
+
else
|
313
|
+
# We're being invoked through a {Locd::Agent} subclass, so invoke
|
314
|
+
# through {Locd::Agent} and filter the results to instance of `self`.
|
315
|
+
Locd::Agent.all.select { |label, agent| agent.is_a? self }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
|
320
|
+
# Labels for all installed Loc'd agents.
|
321
|
+
#
|
322
|
+
# @return [Hamster::Vector<String>]
|
323
|
+
#
|
324
|
+
def self.labels
|
325
|
+
all.keys.sort
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
# Get a Loc'd agent by it's label.
|
330
|
+
#
|
331
|
+
# @param [String] label
|
332
|
+
# The agent's label.
|
333
|
+
#
|
334
|
+
# @return [Locd::Agent]
|
335
|
+
# If a Loc'd agent with `label` is installed.
|
336
|
+
#
|
337
|
+
# @return [nil]
|
338
|
+
# If no Loc'd agent with `label` is installed.
|
339
|
+
#
|
340
|
+
def self.get label
|
341
|
+
path = plist_abs_path label
|
342
|
+
|
343
|
+
if path.file?
|
344
|
+
from_path path
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
# Find a single {Locd::Agent} matching `pattern` or raise.
|
350
|
+
#
|
351
|
+
# Parameters are passed to {Locd::Label.regexp_for_glob} and the resulting
|
352
|
+
# {Regexp} is matched against each agent's {#label}.
|
353
|
+
#
|
354
|
+
# @see Locd::Label.regexp_for_glob
|
355
|
+
#
|
356
|
+
# @param (see .find_all)
|
357
|
+
#
|
358
|
+
# @return [Locd::Agent]
|
359
|
+
#
|
360
|
+
def self.find_only! *find_all_args
|
361
|
+
find_all( *find_all_args ).values.to_a.only!
|
362
|
+
end
|
363
|
+
|
364
|
+
|
365
|
+
# Find all the agents that match a pattern.
|
366
|
+
#
|
367
|
+
# @see Locd::Label.regexp_for_glob
|
368
|
+
#
|
369
|
+
# @param [String | Pattern] pattern
|
370
|
+
# Pattern to match against agent.
|
371
|
+
#
|
372
|
+
# When it's a {String}, passed to {Locd::Pattern.from} to get the pattern.
|
373
|
+
#
|
374
|
+
# @param [**<Symbol, V>] options
|
375
|
+
# Passed to {Locd::Pattern.from} when `pattern` is a {String}.
|
376
|
+
#
|
377
|
+
# @return (see .all)
|
378
|
+
#
|
379
|
+
def self.find_all pattern, **options
|
380
|
+
pattern = Locd::Pattern.from pattern, **options
|
381
|
+
all.select { |label, agent| pattern.match? agent }
|
382
|
+
end
|
383
|
+
|
384
|
+
singleton_class.send :alias_method, :list, :find_all
|
385
|
+
|
386
|
+
|
387
|
+
# Just like {.find_all} but raises if result is empty.
|
388
|
+
#
|
389
|
+
# @param (see .find_all)
|
390
|
+
# @return (see .find_all)
|
391
|
+
#
|
392
|
+
# @raise [NRSER::CountError]
|
393
|
+
# If no agents were found.
|
394
|
+
#
|
395
|
+
def self.find_all! pattern, **options
|
396
|
+
pattern = Locd::Pattern.from pattern, **options
|
397
|
+
|
398
|
+
find_all( pattern ).tap { |agents|
|
399
|
+
if agents.empty?
|
400
|
+
raise NRSER::CountError.new(
|
401
|
+
"No agents found for pattern from #{ pattern.source.inspect }",
|
402
|
+
subject: agents,
|
403
|
+
expected: '#count > 0',
|
404
|
+
)
|
405
|
+
end
|
406
|
+
}
|
407
|
+
end # .find_all!
|
408
|
+
|
409
|
+
# @!endgroup
|
410
|
+
|
411
|
+
|
412
|
+
# @!group Creating Agents
|
413
|
+
# --------------------------------------------------------------------------
|
414
|
+
|
415
|
+
# Render a command string by substituting any `{name}` parts for their
|
416
|
+
# values.
|
417
|
+
#
|
418
|
+
# @example
|
419
|
+
# render_cmd \
|
420
|
+
# cmd_template: "serve --name={label} --port={port}",
|
421
|
+
# label: 'server.test',
|
422
|
+
# workdir: Pathname.new( '~' ).expand_path,
|
423
|
+
# port: 1234
|
424
|
+
# # => "server --name=server.test --port=1234"
|
425
|
+
#
|
426
|
+
# @param [String | Array<String>] cmd_template:
|
427
|
+
# Template for the string. Arrays are just rendered per-entry then
|
428
|
+
# joined.
|
429
|
+
#
|
430
|
+
# @param [String] label:
|
431
|
+
# The agent's {#label}.
|
432
|
+
#
|
433
|
+
# @param [Pathname] workdir:
|
434
|
+
# The agent's {#workdir}.
|
435
|
+
#
|
436
|
+
# @param [Hash<Symbol, #to_s>] **extras
|
437
|
+
# Subclass-specific extra keys and values to make available. Values will
|
438
|
+
# be converted to strings via `#to_s` before substitution.
|
439
|
+
#
|
440
|
+
# @return [String]
|
441
|
+
# Command string with substitutions made.
|
442
|
+
#
|
443
|
+
def self.render_cmd cmd_template:, label:, workdir:, **extras
|
444
|
+
t.match cmd_template,
|
445
|
+
Array, ->( array ) {
|
446
|
+
array.map {|arg|
|
447
|
+
render_cmd \
|
448
|
+
cmd_template: arg,
|
449
|
+
label: label,
|
450
|
+
workdir: workdir,
|
451
|
+
**extras
|
452
|
+
}.shelljoin
|
453
|
+
},
|
454
|
+
|
455
|
+
String, ->( string ) {
|
456
|
+
{
|
457
|
+
label: label,
|
458
|
+
workdir: workdir,
|
459
|
+
**extras,
|
460
|
+
}.reduce( string ) do |cmd, (key, value)|
|
461
|
+
cmd.gsub "{#{ key }}", value.to_s.shellescape
|
462
|
+
end
|
463
|
+
}
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
# Create the `launchd` property list data for a new {Locd::Agent}.
|
468
|
+
#
|
469
|
+
# @param [nil | String | Pathname] log_path:
|
470
|
+
# Optional path to log agent standard outputs to (combined `STDOUT` and
|
471
|
+
# `STDERR`).
|
472
|
+
#
|
473
|
+
# See {.resolve_log_path} for details on how the different types and
|
474
|
+
# values are treated.
|
475
|
+
#
|
476
|
+
# @return [Hash<String, Object>]
|
477
|
+
# {include:file:doc/include/plist.md}
|
478
|
+
#
|
479
|
+
def self.create_plist_data cmd_template:,
|
480
|
+
label:,
|
481
|
+
workdir:,
|
482
|
+
log_path: nil,
|
483
|
+
keep_alive: false,
|
484
|
+
run_at_load: false,
|
485
|
+
**extras
|
486
|
+
# Configure daemon variables...
|
487
|
+
|
488
|
+
# Normalize `workdir` to an expanded {Pathname}
|
489
|
+
workdir = workdir.to_pn.expand_path
|
490
|
+
|
491
|
+
# Resolve the log (`STDOUT` & `STDERR`) path
|
492
|
+
log_path = resolve_log_path(
|
493
|
+
log_path: log_path,
|
494
|
+
workdir: workdir,
|
495
|
+
label: label,
|
496
|
+
).to_s
|
497
|
+
|
498
|
+
# Interpolate variables into command template
|
499
|
+
cmd = render_cmd(
|
500
|
+
cmd_template: cmd_template,
|
501
|
+
label: label,
|
502
|
+
workdir: workdir,
|
503
|
+
**extras
|
504
|
+
)
|
505
|
+
|
506
|
+
# Form the property list hash
|
507
|
+
{
|
508
|
+
# Unique label, format: `locd.<owner>.<name>.<agent.path...>`
|
509
|
+
'Label' => label,
|
510
|
+
|
511
|
+
# What to run
|
512
|
+
'ProgramArguments' => [
|
513
|
+
# TODO Maybe this should be configurable or smarter in some way?
|
514
|
+
'bash',
|
515
|
+
# *login* shell... need this to source the user profile and set up
|
516
|
+
# all the ENV vars
|
517
|
+
'-l',
|
518
|
+
# Run the command in the login shell
|
519
|
+
'-c', cmd,
|
520
|
+
],
|
521
|
+
|
522
|
+
# Directory to run the command in
|
523
|
+
'WorkingDirectory' => workdir.to_s,
|
524
|
+
|
525
|
+
# Where to send STDOUT
|
526
|
+
'StandardOutPath' => log_path,
|
527
|
+
|
528
|
+
# Where to send STDERR (we send both to the same file)
|
529
|
+
'StandardErrorPath' => log_path,
|
530
|
+
|
531
|
+
# Bring the process back up if it goes down (has backoff and stuff
|
532
|
+
# built-in)
|
533
|
+
'KeepAlive' => keep_alive,
|
534
|
+
|
535
|
+
# Start the process when the plist is loaded
|
536
|
+
'RunAtLoad' => run_at_load,
|
537
|
+
|
538
|
+
'ProcessType' => 'Interactive',
|
539
|
+
|
540
|
+
# Extras we need... `launchd` complains in the system log about this
|
541
|
+
# but it's the easiest way to handle it at the moment
|
542
|
+
CONFIG_KEY => {
|
543
|
+
# Save this too why the hell not, might help debuging at least
|
544
|
+
cmd_template: cmd_template,
|
545
|
+
|
546
|
+
# Add subclass-specific extras
|
547
|
+
**extras,
|
548
|
+
}.str_keys,
|
549
|
+
|
550
|
+
# Stuff that *doesn't* work... so you don't try it again, because
|
551
|
+
# Apple's online docs seems totally out of date.
|
552
|
+
#
|
553
|
+
# Not allowed for user agents
|
554
|
+
# 'UserName' => ENV['USER'],
|
555
|
+
#
|
556
|
+
# "The Debug key is no longer respected. Please remove it."
|
557
|
+
# 'Debug' => true,
|
558
|
+
#
|
559
|
+
# Yeah, it would have been nice to just use the plist to store the port,
|
560
|
+
# but this runs into all sorts of impenetrable security mess... gotta
|
561
|
+
# put it somewhere else! Weirdly enough it just totally works outside
|
562
|
+
# of here, so I'm not what the security is really stopping..?
|
563
|
+
#
|
564
|
+
# 'Sockets' => {
|
565
|
+
# 'Listeners' => {
|
566
|
+
# 'SockNodeName' => BIND,
|
567
|
+
# 'SockServiceName' => port,
|
568
|
+
# 'SockType' => 'stream',
|
569
|
+
# 'SockFamily' => 'IPv4',
|
570
|
+
# },
|
571
|
+
# },
|
572
|
+
}.reject { |key, value| value.nil? } # Drop any `nil` values
|
573
|
+
end
|
574
|
+
|
575
|
+
|
576
|
+
# Add an agent, writing a `.plist` to `~/Library/LaunchAgents`.
|
577
|
+
#
|
578
|
+
# Does not start the agent.
|
579
|
+
#
|
580
|
+
# @param [String] label:
|
581
|
+
# The agent's label, which is its:
|
582
|
+
#
|
583
|
+
# 1. Unique identifier in launchd
|
584
|
+
# 2. Domain via the proxy.
|
585
|
+
# 3. Property list filename (plus the `.plist` extension).
|
586
|
+
#
|
587
|
+
# @param [Boolean] force:
|
588
|
+
# Overwrite any existing agent with the same label.
|
589
|
+
#
|
590
|
+
# @param [String | Pathname] workdir:
|
591
|
+
# Working directory for the agent.
|
592
|
+
#
|
593
|
+
# @param [Hash<Symbol, Object>] **kwds
|
594
|
+
# Additional keyword arguments to pass to {.create_plist_data}.
|
595
|
+
#
|
596
|
+
# @return [Locd::Agent]
|
597
|
+
# The new agent.
|
598
|
+
#
|
599
|
+
def self.add label:,
|
600
|
+
force: false,
|
601
|
+
workdir: Pathname.getwd,
|
602
|
+
**kwds
|
603
|
+
logger.debug "Creating {Agent}...",
|
604
|
+
label: label,
|
605
|
+
force: force,
|
606
|
+
workdir: workdir,
|
607
|
+
**kwds
|
608
|
+
|
609
|
+
plist_abs_path = self.plist_abs_path label
|
610
|
+
|
611
|
+
# Handle file already existing
|
612
|
+
if File.exists? plist_abs_path
|
613
|
+
logger.debug "Agent already exists!",
|
614
|
+
label: label,
|
615
|
+
path: plist_abs_path
|
616
|
+
|
617
|
+
if force
|
618
|
+
logger.info "Forcing agent creation (overwrite)",
|
619
|
+
label: label,
|
620
|
+
path: plist_abs_path
|
621
|
+
else
|
622
|
+
raise binding.erb <<~END
|
623
|
+
Agent <%= label %> already exists at:
|
624
|
+
|
625
|
+
<%= plist_abs_path %>
|
626
|
+
|
627
|
+
END
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
plist_data = create_plist_data label: label, workdir: workdir, **kwds
|
632
|
+
logger.debug "Property list created", data: plist_data
|
633
|
+
|
634
|
+
plist_string = Plist::Emit.dump plist_data
|
635
|
+
plist_abs_path.write plist_string
|
636
|
+
logger.debug "Property list written", path: plist_abs_path
|
637
|
+
|
638
|
+
from_path( plist_abs_path ).tap { |agent|
|
639
|
+
logger.debug { ["{Agent} added", agent: agent] }
|
640
|
+
}
|
641
|
+
end # .add
|
642
|
+
|
643
|
+
|
644
|
+
# @!group Removing Agents
|
645
|
+
# ----------------------------------------------------------------------------
|
646
|
+
|
647
|
+
# Find an agent using a label pattern and call {#remove} on it.
|
648
|
+
#
|
649
|
+
# @param (see .find_only!)
|
650
|
+
# @return (see #remove)
|
651
|
+
# @raise See {.find_only!} and {#remove}
|
652
|
+
#
|
653
|
+
def self.remove *find_only_args
|
654
|
+
Locd::Agent.find_only!( *find_only_args ).remove
|
655
|
+
end # .remove
|
656
|
+
|
657
|
+
# @!endgroup
|
658
|
+
|
659
|
+
|
660
|
+
# Attributes
|
661
|
+
# ============================================================================
|
662
|
+
|
663
|
+
# Hash of the agent's `.plist` file (keys and values from the top-level
|
664
|
+
# `<dict>` element).
|
665
|
+
#
|
666
|
+
# @return [Hash<String, V>]
|
667
|
+
# Check out the [plist][] gem for an idea of what types `V` may assume.
|
668
|
+
#
|
669
|
+
# [plist]: http://www.rubydoc.info/gems/plist
|
670
|
+
#
|
671
|
+
attr_reader :plist
|
672
|
+
|
673
|
+
|
674
|
+
# Absolute path to the agent's `.plist` file.
|
675
|
+
#
|
676
|
+
# @return [Pathname]
|
677
|
+
#
|
678
|
+
attr_reader :path
|
679
|
+
|
680
|
+
|
681
|
+
# Constructor
|
682
|
+
# ============================================================================
|
683
|
+
|
684
|
+
def initialize plist:, path:
|
685
|
+
@path = path.to_pn.expand_path
|
686
|
+
@plist = plist
|
687
|
+
@status = :UNKNOWN
|
688
|
+
|
689
|
+
|
690
|
+
# Sanity check...
|
691
|
+
|
692
|
+
unless plist.key? CONFIG_KEY
|
693
|
+
raise ArgumentError.new binding.erb <<~END
|
694
|
+
Not a Loc'd plist (no <%= Locd::Agent::CONFIG_KEY %> key)
|
695
|
+
|
696
|
+
path: <%= path %>
|
697
|
+
plist:
|
698
|
+
|
699
|
+
<%= plist.pretty_inspect %>
|
700
|
+
|
701
|
+
END
|
702
|
+
end
|
703
|
+
|
704
|
+
unless @path.basename( '.plist' ).to_s == label
|
705
|
+
raise ArgumentError.new binding.erb <<~END
|
706
|
+
Label and filename don't match.
|
707
|
+
|
708
|
+
Filename should be `<label>.plist`, found
|
709
|
+
|
710
|
+
label: <%= label %>
|
711
|
+
filename: <%= @path.basename %>
|
712
|
+
path: <%= path %>
|
713
|
+
|
714
|
+
END
|
715
|
+
end
|
716
|
+
|
717
|
+
init_ensure_out_dirs_exist
|
718
|
+
end
|
719
|
+
|
720
|
+
|
721
|
+
# Instance Methods
|
722
|
+
# ============================================================================
|
723
|
+
|
724
|
+
# @return [Boolean]
|
725
|
+
# `true` if the {#log_path} is the default one we generate.
|
726
|
+
def default_log_path?
|
727
|
+
log_path == self.class.default_log_path( workdir, label )
|
728
|
+
end
|
729
|
+
|
730
|
+
|
731
|
+
# @!group Instance Methods: Attribute Readers
|
732
|
+
# ----------------------------------------------------------------------------
|
733
|
+
#
|
734
|
+
# Methods to read proxied and computed attributes.
|
735
|
+
#
|
736
|
+
|
737
|
+
# @return [String]
|
738
|
+
#
|
739
|
+
def label
|
740
|
+
plist['Label'].freeze
|
741
|
+
end
|
742
|
+
|
743
|
+
|
744
|
+
# Current process ID of the agent (if running).
|
745
|
+
#
|
746
|
+
# @param (see #status)
|
747
|
+
#
|
748
|
+
# @return [nil]
|
749
|
+
# No process ID (not running).
|
750
|
+
#
|
751
|
+
# @return [Fixnum]
|
752
|
+
# Process ID.
|
753
|
+
#
|
754
|
+
def pid refresh: false
|
755
|
+
status( refresh: refresh ) && status[:pid]
|
756
|
+
end
|
757
|
+
|
758
|
+
|
759
|
+
def running? refresh: false
|
760
|
+
status( refresh: refresh )[:running]
|
761
|
+
end
|
762
|
+
|
763
|
+
|
764
|
+
def stopped? refresh: false
|
765
|
+
!running?( refresh: refresh )
|
766
|
+
end
|
767
|
+
|
768
|
+
#
|
769
|
+
#
|
770
|
+
# @return [nil]
|
771
|
+
# No last exit code information.
|
772
|
+
#
|
773
|
+
# @return [Fixnum]
|
774
|
+
# Last exit code of process.
|
775
|
+
#
|
776
|
+
def last_exit_code refresh: false
|
777
|
+
status( refresh: refresh ) && status[:last_exit_code]
|
778
|
+
end
|
779
|
+
|
780
|
+
|
781
|
+
def config
|
782
|
+
plist[CONFIG_KEY]
|
783
|
+
end
|
784
|
+
|
785
|
+
|
786
|
+
def cmd_template
|
787
|
+
config['cmd_template']
|
788
|
+
end
|
789
|
+
|
790
|
+
|
791
|
+
# @return [Pathname]
|
792
|
+
# The working directory of the agent.
|
793
|
+
def workdir
|
794
|
+
plist['WorkingDirectory'].to_pn
|
795
|
+
end
|
796
|
+
|
797
|
+
|
798
|
+
# Path the agent is logging `STDOUT` to.
|
799
|
+
#
|
800
|
+
# @return [Pathname]
|
801
|
+
# If the agent is logging `STDOUT` to a file path.
|
802
|
+
#
|
803
|
+
# @return [nil]
|
804
|
+
# If the agent is not logging `STDOUT`.
|
805
|
+
#
|
806
|
+
def out_path
|
807
|
+
plist['StandardOutPath'].to_pn if plist['StandardOutPath']
|
808
|
+
end
|
809
|
+
|
810
|
+
alias_method :log_path, :out_path
|
811
|
+
|
812
|
+
|
813
|
+
# Path the agent is logging `STDERR` to.
|
814
|
+
#
|
815
|
+
# @return [Pathname]
|
816
|
+
# If the agent is logging `STDERR` to a file path.
|
817
|
+
#
|
818
|
+
# @return [nil]
|
819
|
+
# If the agent is not logging `STDERR`.
|
820
|
+
#
|
821
|
+
def err_path
|
822
|
+
plist['StandardErrorPath'].to_pn if plist['StandardErrorPath']
|
823
|
+
end
|
824
|
+
|
825
|
+
# @!endgroup Instance Methods: Attribute Readers
|
826
|
+
|
827
|
+
|
828
|
+
# @!group Instance Methods: `launchctl` Interface
|
829
|
+
# ----------------------------------------------------------------------------
|
830
|
+
|
831
|
+
# The agent's status from parsing `launchctl list`.
|
832
|
+
#
|
833
|
+
# Status is read on demand and cached on the instance.
|
834
|
+
#
|
835
|
+
# @param [Boolean] refresh:
|
836
|
+
# When `true`, will re-read from `launchd` (and cache results)
|
837
|
+
# before returning.
|
838
|
+
#
|
839
|
+
# @return [Hash{pid: (Fixnum | nil), status: (Fixnum | nil)}]
|
840
|
+
# When `launchd` has a status entry for the agent.
|
841
|
+
#
|
842
|
+
# @return [nil]
|
843
|
+
# When `launchd` doesn't have a status entry for the agent.
|
844
|
+
#
|
845
|
+
def status refresh: false
|
846
|
+
if refresh || @status == :UNKNOWN
|
847
|
+
raw_status = Locd::Launchctl.status[label]
|
848
|
+
|
849
|
+
# Happens when the agent is not loaded
|
850
|
+
@status = if raw_status.nil?
|
851
|
+
{
|
852
|
+
loaded: false,
|
853
|
+
running: false,
|
854
|
+
pid: nil,
|
855
|
+
last_exit_code: nil,
|
856
|
+
}
|
857
|
+
else
|
858
|
+
{
|
859
|
+
loaded: true,
|
860
|
+
running: !raw_status[:pid].nil?,
|
861
|
+
pid: raw_status[:pid],
|
862
|
+
last_exit_code: raw_status[:status],
|
863
|
+
}
|
864
|
+
end
|
865
|
+
end
|
866
|
+
|
867
|
+
@status
|
868
|
+
end
|
869
|
+
|
870
|
+
|
871
|
+
# Load the agent by executing `launchctl load [OPTIONS] LABEL`.
|
872
|
+
#
|
873
|
+
# This is a bit low-level; you probably want to use {#start}.
|
874
|
+
#
|
875
|
+
# @param [Boolean] force:
|
876
|
+
# Force the loading of the plist. Ignore the `launchd` *Disabled* key.
|
877
|
+
#
|
878
|
+
# @param [Boolean] write:
|
879
|
+
# Overrides the `launchd` *Disabled* key and sets it to `false`.
|
880
|
+
#
|
881
|
+
# @return [self]
|
882
|
+
#
|
883
|
+
def load force: false, write: false
|
884
|
+
logger.debug "Loading #{ label } agent...", force: force, write: write
|
885
|
+
|
886
|
+
result = Locd::Launchctl.load! path, force: force, write: write
|
887
|
+
|
888
|
+
message = if result.err =~ /service\ already\ loaded/
|
889
|
+
"already loaded"
|
890
|
+
else
|
891
|
+
"LOADED"
|
892
|
+
end
|
893
|
+
|
894
|
+
status_info message, status: status
|
895
|
+
|
896
|
+
self
|
897
|
+
end
|
898
|
+
|
899
|
+
|
900
|
+
# Unload the agent by executing `launchctl unload [OPTIONS] LABEL`.
|
901
|
+
#
|
902
|
+
# This is a bit low-level; you probably want to use {#stop}.
|
903
|
+
#
|
904
|
+
# @param [Boolean] write:
|
905
|
+
# Overrides the `launchd` *Disabled* key and sets it to `true`.
|
906
|
+
#
|
907
|
+
# @return [self]
|
908
|
+
#
|
909
|
+
def unload write: false
|
910
|
+
logger.debug "Unloading #{ label } agent...", write: write
|
911
|
+
|
912
|
+
result = Locd::Launchctl.unload! path, write: write
|
913
|
+
|
914
|
+
status_info "UNLOADED"
|
915
|
+
|
916
|
+
self
|
917
|
+
end
|
918
|
+
|
919
|
+
|
920
|
+
# {#unload} then {#load} the agent.
|
921
|
+
#
|
922
|
+
# @param (see #load)
|
923
|
+
# @return (see #load)
|
924
|
+
#
|
925
|
+
def reload force: false, write: false
|
926
|
+
unload
|
927
|
+
load force: force, write: write
|
928
|
+
end
|
929
|
+
|
930
|
+
|
931
|
+
# Start the agent.
|
932
|
+
#
|
933
|
+
# If `load` is `true`, calls {#load} first, and defaults it's `force` keyword
|
934
|
+
# argument to `true` (the idea being that you actually want the agent to
|
935
|
+
# start, even if it's {#disabled?}).
|
936
|
+
#
|
937
|
+
# @param [Boolean] load:
|
938
|
+
# Call {#load} first, passing it `write` and `force`.
|
939
|
+
#
|
940
|
+
# @param force (see #load)
|
941
|
+
# @param write (see #load)
|
942
|
+
#
|
943
|
+
# @return [self]
|
944
|
+
#
|
945
|
+
def start load: true, force: true, write: false
|
946
|
+
logger.trace "Starting `#{ label }` agent...",
|
947
|
+
load: load,
|
948
|
+
force: force,
|
949
|
+
write: write
|
950
|
+
|
951
|
+
self.load( force: force, write: write ) if load
|
952
|
+
|
953
|
+
Locd::Launchctl.start! label
|
954
|
+
status_info "STARTED"
|
955
|
+
|
956
|
+
self
|
957
|
+
end
|
958
|
+
|
959
|
+
|
960
|
+
# Stop the agent.
|
961
|
+
#
|
962
|
+
# @param [Boolean] unload:
|
963
|
+
# Call {#unload} first, passing it `write`.
|
964
|
+
#
|
965
|
+
# @param write (see #unload)
|
966
|
+
#
|
967
|
+
# @return [self]
|
968
|
+
#
|
969
|
+
def stop unload: true, write: false
|
970
|
+
logger.debug "Stopping `#{ label } agent...`",
|
971
|
+
unload: unload,
|
972
|
+
write: write
|
973
|
+
|
974
|
+
Locd::Launchctl.stop label
|
975
|
+
status_info "STOPPED"
|
976
|
+
|
977
|
+
self.unload( write: write ) if unload
|
978
|
+
|
979
|
+
self
|
980
|
+
end
|
981
|
+
|
982
|
+
|
983
|
+
# Restart the agent ({#stop} then {#start}).
|
984
|
+
#
|
985
|
+
# @param unload (see #stop)
|
986
|
+
# @param force (see #start)
|
987
|
+
# @param write (see #start)
|
988
|
+
#
|
989
|
+
# @return [self]
|
990
|
+
#
|
991
|
+
def restart unload: true, force: true, write: false
|
992
|
+
stop unload: unload
|
993
|
+
start load: unload, force: force, write: write
|
994
|
+
end # #restart
|
995
|
+
|
996
|
+
|
997
|
+
# Remove the agent by removing it's {#path} file. Will {#stop} and
|
998
|
+
# {#unloads} the agent first.
|
999
|
+
#
|
1000
|
+
# @return [self]
|
1001
|
+
#
|
1002
|
+
def remove
|
1003
|
+
stop unload: true
|
1004
|
+
|
1005
|
+
FileUtils.rm path
|
1006
|
+
status_info "REMOVED"
|
1007
|
+
|
1008
|
+
self
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
# @!endgroup Instance Methods: `launchctl` Interface
|
1012
|
+
|
1013
|
+
|
1014
|
+
# Update specific values on the agent, which *may* change it's file path if
|
1015
|
+
# a different label is provided.
|
1016
|
+
#
|
1017
|
+
# **_Does not mutate this instance! Returns a new {Locd::Agent} with the
|
1018
|
+
# updated values._**
|
1019
|
+
#
|
1020
|
+
# @param (see .create_plist_data)
|
1021
|
+
#
|
1022
|
+
# @return [Locd::Agent]
|
1023
|
+
# A new instance with the updated values.
|
1024
|
+
#
|
1025
|
+
def update **values
|
1026
|
+
logger.trace "Updating `#{ label }` agent", **values
|
1027
|
+
|
1028
|
+
# Remove the `cmd_template` if it's nil of an empty array so that
|
1029
|
+
# we use the current one
|
1030
|
+
if values[:cmd_template].nil? ||
|
1031
|
+
(values[:cmd_template].is_a?( Array ) && values[:cmd_template].empty?)
|
1032
|
+
values.delete :cmd_template
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
# Make a new plist
|
1036
|
+
new_plist_data = self.class.create_plist_data(
|
1037
|
+
label: label,
|
1038
|
+
workdir: workdir,
|
1039
|
+
|
1040
|
+
log_path: (
|
1041
|
+
values.key?( :log_path ) ? values.key?( :log_path ) : (
|
1042
|
+
default_log_path? ? nil : log_path
|
1043
|
+
)
|
1044
|
+
),
|
1045
|
+
|
1046
|
+
# Include the config values, which have the `cmd_template` as well as
|
1047
|
+
# any extras. Need to symbolize the keys to make the kwds call work
|
1048
|
+
**config.sym_keys,
|
1049
|
+
|
1050
|
+
# Now merge over with the values we received
|
1051
|
+
**values.except( :log_path )
|
1052
|
+
)
|
1053
|
+
|
1054
|
+
new_label = new_plist_data['Label']
|
1055
|
+
new_plist_abs_path = self.class.plist_abs_path new_label
|
1056
|
+
|
1057
|
+
if new_label == label
|
1058
|
+
# We maintained the same label, overwrite the file
|
1059
|
+
path.write Plist::Emit.dump( new_plist_data )
|
1060
|
+
|
1061
|
+
# Load up the new agent from the same path, reload and return it
|
1062
|
+
self.class.from_path( path ).reload
|
1063
|
+
|
1064
|
+
else
|
1065
|
+
# The label has changed, so we want to make sure we're not overwriting
|
1066
|
+
# another agent
|
1067
|
+
|
1068
|
+
if File.exists? new_plist_abs_path
|
1069
|
+
# There's someone already there! Bail out
|
1070
|
+
raise binding.erb <<-END
|
1071
|
+
A different agent already exists at:
|
1072
|
+
|
1073
|
+
<%= new_plist_abs_path %>
|
1074
|
+
|
1075
|
+
Remove that agent first if you really want to replace it with an
|
1076
|
+
updated version of this one.
|
1077
|
+
|
1078
|
+
END
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
# Ok, we're in the clear (save for the obvious race condition, but,
|
1082
|
+
# hey, it's a development tool, so fuck it... it's not even clear it's
|
1083
|
+
# possible to do an atomic file add from Ruby)
|
1084
|
+
new_plist_abs_path.write Plist::Emit.dump( new_plist_data )
|
1085
|
+
|
1086
|
+
# Remove this agent
|
1087
|
+
remove
|
1088
|
+
|
1089
|
+
# And instantiate and load a new agent from the new path
|
1090
|
+
self.class.from_path( new_plist_abs_path ).load
|
1091
|
+
|
1092
|
+
end
|
1093
|
+
end # #update
|
1094
|
+
|
1095
|
+
|
1096
|
+
|
1097
|
+
# @!group Instance Methods: Language Integration
|
1098
|
+
# ----------------------------------------------------------------------------
|
1099
|
+
|
1100
|
+
# Compare to another agent by their labels.
|
1101
|
+
#
|
1102
|
+
# @param [Locd::Agent] other
|
1103
|
+
# The other agent.
|
1104
|
+
#
|
1105
|
+
# @return [Fixnum]
|
1106
|
+
#
|
1107
|
+
def <=> other
|
1108
|
+
Locd::Label.compare label, other.label
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
|
1112
|
+
def to_h
|
1113
|
+
self.class::TO_H_NAMES.map { |name|
|
1114
|
+
[name, send( name )]
|
1115
|
+
}.to_h
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
|
1119
|
+
def to_json *args
|
1120
|
+
to_h.to_json *args
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
|
1124
|
+
# TODO Doesn't work!
|
1125
|
+
#
|
1126
|
+
def to_yaml *args
|
1127
|
+
to_h.to_yaml *args
|
1128
|
+
end
|
1129
|
+
|
1130
|
+
# @!endgroup Language Integration Instance Methods
|
1131
|
+
|
1132
|
+
|
1133
|
+
protected
|
1134
|
+
# ========================================================================
|
1135
|
+
|
1136
|
+
# Create directories for any output file if they don't already exist.
|
1137
|
+
#
|
1138
|
+
# We *need* to create the directories for the output files *before*
|
1139
|
+
# we ever try to start the agent because otherwise it seems that
|
1140
|
+
# `launchd` *will create them*, but it will *create them as `root`*, then
|
1141
|
+
# try to write to them *as the user*, fail, and crash the process with
|
1142
|
+
# a `78` exit code.
|
1143
|
+
#
|
1144
|
+
# How or why that makes sense to `launchd`, who knows.
|
1145
|
+
#
|
1146
|
+
# But, since it's nice to be able to start the agents through `launchctl`
|
1147
|
+
# or `lunchy`, and having a caveat that *they need to be started through
|
1148
|
+
# Loc'd the first time* sucks, during initialization seems like a
|
1149
|
+
# reasonable time, at least for now (I don't like doing stuff like this
|
1150
|
+
# curing construction, but whatever for the moment, let's get it working).
|
1151
|
+
#
|
1152
|
+
# @return [nil]
|
1153
|
+
#
|
1154
|
+
def init_ensure_out_dirs_exist
|
1155
|
+
[out_path, err_path].compact.each do |path|
|
1156
|
+
dir = path.dirname
|
1157
|
+
unless dir.exist?
|
1158
|
+
logger.debug "Creating directory for output",
|
1159
|
+
label: label,
|
1160
|
+
dir: dir,
|
1161
|
+
path: path
|
1162
|
+
FileUtils.mkdir_p dir
|
1163
|
+
end
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
nil
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
|
1170
|
+
def status_info message, **details
|
1171
|
+
logger.info "Agent `#{ label }` #{ message }", **details
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
# end protected
|
1175
|
+
|
1176
|
+
end # class Locd::Launchd
|
1177
|
+
|
1178
|
+
|
1179
|
+
# Post-Processing
|
1180
|
+
# =======================================================================
|
1181
|
+
|
1182
|
+
require_relative './agent/site'
|
1183
|
+
require_relative './agent/job'
|
1184
|
+
require_relative './agent/system'
|
1185
|
+
require_relative './agent/proxy'
|
1186
|
+
require_relative './agent/rotate_logs'
|