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