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