mongrel2 0.53.0 → 0.54.0

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