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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +32 -0
  3. data/.gitignore +5 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/capify +89 -0
  9. data/bin/min +5 -0
  10. data/docs/lib-codebase-map.md +162 -0
  11. data/docs/lib-dependency-graph.svg +129 -0
  12. data/lib/minestrone/callback.rb +45 -0
  13. data/lib/minestrone/cli/help.rb +131 -0
  14. data/lib/minestrone/cli/help.txt +72 -0
  15. data/lib/minestrone/cli/options.rb +232 -0
  16. data/lib/minestrone/cli.rb +159 -0
  17. data/lib/minestrone/command.rb +177 -0
  18. data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
  19. data/lib/minestrone/configuration/actions/inspect.rb +46 -0
  20. data/lib/minestrone/configuration/actions/invocation.rb +202 -0
  21. data/lib/minestrone/configuration/alias_task.rb +29 -0
  22. data/lib/minestrone/configuration/callbacks.rb +129 -0
  23. data/lib/minestrone/configuration/connections.rb +66 -0
  24. data/lib/minestrone/configuration/execution.rb +139 -0
  25. data/lib/minestrone/configuration/loading.rb +207 -0
  26. data/lib/minestrone/configuration/log_formatters.rb +75 -0
  27. data/lib/minestrone/configuration/namespaces.rb +225 -0
  28. data/lib/minestrone/configuration/servers.rb +70 -0
  29. data/lib/minestrone/configuration/variables.rb +115 -0
  30. data/lib/minestrone/configuration.rb +69 -0
  31. data/lib/minestrone/errors.rb +17 -0
  32. data/lib/minestrone/ext/string.rb +7 -0
  33. data/lib/minestrone/extensions.rb +56 -0
  34. data/lib/minestrone/logger.rb +171 -0
  35. data/lib/minestrone/processable.rb +50 -0
  36. data/lib/minestrone/recipes/deploy/assets.rb +194 -0
  37. data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
  38. data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
  39. data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
  40. data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
  41. data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
  42. data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
  43. data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
  44. data/lib/minestrone/recipes/deploy/scm.rb +22 -0
  45. data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
  46. data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
  47. data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
  48. data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
  49. data/lib/minestrone/recipes/deploy.rb +639 -0
  50. data/lib/minestrone/recipes/standard.rb +23 -0
  51. data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
  52. data/lib/minestrone/server_definition.rb +56 -0
  53. data/lib/minestrone/ssh.rb +81 -0
  54. data/lib/minestrone/task_definition.rb +82 -0
  55. data/lib/minestrone/transfer.rb +205 -0
  56. data/lib/minestrone/version.rb +11 -0
  57. data/lib/minestrone.rb +3 -0
  58. data/minestrone.gemspec +32 -0
  59. data/test/cli/execute_test.rb +130 -0
  60. data/test/cli/help_test.rb +178 -0
  61. data/test/cli/options_test.rb +315 -0
  62. data/test/cli/ui_test.rb +26 -0
  63. data/test/cli_test.rb +17 -0
  64. data/test/command_test.rb +305 -0
  65. data/test/configuration/actions/file_transfer_test.rb +61 -0
  66. data/test/configuration/actions/inspect_test.rb +76 -0
  67. data/test/configuration/actions/invocation_test.rb +258 -0
  68. data/test/configuration/alias_task_test.rb +110 -0
  69. data/test/configuration/callbacks_test.rb +201 -0
  70. data/test/configuration/connections_test.rb +192 -0
  71. data/test/configuration/execution_test.rb +176 -0
  72. data/test/configuration/loading_test.rb +149 -0
  73. data/test/configuration/namespace_dsl_test.rb +325 -0
  74. data/test/configuration/servers_test.rb +100 -0
  75. data/test/configuration/variables_test.rb +191 -0
  76. data/test/configuration_test.rb +77 -0
  77. data/test/deploy/local_dependency_test.rb +61 -0
  78. data/test/deploy/remote_dependency_test.rb +146 -0
  79. data/test/deploy/scm/base_test.rb +55 -0
  80. data/test/deploy/scm/git_test.rb +260 -0
  81. data/test/deploy/scm/none_test.rb +26 -0
  82. data/test/deploy/strategy/copy_test.rb +360 -0
  83. data/test/extensions_test.rb +69 -0
  84. data/test/fixtures/cli_integration.rb +5 -0
  85. data/test/fixtures/config.rb +4 -0
  86. data/test/fixtures/custom.rb +3 -0
  87. data/test/logger_formatting_test.rb +149 -0
  88. data/test/logger_test.rb +134 -0
  89. data/test/recipes_test.rb +26 -0
  90. data/test/server_definition_test.rb +121 -0
  91. data/test/ssh_test.rb +99 -0
  92. data/test/task_definition_test.rb +117 -0
  93. data/test/transfer_test.rb +172 -0
  94. data/test/utils.rb +28 -0
  95. data/test/version_test.rb +11 -0
  96. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ def compact
5
+ self.gsub(/\s+/, ' ')
6
+ end
7
+ 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