resurrected_god 0.14.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.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/Announce.txt +135 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +22 -0
  5. data/README.md +33 -0
  6. data/Rakefile +129 -0
  7. data/bin/god +134 -0
  8. data/doc/god.asciidoc +1592 -0
  9. data/doc/intro.asciidoc +20 -0
  10. data/ext/god/.gitignore +5 -0
  11. data/ext/god/extconf.rb +56 -0
  12. data/ext/god/kqueue_handler.c +133 -0
  13. data/ext/god/netlink_handler.c +182 -0
  14. data/lib/god/behavior.rb +52 -0
  15. data/lib/god/behaviors/clean_pid_file.rb +21 -0
  16. data/lib/god/behaviors/clean_unix_socket.rb +21 -0
  17. data/lib/god/behaviors/notify_when_flapping.rb +51 -0
  18. data/lib/god/cli/command.rb +268 -0
  19. data/lib/god/cli/run.rb +170 -0
  20. data/lib/god/cli/version.rb +23 -0
  21. data/lib/god/compat19.rb +33 -0
  22. data/lib/god/condition.rb +96 -0
  23. data/lib/god/conditions/always.rb +36 -0
  24. data/lib/god/conditions/complex.rb +86 -0
  25. data/lib/god/conditions/cpu_usage.rb +80 -0
  26. data/lib/god/conditions/degrading_lambda.rb +52 -0
  27. data/lib/god/conditions/disk_usage.rb +32 -0
  28. data/lib/god/conditions/file_mtime.rb +28 -0
  29. data/lib/god/conditions/file_touched.rb +44 -0
  30. data/lib/god/conditions/flapping.rb +128 -0
  31. data/lib/god/conditions/http_response_code.rb +184 -0
  32. data/lib/god/conditions/lambda.rb +25 -0
  33. data/lib/god/conditions/memory_usage.rb +82 -0
  34. data/lib/god/conditions/process_exits.rb +66 -0
  35. data/lib/god/conditions/process_running.rb +63 -0
  36. data/lib/god/conditions/socket_responding.rb +142 -0
  37. data/lib/god/conditions/tries.rb +44 -0
  38. data/lib/god/configurable.rb +57 -0
  39. data/lib/god/contact.rb +114 -0
  40. data/lib/god/contacts/airbrake.rb +44 -0
  41. data/lib/god/contacts/campfire.rb +121 -0
  42. data/lib/god/contacts/email.rb +130 -0
  43. data/lib/god/contacts/hipchat.rb +117 -0
  44. data/lib/god/contacts/jabber.rb +75 -0
  45. data/lib/god/contacts/prowl.rb +57 -0
  46. data/lib/god/contacts/scout.rb +55 -0
  47. data/lib/god/contacts/sensu.rb +59 -0
  48. data/lib/god/contacts/slack.rb +98 -0
  49. data/lib/god/contacts/statsd.rb +46 -0
  50. data/lib/god/contacts/twitter.rb +51 -0
  51. data/lib/god/contacts/webhook.rb +74 -0
  52. data/lib/god/driver.rb +238 -0
  53. data/lib/god/errors.rb +24 -0
  54. data/lib/god/event_handler.rb +112 -0
  55. data/lib/god/event_handlers/dummy_handler.rb +13 -0
  56. data/lib/god/event_handlers/kqueue_handler.rb +17 -0
  57. data/lib/god/event_handlers/netlink_handler.rb +13 -0
  58. data/lib/god/logger.rb +109 -0
  59. data/lib/god/metric.rb +87 -0
  60. data/lib/god/process.rb +381 -0
  61. data/lib/god/registry.rb +32 -0
  62. data/lib/god/simple_logger.rb +59 -0
  63. data/lib/god/socket.rb +113 -0
  64. data/lib/god/sugar.rb +62 -0
  65. data/lib/god/sys_logger.rb +45 -0
  66. data/lib/god/system/portable_poller.rb +42 -0
  67. data/lib/god/system/process.rb +50 -0
  68. data/lib/god/system/slash_proc_poller.rb +92 -0
  69. data/lib/god/task.rb +552 -0
  70. data/lib/god/timeline.rb +25 -0
  71. data/lib/god/trigger.rb +43 -0
  72. data/lib/god/version.rb +4 -0
  73. data/lib/god/watch.rb +340 -0
  74. data/lib/god.rb +777 -0
  75. data/test/configs/child_events/child_events.god +44 -0
  76. data/test/configs/child_events/simple_server.rb +3 -0
  77. data/test/configs/child_polls/child_polls.god +37 -0
  78. data/test/configs/child_polls/simple_server.rb +12 -0
  79. data/test/configs/complex/complex.god +59 -0
  80. data/test/configs/complex/simple_server.rb +3 -0
  81. data/test/configs/contact/contact.god +118 -0
  82. data/test/configs/contact/simple_server.rb +3 -0
  83. data/test/configs/daemon_events/daemon_events.god +37 -0
  84. data/test/configs/daemon_events/simple_server.rb +8 -0
  85. data/test/configs/daemon_events/simple_server_stop.rb +11 -0
  86. data/test/configs/daemon_polls/daemon_polls.god +17 -0
  87. data/test/configs/daemon_polls/simple_server.rb +6 -0
  88. data/test/configs/degrading_lambda/degrading_lambda.god +31 -0
  89. data/test/configs/degrading_lambda/tcp_server.rb +15 -0
  90. data/test/configs/keepalive/keepalive.god +9 -0
  91. data/test/configs/keepalive/keepalive.rb +12 -0
  92. data/test/configs/lifecycle/lifecycle.god +25 -0
  93. data/test/configs/matias/matias.god +50 -0
  94. data/test/configs/real.rb +59 -0
  95. data/test/configs/running_load/running_load.god +16 -0
  96. data/test/configs/stop_options/simple_server.rb +12 -0
  97. data/test/configs/stop_options/stop_options.god +39 -0
  98. data/test/configs/stress/simple_server.rb +3 -0
  99. data/test/configs/stress/stress.god +15 -0
  100. data/test/configs/task/logs/.placeholder +0 -0
  101. data/test/configs/task/task.god +26 -0
  102. data/test/configs/test.rb +61 -0
  103. data/test/configs/usr1_trapper.rb +10 -0
  104. data/test/helper.rb +172 -0
  105. data/test/suite.rb +6 -0
  106. data/test/test_airbrake.rb +14 -0
  107. data/test/test_behavior.rb +18 -0
  108. data/test/test_campfire.rb +22 -0
  109. data/test/test_condition.rb +52 -0
  110. data/test/test_conditions_disk_usage.rb +50 -0
  111. data/test/test_conditions_http_response_code.rb +109 -0
  112. data/test/test_conditions_process_running.rb +40 -0
  113. data/test/test_conditions_socket_responding.rb +176 -0
  114. data/test/test_conditions_tries.rb +67 -0
  115. data/test/test_contact.rb +109 -0
  116. data/test/test_driver.rb +26 -0
  117. data/test/test_email.rb +34 -0
  118. data/test/test_event_handler.rb +82 -0
  119. data/test/test_god.rb +710 -0
  120. data/test/test_god_system.rb +201 -0
  121. data/test/test_handlers_kqueue_handler.rb +16 -0
  122. data/test/test_hipchat.rb +23 -0
  123. data/test/test_jabber.rb +29 -0
  124. data/test/test_logger.rb +55 -0
  125. data/test/test_metric.rb +74 -0
  126. data/test/test_process.rb +263 -0
  127. data/test/test_prowl.rb +15 -0
  128. data/test/test_registry.rb +15 -0
  129. data/test/test_sensu.rb +11 -0
  130. data/test/test_slack.rb +57 -0
  131. data/test/test_socket.rb +34 -0
  132. data/test/test_statsd.rb +22 -0
  133. data/test/test_sugar.rb +42 -0
  134. data/test/test_system_portable_poller.rb +17 -0
  135. data/test/test_system_process.rb +30 -0
  136. data/test/test_task.rb +246 -0
  137. data/test/test_timeline.rb +37 -0
  138. data/test/test_trigger.rb +63 -0
  139. data/test/test_watch.rb +286 -0
  140. data/test/test_webhook.rb +22 -0
  141. metadata +476 -0
@@ -0,0 +1,268 @@
1
+ module God
2
+ module CLI
3
+
4
+ class Command
5
+ def initialize(command, options, args)
6
+ @command = command
7
+ @options = options
8
+ @args = args
9
+
10
+ dispatch
11
+ end
12
+
13
+ def setup
14
+ # connect to drb unix socket
15
+ @server = DRbObject.new(nil, God::Socket.socket(@options[:port]))
16
+
17
+ # ping server to ensure that it is responsive
18
+ begin
19
+ @server.ping
20
+ rescue DRb::DRbConnError
21
+ puts "The server is not available (or you do not have permissions to access it)"
22
+ abort
23
+ end
24
+ end
25
+
26
+ def dispatch
27
+ if %w{load status signal log quit terminate}.include?(@command)
28
+ setup
29
+ send("#{@command}_command")
30
+ elsif %w{start stop restart monitor unmonitor remove}.include?(@command)
31
+ setup
32
+ lifecycle_command
33
+ elsif @command == 'check'
34
+ check_command
35
+ else
36
+ puts "Command '#{@command}' is not valid. Run 'god --help' for usage"
37
+ abort
38
+ end
39
+ end
40
+
41
+ def load_command
42
+ file = @args[1]
43
+ action = @args[2] || 'leave'
44
+
45
+ unless ['stop', 'remove', 'leave', ''].include?(action)
46
+ puts "Command '#{@command}' action must be either 'stop', 'remove' or 'leave'"
47
+ exit(1)
48
+ end
49
+
50
+ puts "Sending '#{@command}' command with action '#{action}'"
51
+ puts
52
+
53
+ unless File.exist?(file)
54
+ abort "File not found: #{file}"
55
+ end
56
+
57
+ affected, errors, removed = *@server.running_load(File.read(file), File.expand_path(file), action)
58
+
59
+ # output response
60
+ unless affected.empty?
61
+ puts 'The following tasks were affected:'
62
+ affected.each do |w|
63
+ puts ' ' + w
64
+ end
65
+ end
66
+
67
+ unless removed.empty?
68
+ puts 'The following tasks were removed:'
69
+ removed.each do |w|
70
+ puts ' ' + w
71
+ end
72
+ end
73
+
74
+ unless errors.empty?
75
+ puts errors
76
+ exit(1)
77
+ end
78
+ end
79
+
80
+ def status_command
81
+ exitcode = 0
82
+ statuses = @server.status
83
+ groups = {}
84
+ statuses.each do |name, status|
85
+ g = status[:group] || ''
86
+ groups[g] ||= {}
87
+ groups[g][name] = status
88
+ end
89
+
90
+ if item = @args[1]
91
+ if single = statuses[item]
92
+ # specified task (0 -> up, 1 -> unmonitored, 2 -> other)
93
+ state = single[:state]
94
+ puts "#{item}: #{state}"
95
+ exitcode = state == :up ? 0 : (state == :unmonitored ? 1 : 2)
96
+ elsif groups[item]
97
+ # specified group (0 -> up, N -> other)
98
+ puts "#{item}:"
99
+ groups[item].keys.sort.each do |name|
100
+ state = groups[item][name][:state]
101
+ print " "
102
+ puts "#{name}: #{state}"
103
+ exitcode += 1 unless state == :up
104
+ end
105
+ else
106
+ puts "Task or Group '#{item}' not found."
107
+ exit(1)
108
+ end
109
+ else
110
+ # show all groups and watches
111
+ groups.keys.sort.each do |group|
112
+ puts "#{group}:" unless group.empty?
113
+ groups[group].keys.sort.each do |name|
114
+ state = groups[group][name][:state]
115
+ print " " unless group.empty?
116
+ puts "#{name}: #{state}"
117
+ end
118
+ end
119
+ end
120
+
121
+ exit(exitcode)
122
+ end
123
+
124
+ def signal_command
125
+ # get the name of the watch/group
126
+ name = @args[1]
127
+ signal = @args[2]
128
+
129
+ puts "Sending signal '#{signal}' to '#{name}'"
130
+
131
+ t = Thread.new { loop { sleep(1); STDOUT.print('.'); STDOUT.flush; sleep(1) } }
132
+
133
+ watches = @server.signal(name, signal)
134
+
135
+ # output response
136
+ t.kill; STDOUT.puts
137
+ unless watches.empty?
138
+ puts 'The following watches were affected:'
139
+ watches.each do |w|
140
+ puts ' ' + w
141
+ end
142
+ else
143
+ puts 'No matching task or group'
144
+ end
145
+ end
146
+
147
+ def log_command
148
+ begin
149
+ Signal.trap('INT') { exit }
150
+ name = @args[1]
151
+
152
+ unless name
153
+ puts "You must specify a Task or Group name"
154
+ exit!
155
+ end
156
+
157
+ puts "Please wait..."
158
+ t = Time.at(0)
159
+ loop do
160
+ print @server.running_log(name, t)
161
+ t = Time.now
162
+ sleep 0.25
163
+ end
164
+ rescue God::NoSuchWatchError
165
+ puts "No such watch"
166
+ rescue DRb::DRbConnError
167
+ puts "The server went away"
168
+ end
169
+ end
170
+
171
+ def quit_command
172
+ begin
173
+ @server.terminate
174
+ abort 'Could not stop god'
175
+ rescue DRb::DRbConnError
176
+ puts 'Stopped god'
177
+ end
178
+ end
179
+
180
+ def terminate_command
181
+ t = Thread.new { loop { STDOUT.print('.'); STDOUT.flush; sleep(1) } }
182
+ if @server.stop_all
183
+ t.kill; STDOUT.puts
184
+ puts 'Stopped all watches'
185
+ else
186
+ t.kill; STDOUT.puts
187
+ puts "Could not stop all watches within #{@server.terminate_timeout} seconds"
188
+ end
189
+
190
+ begin
191
+ @server.terminate
192
+ abort 'Could not stop god'
193
+ rescue DRb::DRbConnError
194
+ puts 'Stopped god'
195
+ end
196
+ end
197
+
198
+ def check_command
199
+ Thread.new do
200
+ begin
201
+ event_system = God::EventHandler.event_system
202
+ puts "using event system: #{event_system}"
203
+
204
+ if God::EventHandler.loaded?
205
+ puts "starting event handler"
206
+ God::EventHandler.start
207
+ else
208
+ puts "[fail] event system did not load"
209
+ exit(1)
210
+ end
211
+
212
+ puts 'forking off new process'
213
+
214
+ pid = fork do
215
+ loop { sleep(1) }
216
+ end
217
+
218
+ puts "forked process with pid = #{pid}"
219
+
220
+ God::EventHandler.register(pid, :proc_exit) do
221
+ puts "[ok] process exit event received"
222
+ exit!(0)
223
+ end
224
+
225
+ sleep(1)
226
+
227
+ puts "killing process"
228
+
229
+ ::Process.kill('KILL', pid)
230
+ ::Process.waitpid(pid)
231
+ rescue => e
232
+ puts e.message
233
+ puts e.backtrace.join("\n")
234
+ end
235
+ end
236
+
237
+ sleep(2)
238
+
239
+ puts "[fail] never received process exit event"
240
+ exit(1)
241
+ end
242
+
243
+ def lifecycle_command
244
+ # get the name of the watch/group
245
+ name = @args[1]
246
+
247
+ puts "Sending '#{@command}' command"
248
+
249
+ t = Thread.new { loop { sleep(1); STDOUT.print('.'); STDOUT.flush; sleep(1) } }
250
+
251
+ # send @command
252
+ watches = @server.control(name, @command)
253
+
254
+ # output response
255
+ t.kill; STDOUT.puts
256
+ unless watches.empty?
257
+ puts 'The following watches were affected:'
258
+ watches.each do |w|
259
+ puts ' ' + w
260
+ end
261
+ else
262
+ puts 'No matching task or group'
263
+ end
264
+ end
265
+ end # Command
266
+
267
+ end
268
+ end
@@ -0,0 +1,170 @@
1
+ module God
2
+ module CLI
3
+
4
+ class Run
5
+ def initialize(options)
6
+ @options = options
7
+
8
+ dispatch
9
+ end
10
+
11
+ def dispatch
12
+ # have at_exit start god
13
+ $run = true
14
+
15
+ if @options[:syslog]
16
+ require 'god/sys_logger'
17
+ end
18
+
19
+ # run
20
+ if @options[:daemonize]
21
+ run_daemonized
22
+ else
23
+ run_in_front
24
+ end
25
+ end
26
+
27
+ def attach
28
+ process = System::Process.new(@options[:attach])
29
+ Thread.new do
30
+ loop do
31
+ unless process.exists?
32
+ applog(nil, :info, "Going down because attached process #{@options[:attach]} exited")
33
+ exit!
34
+ end
35
+ sleep 5
36
+ end
37
+ end
38
+ end
39
+
40
+ def default_run
41
+ # make sure we have STDIN/STDOUT redirected immediately
42
+ setup_logging
43
+
44
+ # start attached pid watcher if necessary
45
+ if @options[:attach]
46
+ self.attach
47
+ end
48
+
49
+ if @options[:port]
50
+ God.port = @options[:port]
51
+ end
52
+
53
+ if @options[:events]
54
+ God::EventHandler.load
55
+ end
56
+
57
+ # set log level, defaults to WARN
58
+ if @options[:log_level]
59
+ God.log_level = @options[:log_level]
60
+ else
61
+ God.log_level = @options[:daemonize] ? :warn : :info
62
+ end
63
+
64
+ if @options[:config]
65
+ if !@options[:config].include?('*') && !File.exist?(@options[:config])
66
+ abort "File not found: #{@options[:config]}"
67
+ end
68
+
69
+ # start the event handler
70
+ God::EventHandler.start if God::EventHandler.loaded?
71
+
72
+ load_config @options[:config]
73
+ end
74
+ setup_logging
75
+ end
76
+
77
+ def run_in_front
78
+ require 'god'
79
+
80
+ default_run
81
+ end
82
+
83
+ def run_daemonized
84
+ # trap and ignore SIGHUP
85
+ Signal.trap('HUP') {}
86
+ # trap and log-reopen SIGUSR1
87
+ Signal.trap('USR1') { setup_logging }
88
+
89
+ pid = fork do
90
+ begin
91
+ require 'god'
92
+
93
+ # set pid if requested
94
+ if @options[:pid] # and as deamon
95
+ God.pid = @options[:pid]
96
+ end
97
+
98
+ default_run
99
+
100
+ unless God::EventHandler.loaded?
101
+ puts
102
+ puts "***********************************************************************"
103
+ puts "*"
104
+ puts "* Event conditions are not available for your installation of god."
105
+ puts "* You may still use and write custom conditions using the poll system"
106
+ puts "*"
107
+ puts "***********************************************************************"
108
+ puts
109
+ end
110
+
111
+ rescue => e
112
+ puts e.message
113
+ puts e.backtrace.join("\n")
114
+ abort "There was a fatal system error while starting god (see above)"
115
+ end
116
+ end
117
+
118
+ if @options[:pid]
119
+ File.open(@options[:pid], 'w') { |f| f.write pid }
120
+ end
121
+
122
+ ::Process.detach pid
123
+
124
+ exit
125
+ end
126
+
127
+ def setup_logging
128
+ log_file = God.log_file
129
+ log_file = File.expand_path(@options[:log]) if @options[:log]
130
+ log_file = "/dev/null" if !log_file && @options[:daemonize]
131
+ if log_file
132
+ puts "Sending output to log file: #{log_file}" unless @options[:daemonize]
133
+
134
+ # reset file descriptors
135
+ STDIN.reopen "/dev/null"
136
+ STDOUT.reopen(log_file, "a")
137
+ STDERR.reopen STDOUT
138
+ STDOUT.sync = true
139
+ end
140
+ end
141
+
142
+ def load_config(config)
143
+ files = File.directory?(config) ? Dir['**/*.god'] : Dir[config]
144
+ abort "No files could be found" if files.empty?
145
+ files.each do |god_file|
146
+ unless load_god_file(god_file)
147
+ abort "File '#{god_file}' could not be loaded"
148
+ end
149
+ end
150
+ end
151
+
152
+ def load_god_file(god_file)
153
+ applog(nil, :info, "Loading #{god_file}")
154
+ load File.expand_path(god_file)
155
+ true
156
+ rescue Exception => e
157
+ if e.instance_of?(SystemExit)
158
+ raise
159
+ else
160
+ puts "There was an error in #{god_file}"
161
+ puts "\t" + e.message
162
+ puts "\t" + e.backtrace.join("\n\t")
163
+ false
164
+ end
165
+ end
166
+
167
+ end # Run
168
+
169
+ end
170
+ end
@@ -0,0 +1,23 @@
1
+ module God
2
+ module CLI
3
+
4
+ class Version
5
+ def self.version
6
+ require 'god'
7
+
8
+ # print version
9
+ puts "Version #{God.version}"
10
+ exit
11
+ end
12
+
13
+ def self.version_extended
14
+ puts "Version: #{God.version}"
15
+ puts "Polls: enabled"
16
+ puts "Events: " + God::EventHandler.event_system
17
+
18
+ exit
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ require 'monitor'
2
+
3
+ # Taken from http://redmine.ruby-lang.org/repositories/entry/ruby-19/lib/monitor.rb
4
+
5
+ module MonitorMixin
6
+ class ConditionVariable
7
+ def wait(timeout = nil)
8
+ @monitor.__send__(:mon_check_owner)
9
+ count = @monitor.__send__(:mon_exit_for_cond)
10
+ begin
11
+ @cond.wait(@monitor.instance_variable_get("@mon_mutex"), timeout)
12
+ return true
13
+ ensure
14
+ @monitor.__send__(:mon_enter_for_cond, count)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Taken from http://redmine.ruby-lang.org/repositories/entry/ruby-19/lib/thread.rb
21
+
22
+ class ConditionVariable
23
+ def wait(mutex, timeout=nil)
24
+ begin
25
+ # TODO: mutex should not be used
26
+ @waiters_mutex.synchronize do
27
+ @waiters.push(Thread.current)
28
+ end
29
+ mutex.sleep timeout
30
+ end
31
+ self
32
+ end
33
+ end
@@ -0,0 +1,96 @@
1
+ module God
2
+
3
+ class Condition < Behavior
4
+ attr_accessor :transition, :notify, :info, :phase
5
+
6
+ # Generate a Condition of the given kind. The proper class if found by camel casing the
7
+ # kind (which is given as an underscored symbol).
8
+ # +kind+ is the underscored symbol representing the class (e.g. :foo_bar for God::Conditions::FooBar)
9
+ def self.generate(kind, watch)
10
+ sym = kind.to_s.capitalize.gsub(/_(.)/){$1.upcase}.intern
11
+ c = God::Conditions.const_get(sym).new
12
+
13
+ unless c.kind_of?(PollCondition) || c.kind_of?(EventCondition) || c.kind_of?(TriggerCondition)
14
+ abort "Condition '#{c.class.name}' must subclass God::PollCondition, God::EventCondition, or God::TriggerCondition"
15
+ end
16
+
17
+ if !EventHandler.loaded? && c.kind_of?(EventCondition)
18
+ abort "Condition '#{c.class.name}' requires an event system but none has been loaded"
19
+ end
20
+
21
+ c.watch = watch
22
+ c
23
+ rescue NameError
24
+ raise NoSuchConditionError.new("No Condition found with the class name God::Conditions::#{sym}")
25
+ end
26
+
27
+ def self.valid?(condition)
28
+ valid = true
29
+ if condition.notify
30
+ begin
31
+ Contact.normalize(condition.notify)
32
+ rescue ArgumentError => e
33
+ valid &= Configurable.complain("Attribute 'notify' " + e.message, condition)
34
+ end
35
+ end
36
+ valid
37
+ end
38
+
39
+ # Construct the friendly name of this Condition, looks like:
40
+ #
41
+ # Condition FooBar on Watch 'baz'
42
+ def friendly_name
43
+ "Condition #{self.class.name.split('::').last} on Watch '#{self.watch.name}'"
44
+ end
45
+ end
46
+
47
+ class PollCondition < Condition
48
+ # all poll conditions can specify a poll interval
49
+ attr_accessor :interval
50
+
51
+ # Override this method in your Conditions (optional)
52
+ def before
53
+ end
54
+
55
+ # Override this method in your Conditions (mandatory)
56
+ #
57
+ # Return true if the test passes (everything is ok)
58
+ # Return false otherwise
59
+ def test
60
+ raise AbstractMethodNotOverriddenError.new("PollCondition#test must be overridden in subclasses")
61
+ end
62
+
63
+ # Override this method in your Conditions (optional)
64
+ def after
65
+ end
66
+ end
67
+
68
+ class EventCondition < Condition
69
+ def register
70
+ raise AbstractMethodNotOverriddenError.new("EventCondition#register must be overridden in subclasses")
71
+ end
72
+
73
+ def deregister
74
+ raise AbstractMethodNotOverriddenError.new("EventCondition#deregister must be overridden in subclasses")
75
+ end
76
+ end
77
+
78
+ class TriggerCondition < Condition
79
+ def process(event, payload)
80
+ raise AbstractMethodNotOverriddenError.new("TriggerCondition#process must be overridden in subclasses")
81
+ end
82
+
83
+ def trigger
84
+ self.watch.trigger(self)
85
+ end
86
+
87
+ def register
88
+ Trigger.register(self)
89
+ end
90
+
91
+ def deregister
92
+ Trigger.deregister(self)
93
+ end
94
+ end
95
+
96
+ end
@@ -0,0 +1,36 @@
1
+ module God
2
+ module Conditions
3
+ # Always trigger or never trigger.
4
+ #
5
+ # Examples
6
+ #
7
+ # # Always trigger.
8
+ # on.condition(:always) do |c|
9
+ # c.what = true
10
+ # end
11
+ #
12
+ # # Never trigger.
13
+ # on.condition(:always) do |c|
14
+ # c.what = false
15
+ # end
16
+ class Always < PollCondition
17
+ # The Boolean determining whether this condition will always trigger
18
+ # (true) or never trigger (false).
19
+ attr_accessor :what
20
+
21
+ def initialize
22
+ self.info = "always"
23
+ end
24
+
25
+ def valid?
26
+ valid = true
27
+ valid &= complain("Attribute 'what' must be specified", self) if self.what.nil?
28
+ valid
29
+ end
30
+
31
+ def test
32
+ @what
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,86 @@
1
+ module God
2
+ module Conditions
3
+
4
+ class Complex < PollCondition
5
+ AND = 0x1
6
+ OR = 0x2
7
+ NOT = 0x4
8
+
9
+ def initialize()
10
+ super
11
+
12
+ @oper_stack = []
13
+ @op_stack = []
14
+
15
+ @this = nil
16
+ end
17
+
18
+ def valid?
19
+ @oper_stack.inject(true) { |acc, oper| acc & oper.valid? }
20
+ end
21
+
22
+ def prepare
23
+ @oper_stack.each { |oper| oper.prepare }
24
+ end
25
+
26
+ def new_oper(kind, op)
27
+ oper = Condition.generate(kind, self.watch)
28
+ @oper_stack.push(oper)
29
+ @op_stack.push(op)
30
+ oper
31
+ end
32
+
33
+ def this(kind)
34
+ @this = Condition.generate(kind, self.watch)
35
+ yield @this if block_given?
36
+ end
37
+
38
+ def and(kind)
39
+ oper = new_oper(kind, 0x1)
40
+ yield oper if block_given?
41
+ end
42
+
43
+ def and_not(kind)
44
+ oper = new_oper(kind, 0x5)
45
+ yield oper if block_given?
46
+ end
47
+
48
+ def or(kind)
49
+ oper = new_oper(kind, 0x2)
50
+ yield oper if block_given?
51
+ end
52
+
53
+ def or_not(kind)
54
+ oper = new_oper(kind, 0x6)
55
+ yield oper if block_given?
56
+ end
57
+
58
+ def test
59
+ if @this.nil?
60
+ # Although this() makes sense semantically and therefore
61
+ # encourages easy-to-read conditions, being able to omit it
62
+ # allows for more DRY code in some cases, so we deal with a
63
+ # nil @this here by initially setting res to true or false,
64
+ # depending on whether the first operator used is AND or OR
65
+ # respectively.
66
+ if 0 < @op_stack[0] & AND
67
+ res = true
68
+ else
69
+ res = false
70
+ end
71
+ else
72
+ res = @this.test
73
+ end
74
+
75
+ @op_stack.each do |op|
76
+ cond = @oper_stack.shift
77
+ eval "res " + ((0 < op & AND) ? "&&" : "||") + "= " + ((0 < op & NOT) ? "!" : "") + "cond.test"
78
+ @oper_stack.push cond
79
+ end
80
+
81
+ res
82
+ end
83
+ end
84
+
85
+ end
86
+ end