Sutto-perennial 0.1.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.
- 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
|