rapns 1.0.7 → 2.0.0rc1

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