mongrel2 0.53.0 → 0.54.0

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.
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org/"
2
+ gemspec
@@ -22,7 +22,7 @@ module Mongrel2
22
22
  abort "\n\n>>> Mongrel2 requires Ruby 2.4 or later. <<<\n\n" if RUBY_VERSION < '2.4.0'
23
23
 
24
24
  # Library version constant
25
- VERSION = '0.53.0'
25
+ VERSION = '0.54.0'
26
26
 
27
27
  # Version-control revision constant
28
28
  REVISION = %q$Revision$
@@ -0,0 +1,496 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'gli'
5
+ require 'loggability'
6
+ require 'pastel'
7
+ require 'pathname'
8
+ require 'tty/prompt'
9
+ require 'tty/table'
10
+
11
+ require 'mongrel2' unless defined?( Mongrel2 )
12
+ require 'mongrel2/config'
13
+
14
+
15
+ # A tool for interacting with a Mongrel2 config database and server. This isn't
16
+ # quite a replacement for the real m2sh yet; here's what I have working so far:
17
+ #
18
+ # [√] load Load a config.
19
+ # [√] config Alias for load.
20
+ # [-] shell Starts an interactive shell.
21
+ # [√] access Prints the access log.
22
+ # [√] servers Lists the servers in a config database.
23
+ # [√] hosts Lists the hosts in a server.
24
+ # [√] routes Lists the routes in a host.
25
+ # [√] commit Adds a message to the log.
26
+ # [√] log Prints the commit log.
27
+ # [√] start Starts a server.
28
+ # [√] stop Stops a server.
29
+ # [√] reload Reloads a server.
30
+ # [√] running Tells you what's running.
31
+ # [-] control Connects to the control port.
32
+ # [√] version Prints the Mongrel2 and m2sh version.
33
+ # [√] help Get help, lists commands.
34
+ # [-] uuid Prints out a randomly generated UUID.
35
+ #
36
+ # I just use 'uuidgen' to generate uuids (which is all m2sh does, as
37
+ # well), so I don't plan to implement that. The 'control' command is more-easily
38
+ # accessed via pry+Mongrel2::Control, so I'm not going to implement that, either.
39
+ # Everything else should be analagous to (or better than) the m2sh that comes with
40
+ # mongrel2. I implemented the 'shell' mode, but I found I never used it, and it
41
+ # introduced a dependency on the Termios library, so I removed it.
42
+ #
43
+ module Mongrel2::CLI
44
+ extend Loggability,
45
+ GLI::App
46
+
47
+
48
+ # Write logs to Mongrel2's logger
49
+ log_to :mongrel2
50
+
51
+
52
+ #
53
+ # GLI
54
+ #
55
+
56
+ # Set up global[:description] and options
57
+ program_desc 'Mongrel2 Configurator'
58
+
59
+ # The command version
60
+ version Mongrel2::VERSION
61
+
62
+ # Use an OpenStruct for options instead of a Hash
63
+ use_openstruct( true )
64
+
65
+ # Subcommand options are independent of global[:ones]
66
+ subcommand_option_handling :normal
67
+
68
+ # Strict argument validation
69
+ arguments :strict
70
+
71
+
72
+ # Custom parameter types
73
+ accept Array do |value|
74
+ value.strip.split(/\s*,\s*/)
75
+ end
76
+ accept Pathname do |value|
77
+ Pathname( value.strip )
78
+ end
79
+
80
+
81
+ # Global options
82
+ desc 'Enable debugging output'
83
+ switch [:d, :debug]
84
+
85
+ desc 'Enable verbose output'
86
+ switch [:v, :verbose]
87
+
88
+ desc 'Set log level to LEVEL (one of %s)' % [Loggability::LOG_LEVELS.keys.join(', ')]
89
+ arg_name :LEVEL
90
+ flag [:l, :loglevel], must_match: Loggability::LOG_LEVELS.keys
91
+
92
+ desc "Don't actually do anything, just show what would happen."
93
+ switch [:n, 'dry-run']
94
+
95
+ desc "Additional Ruby libs to require before doing anything."
96
+ flag [:r, 'requires'], type: Array
97
+
98
+ desc "Specify the PATH of the config database to use."
99
+ arg_name :PATH
100
+ flag [:config, :C], default: Mongrel2::DEFAULT_CONFIG_URI
101
+
102
+ desc "Specify the REASON for an action for the event log."
103
+ arg_name :REASON
104
+ flag [:why], type: String
105
+
106
+
107
+ #
108
+ # GLI Event callbacks
109
+ #
110
+
111
+ # Set up global options
112
+ pre do |global, command, options, args|
113
+ self.set_logging_level( global[:l] )
114
+ Loggability.format_with( :color ) if $stdout.tty?
115
+
116
+ # Include a 'lib' directory if there is one
117
+ $LOAD_PATH.unshift( 'lib' ) if File.directory?( 'lib' )
118
+
119
+ self.require_additional_libs( global[:r] ) if global[:r]
120
+
121
+ self.setup_pastel_aliases
122
+ self.setup_output( global )
123
+
124
+ Mongrel2::Config.configure( configdb: global.config ) if global.config
125
+
126
+ true
127
+ end
128
+
129
+
130
+ # Write the error to the log on exceptions.
131
+ on_error do |exception|
132
+
133
+ case exception
134
+ when OptionParser::ParseError, GLI::CustomExit
135
+ msg = exception.full_message(highlight: false, order: :bottom)
136
+ self.log.debug( msg )
137
+ else
138
+ msg = exception.full_message(highlight: true, order: :bottom)
139
+ self.log.error( msg )
140
+ end
141
+
142
+ true
143
+ end
144
+
145
+
146
+
147
+
148
+ ##
149
+ # Registered subcommand modules
150
+ singleton_class.attr_accessor :subcommand_modules
151
+
152
+
153
+ ### Overridden -- Add registered subcommands immediately before running.
154
+ def self::run( * )
155
+ self.add_registered_subcommands
156
+ super
157
+ end
158
+
159
+
160
+ ### Add the specified +mod+ule containing subcommands to the 'mongrel2' command.
161
+ def self::register_subcommands( mod )
162
+ self.subcommand_modules ||= []
163
+ self.subcommand_modules.push( mod )
164
+ mod.extend( GLI::DSL, GLI::AppSupport, Loggability, CommandUtilities )
165
+ mod.log_to( :mongrel2 )
166
+ end
167
+
168
+
169
+ ### Add the commands from the registered subcommand modules.
170
+ def self::add_registered_subcommands
171
+ self.subcommand_modules ||= []
172
+ self.subcommand_modules.each do |mod|
173
+ merged_commands = mod.commands.merge( self.commands )
174
+ self.commands.update( merged_commands )
175
+ command_objs = self.commands_declaration_order | self.commands.values
176
+ self.commands_declaration_order.replace( command_objs )
177
+ end
178
+ end
179
+
180
+
181
+ ### Return the Pastel colorizer.
182
+ ###
183
+ def self::pastel
184
+ @pastel ||= Pastel.new( enabled: $stdout.tty? )
185
+ end
186
+
187
+
188
+ ### Return the TTY prompt used by the command to communicate with the
189
+ ### user.
190
+ def self::prompt
191
+ @prompt ||= TTY::Prompt.new( output: $stderr )
192
+ end
193
+
194
+
195
+ ### Discard the existing HighLine prompt object if one existed. Mostly useful for
196
+ ### testing.
197
+ def self::reset_prompt
198
+ @prompt = nil
199
+ end
200
+
201
+
202
+ ### Set the global logging +level+ if it's defined.
203
+ def self::set_logging_level( level=nil )
204
+ if level
205
+ Loggability.level = level.to_sym
206
+ else
207
+ Loggability.level = :fatal
208
+ end
209
+ end
210
+
211
+
212
+ ### Load any additional Ruby libraries given with the -r global option.
213
+ def self::require_additional_libs( requires)
214
+ requires.each do |path|
215
+ path = "mongrel2/#{path}" unless path.start_with?( 'mongrel2/' )
216
+ require( path )
217
+ end
218
+ end
219
+
220
+
221
+ ### Setup pastel color aliases
222
+ ###
223
+ def self::setup_pastel_aliases
224
+ self.pastel.alias_color( :headline, :bold, :white, :on_black )
225
+ self.pastel.alias_color( :header, :bold, :white )
226
+ self.pastel.alias_color( :success, :bold, :green )
227
+ self.pastel.alias_color( :error, :bold, :red )
228
+ self.pastel.alias_color( :key, :green )
229
+ self.pastel.alias_color( :even_row, :bold )
230
+ self.pastel.alias_color( :odd_row, :reset )
231
+ end
232
+
233
+
234
+ ### Set up the output levels and globals based on the associated +global+ options.
235
+ def self::setup_output( global )
236
+
237
+ # Turn on Ruby debugging and/or verbosity if specified
238
+ if global[:n]
239
+ $DRYRUN = true
240
+ Loggability.level = :warn
241
+ else
242
+ $DRYRUN = false
243
+ end
244
+
245
+ if global[:verbose]
246
+ $VERBOSE = true
247
+ Loggability.level = :info
248
+ end
249
+
250
+ if global[:debug]
251
+ $DEBUG = true
252
+ Loggability.level = :debug
253
+ end
254
+
255
+ if global[:loglevel]
256
+ Loggability.level = global[:loglevel]
257
+ end
258
+
259
+ end
260
+
261
+
262
+ #
263
+ # GLI subcommands
264
+ #
265
+
266
+
267
+ # Convenience module for subcommand registration syntax sugar.
268
+ module Subcommand
269
+
270
+ ### Extension callback -- register the extending object as a subcommand.
271
+ def self::extended( mod )
272
+ Mongrel2::CLI.log.debug "Registering subcommands from %p" % [ mod ]
273
+ Mongrel2::CLI.register_subcommands( mod )
274
+ end
275
+
276
+
277
+ ###############
278
+ module_function
279
+ ###############
280
+
281
+ ### Exit with the specified +exit_code+ after printing the given +message+.
282
+ def exit_now!( message, exit_code=1 )
283
+ raise GLI::CustomExit.new( message, exit_code )
284
+ end
285
+
286
+
287
+ ### Exit with a helpful +message+ and display the usage.
288
+ def help_now!( message=nil )
289
+ exception = OptionParser::ParseError.new( message )
290
+ def exception.exit_code; 64; end
291
+
292
+ raise exception
293
+ end
294
+
295
+
296
+ ### Get the prompt (a TTY::Prompt object)
297
+ def prompt
298
+ return Mongrel2::CLI.prompt
299
+ end
300
+
301
+
302
+ ### Return the global Pastel object for convenient formatting, color, etc.
303
+ def hl
304
+ return Mongrel2::CLI.pastel
305
+ end
306
+
307
+
308
+ ### Return the specified +string+ in the 'headline' ANSI color.
309
+ def headline_string( string )
310
+ return hl.headline( string )
311
+ end
312
+
313
+
314
+ ### Return the specified +string+ in the 'highlight' ANSI color.
315
+ def highlight_string( string )
316
+ return hl.highlight( string )
317
+ end
318
+
319
+
320
+ ### Return the specified +string+ in the 'success' ANSI color.
321
+ def success_string( string )
322
+ return hl.success( string )
323
+ end
324
+
325
+
326
+ ### Return the specified +string+ in the 'error' ANSI color.
327
+ def error_string( string )
328
+ return hl.error( string )
329
+ end
330
+
331
+
332
+ ### Output a table with the given +header+ (an array) and +rows+
333
+ ### (an array of arrays).
334
+ def display_table( header, rows )
335
+ table = TTY::Table.new( header, rows )
336
+ renderer = nil
337
+
338
+ if hl.enabled?
339
+ renderer = TTY::Table::Renderer::Unicode.new(
340
+ table,
341
+ multiline: true,
342
+ padding: [0,1,0,1]
343
+ )
344
+ renderer.border.style = :dim
345
+
346
+ else
347
+ renderer = TTY::Table::Renderer::ASCII.new(
348
+ table,
349
+ multiline: true,
350
+ padding: [0,1,0,1]
351
+ )
352
+ end
353
+
354
+ puts renderer.render
355
+ end
356
+
357
+
358
+ ### Return the count of visible (i.e., non-control) characters in the given +string+.
359
+ def visible_chars( string )
360
+ return string.to_s.gsub(/\e\[.*?m/, '').scan( /\P{Cntrl}/ ).size
361
+ end
362
+
363
+
364
+ ### In dry-run mode, output the description instead of running the provided block and
365
+ ### return the +return_value+.
366
+ ### If dry-run mode is not enabled, yield to the block.
367
+ def unless_dryrun( description, return_value=true )
368
+ if $DRYRUN
369
+ self.log.warn( "DRYRUN> #{description}" )
370
+ return return_value
371
+ else
372
+ return yield
373
+ end
374
+ end
375
+ alias_method :unless_dry_run, :unless_dryrun
376
+
377
+ end # module Subcommand
378
+
379
+
380
+ # Functions for common command tasks
381
+ module CommandUtilities
382
+
383
+ ### Search the current mongrel2 config for a server matching +serverspec+ and
384
+ ### return it as a Mongrel2::Config::Server object.
385
+ def find_server( serverspec=nil )
386
+ server = nil
387
+ servers = Mongrel2::Config.servers
388
+
389
+ raise "No servers are configured." if servers.empty?
390
+
391
+ # If there's only one configured server, just make sure if a serverspec was given
392
+ # that it would have matched.
393
+ if servers.length == 1
394
+ server = servers.first if !serverspec ||
395
+ servers.first.values.values_at( :uuid, :default_host, :name ).include?( serverspec )
396
+
397
+ # Otherwise, require an argument and search for the desired server if there is one
398
+ else
399
+ raise "You must specify a server uuid/hostname/name when more " +
400
+ "than one server is configured." if servers.length > 1 && !serverspec
401
+
402
+ server = servers.find {|s| s.uuid == serverspec } ||
403
+ servers.find {|s| s.default_host == serverspec } ||
404
+ servers.find {|s| s.name == serverspec }
405
+ end
406
+
407
+ raise "No servers match '#{serverspec}'" unless server
408
+
409
+ return server
410
+ end
411
+
412
+
413
+ ### Read command line history from HISTORY_FILE
414
+ def read_history
415
+ histfile = HISTORY_FILE.expand_path
416
+
417
+ if histfile.exist?
418
+ lines = histfile.readlines.collect {|line| line.chomp }
419
+ self.log.debug "Read %d saved history commands from %s." % [ lines.length, histfile ]
420
+ Readline::HISTORY.push( *lines )
421
+ else
422
+ self.log.debug "History file '%s' was empty or non-existant." % [ histfile ]
423
+ end
424
+ end
425
+
426
+
427
+ ### Save command line history to HISTORY_FILE
428
+ def save_history
429
+ histfile = HISTORY_FILE.expand_path
430
+
431
+ lines = Readline::HISTORY.to_a.reverse.uniq.reverse
432
+ lines = lines[ -DEFAULT_HISTORY_SIZE, DEFAULT_HISTORY_SIZE ] if
433
+ lines.length > DEFAULT_HISTORY_SIZE
434
+
435
+ self.log.debug "Saving %d history lines to %s." % [ lines.length, histfile ]
436
+
437
+ histfile.open( File::WRONLY|File::CREAT|File::TRUNC ) do |ofh|
438
+ ofh.puts( *lines )
439
+ end
440
+ end
441
+
442
+
443
+ ### Invoke the user's editor on the given +filename+ and return the exit code
444
+ ### from doing so.
445
+ def edit( filename )
446
+ editor = ENV['EDITOR'] || ENV['VISUAL'] || DEFAULT_EDITOR
447
+ system editor, filename.to_s
448
+ unless $?.success? || editor =~ /vim/i
449
+ raise "Editor exited with an error status (%d)" % [ $?.exitstatus ]
450
+ end
451
+ end
452
+
453
+
454
+ ### Search the PATH for a mongrel2 binary, returning the absolute Pathname to it if found, and
455
+ ### outputting a warning and describing how to set ENV['MONGREL2'] if not.
456
+ def find_mongrel2
457
+ if ENV['MONGREL2']
458
+ m2 = Pathname( ENV['MONGREL2'] )
459
+ error = nil
460
+ if !m2.file?
461
+ error = "but it isn't a plain file."
462
+ elsif !m2.executable?
463
+ error = "but it isn't executable."
464
+ end
465
+
466
+ raise "MONGREL2 was set to #{m2}, #{error}" if error
467
+
468
+ return m2
469
+ else
470
+ m2 = ENV['PATH'].split( File::PATH_SEPARATOR ).
471
+ map {|dir| Pathname(dir) + 'mongrel2' }.
472
+ find {|path| path.executable? }
473
+
474
+ return m2 if m2
475
+
476
+ raise "The 'mongrel2' binary doesn't seem to be in your PATH. Either " +
477
+ "add the appropriate directory to your PATH or set the MONGREL2 " +
478
+ "environment variable to the full path."
479
+ end
480
+
481
+ end
482
+
483
+ end # module CommandUtilities
484
+
485
+ ### Load commands from any files in the specified directory relative to LOAD_PATHs
486
+ def self::commands_from( subdir )
487
+ Gem.find_latest_files( File.join(subdir, '*.rb') ).each do |rbfile|
488
+ self.log.debug " loading %s..." % [ rbfile ]
489
+ require( rbfile )
490
+ end
491
+ end
492
+
493
+
494
+ commands_from 'mongrel2/cli'
495
+
496
+ end # class Mongrel2::CLI