rapns 1.0.7 → 2.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/CHANGELOG.md +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +58 -41
  4. data/bin/rapns +23 -5
  5. data/lib/generators/rapns_generator.rb +2 -4
  6. data/lib/generators/templates/add_app_to_rapns.rb +11 -0
  7. data/lib/generators/templates/create_rapns_apps.rb +15 -0
  8. data/lib/rapns/app.rb +9 -0
  9. data/lib/rapns/daemon/app_runner.rb +131 -0
  10. data/lib/rapns/daemon/connection.rb +5 -3
  11. data/lib/rapns/daemon/delivery_handler.rb +13 -15
  12. data/lib/rapns/daemon/delivery_handler_pool.rb +8 -10
  13. data/lib/rapns/daemon/delivery_queue.rb +36 -4
  14. data/lib/rapns/daemon/feedback_receiver.rb +19 -12
  15. data/lib/rapns/daemon/feeder.rb +8 -10
  16. data/lib/rapns/daemon/logger.rb +5 -3
  17. data/lib/rapns/daemon.rb +52 -38
  18. data/lib/rapns/notification.rb +16 -5
  19. data/lib/rapns/patches.rb +2 -2
  20. data/lib/rapns/version.rb +1 -1
  21. data/lib/rapns.rb +2 -1
  22. data/spec/rapns/daemon/app_runner_spec.rb +207 -0
  23. data/spec/rapns/daemon/connection_spec.rb +177 -236
  24. data/spec/rapns/daemon/delivery_handler_pool_spec.rb +10 -14
  25. data/spec/rapns/daemon/delivery_handler_spec.rb +92 -79
  26. data/spec/rapns/daemon/feedback_receiver_spec.rb +29 -23
  27. data/spec/rapns/daemon/feeder_spec.rb +40 -44
  28. data/spec/rapns/daemon/logger_spec.rb +21 -3
  29. data/spec/rapns/daemon_spec.rb +65 -125
  30. data/spec/rapns/notification_spec.rb +16 -0
  31. data/spec/spec_helper.rb +4 -1
  32. metadata +14 -15
  33. data/History.md +0 -5
  34. data/lib/generators/templates/rapns.yml +0 -31
  35. data/lib/rapns/daemon/certificate.rb +0 -27
  36. data/lib/rapns/daemon/configuration.rb +0 -98
  37. data/lib/rapns/daemon/pool.rb +0 -36
  38. data/spec/rapns/daemon/certificate_spec.rb +0 -22
  39. data/spec/rapns/daemon/configuration_spec.rb +0 -231
@@ -1,20 +1,18 @@
1
1
  module Rapns
2
2
  module Daemon
3
3
  class Feeder
4
- extend DatabaseReconnectable
5
4
  extend InterruptibleSleep
5
+ extend DatabaseReconnectable
6
6
 
7
7
  def self.name
8
- "Feeder"
8
+ 'Feeder'
9
9
  end
10
10
 
11
- def self.start(foreground)
12
- reconnect_database unless foreground
13
-
11
+ def self.start(poll)
14
12
  loop do
15
13
  break if @stop
16
14
  enqueue_notifications
17
- interruptible_sleep Rapns::Daemon.configuration.push.poll
15
+ interruptible_sleep poll
18
16
  end
19
17
  end
20
18
 
@@ -28,10 +26,10 @@ module Rapns
28
26
  def self.enqueue_notifications
29
27
  begin
30
28
  with_database_reconnect_and_retry do
31
- if Rapns::Daemon.delivery_queue.notifications_processed?
32
- Rapns::Notification.ready_for_delivery.each do |notification|
33
- Rapns::Daemon.delivery_queue.push(notification)
34
- end
29
+ ready_apps = Rapns::Daemon::AppRunner.ready
30
+ batch_size = Rapns::Daemon.config.batch_size
31
+ Rapns::Notification.ready_for_delivery.find_each(:batch_size => batch_size) do |notification|
32
+ Rapns::Daemon::AppRunner.deliver(notification) if ready_apps.include?(notification.app)
35
33
  end
36
34
  end
37
35
  rescue StandardError => e
@@ -3,8 +3,9 @@ module Rapns
3
3
  class Logger
4
4
  def initialize(options)
5
5
  @options = options
6
- log_path = File.join(Rails.root, 'log', 'rapns.log')
7
- @logger = ActiveSupport::BufferedLogger.new(log_path, Rails.logger.level)
6
+ log = File.open(File.join(Rails.root, 'log', 'rapns.log'), 'w')
7
+ log.sync = true
8
+ @logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
8
9
  @logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
9
10
  end
10
11
 
@@ -25,7 +26,8 @@ module Rapns
25
26
 
26
27
  def log(where, msg, prefix = nil)
27
28
  if msg.is_a?(Exception)
28
- msg = "#{msg.class.name}, #{msg.message}"
29
+ formatted_backtrace = msg.backtrace.join("\n")
30
+ msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
29
31
  end
30
32
 
31
33
  formatted_msg = "[#{Time.now.to_s(:db)}] "
data/lib/rapns/daemon.rb CHANGED
@@ -3,64 +3,81 @@ require 'socket'
3
3
  require 'pathname'
4
4
 
5
5
  require 'rapns/daemon/interruptible_sleep'
6
- require 'rapns/daemon/configuration'
7
- require 'rapns/daemon/certificate'
8
6
  require 'rapns/daemon/delivery_error'
9
7
  require 'rapns/daemon/disconnection_error'
10
- require 'rapns/daemon/pool'
11
8
  require 'rapns/daemon/connection'
12
9
  require 'rapns/daemon/database_reconnectable'
13
10
  require 'rapns/daemon/delivery_queue'
14
11
  require 'rapns/daemon/delivery_handler'
15
12
  require 'rapns/daemon/delivery_handler_pool'
16
13
  require 'rapns/daemon/feedback_receiver'
14
+ require 'rapns/daemon/app_runner'
17
15
  require 'rapns/daemon/feeder'
18
16
  require 'rapns/daemon/logger'
19
17
 
20
18
  module Rapns
21
19
  module Daemon
20
+ extend DatabaseReconnectable
21
+
22
22
  class << self
23
- attr_accessor :logger, :configuration, :certificate,
24
- :delivery_queue, :delivery_handler_pool, :foreground
25
- alias_method :foreground?, :foreground
23
+ attr_accessor :logger, :config
26
24
  end
27
25
 
28
- def self.start(environment, foreground)
29
- @foreground = foreground
26
+ def self.start(environment, config)
27
+ self.config = config
28
+ self.logger = Logger.new(:foreground => config.foreground, :airbrake_notify => config.airbrake_notify)
30
29
  setup_signal_hooks
31
30
 
32
- self.configuration = Configuration.new(environment, File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
33
- configuration.load
34
-
35
- self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration.airbrake_notify)
36
-
37
- self.certificate = Certificate.new(configuration.certificate)
38
- certificate.load
39
-
40
- self.delivery_queue = DeliveryQueue.new
41
-
42
- daemonize unless foreground?
31
+ unless config.foreground
32
+ daemonize
33
+ reconnect_database
34
+ end
43
35
 
44
36
  write_pid_file
37
+ ensure_upgraded
38
+ AppRunner.sync
39
+ Feeder.start(config.push_poll)
40
+ end
41
+
42
+ protected
45
43
 
46
- self.delivery_handler_pool = DeliveryHandlerPool.new(configuration.push.connections)
47
- delivery_handler_pool.populate
44
+ def self.ensure_upgraded
45
+ count = 0
46
+
47
+ begin
48
+ count = Rapns::App.count
49
+ rescue ActiveRecord::StatementInvalid
50
+ puts "!!!! RAPNS NOT STARTED !!!!"
51
+ puts
52
+ puts "As of version v2.0.0 apps are configured in the database instead of rapns.yml."
53
+ puts "Please run 'rails g rapns' to generate the new migrations and create your apps with Rapns::App."
54
+ puts "See https://github.com/ileitch/rapns for further instructions."
55
+ puts
56
+ exit 1
57
+ end
48
58
 
49
- logger.info('Ready')
59
+ if count == 0
60
+ puts "!!!! RAPNS NOT STARTED !!!!"
61
+ puts
62
+ puts "You must create an Rapns::App."
63
+ puts "See https://github.com/ileitch/rapns for instructions."
64
+ puts
65
+ exit 1
66
+ end
50
67
 
51
- FeedbackReceiver.start
52
- Feeder.start(foreground?)
68
+ if File.exists?(File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
69
+ logger.warn("Since 2.0.0 rapns uses command-line options instead of a configuration file. Please remove config/rapns/rapns.yml.")
70
+ end
53
71
  end
54
72
 
55
- protected
56
-
57
73
  def self.setup_signal_hooks
58
74
  @shutting_down = false
59
75
 
76
+ Signal.trap('SIGHUP') { AppRunner.sync }
77
+ Signal.trap('SIGUSR1') { AppRunner.debug }
78
+
60
79
  ['SIGINT', 'SIGTERM'].each do |signal|
61
- Signal.trap(signal) do
62
- handle_shutdown_signal
63
- end
80
+ Signal.trap(signal) { handle_shutdown_signal }
64
81
  end
65
82
  end
66
83
 
@@ -72,9 +89,8 @@ module Rapns
72
89
 
73
90
  def self.shutdown
74
91
  puts "\nShutting down..."
75
- Rapns::Daemon::FeedbackReceiver.stop
76
- Rapns::Daemon::Feeder.stop
77
- Rapns::Daemon.delivery_handler_pool.drain if Rapns::Daemon.delivery_handler_pool
92
+ Feeder.stop
93
+ AppRunner.stop
78
94
  delete_pid_file
79
95
  end
80
96
 
@@ -92,19 +108,17 @@ module Rapns
92
108
  end
93
109
 
94
110
  def self.write_pid_file
95
- if !configuration.pid_file.blank?
111
+ if !config.pid_file.blank?
96
112
  begin
97
- File.open(configuration.pid_file, 'w') do |f|
98
- f.puts $$
99
- end
113
+ File.open(config.pid_file, 'w') { |f| f.puts Process.pid }
100
114
  rescue SystemCallError => e
101
- logger.error("Failed to write PID to '#{configuration.pid_file}': #{e.inspect}")
115
+ logger.error("Failed to write PID to '#{config.pid_file}': #{e.inspect}")
102
116
  end
103
117
  end
104
118
  end
105
119
 
106
120
  def self.delete_pid_file
107
- pid_file = configuration.pid_file
121
+ pid_file = config.pid_file
108
122
  File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
109
123
  end
110
124
  end
@@ -48,13 +48,24 @@ module Rapns
48
48
  ActiveSupport::JSON.decode(read_attribute(:attributes_for_device)) if read_attribute(:attributes_for_device)
49
49
  end
50
50
 
51
+ MDM_OVERIDE_KEY = '__rapns_mdm__'
52
+ def mdm=(magic)
53
+ self.attributes_for_device = {MDM_OVERIDE_KEY => magic}
54
+ end
55
+
51
56
  def as_json
52
57
  json = ActiveSupport::OrderedHash.new
53
- json['aps'] = ActiveSupport::OrderedHash.new
54
- json['aps']['alert'] = alert if alert
55
- json['aps']['badge'] = badge if badge
56
- json['aps']['sound'] = sound if sound
57
- attributes_for_device.each { |k, v| json[k.to_s] = v.to_s } if attributes_for_device
58
+
59
+ if attributes_for_device && attributes_for_device.key?(MDM_OVERIDE_KEY)
60
+ json['mdm'] = attributes_for_device[MDM_OVERIDE_KEY]
61
+ else
62
+ json['aps'] = ActiveSupport::OrderedHash.new
63
+ json['aps']['alert'] = alert if alert
64
+ json['aps']['badge'] = badge if badge
65
+ json['aps']['sound'] = sound if sound
66
+ attributes_for_device.each { |k, v| json[k.to_s] = v.to_s } if attributes_for_device
67
+ end
68
+
58
69
  json
59
70
  end
60
71
 
data/lib/rapns/patches.rb CHANGED
@@ -1,5 +1,5 @@
1
- if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql'
2
- if Rails::VERSION::STRING == '3.1.0' || Rails::VERSION::STRING == '3.1.1'
1
+ if Rails::VERSION::STRING == '3.1.0' || Rails::VERSION::STRING == '3.1.1'
2
+ if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql'
3
3
  STDERR.puts "[WARNING] Detected Rails #{Rails::VERSION::STRING}, patching PostgreSQLAdapter to fix reconnection bug: https://github.com/rails/rails/issues/3160"
4
4
  require "rapns/patches/rails/#{Rails::VERSION::STRING}/postgresql_adapter.rb"
5
5
  end
data/lib/rapns/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rapns
2
- VERSION = '1.0.7'
2
+ VERSION = '2.0.0rc1'
3
3
  end
data/lib/rapns.rb CHANGED
@@ -4,4 +4,5 @@ require 'rapns/version'
4
4
  require 'rapns/binary_notification_validator'
5
5
  require 'rapns/device_token_format_validator'
6
6
  require 'rapns/notification'
7
- require 'rapns/feedback'
7
+ require 'rapns/feedback'
8
+ require 'rapns/app'
@@ -0,0 +1,207 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rapns::Daemon::AppRunner do
4
+ let(:app) { stub(:key => 'app', :certificate => 'cert', :password => '', :connections => 1) }
5
+ let(:queue) { stub(:notifications_processed? => true, :push => nil) }
6
+ let(:receiver) { stub(:start => nil, :stop => nil) }
7
+ let(:handler) { stub(:start => nil, :stop => nil) }
8
+ let(:push_config) { stub(:host => 'gateway.push.apple.com', :port => 2195) }
9
+ let(:feedback_config) { stub(:host => 'feedback.push.apple.com', :port => 2196, :poll => 60) }
10
+ let(:runner) { Rapns::Daemon::AppRunner.new(app, push_config.host, push_config.port,
11
+ feedback_config.host, feedback_config.port, feedback_config.poll) }
12
+
13
+ before do
14
+ Rapns::Daemon::DeliveryQueue.stub(:new => queue)
15
+ Rapns::Daemon::FeedbackReceiver.stub(:new => receiver)
16
+ Rapns::Daemon::DeliveryHandler.stub(:new => handler)
17
+ end
18
+
19
+ after { Rapns::Daemon::AppRunner.all.clear }
20
+
21
+ describe 'start' do
22
+ it 'starts a feedback receiver' do
23
+ Rapns::Daemon::FeedbackReceiver.should_receive(:new).with(app.key, feedback_config.host, feedback_config.port, feedback_config.poll, app.certificate, app.password)
24
+ receiver.should_receive(:start)
25
+ runner.start
26
+ end
27
+
28
+ it 'starts a delivery handler for each connection' do
29
+ Rapns::Daemon::DeliveryHandler.should_receive(:new).with(queue, app.key, push_config.host,
30
+ push_config.port, app.certificate, app.password)
31
+ handler.should_receive(:start)
32
+ runner.start
33
+ end
34
+ end
35
+
36
+ describe 'deliver' do
37
+ let(:notification) { stub }
38
+
39
+ it 'enqueues the notification' do
40
+ queue.should_receive(:push).with(notification)
41
+ runner.deliver(notification)
42
+ end
43
+ end
44
+
45
+ describe 'stop' do
46
+ before { runner.start }
47
+
48
+ it 'stops the delivery handlers' do
49
+ handler.should_receive(:stop)
50
+ runner.stop
51
+ end
52
+
53
+ it 'stops the feedback receiver' do
54
+ receiver.should_receive(:stop)
55
+ runner.stop
56
+ end
57
+ end
58
+
59
+ describe 'ready?' do
60
+ it 'is ready if all notifications have been processed' do
61
+ queue.stub(:notifications_processed? => true)
62
+ runner.ready?.should be_true
63
+ end
64
+
65
+ it 'is not ready if not all notifications have been processed' do
66
+ queue.stub(:notifications_processed? => false)
67
+ runner.ready?.should be_false
68
+ end
69
+ end
70
+
71
+ describe 'sync' do
72
+ let(:new_app) { stub(:key => 'app', :certificate => 'cert', :password => '', :connections => 1) }
73
+ before { runner.start }
74
+
75
+ it 'reduces the number of handlers if needed' do
76
+ handler.should_receive(:stop)
77
+ new_app.stub(:connections => app.connections - 1)
78
+ runner.sync(new_app)
79
+ end
80
+
81
+ it 'increases the number of handlers if needed' do
82
+ new_handler = stub
83
+ Rapns::Daemon::DeliveryHandler.should_receive(:new).and_return(new_handler)
84
+ new_handler.should_receive(:start)
85
+ new_app.stub(:connections => app.connections + 1)
86
+ runner.sync(new_app)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe Rapns::Daemon::AppRunner, 'stop' do
92
+ let(:runner) { stub }
93
+ before { Rapns::Daemon::AppRunner.all['app'] = runner }
94
+ after { Rapns::Daemon::AppRunner.all.clear }
95
+
96
+ it 'stops all runners' do
97
+ runner.should_receive(:stop)
98
+ Rapns::Daemon::AppRunner.stop
99
+ end
100
+ end
101
+
102
+ describe Rapns::Daemon::AppRunner, 'deliver' do
103
+ let(:runner) { stub }
104
+ let(:notification) { stub(:app => 'app') }
105
+ let(:logger) { stub(:error => nil) }
106
+
107
+ before do
108
+ Rapns::Daemon.stub(:logger => logger)
109
+ Rapns::Daemon::AppRunner.all['app'] = runner
110
+ end
111
+
112
+ after { Rapns::Daemon::AppRunner.all.clear }
113
+
114
+ it 'delivers the notification' do
115
+ runner.should_receive(:deliver).with(notification)
116
+ Rapns::Daemon::AppRunner.deliver(notification)
117
+ end
118
+
119
+ it 'logs an error if there is no runner to deliver the notification' do
120
+ notification.stub(:app => 'unknonw', :id => 123)
121
+ logger.should_receive(:error).with("No such app '#{notification.app}' for notification #{notification.id}.")
122
+ Rapns::Daemon::AppRunner.deliver(notification)
123
+ end
124
+ end
125
+
126
+ describe Rapns::Daemon::AppRunner, 'ready' do
127
+ let(:runner1) { stub(:ready? => true) }
128
+ let(:runner2) { stub(:ready? => false) }
129
+
130
+ before do
131
+ Rapns::Daemon::AppRunner.all['app1'] = runner1
132
+ Rapns::Daemon::AppRunner.all['app2'] = runner2
133
+ end
134
+
135
+ after { Rapns::Daemon::AppRunner.all.clear }
136
+
137
+ it 'returns apps that are ready for more notifications' do
138
+ Rapns::Daemon::AppRunner.ready.should == ['app1']
139
+ end
140
+ end
141
+
142
+ describe Rapns::Daemon::AppRunner, 'sync' do
143
+ let(:app) { stub(:key => 'app') }
144
+ let(:new_app) { stub(:key => 'new_app') }
145
+ let(:runner) { stub(:sync => nil, :stop => nil) }
146
+ let(:new_runner) { stub }
147
+ let(:logger) { stub(:error => nil) }
148
+ let(:config) { stub(:feedback_poll => 60) }
149
+
150
+ before do
151
+ Rapns::Daemon.stub(:config => config, :logger => logger)
152
+ Rapns::Daemon::AppRunner.all['app'] = runner
153
+ Rapns::App.stub(:all => [app])
154
+ end
155
+
156
+ after { Rapns::Daemon::AppRunner.all.clear }
157
+
158
+ it 'loads all apps' do
159
+ Rapns::App.should_receive(:all)
160
+ Rapns::Daemon::AppRunner.sync
161
+ end
162
+
163
+ it 'instructs existing runners to sync' do
164
+ runner.should_receive(:sync).with(app)
165
+ Rapns::Daemon::AppRunner.sync
166
+ end
167
+
168
+ it 'starts a runner for a new app with a production certificate' do
169
+ new_app.stub(:certificate => 'Apple Production IOS Push Services')
170
+ Rapns::App.stub(:all => [new_app])
171
+ new_runner = stub
172
+ Rapns::Daemon::AppRunner.should_receive(:new).with(new_app, 'gateway.push.apple.com', 2195,
173
+ 'feedback.push.apple.com', 2196, config.feedback_poll).and_return(new_runner)
174
+ new_runner.should_receive(:start)
175
+ Rapns::Daemon::AppRunner.sync
176
+ end
177
+
178
+ it 'starts a runner for a new app with a development certificate' do
179
+ new_app.stub(:certificate => 'Apple Development IOS Push Services')
180
+ Rapns::App.stub(:all => [new_app])
181
+ new_runner = stub
182
+ Rapns::Daemon::AppRunner.should_receive(:new).with(new_app, 'gateway.sandbox.push.apple.com', 2195,
183
+ 'feedback.sandbox.push.apple.com', 2196, config.feedback_poll).and_return(new_runner)
184
+ new_runner.should_receive(:start)
185
+ Rapns::Daemon::AppRunner.sync
186
+ end
187
+
188
+ it 'logs an error if the environment cannot be determined from the certificate' do
189
+ new_app.stub(:certificate => 'wat')
190
+ Rapns::App.stub(:all => [new_app])
191
+ Rapns::Daemon.logger.should_receive(:error).with("Could not detect environment for app 'new_app'.")
192
+ Rapns::Daemon::AppRunner.sync
193
+ end
194
+
195
+ it 'does not attempt to start an AppRunner if the environment could not be detected' do
196
+ new_app.stub(:certificate => 'wat')
197
+ Rapns::App.stub(:all => [new_app])
198
+ Rapns::Daemon::AppRunner.should_not_receive(:new)
199
+ Rapns::Daemon::AppRunner.sync
200
+ end
201
+
202
+ it 'deletes old apps' do
203
+ Rapns::App.stub(:all => [])
204
+ runner.should_receive(:stop)
205
+ Rapns::Daemon::AppRunner.sync
206
+ end
207
+ end