daemon-kit 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/History.txt +17 -0
  2. data/Manifest.txt +62 -0
  3. data/PostInstall.txt +6 -0
  4. data/README.textile +94 -0
  5. data/Rakefile +31 -0
  6. data/TODO.txt +24 -0
  7. data/app_generators/daemon_kit/USAGE +7 -0
  8. data/app_generators/daemon_kit/daemon_kit_generator.rb +120 -0
  9. data/app_generators/daemon_kit/templates/README +48 -0
  10. data/app_generators/daemon_kit/templates/Rakefile +4 -0
  11. data/app_generators/daemon_kit/templates/bin/daemon.erb +7 -0
  12. data/app_generators/daemon_kit/templates/config/boot.rb +68 -0
  13. data/app_generators/daemon_kit/templates/config/environment.rb +19 -0
  14. data/app_generators/daemon_kit/templates/config/environments/development.rb +0 -0
  15. data/app_generators/daemon_kit/templates/config/environments/production.rb +0 -0
  16. data/app_generators/daemon_kit/templates/config/environments/test.rb +0 -0
  17. data/app_generators/daemon_kit/templates/config/initializers/readme +11 -0
  18. data/app_generators/daemon_kit/templates/libexec/daemon.erb +18 -0
  19. data/bin/daemon_kit +19 -0
  20. data/daemon_generators/amqp/USAGE +5 -0
  21. data/daemon_generators/amqp/amqp_generator.rb +65 -0
  22. data/daemon_generators/amqp/templates/config/amqp.yml +28 -0
  23. data/daemon_generators/amqp/templates/config/initializers/amqp.rb +7 -0
  24. data/daemon_generators/amqp/templates/libexec/daemon.rb +29 -0
  25. data/daemon_generators/cron/USAGE +5 -0
  26. data/daemon_generators/cron/cron_generator.rb +64 -0
  27. data/daemon_generators/cron/templates/config/initializers/cron.rb +7 -0
  28. data/daemon_generators/cron/templates/libexec/daemon.rb +39 -0
  29. data/daemon_generators/jabber/USAGE +5 -0
  30. data/daemon_generators/jabber/jabber_generator.rb +65 -0
  31. data/daemon_generators/jabber/templates/config/initializers/jabber.rb +7 -0
  32. data/daemon_generators/jabber/templates/config/jabber.yml +26 -0
  33. data/daemon_generators/jabber/templates/libexec/daemon.rb +27 -0
  34. data/lib/daemon_kit.rb +14 -0
  35. data/lib/daemon_kit/amqp.rb +41 -0
  36. data/lib/daemon_kit/application.rb +34 -0
  37. data/lib/daemon_kit/cron.rb +38 -0
  38. data/lib/daemon_kit/initializer.rb +255 -0
  39. data/lib/daemon_kit/jabber.rb +172 -0
  40. data/lib/daemon_kit/patches/force_kill_wait.rb +120 -0
  41. data/lib/daemon_kit/tasks.rb +2 -0
  42. data/lib/daemon_kit/tasks/framework.rake +75 -0
  43. data/rubygems_generators/install_rspec/USAGE +5 -0
  44. data/rubygems_generators/install_rspec/install_rspec_generator.rb +57 -0
  45. data/rubygems_generators/install_rspec/templates/spec.rb +11 -0
  46. data/rubygems_generators/install_rspec/templates/spec/spec.opts +1 -0
  47. data/rubygems_generators/install_rspec/templates/spec/spec_helper.rb +10 -0
  48. data/rubygems_generators/install_rspec/templates/tasks/rspec.rake +21 -0
  49. data/script/console +10 -0
  50. data/script/destroy +14 -0
  51. data/script/generate +14 -0
  52. data/script/txt2html +71 -0
  53. data/spec/daemon_kit_spec.rb +7 -0
  54. data/spec/initializer_spec.rb +31 -0
  55. data/spec/spec.opts +1 -0
  56. data/spec/spec_helper.rb +30 -0
  57. data/tasks/rspec.rake +21 -0
  58. data/test/test_amqp_generator.rb +48 -0
  59. data/test/test_cron_generator.rb +45 -0
  60. data/test/test_daemon-kit_generator.rb +67 -0
  61. data/test/test_generator_helper.rb +29 -0
  62. data/test/test_jabber_generator.rb +49 -0
  63. metadata +168 -0
@@ -0,0 +1,27 @@
1
+ # Generated jabber daemon
2
+
3
+ # Do your post daemonization configuration here
4
+ # At minimum you need just the first line (without the block), or a lot
5
+ # of strange things might start happening...
6
+ DaemonKit::Application.running! do |config|
7
+ # Trap signals with blocks or procs
8
+ # config.trap( 'INT' ) do
9
+ # # do something clever
10
+ # end
11
+ # config.trap( 'TERM', Proc.new { puts 'Going down' } )
12
+ end
13
+
14
+ # IMPORTANT CONFIGURATION NOTE
15
+ #
16
+ # Please review and update 'config/jabber.yml' accordingly or this
17
+ # daemon won't work as advertised.
18
+
19
+ # This block gets called every time a message has been received from a
20
+ # valid master.
21
+ DaemonKit::Jabber.received_messages do |message|
22
+ # Simple echo service
23
+ DaemonKit::Jabber.deliver( message.from, message.body )
24
+ end
25
+
26
+ # Run our Jabber bot
27
+ DaemonKit::Jabber.run
data/lib/daemon_kit.rb ADDED
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+
6
+ module DaemonKit
7
+ VERSION = '0.1.3'
8
+
9
+ autoload :Initializer, 'daemon_kit/initializer'
10
+ autoload :Application, 'daemon_kit/application'
11
+ autoload :Cron, 'daemon_kit/cron'
12
+ autoload :Jabber, 'daemon_kit/jabber'
13
+ autoload :AMQP, 'daemon_kit/amqp'
14
+ end
@@ -0,0 +1,41 @@
1
+ require 'yaml'
2
+
3
+ module DaemonKit
4
+ # Thin wrapper around the amqp gem, specifically designed to ease
5
+ # configuration of a AMQP consumer daemon and provide some added
6
+ # simplicity
7
+ class AMQP
8
+
9
+ @@instance = nil
10
+
11
+ class << self
12
+
13
+ def instance
14
+ @instance ||= (
15
+ config = YAML.load_file( "#{DAEMON_ROOT}/config/amqp.yml" )[DAEMON_ENV]
16
+ raise ArgumentError, "Missing AMQP configuration for #{DAEMON_ENV} environment" if config.nil?
17
+ new( config )
18
+ )
19
+ end
20
+
21
+ private :new
22
+
23
+ def run(&block)
24
+ instance.run(&block)
25
+ end
26
+ end
27
+
28
+ def initialize( config = {} )
29
+ @config = config.inject({}) { |m,c| m[c[0].to_sym] = c[1]; m } # symbolize_keys
30
+ end
31
+
32
+ def run(&block)
33
+ # Ensure graceful shutdown of the connection to the broker
34
+ DaemonKit.trap('INT') { ::AMQP.stop { ::EM.stop } }
35
+ DaemonKit.trap('TERM') { ::AMQP.stop { ::EM.stop } }
36
+
37
+ # Start our event loop
38
+ ::AMQP.start(@config, &block)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ require 'daemons'
2
+
3
+ module DaemonKit
4
+
5
+ # Class responsible for making the daemons run and keep them running.
6
+ class Application
7
+
8
+ class << self
9
+
10
+ # Run the file as a daemon
11
+ def run( file )
12
+ raise DaemonNotFound.new( file ) unless File.exist?( file )
13
+
14
+ app_name = DaemonKit.configuration.daemon_name || File.basename( file )
15
+ options = { :backtrace => true, :log_output => true, :app_name => app_name }
16
+
17
+ options[:dir_mode] = DaemonKit.configuration.dir_mode || :normal
18
+ options[:dir] = DaemonKit.configuration.dir || "log"
19
+ options[:multiple] = DaemonKit.configuration.multiple
20
+ options[:force_kill_wait] = DaemonKit.configuration.force_kill_wait if DaemonKit.configuration.force_kill_wait
21
+
22
+ Daemons.run( file, options )
23
+ end
24
+
25
+ # Call this from inside a daemonized process to complete the
26
+ # initialization process
27
+ def running!
28
+ DaemonKit::Initializer.continue!
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ module DaemonKit
2
+
3
+ # Thin wrapper around rufus-scheduler gem, specifically designed to ease
4
+ # configuration of a scheduler and provide some added simplicity.
5
+ class Cron
6
+
7
+ @@instance = nil
8
+
9
+ attr_reader :scheduler
10
+
11
+ class << self
12
+
13
+ def instance
14
+ @instance ||= new
15
+ end
16
+
17
+ def scheduler
18
+ instance.scheduler
19
+ end
20
+
21
+ private :new
22
+
23
+ def run
24
+ DaemonKit.logger.info "Starting rufus-scheduler"
25
+
26
+ begin
27
+ instance.scheduler.join
28
+ rescue Interrupt
29
+ DaemonKit.logger.warn "Scheduler interrupted"
30
+ end
31
+ end
32
+ end
33
+
34
+ def initialize
35
+ @scheduler = Rufus::Scheduler.start_new
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,255 @@
1
+ require 'logger'
2
+ require 'pathname'
3
+
4
+ DAEMON_ENV = (ENV['DAEMON_ENV'] || 'development').dup unless defined?(DAEMON_ENV)
5
+
6
+ $:.unshift File.dirname(__FILE__) + '/..'
7
+ require 'daemon_kit'
8
+
9
+ module DaemonKit
10
+
11
+ class << self
12
+
13
+ def logger
14
+ @logger
15
+ end
16
+
17
+ def logger=( logger )
18
+ @logger = logger
19
+ end
20
+
21
+ def configuration
22
+ @configuration
23
+ end
24
+
25
+ def configuration=( configuration )
26
+ @configuration = configuration
27
+ end
28
+
29
+ def trap( *args, &block )
30
+ self.configuration.trap( *args, &block )
31
+ end
32
+
33
+ end
34
+
35
+
36
+ # This class does all the nightmare work of setting up a working
37
+ # environment for your daemon.
38
+ class Initializer
39
+
40
+ attr_reader :configuration
41
+
42
+ def self.run( configuration = Configuration.new )
43
+ yield configuration if block_given?
44
+ initializer = new configuration
45
+ initializer.before_daemonize
46
+ initializer
47
+ end
48
+
49
+ def self.continue!
50
+ initializer = new DaemonKit.configuration
51
+ initializer.after_daemonize
52
+ end
53
+
54
+ def self.shutdown
55
+ DaemonKit.logger.warn "Shutting down"
56
+ exit
57
+ end
58
+
59
+ def initialize( configuration )
60
+ @configuration = configuration
61
+ end
62
+
63
+ def before_daemonize
64
+ DaemonKit.configuration = @configuration
65
+
66
+ set_load_path
67
+ load_gems
68
+ load_patches
69
+ load_environment
70
+ end
71
+
72
+ def after_daemonize
73
+ initialize_logger
74
+ initialize_signal_traps
75
+ end
76
+
77
+ def set_load_path
78
+ configuration.load_paths.each do |d|
79
+ $:.unshift( "#{DAEMON_ROOT}/#{d}" ) if File.directory?( "#{DAEMON_ROOT}/#{d}" )
80
+ end
81
+ end
82
+
83
+ def load_gems
84
+
85
+ end
86
+
87
+ def load_patches
88
+ if !!configuration.force_kill_wait
89
+ require 'daemon_kit/patches/force_kill_wait'
90
+ end
91
+ end
92
+
93
+ def load_environment
94
+ return if @environment_loaded
95
+ @environment_loaded = true
96
+
97
+ config = configuration
98
+
99
+ eval(IO.read(configuration.environment_path), binding, configuration.environment_path)
100
+
101
+ eval(IO.read(configuration.daemon_initializer), binding, configuration.daemon_initializer) if File.exist?( configuration.daemon_initializer )
102
+ end
103
+
104
+ def initialize_logger
105
+ return if DaemonKit.logger
106
+
107
+ unless logger = configuration.logger
108
+ logger = Logger.new( configuration.log_path )
109
+ logger.level = configuration.log_level
110
+ end
111
+
112
+ DaemonKit.logger = logger
113
+
114
+ configuration.trap("USR1") {
115
+ DaemonKit.logger.level = DaemonKit.logger.debug? ? Logger::INFO : Logger::DEBUG
116
+ DaemonKit.logger.info "Log level changed to #{DaemonKit.logger.debug? ? 'DEBUG' : 'INFO' }"
117
+ }
118
+ configuration.trap("USR2") {
119
+ DaemonKit.logger.level = Logger::DEBUG
120
+ DaemonKit.logger.info "Log level changed to DEBUG"
121
+ }
122
+
123
+ DaemonKit.logger.info "DaemonKit up and running in #{DAEMON_ENV} mode"
124
+ end
125
+
126
+ def initialize_signal_traps
127
+ term_proc = Proc.new { DaemonKit::Initializer.shutdown }
128
+ configuration.trap( 'INT', term_proc )
129
+ configuration.trap( 'TERM', term_proc )
130
+ end
131
+
132
+ end
133
+
134
+ # Holds our various configuration values
135
+ class Configuration
136
+ # Root to the daemon
137
+ attr_reader :root_path
138
+
139
+ # List of load paths
140
+ attr_accessor :load_paths
141
+
142
+ # The log level to use, defaults to DEBUG
143
+ attr_accessor :log_level
144
+
145
+ # Path to the log file, defaults to 'log/<environment>.log'
146
+ attr_accessor :log_path
147
+
148
+ # :system,
149
+ attr_accessor :dir_mode
150
+
151
+ # Path to the log file, defaults to 'log/<environment>.log'
152
+ attr_accessor :dir
153
+
154
+ # Provide a custom logger to use
155
+ attr_accessor :logger
156
+
157
+ # The application name
158
+ attr_accessor :daemon_name
159
+
160
+ # Allow multiple copies to run?
161
+ attr_accessor :multiple
162
+
163
+ # Use the force kill patch? Give the number of seconds
164
+ attr_accessor :force_kill_wait
165
+
166
+ # Collection of signal traps
167
+ attr_reader :signal_traps
168
+
169
+ def initialize
170
+ set_root_path!
171
+
172
+ self.load_paths = default_load_paths
173
+ self.log_level = default_log_level
174
+ self.log_path = default_log_path
175
+
176
+ self.multiple = false
177
+ self.force_kill_wait = false
178
+
179
+ @signal_traps = {}
180
+ end
181
+
182
+ def environment
183
+ ::DAEMON_ENV
184
+ end
185
+
186
+ # The path to the current environment's file (<tt>development.rb</tt>, etc.). By
187
+ # default the file is at <tt>config/environments/#{environment}.rb</tt>.
188
+ def environment_path
189
+ "#{root_path}/config/environments/#{environment}.rb"
190
+ end
191
+
192
+ def daemon_initializer
193
+ "#{root_path}/config/initializers/#{self.daemon_name}.rb"
194
+ end
195
+
196
+ # Add a trap for the specified signal, can be code block or a proc
197
+ def trap( signal, proc = nil, &block )
198
+ return if proc.nil? && !block_given?
199
+
200
+ unless @signal_traps.has_key?( signal )
201
+ set_trap( signal )
202
+ end
203
+
204
+ @signal_traps[signal].unshift( proc || block )
205
+ end
206
+
207
+ protected
208
+
209
+ def run_traps( signal )
210
+ DaemonKit.logger.info "Running signal traps for #{signal}"
211
+ self.signal_traps[ signal ].each { |trap| trap.call }
212
+ end
213
+
214
+ private
215
+
216
+ def set_trap( signal )
217
+ DaemonKit.logger.info "Setting up trap for #{signal}"
218
+ @signal_traps[ signal ] = []
219
+ Signal.trap( signal, Proc.new { self.run_traps( signal ) } )
220
+ end
221
+
222
+ def set_root_path!
223
+ raise "DAEMON_ROOT is not set" unless defined?(::DAEMON_ROOT)
224
+ raise "DAEMON_ROOT is not a directory" unless defined?(::DAEMON_ROOT)
225
+
226
+ @root_path =
227
+ # Pathname is incompatible with Windows, but Windows doesn't have
228
+ # real symlinks so File.expand_path is safe.
229
+ if RUBY_PLATFORM =~ /(:?mswin|mingw)/
230
+ File.expand_path(::DAEMON_ROOT)
231
+
232
+ # Otherwise use Pathname#realpath which respects symlinks.
233
+ else
234
+ Pathname.new(::DAEMON_ROOT).realpath.to_s
235
+ end
236
+
237
+ Object.const_set(:RELATIVE_DAEMON_ROOT, ::DAEMON_ROOT.dup) unless defined?(::RELATIVE_DAEMON_ROOT)
238
+ ::DAEMON_ROOT.replace @root_path
239
+ end
240
+
241
+ def default_load_paths
242
+ [ 'lib' ]
243
+ end
244
+
245
+ def default_log_path
246
+ File.join(root_path, 'log', "#{environment}.log")
247
+ end
248
+
249
+ def default_log_level
250
+ environment == 'production' ? Logger::INFO : Logger::DEBUG
251
+ end
252
+ end
253
+
254
+
255
+ end
@@ -0,0 +1,172 @@
1
+ require 'yaml'
2
+
3
+ module DaemonKit
4
+ # Thin wrapper around xmpp4r-simple, specifically designed to ease
5
+ # configuration of a jabber daemon and provide some added simplicity.
6
+ class Jabber
7
+
8
+ # Jabber connection
9
+ attr_reader :connection
10
+
11
+ @@instance = nil
12
+ @@message_handler = nil
13
+ @@presence_handler = nil
14
+ @@subscription_handler = nil
15
+
16
+ class << self
17
+
18
+ # Deliver a message to the specified jid.
19
+ def deliver( jid, message )
20
+ instance.connection.deliver( jid, message )
21
+ end
22
+
23
+ # Use this instead of initializing, keeps it singleton
24
+ def instance
25
+ @instance ||= (
26
+ config = YAML.load_file( "#{DAEMON_ROOT}/config/jabber.yml" )[DAEMON_ENV]
27
+ raise ArgumentError, "Missing Jabber configuration for #{DAEMON_ENV} environment" if config.nil?
28
+ new( config )
29
+ )
30
+ @instance.startup!
31
+ end
32
+ private :new
33
+
34
+ def run
35
+ DaemonKit.logger.info "Starting jabber loop"
36
+
37
+ loop do
38
+ process_messages
39
+ process_updates
40
+ process_subscriptions
41
+
42
+ begin
43
+ sleep 1
44
+ rescue Interrupt
45
+ DaemonKit.logger.warn "Jabber loop interrupted"
46
+ break
47
+ end
48
+ end
49
+ end
50
+
51
+ def process_messages
52
+ @message_handler ||= Proc.new { |m| DaemonKit.logger.info "Received message from #{m.from}: #{m.body}" }
53
+
54
+ instance.valid_messages { |m| @message_handler.call(m) }
55
+ end
56
+
57
+ def process_updates
58
+ @presence_handler ||= Proc.new { |friend, old_presence, new_presence|
59
+ DaemonKit.logger.debug "Received presence update: #{friend} went from #{old_presence} to #{new_presence}"
60
+ }
61
+
62
+ instance.connection.presence_updates { |friend, old_presence, new_presence|
63
+ @presence_handler.call(friend, old_presence, new_presence)
64
+ }
65
+
66
+ end
67
+
68
+ def process_subscriptions
69
+ @subscription_handler ||= Proc.new { |friend,presence| DaemonKit.logger.debug "Received presence update from #{friend}: #{presence}" }
70
+
71
+ instance.connection.subscription_requests { |friend,presence| @subscription_handler.call(friend,presence) }
72
+ end
73
+
74
+ def received_messages(&block)
75
+ @message_handler = block
76
+ end
77
+
78
+ def presence_updates(&block)
79
+ @presence_handler = block
80
+ end
81
+
82
+ def subscription_requests(&block)
83
+ @subscription_handler = block
84
+ end
85
+
86
+ end
87
+
88
+ def initialize( options = {} )
89
+ @jabber_id = options.delete("jabber_id")
90
+ @password = options.delete("password")
91
+ @resource = options.delete("resource") || 'daemon_kit'
92
+ @masters = options.delete("masters") || []
93
+ @supporters = options.delete("supporters") || []
94
+
95
+ raise ArgumentError if [ @jabber_id, @password ].any? { |a| a.nil? }
96
+ end
97
+
98
+ def startup!
99
+ return self if @booted
100
+
101
+ connect!
102
+ setup_roster!
103
+
104
+ DaemonKit.trap( 'INT', Proc.new { self.shutdown! } )
105
+ DaemonKit.trap( 'TERM', Proc.new { self.shutdown! } )
106
+
107
+ @booted = true
108
+
109
+ self
110
+ end
111
+
112
+ def shutdown!
113
+ DaemonKit.logger.warn "Disconnecting jabber connection"
114
+ self.connection.disconnect
115
+ end
116
+
117
+ def contacts
118
+ @masters + @supporters
119
+ end
120
+
121
+ def valid_messages(&block)
122
+ self.connection.received_messages.each do |message|
123
+ next unless valid_master?( message.from )
124
+
125
+ busy do
126
+ block.call message
127
+ end
128
+ end
129
+ end
130
+
131
+ def valid_master?( jid )
132
+ @masters.include?( jid.strip.to_s )
133
+ end
134
+
135
+ def busy(&block)
136
+ self.connection.status(:dnd, "Working...")
137
+ yield
138
+ self.connection.status(:chat, self.status_line )
139
+ end
140
+
141
+ def status_line
142
+ "#{DaemonKit.configuration.daemon_name} ready for instructions"
143
+ end
144
+
145
+ private
146
+
147
+ def connect!
148
+ jid = @jabber_id + '/' + @resource
149
+
150
+ @connection = ::Jabber::Simple.new( jid, @password, nil, self.status_line )
151
+ end
152
+
153
+ def setup_roster!
154
+ # cleanup the roster
155
+ self.connection.roster.items.each_pair do |jid, roster_item|
156
+ jid = jid.strip.to_s
157
+ unless self.contacts.include?( jid )
158
+ self.connection.remove( jid )
159
+ end
160
+ end
161
+
162
+ # add missing contacts
163
+ self.contacts.each do |jid|
164
+ unless self.connection.subscribed_to?( jid )
165
+ self.connection.add( jid )
166
+ #self.connection.accept_subscription( jid )
167
+ end
168
+ end
169
+ end
170
+
171
+ end
172
+ end