locd 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
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