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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +108 -0
  3. data/.gitmodules +9 -0
  4. data/.qb-options.yml +4 -0
  5. data/.rspec +3 -0
  6. data/.travis.yml +5 -0
  7. data/.yardopts +7 -0
  8. data/Gemfile +10 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +72 -0
  11. data/Rakefile +6 -0
  12. data/VERSION +1 -0
  13. data/config/default.yml +24 -0
  14. data/doc/files/design/domains_and_labels.md +117 -0
  15. data/doc/files/topics/agents.md +16 -0
  16. data/doc/files/topics/launchd.md +15 -0
  17. data/doc/files/topics/plists.md +39 -0
  18. data/doc/include/plist.md +3 -0
  19. data/exe/locd +24 -0
  20. data/lib/locd.rb +75 -0
  21. data/lib/locd/agent.rb +1186 -0
  22. data/lib/locd/agent/job.rb +142 -0
  23. data/lib/locd/agent/proxy.rb +111 -0
  24. data/lib/locd/agent/rotate_logs.rb +82 -0
  25. data/lib/locd/agent/site.rb +174 -0
  26. data/lib/locd/agent/system.rb +270 -0
  27. data/lib/locd/cli.rb +4 -0
  28. data/lib/locd/cli/command.rb +9 -0
  29. data/lib/locd/cli/command/agent.rb +310 -0
  30. data/lib/locd/cli/command/base.rb +243 -0
  31. data/lib/locd/cli/command/job.rb +110 -0
  32. data/lib/locd/cli/command/main.rb +201 -0
  33. data/lib/locd/cli/command/proxy.rb +177 -0
  34. data/lib/locd/cli/command/rotate_logs.rb +152 -0
  35. data/lib/locd/cli/command/site.rb +47 -0
  36. data/lib/locd/cli/table.rb +157 -0
  37. data/lib/locd/config.rb +237 -0
  38. data/lib/locd/config/base.rb +93 -0
  39. data/lib/locd/errors.rb +65 -0
  40. data/lib/locd/label.rb +61 -0
  41. data/lib/locd/launchctl.rb +209 -0
  42. data/lib/locd/logging.rb +360 -0
  43. data/lib/locd/newsyslog.rb +402 -0
  44. data/lib/locd/pattern.rb +193 -0
  45. data/lib/locd/proxy.rb +272 -0
  46. data/lib/locd/proxymachine.rb +34 -0
  47. data/lib/locd/util.rb +49 -0
  48. data/lib/locd/version.rb +26 -0
  49. data/locd.gemspec +66 -0
  50. metadata +262 -0
@@ -0,0 +1,270 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Refinements
5
+ # =======================================================================
6
+
7
+ using NRSER
8
+ using NRSER::Types
9
+
10
+
11
+ # Definitions
12
+ # =======================================================================
13
+
14
+ # Mixin for common stuff that system agents do different.
15
+ #
16
+ # System agents are specific types of {Locd::Agent} built in to Loc'd to
17
+ # handle things like proxying HTTP request site ({Locd::Agent::Proxy}),
18
+ # rotating logs so they don't get unwieldily ({Locd::Agent::RotaeLogs}),
19
+ # and probably more in the future, like serving a site index, API, etc.
20
+ #
21
+ # System agents are singular - there's only one of each for a Loc'd
22
+ # label namespace, which is set via the configuration, and Loc'd uses
23
+ # that label namespace, together with the each system agent's `.label_name`,
24
+ # to form the agent's unique label, which is how it finds the system agents.
25
+ #
26
+ # This was really just to make it simpler by side-stepping how to handle
27
+ # many proxies and log rotators and such because besides the dev/release
28
+ # situation described above I can't really think of any reason you would want
29
+ # to create and manage multiple of each system agent.
30
+ #
31
+ # As a consequence, {Locd::Agent} subclasses that mix {Locd::Agent::System}
32
+ # in must define a `.label_name` class method.
33
+ #
34
+ module Locd::Agent::System
35
+
36
+ @@classes = Hamster::Set.new
37
+
38
+ def self.classes
39
+ @@classes
40
+ end
41
+
42
+
43
+ def self.label? label
44
+ label.is_a?( String ) \
45
+ && label.start_with?( Locd.config[:namespace, :label] )
46
+ end
47
+
48
+
49
+ # Is a plist for one of this config's system agents?
50
+ #
51
+ # @param plist (see Locd::Agent.plist?)
52
+ #
53
+ # @return [Boolean]
54
+ # `true` if the plist is for a system agent for this Loc'd config.
55
+ #
56
+ def self.plist? plist
57
+ !!(
58
+ plist.dig( Locd::Agent::CONFIG_KEY, 'is_system' ) \
59
+ && label?( plist['Label'] )
60
+ )
61
+ end # .plist?
62
+
63
+
64
+ # Find the concrete {Locd::Agent} subclass (that has mixed in
65
+ # {Locd::Agent::System}) for a property list.
66
+ #
67
+ # @param plist (see Locd::Agent.plist?)
68
+ #
69
+ # @return [Class<Locd::Agent>]
70
+ # If the plist is for one of {.classes}, returns that class.
71
+ #
72
+ # @return [nil]
73
+ # If the plist is:
74
+ #
75
+ # 1. Not a system plist (see {.plist?})
76
+ #
77
+ # 2. If none of {.classes} claim it (via their `.plist?` method).
78
+ #
79
+ # Though, really, this one shouldn't happen except in weird version
80
+ # switching situations maybe or something.
81
+ #
82
+ def self.class_for plist
83
+ return nil unless plist?( plist )
84
+
85
+ classes.to_a.find_bounded( max: 1 ) { |cls| cls.plist? plist }.first
86
+ end # .class_for
87
+
88
+
89
+ def self.included base
90
+ base.extend ClassMethods
91
+ @@classes = @@classes.add base
92
+ end
93
+
94
+
95
+ # Mixed in to classes themselves that include {Locd::Agent::System}.
96
+ #
97
+ module ClassMethods
98
+
99
+ def label
100
+ "#{ Locd.config[:namespace, :label] }.#{ label_name }"
101
+ end
102
+
103
+
104
+ # The property lists for system agents are identified by their unique
105
+ # label (unique to the label namespace).
106
+ #
107
+ # @param (see Locd::Agent.plist?)
108
+ # @return (see Locd::Agent.plist?)
109
+ #
110
+ def plist? plist
111
+ plist['Label'] == self.label
112
+ end
113
+
114
+
115
+ # Overridden as convenience, defaults the label to `.label`.
116
+ #
117
+ # @param (see Locd::Agent.plist_abs_path)
118
+ # @return (see Locd::Agent.plist_abs_path)
119
+ #
120
+ def plist_abs_path label = self.label
121
+ super label
122
+ end # .plist_abs_path
123
+
124
+
125
+ # By default system agent logs are placed in the Loc'd log directory,
126
+ # which is `<locd_home>/log` and named `<label>.log`.
127
+ #
128
+ # It does not depend on the `workdir` parameter at all - `workdir` is
129
+ # only included to conform to the {Locd::Agent.default_log_path} signature.
130
+ #
131
+ # @example
132
+ # # Considering
133
+ # Locd.config.log_dir
134
+ # # => #<Pathname:/Users/nrser/.locd/log>
135
+ #
136
+ # # then
137
+ # default_log_path _, 'com.nrser.locd.proxy'
138
+ # # => #<Pathname:/Users/nrser/.locd/log/com.nrser.locd.proxy.log>
139
+ #
140
+ # @see Locd::Config#log_dir
141
+ # @see Locd.config
142
+ # @see Locd::Agent.default_log_path
143
+ #
144
+ # @param [Pathname] workdir
145
+ # Not used.
146
+ #
147
+ # @param [String] label
148
+ # The agent's label.
149
+ #
150
+ # @return [Pathname]
151
+ # Absolute path to the log file.
152
+ #
153
+ def default_log_path workdir, label
154
+ Locd.config.log_dir / "#{ label }.log"
155
+ end
156
+
157
+
158
+ # @!group Querying
159
+ # --------------------------------------------------------------------------
160
+
161
+ # Get the agent.
162
+ #
163
+ # @return [Locd::Agent]
164
+ # If the agent exists.
165
+ #
166
+ # @return [nil]
167
+ # If the agent does not exist.
168
+ #
169
+ def get
170
+ super label
171
+ end
172
+
173
+ # @!endgroup
174
+
175
+
176
+ # @!group Creating the Agent
177
+ # --------------------------------------------------------------------------
178
+
179
+ # Wrapper that keyword arguments to `.add` and `.create_plist_data` are
180
+ # passed through before forwarding up to the super method.
181
+ #
182
+ # Provides default values and checks that necessary values like `label`
183
+ # and `is_system` are either not provided in the call or match the
184
+ # expected values.
185
+ #
186
+ # @param [String] bin:
187
+ # The executable that will be used by system agents to call Loc'd.
188
+ #
189
+ # @param workdir: (see Locd::Agent.add)
190
+ #
191
+ # @param **kwds
192
+ # Merged into the returned {Hash}.
193
+ #
194
+ # @raise [ArgumentError]
195
+ # If `kwds` contains bad values.
196
+ #
197
+ def default_write_kwds bin: Locd.config[:bin],
198
+ workdir: Locd.config.home_dir,
199
+ **kwds
200
+
201
+ if kwds.key?( :is_system ) && kwds[:is_system] != true
202
+ raise ArgumentError.new binding.erb <<~END
203
+ The `:is_system` keyword **must** be `true` for system agents.
204
+
205
+ It's how we recognize them!
206
+
207
+ Found
208
+
209
+ <%= kwds[:is_system] %>
210
+
211
+ END
212
+ end
213
+
214
+ if kwds.key?( :label ) && kwds[:label] != self.label
215
+ raise ArgumentError.new binding.erb <<~END
216
+ Can not customize system agent labels!
217
+
218
+ It **must** be `<%= self.label %>`, because that's how Loc'd finds it.
219
+
220
+ You can change the part before the `.proxy` via the
221
+
222
+ locd.namespace.label
223
+
224
+ configuration value. Create or edit the file at
225
+
226
+ <%= Locd.config.user_config_path %>
227
+
228
+ to define overrides (nested YAML hashes, deep merged).
229
+
230
+ END
231
+ end
232
+
233
+ {
234
+ bin: bin,
235
+ workdir: workdir,
236
+ **kwds,
237
+ is_system: true,
238
+ label: self.label,
239
+ }
240
+ end
241
+
242
+
243
+ # Wrapper that passes keywords though {#default_write_kwds} before
244
+ # calling the super method.
245
+ #
246
+ # @param (see Locd::Agent.create_plist_data)
247
+ # @return (see Locd::Agent.create_plist_data)
248
+ # @raise (see Locd::Agent.create_plist_data)
249
+ #
250
+ def create_plist_data **kwds
251
+ super **default_write_kwds( **kwds )
252
+ end
253
+
254
+
255
+ # Wrapper that passes keywords though {#default_write_kwds} before
256
+ # calling the super method.
257
+ #
258
+ # @param (see Locd::Agent.add)
259
+ # @return (see Locd::Agent.add)
260
+ # @raise (see Locd::Agent.add)
261
+ #
262
+ def add **kwds
263
+ super **default_write_kwds( **kwds )
264
+ end # .add
265
+
266
+ # @!endgroup Creating the Agent
267
+
268
+ end # module ClassMethods
269
+
270
+ end # module Locd::Agent::System
data/lib/locd/cli.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Locd::CLI; end
2
+
3
+ require_relative './cli/table'
4
+ require_relative './cli/command'
@@ -0,0 +1,9 @@
1
+ module Locd::CLI::Command; end
2
+
3
+ require_relative './command/base'
4
+ require_relative './command/agent'
5
+ require_relative './command/site'
6
+ require_relative './command/job'
7
+ require_relative './command/proxy'
8
+ require_relative './command/rotate_logs'
9
+ require_relative './command/main'
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requirements
4
+ # =======================================================================
5
+
6
+ # Stdlib
7
+ # -----------------------------------------------------------------------
8
+
9
+ # Deps
10
+ # -----------------------------------------------------------------------
11
+
12
+ # Project / Package
13
+ # -----------------------------------------------------------------------
14
+
15
+
16
+ # Refinements
17
+ # =======================================================================
18
+
19
+ using NRSER
20
+ using NRSER::Types
21
+
22
+
23
+ # Definitions
24
+ # =======================================================================
25
+
26
+ # CLI interface using the `thor` gem.
27
+ #
28
+ # @see http://whatisthor.com/
29
+ #
30
+ class Locd::CLI::Command::Agent < Locd::CLI::Command::Base
31
+
32
+ # Helpers
33
+ # ==========================================================================
34
+ #
35
+
36
+ def self.agent_class
37
+ Locd::Agent
38
+ end # .agent_class
39
+
40
+ def self.agent_type
41
+ agent_class.name.split( '::' ).last.downcase
42
+ end
43
+
44
+
45
+ protected
46
+ # ==========================================================================
47
+
48
+ def agent_class
49
+ self.class.agent_class
50
+ end
51
+
52
+
53
+ def agent_type
54
+ self.class.agent_type
55
+ end
56
+
57
+
58
+ def agent_table agents
59
+ Locd::CLI::Table.build do |t|
60
+ t.col "PID", &:pid
61
+ t.col "LEC", desc: "Last Exit Code", &:last_exit_code
62
+ t.col "Label", &:label
63
+ t.col "File" do |agent| agent_file agent end
64
+
65
+ t.rows agents
66
+ end
67
+ end
68
+
69
+
70
+ # Find exactly one {Locd::Agent} for a `pattern`, using the any `:pattern`
71
+ # shared options provided, and raising if there are no matches or more
72
+ # than one.
73
+ #
74
+ # @param pattern (see Locd::Agent.find_only!)
75
+ #
76
+ # @return [Locd::Agent]
77
+ # Matched agent.
78
+ #
79
+ # @raise If more or less than one agent is matched.
80
+ #
81
+ def find_only! pattern
82
+ agent_class.find_only! pattern, **option_kwds( groups: :pattern )
83
+ end
84
+
85
+
86
+ def find_multi! pattern
87
+ # Behavior depend on the `:all` option...
88
+ if options[:all]
89
+ # `:all` is set, so we find all the agents for the pattern, raising
90
+ # if we don't find any
91
+ agent_class.find_all!(
92
+ pattern,
93
+ **option_kwds( groups: :pattern )
94
+ ).values
95
+ else
96
+ # `:all` is not set, so we need to find exactly one or error
97
+ [find_only!( pattern )]
98
+ end
99
+ end
100
+
101
+ # end protected
102
+ public
103
+
104
+
105
+ # Shared Options
106
+ # ============================================================================
107
+
108
+ shared_option :long,
109
+ groups: :respond_with_agents,
110
+ desc: "Display agent details",
111
+ aliases: '-l',
112
+ type: :boolean
113
+
114
+ # `:pattern` Group
115
+ # ----------------------------------------------------------------------------
116
+ #
117
+ # Options when provided a `PATTERN` argument used to find agents by label.
118
+ #
119
+
120
+ shared_option :exact,
121
+ groups: :pattern,
122
+ desc: "Force PATTERN to match entire label string",
123
+ aliases: '-x',
124
+ type: :boolean
125
+
126
+ shared_option :recursive,
127
+ groups: :pattern,
128
+ desc: "Workdir patterns match all subdirs too",
129
+ aliases: '-r',
130
+ type: :boolean
131
+
132
+ shared_option :all,
133
+ groups: :multi,
134
+ desc: "Apply to ALL agents that PATTERN matches",
135
+ aliases: '-a',
136
+ type: :boolean
137
+
138
+
139
+ # `:write` Group
140
+ # ----------------------------------------------------------------------------
141
+ #
142
+ # Options when writing an agent `.plist` (`create`, `update`).
143
+ #
144
+
145
+ shared_option :label,
146
+ groups: :write,
147
+ desc: "Agent label, which is also the domain the proxy will serve it at",
148
+ aliases: ['--name', '-n'],
149
+ type: :string #,
150
+ # required: true
151
+
152
+ shared_option :workdir,
153
+ groups: :write,
154
+ desc: "Working directory for the agent's command",
155
+ aliases: ['--dir'],
156
+ type: :string
157
+
158
+ shared_option :log_path,
159
+ groups: :write,
160
+ desc: "Path to log agent's STDOUT and STDERR (combined)",
161
+ aliases: ['--log'],
162
+ type: :string
163
+
164
+
165
+ shared_option :force,
166
+ groups: :add,
167
+ desc: "Overwrite any existing agent",
168
+ aliases: '-f',
169
+ type: :boolean,
170
+ default: false
171
+
172
+
173
+ shared_option :load,
174
+ groups: :add,
175
+ desc: "Load the agent into `launchd`",
176
+ type: :boolean,
177
+ default: true
178
+
179
+
180
+ shared_option :unload,
181
+ groups: :start,
182
+ desc: "Unload the agent from `launchd` after stopping",
183
+ type: :boolean,
184
+ default: true
185
+
186
+ # Commands
187
+ # ============================================================================
188
+
189
+ # Querying
190
+ # ----------------------------------------------------------------------------
191
+
192
+ desc "ls [PATTERN]",
193
+ "List agents"
194
+ map list: :ls
195
+ include_options :long,
196
+ groups: :pattern
197
+ def ls pattern = nil
198
+ results = if pattern.nil?
199
+ agent_class.all
200
+ else
201
+ agent_class.list pattern, **option_kwds( groups: :pattern )
202
+ end
203
+
204
+ respond results.values.sort
205
+ end
206
+
207
+
208
+ # Write Commands
209
+ # ----------------------------------------------------------------------------
210
+
211
+ desc "add CMD_TEMPLATE...",
212
+ "Add agents that runs a command in the current directory"
213
+ include_options groups: [:write, :add, :respond_with_agents]
214
+ def add *cmd_template
215
+ agent = agent_class.add \
216
+ cmd_template: cmd_template,
217
+ **option_kwds( groups: :write )
218
+
219
+ logger.info "`#{ agent.label }` agent created."
220
+
221
+ agent.load if options[:load]
222
+
223
+ respond agent
224
+ end
225
+
226
+
227
+ desc "update PATTERN [OPTIONS] [-- CMD_TEMPLATE...]",
228
+ "Update an existing agent"
229
+ include_options groups: [:pattern, :write, :respond_with_agents]
230
+ def update pattern, *cmd_template
231
+ agent = find_only! pattern
232
+
233
+ new_agent = agent.update \
234
+ cmd_template: cmd_template,
235
+ **option_kwds( groups: :write )
236
+
237
+ logger.info "Agent `#{ agent.label }` updated"
238
+
239
+ respond agent
240
+ end
241
+
242
+
243
+ desc "open PATTERN",
244
+ "Open an agent's URL in the browser"
245
+ include_options groups: [:pattern, :multi]
246
+ def open pattern
247
+ find_multi!( pattern ).each do |agent|
248
+ Cmds! "open %s", agent.url
249
+ logger.info "Opened agent `#{ agent.label }` at #{ agent.url }"
250
+ end
251
+ end
252
+
253
+
254
+ desc "rm PATTERN",
255
+ "Remove (uninstall, delete) a agent"
256
+ map remove: :rm
257
+ include_options groups: [:pattern, :multi]
258
+ def rm pattern
259
+ agent_class.remove pattern, **option_kwds( :exact )
260
+ end
261
+
262
+
263
+ desc "plist PATTERN",
264
+ "Show an agent's launchd property list"
265
+ include_options groups: :pattern
266
+ def plist pattern
267
+ # TODO Why doesn't this use {#find_only!}?
268
+ agent = agent_class.find_only! pattern, **option_kwds( :exact )
269
+
270
+ if options[:json] || options[:yaml]
271
+ respond agent.plist
272
+ else
273
+ respond agent.path.read
274
+ end
275
+ end
276
+
277
+
278
+ desc "start PATTERN",
279
+ "Start an agent"
280
+ include_options groups: :pattern
281
+ option :write,
282
+ desc: "Set `launchd` *Disabled* key to `false`",
283
+ type: :boolean
284
+ def start pattern
285
+ find_only!( pattern ).start
286
+ end
287
+
288
+
289
+ desc "stop PATTERN",
290
+ "Stop an agent"
291
+ include_options groups: [:pattern, :stop]
292
+ option :write,
293
+ desc: "Set `launchd` *Disabled* key to `true`",
294
+ type: :boolean
295
+ def stop pattern
296
+ find_only!( pattern ).stop **option_kwds( :unload )
297
+ end
298
+
299
+
300
+ desc "restart PATTERN",
301
+ "Restart an agent"
302
+ include_options groups: [:pattern, :stop]
303
+ option :write,
304
+ desc: "Set `launchd` *Disabled* key to `false`",
305
+ type: :boolean
306
+ def restart pattern
307
+ find_only!( pattern ).restart
308
+ end
309
+
310
+ end # class Locd::CLI::Command::Agent