minestrone 0.0.1
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/.github/workflows/main.yml +32 -0
- data/.gitignore +5 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/capify +89 -0
- data/bin/min +5 -0
- data/docs/lib-codebase-map.md +162 -0
- data/docs/lib-dependency-graph.svg +129 -0
- data/lib/minestrone/callback.rb +45 -0
- data/lib/minestrone/cli/help.rb +131 -0
- data/lib/minestrone/cli/help.txt +72 -0
- data/lib/minestrone/cli/options.rb +232 -0
- data/lib/minestrone/cli.rb +159 -0
- data/lib/minestrone/command.rb +177 -0
- data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
- data/lib/minestrone/configuration/actions/inspect.rb +46 -0
- data/lib/minestrone/configuration/actions/invocation.rb +202 -0
- data/lib/minestrone/configuration/alias_task.rb +29 -0
- data/lib/minestrone/configuration/callbacks.rb +129 -0
- data/lib/minestrone/configuration/connections.rb +66 -0
- data/lib/minestrone/configuration/execution.rb +139 -0
- data/lib/minestrone/configuration/loading.rb +207 -0
- data/lib/minestrone/configuration/log_formatters.rb +75 -0
- data/lib/minestrone/configuration/namespaces.rb +225 -0
- data/lib/minestrone/configuration/servers.rb +70 -0
- data/lib/minestrone/configuration/variables.rb +115 -0
- data/lib/minestrone/configuration.rb +69 -0
- data/lib/minestrone/errors.rb +17 -0
- data/lib/minestrone/ext/string.rb +7 -0
- data/lib/minestrone/extensions.rb +56 -0
- data/lib/minestrone/logger.rb +171 -0
- data/lib/minestrone/processable.rb +50 -0
- data/lib/minestrone/recipes/deploy/assets.rb +194 -0
- data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
- data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
- data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
- data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
- data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
- data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
- data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
- data/lib/minestrone/recipes/deploy/scm.rb +22 -0
- data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
- data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
- data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
- data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
- data/lib/minestrone/recipes/deploy.rb +639 -0
- data/lib/minestrone/recipes/standard.rb +23 -0
- data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
- data/lib/minestrone/server_definition.rb +56 -0
- data/lib/minestrone/ssh.rb +81 -0
- data/lib/minestrone/task_definition.rb +82 -0
- data/lib/minestrone/transfer.rb +205 -0
- data/lib/minestrone/version.rb +11 -0
- data/lib/minestrone.rb +3 -0
- data/minestrone.gemspec +32 -0
- data/test/cli/execute_test.rb +130 -0
- data/test/cli/help_test.rb +178 -0
- data/test/cli/options_test.rb +315 -0
- data/test/cli/ui_test.rb +26 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +305 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +76 -0
- data/test/configuration/actions/invocation_test.rb +258 -0
- data/test/configuration/alias_task_test.rb +110 -0
- data/test/configuration/callbacks_test.rb +201 -0
- data/test/configuration/connections_test.rb +192 -0
- data/test/configuration/execution_test.rb +176 -0
- data/test/configuration/loading_test.rb +149 -0
- data/test/configuration/namespace_dsl_test.rb +325 -0
- data/test/configuration/servers_test.rb +100 -0
- data/test/configuration/variables_test.rb +191 -0
- data/test/configuration_test.rb +77 -0
- data/test/deploy/local_dependency_test.rb +61 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +260 -0
- data/test/deploy/scm/none_test.rb +26 -0
- data/test/deploy/strategy/copy_test.rb +360 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +4 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_formatting_test.rb +149 -0
- data/test/logger_test.rb +134 -0
- data/test/recipes_test.rb +26 -0
- data/test/server_definition_test.rb +121 -0
- data/test/ssh_test.rb +99 -0
- data/test/task_definition_test.rb +117 -0
- data/test/transfer_test.rb +172 -0
- data/test/utils.rb +28 -0
- data/test/version_test.rb +11 -0
- metadata +258 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/server_definition'
|
|
4
|
+
require 'minestrone/errors'
|
|
5
|
+
|
|
6
|
+
module Minestrone
|
|
7
|
+
class Configuration
|
|
8
|
+
module Servers
|
|
9
|
+
def initialize_servers #:nodoc:
|
|
10
|
+
@server = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Define the server. The host may include user and port information, or
|
|
14
|
+
# those may be supplied as options:
|
|
15
|
+
#
|
|
16
|
+
# server "www@example.com"
|
|
17
|
+
# server "app.example.com", :user => "deploy"
|
|
18
|
+
|
|
19
|
+
def server(host, options = {})
|
|
20
|
+
raise ArgumentError, "server accepts one host and an optional options hash" unless options.is_a?(Hash)
|
|
21
|
+
raise ArgumentError, "you may only define one server" if @server
|
|
22
|
+
|
|
23
|
+
@server = server_definition_from(host, options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the configured server. If the SERVER environment variable
|
|
27
|
+
# is set, it replaces the configured host name while preserving configured
|
|
28
|
+
# connection options such as user, port, and SSH options.
|
|
29
|
+
|
|
30
|
+
def resolved_server
|
|
31
|
+
if env_key = server_override_env
|
|
32
|
+
host = ENV[env_key].strip
|
|
33
|
+
raise ArgumentError, "#{env_key} must name a single server" if host.empty? || host.include?(',')
|
|
34
|
+
|
|
35
|
+
options = @server ? connection_options_for(@server) : {}
|
|
36
|
+
server_definition_from(host, options)
|
|
37
|
+
else
|
|
38
|
+
@server or raise Minestrone::NoMatchingServersError, "no server configured"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
protected
|
|
44
|
+
|
|
45
|
+
def server_override_env
|
|
46
|
+
if ENV.key?('SERVER')
|
|
47
|
+
'SERVER'
|
|
48
|
+
elsif ENV.key?('HOSTS')
|
|
49
|
+
'HOSTS'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def server_definition_from(host, options = {})
|
|
54
|
+
raise ArgumentError, "server must be defined as a string" unless host.is_a?(String)
|
|
55
|
+
|
|
56
|
+
host = host.strip
|
|
57
|
+
raise ArgumentError, "server value must be a single hostname" if host.empty? || host.include?(',')
|
|
58
|
+
|
|
59
|
+
ServerDefinition.new(host, options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def connection_options_for(server)
|
|
63
|
+
options = server.options.dup
|
|
64
|
+
options[:user] = server.user if server.user
|
|
65
|
+
options[:port] = server.port if server.port
|
|
66
|
+
options
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
class Configuration
|
|
5
|
+
module Variables
|
|
6
|
+
def self.included(base) #:nodoc:
|
|
7
|
+
%w(respond_to? method_missing).each do |m|
|
|
8
|
+
base_name = m[/^\w+/]
|
|
9
|
+
punct = m[/\W+$/]
|
|
10
|
+
base.send :alias_method, "#{base_name}_without_variables#{punct}", m
|
|
11
|
+
base.send :alias_method, m, "#{base_name}_with_variables#{punct}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The hash of variables that have been defined in this configuration instance.
|
|
16
|
+
attr_reader :variables
|
|
17
|
+
|
|
18
|
+
# Set a variable to the given value.
|
|
19
|
+
def set(variable, *args, &block)
|
|
20
|
+
if variable.to_s !~ /^[_a-z]/
|
|
21
|
+
raise ArgumentError, "invalid variable `#{variable}' (variables must begin with an underscore, or a lower-case letter)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if !block_given? && args.empty? || block_given? && !args.empty?
|
|
25
|
+
raise ArgumentError, "you must specify exactly one of either a value or a block"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if args.length > 1
|
|
29
|
+
raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
value = args.empty? ? block : args.first
|
|
33
|
+
sym = variable.to_sym
|
|
34
|
+
@variables[sym] = value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
alias :[]= :set
|
|
38
|
+
|
|
39
|
+
# Removes any trace of the given variable.
|
|
40
|
+
def unset(variable)
|
|
41
|
+
sym = variable.to_sym
|
|
42
|
+
@original_procs.delete(sym)
|
|
43
|
+
@variables.delete(sym)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns true if the variable has been defined, and false otherwise.
|
|
47
|
+
def exists?(variable)
|
|
48
|
+
@variables.key?(variable.to_sym)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# If the variable was originally a proc value, it will be reset to it's
|
|
52
|
+
# original proc value. Otherwise, this method does nothing. It returns
|
|
53
|
+
# true if the variable was actually reset.
|
|
54
|
+
def reset!(variable)
|
|
55
|
+
sym = variable.to_sym
|
|
56
|
+
|
|
57
|
+
if @original_procs.key?(sym)
|
|
58
|
+
@variables[sym] = @original_procs.delete(sym)
|
|
59
|
+
true
|
|
60
|
+
else
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Access a named variable. If the value of the variable responds_to? :call,
|
|
66
|
+
# #call will be invoked (without parameters) and the return value cached
|
|
67
|
+
# and returned.
|
|
68
|
+
|
|
69
|
+
def fetch(variable, *args)
|
|
70
|
+
if !args.empty? && block_given?
|
|
71
|
+
raise ArgumentError, "you must specify either a default value or a block, but not both"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sym = variable.to_sym
|
|
75
|
+
|
|
76
|
+
if !@variables.key?(sym)
|
|
77
|
+
return args.first unless args.empty?
|
|
78
|
+
return yield(variable) if block_given?
|
|
79
|
+
raise IndexError, "`#{variable}' not found"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if @variables[sym].respond_to?(:call)
|
|
83
|
+
@original_procs[sym] = @variables[sym]
|
|
84
|
+
@variables[sym] = @variables[sym].call
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@variables[sym]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def [](variable)
|
|
91
|
+
fetch(variable, nil)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def initialize_variables #:nodoc:
|
|
95
|
+
@variables = {}
|
|
96
|
+
@original_procs = {}
|
|
97
|
+
|
|
98
|
+
set :ssh_options, {}
|
|
99
|
+
set :logger, logger
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def respond_to_with_variables?(sym, include_priv = false) #:nodoc:
|
|
103
|
+
@variables.has_key?(sym.to_sym) || respond_to_without_variables?(sym, include_priv)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def method_missing_with_variables(sym, *args, &block) #:nodoc:
|
|
107
|
+
if args.length == 0 && block.nil? && @variables.has_key?(sym)
|
|
108
|
+
self[sym]
|
|
109
|
+
else
|
|
110
|
+
method_missing_without_variables(sym, *args, &block)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'minestrone/logger'
|
|
2
|
+
|
|
3
|
+
require 'minestrone/configuration/alias_task'
|
|
4
|
+
require 'minestrone/configuration/callbacks'
|
|
5
|
+
require 'minestrone/configuration/connections'
|
|
6
|
+
require 'minestrone/configuration/execution'
|
|
7
|
+
require 'minestrone/configuration/loading'
|
|
8
|
+
require 'minestrone/configuration/log_formatters'
|
|
9
|
+
require 'minestrone/configuration/namespaces'
|
|
10
|
+
require 'minestrone/configuration/servers'
|
|
11
|
+
require 'minestrone/configuration/variables'
|
|
12
|
+
|
|
13
|
+
require 'minestrone/configuration/actions/file_transfer'
|
|
14
|
+
require 'minestrone/configuration/actions/inspect'
|
|
15
|
+
require 'minestrone/configuration/actions/invocation'
|
|
16
|
+
|
|
17
|
+
module Minestrone
|
|
18
|
+
|
|
19
|
+
#
|
|
20
|
+
# Represents a specific Minestrone configuration. A Configuration instance
|
|
21
|
+
# may be used to load multiple recipe files, define and describe tasks,
|
|
22
|
+
# define a server, and set configuration variables.
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
class Configuration
|
|
26
|
+
|
|
27
|
+
# The logger instance defined for this configuration.
|
|
28
|
+
attr_accessor :debug, :logger, :dry_run
|
|
29
|
+
|
|
30
|
+
def initialize(options = {}) #:nodoc:
|
|
31
|
+
@debug = false
|
|
32
|
+
@dry_run = false
|
|
33
|
+
@logger = Logger.new(options)
|
|
34
|
+
|
|
35
|
+
initialize_connections
|
|
36
|
+
initialize_execution
|
|
37
|
+
initialize_loading
|
|
38
|
+
initialize_namespaces
|
|
39
|
+
initialize_servers
|
|
40
|
+
initialize_variables
|
|
41
|
+
initialize_invocation
|
|
42
|
+
initialize_callbacks
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# make the DSL easier to read when using lazy evaluation via lambdas
|
|
46
|
+
alias defer lambda
|
|
47
|
+
|
|
48
|
+
# The includes must come at the bottom, since they may redefine methods
|
|
49
|
+
# defined in the base class.
|
|
50
|
+
include AliasTask, Connections, Execution, Loading, LogFormatters, Namespaces, Servers, Variables
|
|
51
|
+
|
|
52
|
+
# Mix in the actions
|
|
53
|
+
include Actions::FileTransfer, Actions::Inspect, Actions::Invocation
|
|
54
|
+
|
|
55
|
+
# Must mix last, because it hooks into previously defined methods
|
|
56
|
+
include Callbacks
|
|
57
|
+
|
|
58
|
+
(self.instance_methods & Kernel.methods).select do |name|
|
|
59
|
+
# Select the instance methods owned by the Configuration class.
|
|
60
|
+
self.instance_method(name).owner.to_s.start_with?("Minestrone::Configuration")
|
|
61
|
+
end.select do |name|
|
|
62
|
+
# Of those, select methods that are being shadowed by the Kernel module in the Namespace class.
|
|
63
|
+
Namespaces::Namespace.method_defined?(name) && Namespaces::Namespace.instance_method(name).owner == Kernel
|
|
64
|
+
end.each do |name|
|
|
65
|
+
# Undefine the shadowed methods, since we want Namespace objects to defer handling to the Configuration object.
|
|
66
|
+
Namespaces::Namespace.send(:undef_method, name)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Minestrone
|
|
2
|
+
Error = Class.new(RuntimeError)
|
|
3
|
+
|
|
4
|
+
CaptureError = Class.new(Minestrone::Error)
|
|
5
|
+
NoSuchTaskError = Class.new(Minestrone::Error)
|
|
6
|
+
NoMatchingServersError = Class.new(Minestrone::Error)
|
|
7
|
+
|
|
8
|
+
class RemoteError < Error
|
|
9
|
+
attr_accessor :host
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ConnectionError = Class.new(Minestrone::RemoteError)
|
|
13
|
+
TransferError = Class.new(Minestrone::RemoteError)
|
|
14
|
+
CommandError = Class.new(Minestrone::RemoteError)
|
|
15
|
+
|
|
16
|
+
LocalArgumentError = Class.new(Minestrone::Error)
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
class ExtensionProxy #:nodoc:
|
|
5
|
+
def initialize(config, mod)
|
|
6
|
+
@config = config
|
|
7
|
+
extend(mod)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(sym, *args, &block)
|
|
11
|
+
@config.send(sym, *args, &block)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Holds the set of registered plugins, keyed by name (where the name is a symbol).
|
|
16
|
+
EXTENSIONS = {}
|
|
17
|
+
|
|
18
|
+
# Register the given module as a plugin with the given name. It will henceforth
|
|
19
|
+
# be available via a proxy object on Configuration instances, accessible by
|
|
20
|
+
# a method with the given name.
|
|
21
|
+
|
|
22
|
+
def self.plugin(name, mod)
|
|
23
|
+
name = name.to_sym
|
|
24
|
+
return false if EXTENSIONS.has_key?(name)
|
|
25
|
+
|
|
26
|
+
methods = Minestrone::Configuration.public_instance_methods +
|
|
27
|
+
Minestrone::Configuration.protected_instance_methods +
|
|
28
|
+
Minestrone::Configuration.private_instance_methods
|
|
29
|
+
|
|
30
|
+
if methods.any? { |m| m.to_sym == name }
|
|
31
|
+
raise Minestrone::Error, "registering a plugin named `#{name}' would shadow a method on Minestrone::Configuration with the same name"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Minestrone::Configuration.class_eval <<-STR, __FILE__, __LINE__+1
|
|
35
|
+
def #{name}
|
|
36
|
+
@__#{name}_proxy ||= Minestrone::ExtensionProxy.new(self, Minestrone::EXTENSIONS[#{name.inspect}])
|
|
37
|
+
end
|
|
38
|
+
STR
|
|
39
|
+
|
|
40
|
+
EXTENSIONS[name] = mod
|
|
41
|
+
return true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Unregister the plugin with the given name.
|
|
45
|
+
|
|
46
|
+
def self.remove_plugin(name)
|
|
47
|
+
name = name.to_sym
|
|
48
|
+
|
|
49
|
+
if EXTENSIONS.delete(name)
|
|
50
|
+
Minestrone::Configuration.send(:remove_method, name)
|
|
51
|
+
return true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
class Logger #:nodoc:
|
|
5
|
+
attr_accessor :level, :device, :disable_formatters
|
|
6
|
+
|
|
7
|
+
IMPORTANT = 0
|
|
8
|
+
INFO = 1
|
|
9
|
+
DEBUG = 2
|
|
10
|
+
TRACE = 3
|
|
11
|
+
|
|
12
|
+
MAX_LEVEL = 3
|
|
13
|
+
|
|
14
|
+
COLORS = {
|
|
15
|
+
:none => "0",
|
|
16
|
+
:black => "30",
|
|
17
|
+
:red => "31",
|
|
18
|
+
:green => "32",
|
|
19
|
+
:yellow => "33",
|
|
20
|
+
:blue => "34",
|
|
21
|
+
:magenta => "35",
|
|
22
|
+
:cyan => "36",
|
|
23
|
+
:white => "37"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
STYLES = {
|
|
27
|
+
:bright => 1,
|
|
28
|
+
:dim => 2,
|
|
29
|
+
:underscore => 4,
|
|
30
|
+
:blink => 5,
|
|
31
|
+
:reverse => 7,
|
|
32
|
+
:hidden => 8
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@default_formatters = [
|
|
36
|
+
# TRACE
|
|
37
|
+
{ :match => /command finished/, :color => :white, :style => :dim, :level => 3, :priority => -10 },
|
|
38
|
+
{ :match => /executing locally/, :color => :yellow, :level => 3, :priority => -20 },
|
|
39
|
+
|
|
40
|
+
# DEBUG
|
|
41
|
+
{ :match => /executing `.*/, :color => :green, :level => 2, :priority => -10, :timestamp => true },
|
|
42
|
+
{ :match => /.*/, :color => :yellow, :level => 2, :priority => -30 },
|
|
43
|
+
|
|
44
|
+
# INFO
|
|
45
|
+
{ :match => /.*out\] (fatal:|ERROR:).*/, :color => :red, :level => 1, :priority => -10 },
|
|
46
|
+
{ :match => /Permission denied/, :color => :red, :level => 1, :priority => -20 },
|
|
47
|
+
{ :match => /sh: .+: command not found/, :color => :magenta, :level => 1, :priority => -30 },
|
|
48
|
+
|
|
49
|
+
# IMPORTANT
|
|
50
|
+
{ :match => /^err ::/, :color => :red, :level => 0, :priority => -10 },
|
|
51
|
+
{ :match => /.*/, :color => :blue, :level => 0, :priority => -20 }
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
@formatters = @default_formatters
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
def default_formatters
|
|
58
|
+
@default_formatters
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def default_formatters=(defaults = nil)
|
|
62
|
+
@default_formatters = [defaults].flatten
|
|
63
|
+
|
|
64
|
+
# reset the formatters
|
|
65
|
+
@formatters = @default_formatters
|
|
66
|
+
@sorted_formatters = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def add_formatter(options) #:nodoc:
|
|
70
|
+
@formatters.push(options)
|
|
71
|
+
@sorted_formatters = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def sorted_formatters
|
|
75
|
+
# Sort matchers in reverse order so we can break if we found a match.
|
|
76
|
+
@sorted_formatters ||= @formatters.sort_by { |i| -(i[:priority] || i[:prio] || 0) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize(options = {})
|
|
81
|
+
output = options[:output] || $stderr
|
|
82
|
+
|
|
83
|
+
if output.respond_to?(:puts)
|
|
84
|
+
@device = output
|
|
85
|
+
else
|
|
86
|
+
@device = File.open(output.to_str, "a")
|
|
87
|
+
@needs_close = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@options = options
|
|
91
|
+
@level = options[:level] || 0
|
|
92
|
+
@disable_formatters = options[:disable_formatters]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def close
|
|
96
|
+
device.close if @needs_close
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def log(level, message, line_prefix = nil)
|
|
100
|
+
if level <= self.level
|
|
101
|
+
|
|
102
|
+
# Only format output if device is a TTY and formatters are not disabled
|
|
103
|
+
if device.tty? && !@disable_formatters
|
|
104
|
+
color = :none
|
|
105
|
+
style = nil
|
|
106
|
+
|
|
107
|
+
Logger.sorted_formatters.each do |formatter|
|
|
108
|
+
if (formatter[:level] == level || formatter[:level].nil?)
|
|
109
|
+
if message =~ formatter[:match] || formatter[:match] =~ line_prefix.to_s
|
|
110
|
+
color = formatter[:color] if formatter[:color]
|
|
111
|
+
style = formatter[:style] || formatter[:attribute] # (support original cap colors)
|
|
112
|
+
message.gsub!(formatter[:match], formatter[:replace]) if formatter[:replace]
|
|
113
|
+
message = formatter[:prepend] + message unless formatter[:prepend].nil?
|
|
114
|
+
message = message + formatter[:append] unless formatter[:append].nil?
|
|
115
|
+
message = Time.now.strftime('%Y-%m-%d %T') + ' ' + message if formatter[:timestamp]
|
|
116
|
+
break unless formatter[:replace]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if color == :hide
|
|
122
|
+
# Don't do anything if color is set to :hide
|
|
123
|
+
return false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
term_color = COLORS[color]
|
|
127
|
+
term_style = STYLES[style]
|
|
128
|
+
|
|
129
|
+
# Don't format message if no color or style
|
|
130
|
+
unless color == :none and style.nil?
|
|
131
|
+
unless line_prefix.nil?
|
|
132
|
+
line_prefix = format(line_prefix, term_color, term_style, nil)
|
|
133
|
+
end
|
|
134
|
+
message = format(message, term_color, term_style)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)]
|
|
139
|
+
|
|
140
|
+
message.lines.each do |line|
|
|
141
|
+
if line_prefix
|
|
142
|
+
device.puts "#{indent} [#{line_prefix}] #{line.strip}\n"
|
|
143
|
+
else
|
|
144
|
+
device.puts "#{indent} #{line.strip}\n"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def important(message, line_prefix = nil)
|
|
151
|
+
log(IMPORTANT, message, line_prefix)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def info(message, line_prefix = nil)
|
|
155
|
+
log(INFO, message, line_prefix)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def debug(message, line_prefix = nil)
|
|
159
|
+
log(DEBUG, message, line_prefix)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def trace(message, line_prefix = nil)
|
|
163
|
+
log(TRACE, message, line_prefix)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format(message, color, style, nl = "\n")
|
|
167
|
+
style = "#{style};" if style
|
|
168
|
+
"\e[#{style}#{color}m" + message.to_s.strip + "\e[0m#{nl}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Minestrone
|
|
2
|
+
module Processable
|
|
3
|
+
module SessionAssociation
|
|
4
|
+
def self.on(exception, session)
|
|
5
|
+
unless exception.respond_to?(:session)
|
|
6
|
+
exception.extend(self)
|
|
7
|
+
exception.session = session
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
return exception
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_accessor :session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_iteration(wait = nil, &block)
|
|
17
|
+
ensure_session { |session| session.preprocess }
|
|
18
|
+
|
|
19
|
+
return false if block && !block.call(self)
|
|
20
|
+
|
|
21
|
+
readers = session.listeners.keys.reject { |io| io.closed? }
|
|
22
|
+
writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? }
|
|
23
|
+
|
|
24
|
+
if readers.any? || writers.any?
|
|
25
|
+
readers, writers, = IO.select(readers, writers, nil, wait)
|
|
26
|
+
else
|
|
27
|
+
return false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if readers
|
|
31
|
+
ensure_session do |session|
|
|
32
|
+
ios = session.listeners.keys
|
|
33
|
+
session.postprocess(ios & readers, ios & writers)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ensure_session
|
|
41
|
+
begin
|
|
42
|
+
yield session
|
|
43
|
+
rescue Exception => error
|
|
44
|
+
raise SessionAssociation.on(error, session)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
session
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|