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,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Requirements
4
+ # =======================================================================
5
+
6
+ # Stdlib
7
+ # -----------------------------------------------------------------------
8
+ require 'shellwords'
9
+
10
+ # Deps
11
+ # -----------------------------------------------------------------------
12
+ require 'thor'
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
+ # Abstract base for CLI interface commands using the `thor` gem.
29
+ #
30
+ # @abstract
31
+ #
32
+ # @see http://whatisthor.com/
33
+ #
34
+ class Locd::CLI::Command::Base < Thor
35
+
36
+ # Helpers
37
+ # ============================================================================
38
+ #
39
+ protected
40
+
41
+ # Swap `$stdout` for a {StringIO}, call `block`, swap original `$stdout`
42
+ # back in and return the string contents.
43
+ #
44
+ # 'Cause we got that damn threaded logging going on, we want to write
45
+ # output all as one string using `$stdout.write`, but some stuff like
46
+ # {Thor::Shell::Basic#print_table} just write to {#stdout} (which resolves
47
+ # to `$stdout`) and don't offer an option to returned a formatted string
48
+ # instead.
49
+ #
50
+ # This seems like the simplest way to handle it, though it may still run
51
+ # into trouble with the threads, we shall see...
52
+ #
53
+ # @param [Proc] &block
54
+ # Block to run that writes to `$stdout`
55
+ #
56
+ # @return [String]
57
+ #
58
+ def capture_stdout &block
59
+ io = StringIO.new
60
+ stdout = $stdout
61
+ $stdout = io
62
+ block.call
63
+ $stdout = stdout
64
+ io.string
65
+ end
66
+
67
+
68
+ def agent_file agent
69
+ agent.path.to_s.sub( /\A#{ Regexp.escape( ENV['HOME'] ) }/, '~' )
70
+ end
71
+
72
+
73
+ def render_table table
74
+ string = capture_stdout do
75
+ print_table table.to_a
76
+ end
77
+
78
+ width = string.lines.map( &:length ).max
79
+
80
+ return (
81
+ string.lines[0] +
82
+ "-" * width + "\n" +
83
+ string.lines[1..-1].join +
84
+ "---\n" +
85
+ table.footer +
86
+ "\n\n"
87
+ )
88
+
89
+ string.lines.each_with_index.map { |line, index|
90
+ if index == 0
91
+ '# ' + line
92
+ else
93
+ ' ' + line
94
+ end
95
+ }.join
96
+ end
97
+
98
+
99
+ def render_text object
100
+ # Does it look like an array/list?
101
+ if NRSER.array_like? object
102
+ # It does! Convert it to an actual array to make life easier
103
+ array = object.to_a
104
+
105
+ return "(EMPTY)" if array.empty?
106
+
107
+ # Is it a list of agents?
108
+ if array.all? { |entry| entry.is_a? Locd::Agent }
109
+ # Ok, we want to display them. What mode are we in accoring to the
110
+ # options?
111
+ if options[:long]
112
+ # We're in long-mode, render a table
113
+ render_table agent_table( array )
114
+ else
115
+ # We're in regular mode, render each agent on it's own line by
116
+ # recurring
117
+ array.map( &method( __method__ ) ).join( "\n" )
118
+ end
119
+
120
+ # Is it a list of arrays?
121
+ elsif array.all? { |entry| entry.is_a? Array }
122
+ # It is, let's render that as a table
123
+ render_table message
124
+ else
125
+ # It's not, let's just render each entry on it's own line by
126
+ # recurring
127
+ message.map( &method( __method__ ) ).join( "\n" )
128
+ end
129
+
130
+ else
131
+ # It's not array-ish. Special-case {Locd::Agent} instances and render
132
+ # the rest as `#to_s`
133
+ case object
134
+ when Locd::Agent::Site
135
+ # TODO Want to add options for this, but for now just render agent
136
+ # URLs 'cause they have the label in them and are
137
+ # `cmd+click`-able in iTerm2 to open, which is most useful
138
+ object.url
139
+ when Locd::Agent
140
+ object.label
141
+ else
142
+ object.to_s
143
+ end
144
+ end
145
+ end # #render_text
146
+
147
+
148
+ def respond message, title: nil
149
+ formatted = if options[:json]
150
+ begin
151
+ JSON.pretty_generate message
152
+ rescue JSON::GeneratorError => error
153
+ logger.debug "Error generating 'pretty' JSON, falling back to dump",
154
+ error: error
155
+
156
+ JSON.dump message
157
+ end
158
+
159
+ elsif options[:yaml] || NRSER.hash_like?( message )
160
+ # TODO This sucks, but it's the easiest way to get "nice" YAML :/
161
+ YAML.dump JSON.load( JSON.dump message )
162
+
163
+ else
164
+ render_text message
165
+ end
166
+
167
+ if title
168
+ formatted = (
169
+ "# #{ title }\n#{ '#' * 78}\n#\n" + formatted
170
+ )
171
+ end
172
+
173
+ # Be puts-like
174
+ unless formatted[-1] == "\n"
175
+ formatted = formatted + "\n"
176
+ end
177
+
178
+ $stdout.write formatted
179
+ end # #respond
180
+
181
+
182
+ # Hook called from {Thor::Command#run} when an error occurs. Errors are
183
+ # not recoverable from my understanding, so this method should provide
184
+ # necessary feedback to the user and exit with an error code for errors
185
+ # it expects and re-raise those that it doesn't.
186
+ #
187
+ def on_run_error error, command, args
188
+ case error
189
+ when NRSER::CountError
190
+ if error.count == 0
191
+ logger.error "No results"
192
+ else
193
+ logger.error "Too many results:\n\n#{ render_text error.subject }\n"
194
+ end
195
+
196
+ # If the command supports `--all`, let them know that they can use it
197
+ if command.options[:all]
198
+ logger.info "You can use `--all` to to operate on ALL matches."
199
+ logger.info "See `locd help #{ command.name }`\n"
200
+ end
201
+
202
+ exit 1
203
+ else
204
+ raise error
205
+ end
206
+ end
207
+
208
+
209
+ # Find exactly one {Locd::Agent} for a `pattern`, using the any `:pattern`
210
+ # shared options provided, and raising if there are no matches or more
211
+ # than one.
212
+ #
213
+ # @param pattern (see Locd::Agent.find_only!)
214
+ #
215
+ # @return [Locd::Agent]
216
+ # Matched agent.
217
+ #
218
+ # @raise If more or less than one agent is matched.
219
+ #
220
+ def find_only! pattern
221
+ Locd::Agent.find_only! pattern, **option_kwds( groups: :pattern )
222
+ end
223
+
224
+
225
+ def find_multi! pattern
226
+ # Behavior depend on the `:all` option...
227
+ if options[:all]
228
+ # `:all` is set, so we find all the agents for the pattern, raising
229
+ # if we don't find any
230
+ Locd::Agent.find_all!(
231
+ pattern,
232
+ **option_kwds( groups: :pattern )
233
+ ).values
234
+ else
235
+ # `:all` is not set, so we need to find exactly one or error
236
+ [find_only!( pattern )]
237
+ end
238
+ end
239
+
240
+ # end protected
241
+ public
242
+
243
+ end # module Locd::CLI::Command::Base
@@ -0,0 +1,110 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+
5
+ # Refinements
6
+ # =======================================================================
7
+
8
+ using NRSER
9
+ using NRSER::Types
10
+
11
+
12
+ # Definitions
13
+ # =======================================================================
14
+
15
+ # TODO Doc me pls
16
+ #
17
+ class Locd::CLI::Command::Job < Locd::CLI::Command::Agent
18
+
19
+ # Helpers
20
+ # ============================================================================
21
+ #
22
+
23
+ def self.agent_class
24
+ Locd::Agent::Job
25
+ end
26
+
27
+ def self.parse_every_option value
28
+ case value
29
+ when /\A\d+\z/, /\A\d+s\z/
30
+ value.to_i
31
+ when /\A\d+m\z/
32
+ value[0...-1].to_i * 60
33
+ when /\A\d+h\z/
34
+ value[0...-1].to_i * 60 * 60
35
+ when /\A\d+d\z/
36
+ value[0...-1].to_i * 60 * 60 * 24
37
+ else
38
+ raise ArgumentError,
39
+ "Can't parse `every` option value: #{ value.inspect }"
40
+ end
41
+ end
42
+
43
+
44
+ protected
45
+ # ========================================================================
46
+
47
+ def agent_table agents
48
+ Locd::CLI::Table.build do |t|
49
+ t.col "PID", &:pid
50
+ t.col "LEC", desc: "Last Exit Code", &:last_exit_code
51
+ t.col "File" do |agent| agent_file agent end
52
+
53
+ t.rows agents
54
+ end
55
+ end
56
+
57
+ # end protected
58
+ public
59
+
60
+
61
+ # Write Commands
62
+ # ----------------------------------------------------------------------------
63
+
64
+ desc "add CMD_TEMPLATE...",
65
+ "Add job that runs in the current directory"
66
+ include_options groups: [:write, :add, :respond_with_agents]
67
+ option :every,
68
+ desc: "How often to start the job",
69
+ type: :string
70
+ option :at,
71
+ desc: "Day/time to start the job",
72
+ type: :hash
73
+ def add *cmd_template
74
+ logger.debug "#{ self.class.name }##{ __method__ }",
75
+ options: options,
76
+ cmd_template: cmd_template,
77
+ shared: self.class.shared_method_options
78
+
79
+ if options[:every] && options[:at]
80
+ logger.error "Don't supply both `every` and `at` options",
81
+ options: options
82
+ exit 1
83
+ end
84
+
85
+ start_interval = if options[:every]
86
+ self.class.parse_every_option options[:every]
87
+ elsif options[:at]
88
+ options[:at].map { |key, value|
89
+ [key.to_sym, value.to_i]
90
+ }.to_h
91
+ else
92
+ raise Thor::RequiredArgumentMissingError,
93
+ "Must provide `every` or `at` option"
94
+ end
95
+
96
+ logger.debug "Parsed start interval", start_interval: start_interval
97
+
98
+ job = agent_class.add \
99
+ cmd_template: cmd_template,
100
+ start_interval: start_interval,
101
+ **option_kwds( groups: :write )
102
+
103
+ logger.info "`#{ job.label }` job added."
104
+
105
+ job.load if options[:load]
106
+
107
+ respond job
108
+ end
109
+
110
+ end # class Locd::CLI::Command::Job
@@ -0,0 +1,201 @@
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::Main < Locd::CLI::Command::Base
31
+
32
+ # Constants
33
+ # ============================================================================
34
+
35
+ # Just the symbol log levels from {SemnaticLogger::LEVELS} converted to
36
+ # frozen strings
37
+ #
38
+ # @return [Array<String>]
39
+ #
40
+ LOG_LEVEL_STRINGS = SemanticLogger::LEVELS.map { |sym| sym.to_s.freeze }
41
+
42
+
43
+ # Mixins
44
+ # ============================================================================
45
+
46
+ # Dynamically add {.logger} and {#logger} methods.
47
+ #
48
+ # Needs to be in `no_commands` or Thor gets upset.
49
+ #
50
+ # no_commands { include SemanticLogger::Loggable }
51
+
52
+
53
+ # Global (Class-Level) Options
54
+ # ============================================================================
55
+ #
56
+ # Applicable to all commands.
57
+ #
58
+
59
+ class_option :log_level,
60
+ desc: "Set log level",
61
+ type: :string,
62
+ enum: LOG_LEVEL_STRINGS.
63
+ zip( LOG_LEVEL_STRINGS.map { |level| level[0] } ).
64
+ flatten,
65
+ default: ENV['LOCD_LOG_LEVEL']
66
+
67
+
68
+ class_option :debug,
69
+ desc: "Set log level to debug",
70
+ type: :boolean
71
+
72
+
73
+ class_option :trace,
74
+ desc: "Set log level to trace",
75
+ type: :boolean
76
+
77
+
78
+ class_option :verbose,
79
+ desc: "Turn on DEBUG-level logging",
80
+ aliases: '-v',
81
+ type: :boolean
82
+
83
+
84
+ class_option :json,
85
+ desc: "Output in JSON format (to STDOUT)",
86
+ type: :boolean
87
+
88
+
89
+ class_option :yaml,
90
+ desc: "Output in YAML format (to STDOUT)",
91
+ type: :boolean
92
+
93
+
94
+ # Construction
95
+ # ============================================================================
96
+
97
+ # Wrap {Thor#initialize} to call {#init_setup_logging} after letting `super`
98
+ # do it's thing so logging is setup before we do anything else.
99
+ #
100
+ def initialize args = [], local_options = {}, config = {}
101
+ super args, local_options, config
102
+
103
+ # If anything raises in here, the command seems to just silently exit..?
104
+ begin
105
+ init_setup_logging!
106
+ rescue Exception => e
107
+ $stderr.write \
108
+ "ERROR: #{ e.message } #{ e.class }\n#{ e.backtrace.join( "\n" ) }\n"
109
+ end
110
+
111
+ logger.debug "initialized",
112
+ args: args,
113
+ local_options: local_options,
114
+ options: self.options
115
+
116
+ end
117
+
118
+
119
+ # Helpers
120
+ # ============================================================================
121
+ #
122
+ protected
123
+
124
+ # Setup logging using {#options}.
125
+ #
126
+ def init_setup_logging!
127
+ kwds = {}
128
+
129
+ level = if options[:trace]
130
+ :trace
131
+ elsif options[:debug]
132
+ :debug
133
+ elsif options[:log_level]
134
+ LOG_LEVEL_STRINGS.find_only { |level|
135
+ level.start_with? options[:log_level]
136
+ }.to_sym
137
+ end
138
+
139
+ Locd::Logging.level = level unless level.nil?
140
+
141
+ if [:trace, :debug].include? Locd::Logging.level
142
+ logger.send Locd::Logging.level, "Hello! We about to start the show..."
143
+ end
144
+ end
145
+
146
+ # end protected
147
+ public
148
+
149
+
150
+ # Commands
151
+ # ============================================================================
152
+
153
+ # Querying
154
+ # ----------------------------------------------------------------------------
155
+
156
+ desc "version",
157
+ "Print the version"
158
+ def version
159
+ respond Locd::VERSION
160
+ end
161
+
162
+
163
+ # System
164
+ # ----------------------------------------------------------------------------
165
+
166
+
167
+ desc "config",
168
+ "Dump config"
169
+ def config
170
+ respond Locd.config.to_h
171
+ end
172
+
173
+
174
+ # Sub-Commands
175
+ # ============================================================================
176
+
177
+ desc 'site SUBCOMMAND...',
178
+ 'Deal with site agents.'
179
+ subcommand 'site',
180
+ Locd::CLI::Command::Site
181
+
182
+
183
+ desc 'job SUBCOMMAND...',
184
+ 'Deal with job agents.'
185
+ subcommand 'job',
186
+ Locd::CLI::Command::Job
187
+
188
+
189
+ desc 'proxy SUBCOMMAND...',
190
+ 'Deal with the HTTP proxy that routes requests to servers'
191
+ subcommand 'proxy',
192
+ Locd::CLI::Command::Proxy
193
+
194
+
195
+ desc 'rotate-logs SUBCOMMAND...',
196
+ 'Deal with the log rotation job'
197
+ # map :'rotate-logs' => :rotate_logs
198
+ subcommand 'rotate_logs',
199
+ Locd::CLI::Command::RotateLogs
200
+
201
+ end # class Locd::CLI::Command::Main