Sutto-perennial 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/perennial/argument_parser.rb +60 -0
- data/lib/perennial/core_ext/attribute_accessors.rb +129 -0
- data/lib/perennial/core_ext/blank.rb +51 -0
- data/lib/perennial/core_ext/misc.rb +118 -0
- data/lib/perennial/core_ext.rb +4 -0
- data/lib/perennial/daemon.rb +120 -0
- data/lib/perennial/dispatchable.rb +110 -0
- data/lib/perennial/exceptions.rb +6 -0
- data/lib/perennial/hookable.rb +63 -0
- data/lib/perennial/loader.rb +97 -0
- data/lib/perennial/loggable.rb +15 -0
- data/lib/perennial/logger.rb +106 -0
- data/lib/perennial/manifest.rb +32 -0
- data/lib/perennial/option_parser.rb +108 -0
- data/lib/perennial/settings.rb +87 -0
- data/lib/perennial.rb +21 -0
- data/test/dispatchable_test.rb +129 -0
- data/test/hookable_test.rb +61 -0
- data/test/loader_test.rb +1 -0
- data/test/loggable_test.rb +38 -0
- data/test/logger_test.rb +57 -0
- data/test/settings_test.rb +99 -0
- data/test/test_helper.rb +38 -0
- data/vendor/fakefs/LICENSE +20 -0
- data/vendor/fakefs/README.markdown +37 -0
- data/vendor/fakefs/Rakefile +3 -0
- data/vendor/fakefs/lib/fakefs.rb +448 -0
- data/vendor/fakefs/test/fakefs_test.rb +511 -0
- data/vendor/fakefs/test/verify.rb +27 -0
- metadata +87 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module Perennial
|
4
|
+
class ArgumentParser
|
5
|
+
|
6
|
+
attr_reader :arguments
|
7
|
+
|
8
|
+
def initialize(array)
|
9
|
+
@raw_arguments = array.dup
|
10
|
+
@arguments = []
|
11
|
+
@results = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_hash
|
15
|
+
@results ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse!
|
19
|
+
while !@raw_arguments.empty?
|
20
|
+
current = @raw_arguments.shift
|
21
|
+
if option?(current)
|
22
|
+
process_option(current, @raw_arguments.first)
|
23
|
+
else
|
24
|
+
@arguments.push current
|
25
|
+
end
|
26
|
+
end
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.parse(args = ARGV)
|
31
|
+
parser = self.new(args)
|
32
|
+
parser.parse!
|
33
|
+
return parser.arguments, parser.to_hash
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def process_option(current, next_arg)
|
39
|
+
name = clean(current)
|
40
|
+
if name.include?("=")
|
41
|
+
real_name, raw_value = name.split("=", 2)
|
42
|
+
@results[real_name] = Shellwords.shellwords(raw_value).join(" ")
|
43
|
+
elsif !(next_arg.nil? || option?(next_arg))
|
44
|
+
@raw_arguments.shift
|
45
|
+
@results[name] = next_arg
|
46
|
+
else
|
47
|
+
@results[name] = true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def clean(name)
|
52
|
+
name.gsub /^\-+/, ''
|
53
|
+
end
|
54
|
+
|
55
|
+
def option?(argument)
|
56
|
+
argument.strip =~ /^\-+/
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# cattr_* and class_inheritable_* are taken from
|
2
|
+
# ActiveSupport. Included here to help keep the
|
3
|
+
# codebase simple / clean.
|
4
|
+
class Class
|
5
|
+
def cattr_reader(*syms)
|
6
|
+
syms.flatten.each do |sym|
|
7
|
+
next if sym.is_a?(Hash)
|
8
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
9
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
10
|
+
@@#{sym} = nil # @@hair_colors = nil
|
11
|
+
end # end
|
12
|
+
#
|
13
|
+
def self.#{sym} # def self.hair_colors
|
14
|
+
@@#{sym} # @@hair_colors
|
15
|
+
end # end
|
16
|
+
#
|
17
|
+
def #{sym} # def hair_colors
|
18
|
+
@@#{sym} # @@hair_colors
|
19
|
+
end # end
|
20
|
+
EOS
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def cattr_writer(*syms)
|
25
|
+
options = syms.extract_options!
|
26
|
+
syms.flatten.each do |sym|
|
27
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
28
|
+
unless defined? @@#{sym} # unless defined? @@hair_colors
|
29
|
+
@@#{sym} = nil # @@hair_colors = nil
|
30
|
+
end # end
|
31
|
+
#
|
32
|
+
def self.#{sym}=(obj) # def self.hair_colors=(obj)
|
33
|
+
@@#{sym} = obj # @@hair_colors = obj
|
34
|
+
end # end
|
35
|
+
#
|
36
|
+
#{" #
|
37
|
+
def #{sym}=(obj) # def hair_colors=(obj)
|
38
|
+
@@#{sym} = obj # @@hair_colors = obj
|
39
|
+
end # end
|
40
|
+
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
41
|
+
EOS
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cattr_accessor(*syms)
|
46
|
+
cattr_reader(*syms)
|
47
|
+
cattr_writer(*syms)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Defines class-level inheritable attribute reader. Attributes are available to subclasses,
|
51
|
+
# each subclass has a copy of parent's attribute.
|
52
|
+
#
|
53
|
+
# @param *syms<Array[#to_s]> Array of attributes to define inheritable reader for.
|
54
|
+
# @return <Array[#to_s]> Array of attributes converted into inheritable_readers.
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
#
|
58
|
+
# @todo Do we want to block instance_reader via :instance_reader => false
|
59
|
+
# @todo It would be preferable that we do something with a Hash passed in
|
60
|
+
# (error out or do the same as other methods above) instead of silently
|
61
|
+
# moving on). In particular, this makes the return value of this function
|
62
|
+
# less useful.
|
63
|
+
def class_inheritable_reader(*ivars)
|
64
|
+
instance_reader = ivars.pop[:reader] if ivars.last.is_a?(Hash)
|
65
|
+
|
66
|
+
ivars.each do |ivar|
|
67
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
68
|
+
def self.#{ivar}
|
69
|
+
return @#{ivar} if self.object_id == #{self.object_id} || defined?(@#{ivar})
|
70
|
+
ivar = superclass.#{ivar}
|
71
|
+
return nil if ivar.nil? && !#{self}.instance_variable_defined?("@#{ivar}")
|
72
|
+
@#{ivar} = ivar && !ivar.is_a?(Module) && !ivar.is_a?(Numeric) && !ivar.is_a?(TrueClass) && !ivar.is_a?(FalseClass) ? ivar.dup : ivar
|
73
|
+
end
|
74
|
+
RUBY
|
75
|
+
unless instance_reader == false
|
76
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
77
|
+
def #{ivar}
|
78
|
+
self.class.#{ivar}
|
79
|
+
end
|
80
|
+
RUBY
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Defines class-level inheritable attribute writer. Attributes are available to subclasses,
|
86
|
+
# each subclass has a copy of parent's attribute.
|
87
|
+
#
|
88
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
89
|
+
# define inheritable writer for.
|
90
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
91
|
+
# @return <Array[#to_s]> An Array of the attributes that were made into inheritable writers.
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
#
|
95
|
+
# @todo We need a style for class_eval <<-HEREDOC. I'd like to make it
|
96
|
+
# class_eval(<<-RUBY, __FILE__, __LINE__), but we should codify it somewhere.
|
97
|
+
def class_inheritable_writer(*ivars)
|
98
|
+
instance_writer = ivars.pop[:writer] if ivars.last.is_a?(Hash)
|
99
|
+
ivars.each do |ivar|
|
100
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
101
|
+
def self.#{ivar}=(obj)
|
102
|
+
@#{ivar} = obj
|
103
|
+
end
|
104
|
+
RUBY
|
105
|
+
unless instance_writer == false
|
106
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
107
|
+
def #{ivar}=(obj) self.class.#{ivar} = obj end
|
108
|
+
RUBY
|
109
|
+
end
|
110
|
+
|
111
|
+
self.send("#{ivar}=", yield) if block_given?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Defines class-level inheritable attribute accessor. Attributes are available to subclasses,
|
116
|
+
# each subclass has a copy of parent's attribute.
|
117
|
+
#
|
118
|
+
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to
|
119
|
+
# define inheritable accessor for.
|
120
|
+
# @option syms :instance_writer<Boolean> if true, instance-level inheritable attribute writer is defined.
|
121
|
+
# @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
|
122
|
+
#
|
123
|
+
# @api public
|
124
|
+
def class_inheritable_accessor(*syms, &block)
|
125
|
+
class_inheritable_reader(*syms)
|
126
|
+
class_inheritable_writer(*syms, &block)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# blank? parts taken from the active support source
|
2
|
+
# code, used under the MIT License.
|
3
|
+
|
4
|
+
class Object
|
5
|
+
def blank?
|
6
|
+
respond_to?(:empty?) ? empty? : !self
|
7
|
+
end
|
8
|
+
|
9
|
+
def present?
|
10
|
+
!blank?
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
class NilClass #:nodoc:
|
16
|
+
def blank?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FalseClass #:nodoc:
|
22
|
+
def blank?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class TrueClass #:nodoc:
|
28
|
+
def blank?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Array #:nodoc:
|
34
|
+
alias_method :blank?, :empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
class Hash #:nodoc:
|
38
|
+
alias_method :blank?, :empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
class String #:nodoc:
|
42
|
+
def blank?
|
43
|
+
self !~ /\S/
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Numeric #:nodoc:
|
48
|
+
def blank?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
def metaclass
|
4
|
+
class << self; self; end
|
5
|
+
end
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
class Inflector
|
10
|
+
class << self
|
11
|
+
|
12
|
+
def underscore(camel_cased_word)
|
13
|
+
camel_cased_word.to_s.gsub(/::/, '/').
|
14
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
15
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
16
|
+
tr("-", "_").
|
17
|
+
downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
21
|
+
if first_letter_in_uppercase
|
22
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
23
|
+
else
|
24
|
+
lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Kernel
|
32
|
+
|
33
|
+
# From http://oldrcrs.rubypal.com/rcr/show/309
|
34
|
+
def __DIR__(offset = 0)
|
35
|
+
(/^(.+)?:\d+/ =~ caller[offset + 1]) ? File.dirname($1) : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Shorthand for lambda
|
39
|
+
# e.g. L{|r| puts r}
|
40
|
+
def L(&blk)
|
41
|
+
lambda(&blk)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Shorthand for Proc.new
|
45
|
+
# e.g. P{|r| puts r}
|
46
|
+
def P(&blk)
|
47
|
+
Proc.new(&blk)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class String
|
52
|
+
def /(*args)
|
53
|
+
File.join(self, *args)
|
54
|
+
end
|
55
|
+
|
56
|
+
def underscore
|
57
|
+
Inflector.underscore(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def camelize(capitalize_first_letter = true)
|
61
|
+
Inflector.camelize(self, capitalize_first_letter)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_pathname
|
65
|
+
Pathname.new(self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class Array
|
70
|
+
def extract_options!
|
71
|
+
last.is_a?(Hash) ? pop : {}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Hash
|
76
|
+
def symbolize_keys
|
77
|
+
hash = self.dup
|
78
|
+
hash.symbolize_keys!
|
79
|
+
return hash
|
80
|
+
end
|
81
|
+
|
82
|
+
def symbolize_keys!
|
83
|
+
hash = {}
|
84
|
+
self.each_pair { |k,v| hash[k.to_sym] = v }
|
85
|
+
replace hash
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Module
|
90
|
+
|
91
|
+
def has_libary(*items)
|
92
|
+
namespace = self.to_s.underscore
|
93
|
+
items.each do |item|
|
94
|
+
require File.join(namespace, item.to_s.underscore)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def extends_library(*items)
|
99
|
+
namespace = self.to_s.underscore
|
100
|
+
items.each do |item|
|
101
|
+
klass = item.to_s.camelize.to_sym
|
102
|
+
# Load if it isn't loaded already.
|
103
|
+
const_get(klass) unless const_defined?(klass)
|
104
|
+
# And finally load the file.
|
105
|
+
require File.join(namespace, item.to_s.underscore)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def attempt_require(*files)
|
110
|
+
files.each do |file|
|
111
|
+
begin
|
112
|
+
require file
|
113
|
+
rescue LoadError
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Perennial
|
2
|
+
# = Perennial::Daemon provides a relatively simple
|
3
|
+
# interface to creating unix daemons from a given process.
|
4
|
+
# Namely, it focuses on three providing tools for
|
5
|
+
# a couple of simple purposes.
|
6
|
+
#
|
7
|
+
# == Turning a process into a daemon
|
8
|
+
#
|
9
|
+
# This approach is as as simple as calling
|
10
|
+
# Perennial::Daemon.daemonize! - the current process
|
11
|
+
# will then be a daemon, the non-daemon processes
|
12
|
+
# will be killed and if started by the loader, it will
|
13
|
+
# also write a pid out.
|
14
|
+
#
|
15
|
+
# == Checking whether a process is alive
|
16
|
+
#
|
17
|
+
# Perennial::Daemon.alive?(pid) is a super simple way
|
18
|
+
# to check if a pid (as an integer) is current active / alive,
|
19
|
+
#
|
20
|
+
# Perennial::Daemon.any_alive? takes a type and tells you if
|
21
|
+
# any processes with the associated type are alive.
|
22
|
+
#
|
23
|
+
# == Storing / Retrieving Pids for a Given Loader Type
|
24
|
+
#
|
25
|
+
# Perennial::Daemon.daemonize! will store the pid for
|
26
|
+
# the current process if Perennial::Loader.current_type
|
27
|
+
# is present.
|
28
|
+
#
|
29
|
+
# Perennial::Daemon.any_alive?(type = :all) and
|
30
|
+
# Perennial::Daemon.kill_all(type = :all) can be
|
31
|
+
# used to deal with checking the status of and killing
|
32
|
+
# processes associated with a given loader type.
|
33
|
+
class Daemon
|
34
|
+
class << self
|
35
|
+
|
36
|
+
def any_alive?(type = :all)
|
37
|
+
!pid_file_for(type).empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true / false depending on whether
|
41
|
+
# a process with the given pid exists.
|
42
|
+
def alive?(pid)
|
43
|
+
return Process.getpgid(pid) != -1
|
44
|
+
rescue Errno::ESRCH
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Kills all processes associated with a certain app type.
|
49
|
+
# E.g. Given a loader is starting a :client, kill_all(:client)
|
50
|
+
# would kill all associated processes (usually after daemonizing)
|
51
|
+
# and kill_all would do the same - but if we also started
|
52
|
+
# a process for :server, kill_all(:client wouldn't kill the
|
53
|
+
# the process where as kill_all would.
|
54
|
+
def kill_all(type = :all)
|
55
|
+
kill_all_from(pid_file_for(type))
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
# Converts the current process into a Unix-daemon using
|
60
|
+
# the double fork approach. Also, changes process file
|
61
|
+
# mask to 000 and reopens STDIN / OUT to /dev/null
|
62
|
+
def daemonize!
|
63
|
+
exit if fork
|
64
|
+
Process.setsid
|
65
|
+
exit if fork
|
66
|
+
self.write_pid
|
67
|
+
File.umask 0000
|
68
|
+
STDIN.reopen "/dev/null"
|
69
|
+
STDOUT.reopen "/dev/null", "a"
|
70
|
+
STDERR.reopen STDOUT
|
71
|
+
Perennial::Settings.verbose = false
|
72
|
+
end
|
73
|
+
|
74
|
+
# Cleans up processes for the current application type
|
75
|
+
# (deteremined by the loader) and then removes the pid file.
|
76
|
+
def cleanup!
|
77
|
+
f = pids_file_for(Loader.current_type)
|
78
|
+
FileUtils.rm_f(f) if (pids_from(f) - Process.pid).blank?
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array of pid's associated with a given type.
|
82
|
+
def pids_for_type(type)
|
83
|
+
pids_from(pid_file_for(type))
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def kill_all_from(file)
|
89
|
+
pids = pids_from(file)
|
90
|
+
pids.each { |p| Process.kill("TERM", p) unless p == Process.pid }
|
91
|
+
FileUtils.rm_f(file)
|
92
|
+
rescue => e
|
93
|
+
STDOUT.puts e.inspect
|
94
|
+
end
|
95
|
+
|
96
|
+
def pid_file_for(type)
|
97
|
+
type = "*" if type == :all
|
98
|
+
Settings.root / "tmp" / "pids" / "#{type.to_s.underscore}.pid"
|
99
|
+
end
|
100
|
+
|
101
|
+
def pids_from(files)
|
102
|
+
pids = []
|
103
|
+
Dir[files].each do |file|
|
104
|
+
pids += File.read(file).split("\n").map { |l| l.strip.to_i(10) }
|
105
|
+
end
|
106
|
+
return pids.uniq.select { |p| alive?(p) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def write_pid
|
110
|
+
type = Loader.current_type
|
111
|
+
return if type.blank?
|
112
|
+
f = pid_file_for(type)
|
113
|
+
pids = pids_from(f)
|
114
|
+
pids << Process.pid unless pids.include?(Process.pid)
|
115
|
+
File.open(f, "w+") { |f| f.puts pids.join("\n") }
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Perennial
|
2
|
+
|
3
|
+
# = Perennial::Dispatchable
|
4
|
+
# A Generic mixin which lets you define an object
|
5
|
+
# Which accepts handlers which can have arbitrary
|
6
|
+
# events dispatched.
|
7
|
+
# == Usage
|
8
|
+
#
|
9
|
+
# class X
|
10
|
+
# include Perennial::Dispatchable
|
11
|
+
# self.handlers << SomeHandler.new
|
12
|
+
# end
|
13
|
+
# X.new.dispatch(:name, {:args => "Values"})
|
14
|
+
#
|
15
|
+
# Will first check if SomeHandler#handle_name exists,
|
16
|
+
# calling handle_name({:args => "Values"}) if it does,
|
17
|
+
# otherwise calling SomeHandler#handle(:name, {:args => "Values"})
|
18
|
+
module Dispatchable
|
19
|
+
|
20
|
+
def self.handler_mapping
|
21
|
+
@@handler_mapping ||= Hash.new { |h,k| h[k] = [] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(parent)
|
25
|
+
parent.class_eval do
|
26
|
+
include InstanceMethods
|
27
|
+
extend ClassMethods
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
|
33
|
+
# Returns the handlers registered on this class,
|
34
|
+
# used inside +dispatch+.
|
35
|
+
def handlers
|
36
|
+
self.class.handlers
|
37
|
+
end
|
38
|
+
|
39
|
+
# Dispatch an 'event' with a given name to the handlers
|
40
|
+
# registered on the current class. Used as a nicer way of defining
|
41
|
+
# behaviours that should occur under a given set of circumstances.
|
42
|
+
# == Params
|
43
|
+
# +name+: The name of the current event
|
44
|
+
# +opts+: an optional hash of options to pass
|
45
|
+
def dispatch(name, opts = {})
|
46
|
+
# The full handler name is the method we call given it exists.
|
47
|
+
full_handler_name = :"handle_#{name.to_s.underscore}"
|
48
|
+
# First, dispatch locally if the method is defined.
|
49
|
+
self.send(full_handler_name, opts) if self.respond_to?(full_handler_name)
|
50
|
+
# Iterate through all of the registered handlers,
|
51
|
+
# If there is a method named handle_<event_name>
|
52
|
+
# defined we sent that otherwise we call the handle
|
53
|
+
# method on the handler. Note that the handle method
|
54
|
+
# is the only required aspect of a handler. An improved
|
55
|
+
# version of this would likely cache the respond_to?
|
56
|
+
# call.
|
57
|
+
self.handlers.each do |handler|
|
58
|
+
if handler.respond_to?(full_handler_name)
|
59
|
+
handler.send(full_handler_name, opts)
|
60
|
+
else
|
61
|
+
handler.handle name, opts
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# If we get the HaltHandlerProcessing exception, we
|
65
|
+
# catch it and continue on our way. In essence, we
|
66
|
+
# stop the dispatch of events to the next set of the
|
67
|
+
# handlers.
|
68
|
+
rescue HaltHandlerProcessing => e
|
69
|
+
Logger.info "Halting processing chain"
|
70
|
+
rescue Exception => e
|
71
|
+
Logger.log_exception(e)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
module ClassMethods
|
77
|
+
|
78
|
+
# Return an array of all registered handlers, ordered
|
79
|
+
# by their class and then the order of insertion. Please
|
80
|
+
# note that this will include ALL handlers up the inheritance
|
81
|
+
# chain unless false is passed as the only argument.
|
82
|
+
def handlers(recursive = true)
|
83
|
+
handlers = []
|
84
|
+
if recursive && superclass.respond_to?(:handlers)
|
85
|
+
handlers += superclass.handlers(recursive)
|
86
|
+
end
|
87
|
+
handlers += Dispatchable.handler_mapping[self]
|
88
|
+
return handlers
|
89
|
+
end
|
90
|
+
|
91
|
+
# Assigns a new array of handlers and assigns each - Note that
|
92
|
+
# this will only set this classes handlers, it will not override
|
93
|
+
# those for others above / below it in the inheritance chain.
|
94
|
+
def handlers=(new_value)
|
95
|
+
Dispatchable.handler_mapping.delete self
|
96
|
+
[*new_value].each { |h| register_handler h }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Appends a handler to the list of handlers for this object.
|
100
|
+
# Handlers are called in the order they are registered.
|
101
|
+
def register_handler(handler)
|
102
|
+
unless handler.blank? || !handler.respond_to?(:handle)
|
103
|
+
Dispatchable.handler_mapping[self] << handler
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Perennial
|
2
|
+
# = Perennial::Hookable
|
3
|
+
#
|
4
|
+
# Perennial::Hookable provides a generic set of functionality
|
5
|
+
# for implementing simple block-based hooks / callbacks. On
|
6
|
+
# a class level, this makes it easy for you to do event driven
|
7
|
+
# programming in that code can be registered to be run
|
8
|
+
# when something happens.
|
9
|
+
#
|
10
|
+
# Hookable differs from Perennial::Dispatchable in that it is
|
11
|
+
# designed to be lightweight / used for things like setting things
|
12
|
+
# up without all of the overhead of defining handlers / dispatching
|
13
|
+
# messages.
|
14
|
+
module Hookable
|
15
|
+
|
16
|
+
def self.included(parent)
|
17
|
+
parent.class_eval do
|
18
|
+
extend ClassMethods
|
19
|
+
cattr_accessor :hooks
|
20
|
+
self.hooks = Hash.new { |h,k| h[k] = [] }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
# Append a hook for a given type of hook in order
|
27
|
+
# to be called later on via invoke_hooks!
|
28
|
+
def append_hook(type, &blk)
|
29
|
+
self.hooks_for(type) << blk unless blk.blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return all of the existing hooks or an empty
|
33
|
+
# for a given hook type.
|
34
|
+
def hooks_for(type)
|
35
|
+
self.hooks[type.to_sym]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Invoke (call) all of the hooks for a given
|
39
|
+
# type.
|
40
|
+
def invoke_hooks!(type)
|
41
|
+
hooks_for(type).each { |hook| hook.call }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Defines a set of handy methods to make it
|
45
|
+
# easy to define simplistic block based hooks
|
46
|
+
# on an arbitrary class.
|
47
|
+
def define_hook(*args)
|
48
|
+
klass = self.metaclass
|
49
|
+
args.map { |a| a.to_sym }.each do |name|
|
50
|
+
|
51
|
+
klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
52
|
+
def #{name}(&blk) # def before_run(&blk)
|
53
|
+
append_hook(:#{name}, &blk) # append_hook(:before_run, &blk)
|
54
|
+
end # end
|
55
|
+
RUBY
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|