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,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