daemon-kit 0.1.3

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 (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