rapns 3.0.0.beta.1 → 3.0.0.rc.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +75 -106
- data/bin/rapns +14 -14
- data/config/database.yml +8 -0
- data/lib/generators/rapns_generator.rb +5 -1
- data/lib/generators/templates/add_gcm.rb +8 -0
- data/lib/generators/templates/rapns.rb +39 -0
- data/lib/rapns.rb +1 -0
- data/lib/rapns/apns/notification.rb +2 -0
- data/lib/rapns/apns/required_fields_validator.rb +14 -0
- data/lib/rapns/configuration.rb +24 -33
- data/lib/rapns/daemon.rb +18 -14
- data/lib/rapns/daemon/apns/app_runner.rb +1 -1
- data/lib/rapns/daemon/apns/delivery.rb +1 -1
- data/lib/rapns/daemon/apns/feedback_receiver.rb +1 -1
- data/lib/rapns/daemon/app_runner.rb +13 -7
- data/lib/rapns/daemon/delivery_handler.rb +0 -4
- data/lib/rapns/daemon/feeder.rb +1 -5
- data/lib/rapns/daemon/logger.rb +22 -9
- data/lib/rapns/version.rb +1 -1
- data/lib/tasks/cane.rake +2 -3
- data/lib/tasks/test.rake +1 -2
- data/spec/unit/apns/notification_spec.rb +12 -2
- data/spec/unit/configuration_spec.rb +38 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +2 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +10 -4
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +7 -7
- data/spec/unit/daemon/app_runner_spec.rb +51 -0
- data/spec/unit/daemon/database_reconnectable_spec.rb +2 -0
- data/spec/unit/daemon/delivery_error_spec.rb +2 -2
- data/spec/unit/daemon/delivery_handler_shared.rb +10 -1
- data/spec/unit/daemon/gcm/app_runner_spec.rb +3 -1
- data/spec/unit/daemon/logger_spec.rb +10 -2
- data/spec/unit/daemon_spec.rb +31 -14
- data/spec/unit_spec_helper.rb +3 -7
- metadata +10 -4
data/lib/rapns/daemon.rb
CHANGED
@@ -31,15 +31,15 @@ module Rapns
|
|
31
31
|
extend DatabaseReconnectable
|
32
32
|
|
33
33
|
class << self
|
34
|
-
attr_accessor :logger
|
34
|
+
attr_accessor :logger
|
35
35
|
end
|
36
36
|
|
37
|
-
def self.start
|
38
|
-
self.
|
39
|
-
|
37
|
+
def self.start
|
38
|
+
self.logger = Logger.new(:foreground => Rapns.config.foreground,
|
39
|
+
:airbrake_notify => Rapns.config.airbrake_notify)
|
40
40
|
setup_signal_hooks
|
41
41
|
|
42
|
-
unless config.foreground
|
42
|
+
unless Rapns.config.foreground
|
43
43
|
daemonize
|
44
44
|
reconnect_database
|
45
45
|
end
|
@@ -47,7 +47,7 @@ module Rapns
|
|
47
47
|
write_pid_file
|
48
48
|
ensure_upgraded
|
49
49
|
AppRunner.sync
|
50
|
-
Feeder.start(config.push_poll)
|
50
|
+
Feeder.start(Rapns.config.push_poll)
|
51
51
|
end
|
52
52
|
|
53
53
|
protected
|
@@ -61,18 +61,22 @@ module Rapns
|
|
61
61
|
puts "!!!! RAPNS NOT STARTED !!!!"
|
62
62
|
puts
|
63
63
|
puts "As of version v2.0.0 apps are configured in the database instead of rapns.yml."
|
64
|
-
puts "Please run 'rails g rapns' to generate the new migrations and create your
|
64
|
+
puts "Please run 'rails g rapns' to generate the new migrations and create your app."
|
65
65
|
puts "See https://github.com/ileitch/rapns for further instructions."
|
66
66
|
puts
|
67
67
|
exit 1
|
68
68
|
end
|
69
69
|
|
70
70
|
if count == 0
|
71
|
-
logger.warn("You have not created an
|
71
|
+
logger.warn("You have not created an app yet. See https://github.com/ileitch/rapns for instructions.")
|
72
72
|
end
|
73
73
|
|
74
74
|
if File.exists?(File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
|
75
|
-
logger.warn(
|
75
|
+
logger.warn(<<-EOS)
|
76
|
+
Since 2.0.0 rapns uses command-line options and a Ruby based configuration file.
|
77
|
+
Please run 'rails g rapns' to generate a new configuration file into config/initializers.
|
78
|
+
Remove config/rapns/rapns.yml to avoid this warning.
|
79
|
+
EOS
|
76
80
|
end
|
77
81
|
end
|
78
82
|
|
@@ -80,7 +84,7 @@ module Rapns
|
|
80
84
|
@shutting_down = false
|
81
85
|
|
82
86
|
Signal.trap('SIGHUP') { AppRunner.sync }
|
83
|
-
Signal.trap('
|
87
|
+
Signal.trap('SIGUSR2') { AppRunner.debug }
|
84
88
|
|
85
89
|
['SIGINT', 'SIGTERM'].each do |signal|
|
86
90
|
Signal.trap(signal) { handle_shutdown_signal }
|
@@ -101,17 +105,17 @@ module Rapns
|
|
101
105
|
end
|
102
106
|
|
103
107
|
def self.write_pid_file
|
104
|
-
if !config.pid_file.blank?
|
108
|
+
if !Rapns.config.pid_file.blank?
|
105
109
|
begin
|
106
|
-
File.open(config.pid_file, 'w') { |f| f.puts Process.pid }
|
110
|
+
File.open(Rapns.config.pid_file, 'w') { |f| f.puts Process.pid }
|
107
111
|
rescue SystemCallError => e
|
108
|
-
logger.error("Failed to write PID to '#{config.pid_file}': #{e.inspect}")
|
112
|
+
logger.error("Failed to write PID to '#{Rapns.config.pid_file}': #{e.inspect}")
|
109
113
|
end
|
110
114
|
end
|
111
115
|
end
|
112
116
|
|
113
117
|
def self.delete_pid_file
|
114
|
-
pid_file = config.pid_file
|
118
|
+
pid_file = Rapns.config.pid_file
|
115
119
|
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
116
120
|
end
|
117
121
|
|
@@ -16,7 +16,7 @@ module Rapns
|
|
16
16
|
protected
|
17
17
|
|
18
18
|
def started
|
19
|
-
poll = Rapns
|
19
|
+
poll = Rapns.config[:feedback_poll]
|
20
20
|
host, port = ENVIRONMENTS[app.environment.to_sym][:feedback]
|
21
21
|
@feedback_receiver = FeedbackReceiver.new(app, host, port, poll)
|
22
22
|
@feedback_receiver.start
|
@@ -25,7 +25,7 @@ module Rapns
|
|
25
25
|
def perform
|
26
26
|
begin
|
27
27
|
@connection.write(@notification.to_binary)
|
28
|
-
check_for_error if Rapns
|
28
|
+
check_for_error if Rapns.config.check_for_errors
|
29
29
|
mark_delivered
|
30
30
|
Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.device_token}")
|
31
31
|
rescue Rapns::DeliveryError, Rapns::Apns::DisconnectionError => error
|
@@ -62,7 +62,7 @@ module Rapns
|
|
62
62
|
Rapns::Daemon.logger.info("[FeedbackReceiver:#{@app.name}] Delivery failed at #{formatted_failed_at} for #{device_token}")
|
63
63
|
feedback = Rapns::Apns::Feedback.create!(:failed_at => failed_at, :device_token => device_token, :app => @app)
|
64
64
|
begin
|
65
|
-
Rapns.
|
65
|
+
Rapns.config.apns_feedback_callback.call(feedback) if Rapns.config.apns_feedback_callback
|
66
66
|
rescue StandardError => e
|
67
67
|
Rapns::Daemon.logger.error(e)
|
68
68
|
end
|
@@ -8,7 +8,7 @@ module Rapns
|
|
8
8
|
@runners = {}
|
9
9
|
|
10
10
|
def self.enqueue(notification)
|
11
|
-
if app =
|
11
|
+
if app = runners[notification.app_id]
|
12
12
|
app.enqueue(notification)
|
13
13
|
else
|
14
14
|
Rapns::Daemon.logger.error("No such app '#{notification.app_id}' for notification #{notification.id}.")
|
@@ -43,11 +43,11 @@ module Rapns
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def self.stop
|
46
|
-
|
46
|
+
runners.values.map(&:stop)
|
47
47
|
end
|
48
48
|
|
49
49
|
def self.debug
|
50
|
-
|
50
|
+
runners.values.map(&:debug)
|
51
51
|
end
|
52
52
|
|
53
53
|
def self.idle
|
@@ -60,10 +60,6 @@ module Rapns
|
|
60
60
|
@app = app
|
61
61
|
end
|
62
62
|
|
63
|
-
def new_delivery_handler
|
64
|
-
raise NotImplementedError
|
65
|
-
end
|
66
|
-
|
67
63
|
def started
|
68
64
|
end
|
69
65
|
|
@@ -73,6 +69,7 @@ module Rapns
|
|
73
69
|
def start
|
74
70
|
app.connections.times { handlers << start_handler }
|
75
71
|
started
|
72
|
+
Rapns::Daemon.logger.info("[#{app.name}] Started, #{handlers_str}.")
|
76
73
|
end
|
77
74
|
|
78
75
|
def stop
|
@@ -87,10 +84,13 @@ module Rapns
|
|
87
84
|
def sync(app)
|
88
85
|
@app = app
|
89
86
|
diff = handlers.size - app.connections
|
87
|
+
return if diff == 0
|
90
88
|
if diff > 0
|
91
89
|
diff.times { handlers.pop.stop }
|
90
|
+
Rapns::Daemon.logger.info("[#{app.name}] Terminated #{handlers_str(diff)}. #{handlers_str} remaining.")
|
92
91
|
else
|
93
92
|
diff.abs.times { handlers << start_handler }
|
93
|
+
Rapns::Daemon.logger.info("[#{app.name}] Added #{handlers_str(diff)}. #{handlers_str} remaining.")
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
@@ -124,6 +124,12 @@ module Rapns
|
|
124
124
|
def handlers
|
125
125
|
@handler ||= []
|
126
126
|
end
|
127
|
+
|
128
|
+
def handlers_str(count = app.connections)
|
129
|
+
count = count.abs
|
130
|
+
str = count == 1 ? 'handler' : 'handlers'
|
131
|
+
"#{count} #{str}"
|
132
|
+
end
|
127
133
|
end
|
128
134
|
end
|
129
135
|
end
|
data/lib/rapns/daemon/feeder.rb
CHANGED
@@ -4,10 +4,6 @@ module Rapns
|
|
4
4
|
extend InterruptibleSleep
|
5
5
|
extend DatabaseReconnectable
|
6
6
|
|
7
|
-
def self.name
|
8
|
-
'Feeder'
|
9
|
-
end
|
10
|
-
|
11
7
|
def self.start(poll)
|
12
8
|
loop do
|
13
9
|
enqueue_notifications
|
@@ -26,7 +22,7 @@ module Rapns
|
|
26
22
|
def self.enqueue_notifications
|
27
23
|
begin
|
28
24
|
with_database_reconnect_and_retry do
|
29
|
-
batch_size = Rapns
|
25
|
+
batch_size = Rapns.config.batch_size
|
30
26
|
idle = Rapns::Daemon::AppRunner.idle.map(&:app)
|
31
27
|
Rapns::Notification.ready_for_delivery.for_apps(idle).limit(batch_size).each do |notification|
|
32
28
|
Rapns::Daemon::AppRunner.enqueue(notification)
|
data/lib/rapns/daemon/logger.rb
CHANGED
@@ -3,10 +3,17 @@ module Rapns
|
|
3
3
|
class Logger
|
4
4
|
def initialize(options)
|
5
5
|
@options = options
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
|
7
|
+
begin
|
8
|
+
log = File.open(File.join(Rails.root, 'log', 'rapns.log'), 'a')
|
9
|
+
log.sync = true
|
10
|
+
@logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
|
11
|
+
@logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
|
12
|
+
rescue Errno::ENOENT, Errno::EPERM => e
|
13
|
+
@logger = nil
|
14
|
+
error(e)
|
15
|
+
error('Logging disabled.')
|
16
|
+
end
|
10
17
|
end
|
11
18
|
|
12
19
|
def info(msg)
|
@@ -15,16 +22,16 @@ module Rapns
|
|
15
22
|
|
16
23
|
def error(msg, options = {})
|
17
24
|
airbrake_notify(msg) if notify_via_airbrake?(msg, options)
|
18
|
-
log(:error, msg, 'ERROR')
|
25
|
+
log(:error, msg, 'ERROR', STDERR)
|
19
26
|
end
|
20
27
|
|
21
28
|
def warn(msg)
|
22
|
-
log(:warn, msg, 'WARNING')
|
29
|
+
log(:warn, msg, 'WARNING', STDERR)
|
23
30
|
end
|
24
31
|
|
25
32
|
private
|
26
33
|
|
27
|
-
def log(where, msg, prefix = nil)
|
34
|
+
def log(where, msg, prefix = nil, io = STDOUT)
|
28
35
|
if msg.is_a?(Exception)
|
29
36
|
formatted_backtrace = msg.backtrace.join("\n")
|
30
37
|
msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
|
@@ -33,8 +40,14 @@ module Rapns
|
|
33
40
|
formatted_msg = "[#{Time.now.to_s(:db)}] "
|
34
41
|
formatted_msg << "[#{prefix}] " if prefix
|
35
42
|
formatted_msg << msg
|
36
|
-
|
37
|
-
|
43
|
+
|
44
|
+
if io == STDERR
|
45
|
+
io.puts formatted_msg
|
46
|
+
elsif @options[:foreground]
|
47
|
+
io.puts formatted_msg
|
48
|
+
end
|
49
|
+
|
50
|
+
@logger.send(where, formatted_msg) if @logger
|
38
51
|
end
|
39
52
|
|
40
53
|
def airbrake_notify(e)
|
data/lib/rapns/version.rb
CHANGED
data/lib/tasks/cane.rake
CHANGED
@@ -3,12 +3,11 @@ begin
|
|
3
3
|
|
4
4
|
desc "Run cane to check quality metrics"
|
5
5
|
Cane::RakeTask.new(:quality) do |cane|
|
6
|
-
cane.add_threshold 'coverage/covered_percent', :>=,
|
6
|
+
cane.add_threshold 'coverage/covered_percent', :>=, 99
|
7
7
|
cane.no_style = false
|
8
8
|
cane.style_measure = 1000
|
9
9
|
cane.no_doc = true
|
10
|
-
cane.abc_max =
|
11
|
-
cane.abc_exclude = %w(Rapns::Daemon::Gcm::Delivery#handle_errors)
|
10
|
+
cane.abc_max = 20
|
12
11
|
end
|
13
12
|
|
14
13
|
namespace :spec do
|
data/lib/tasks/test.rake
CHANGED
@@ -16,12 +16,11 @@ namespace :test do
|
|
16
16
|
pwd = Dir.pwd
|
17
17
|
|
18
18
|
cmd("bundle exec rails new #{path} --skip-bundle")
|
19
|
-
branch = cmd("git branch | grep '\*'").split(' ').last
|
20
19
|
|
21
20
|
begin
|
22
21
|
Dir.chdir(path)
|
23
22
|
cmd('echo "gem \'rake\'" >> Gemfile')
|
24
|
-
cmd("echo \"gem 'rapns', :
|
23
|
+
cmd("echo \"gem 'rapns', :path => '#{rapns_root}'\" >> Gemfile")
|
25
24
|
cmd('bundle install')
|
26
25
|
cmd('bundle exec rails g rapns')
|
27
26
|
cmd('bundle exec rake db:migrate')
|
@@ -25,13 +25,23 @@ describe Rapns::Apns::Notification do
|
|
25
25
|
notification.errors[:base].include?("APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes.").should be_true
|
26
26
|
end
|
27
27
|
|
28
|
-
it "should default the sound to
|
29
|
-
notification.sound.should
|
28
|
+
it "should default the sound to 'default'" do
|
29
|
+
notification.sound.should eq('default')
|
30
30
|
end
|
31
31
|
|
32
32
|
it "should default the expiry to 1 day" do
|
33
33
|
notification.expiry.should == 1.day.to_i
|
34
34
|
end
|
35
|
+
|
36
|
+
# The notification must contain one of alert, sound or badge.
|
37
|
+
# @see https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html
|
38
|
+
it "should not be valid if there is none of alert,sound,badge present" do
|
39
|
+
notification.alert = nil
|
40
|
+
notification.sound = nil
|
41
|
+
notification.badge = nil
|
42
|
+
notification.valid?.should be_false
|
43
|
+
notification.errors[:base].should include("APN Notification must contain one of alert, badge, or sound")
|
44
|
+
end
|
35
45
|
end
|
36
46
|
|
37
47
|
describe Rapns::Apns::Notification, "when assigning the device token" do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'unit_spec_helper'
|
2
|
+
|
3
|
+
describe Rapns do
|
4
|
+
let(:config) { stub }
|
5
|
+
|
6
|
+
before { Rapns.stub(:config => config) }
|
7
|
+
|
8
|
+
it 'can yields a config block' do
|
9
|
+
expect { |b| Rapns.configure(&b) }.to yield_with_args(config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Rapns::Configuration do
|
14
|
+
let(:config) { Rapns::Configuration.new }
|
15
|
+
|
16
|
+
it 'configures a feedback callback' do
|
17
|
+
b = Proc.new {}
|
18
|
+
config.on_apns_feedback(&b)
|
19
|
+
config.apns_feedback_callback.should == b
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'can be updated' do
|
23
|
+
new_config = Rapns::Configuration.new
|
24
|
+
new_config.batch_size = 100
|
25
|
+
expect { config.update(new_config) }.to change(config, :batch_size).to(100)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets the pid_file relative if not absolute' do
|
29
|
+
Rails.stub(:root => '/rails')
|
30
|
+
config.pid_file = 'tmp/rapns.pid'
|
31
|
+
config.pid_file.should == '/rails/tmp/rapns.pid'
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'does not alter an absolute pid_file path' do
|
35
|
+
config.pid_file = '/tmp/rapns.pid'
|
36
|
+
config.pid_file.should == '/tmp/rapns.pid'
|
37
|
+
end
|
38
|
+
end
|
@@ -11,8 +11,10 @@ describe Rapns::Daemon::Apns::AppRunner do
|
|
11
11
|
let(:handler) { stub(:start => nil, :stop => nil, :queue= => nil) }
|
12
12
|
let(:receiver) { stub(:start => nil, :stop => nil) }
|
13
13
|
let(:config) { {:feedback_poll => 60 } }
|
14
|
+
let(:logger) { stub(:info => nil) }
|
14
15
|
|
15
16
|
before do
|
17
|
+
Rapns::Daemon.stub(:logger => logger)
|
16
18
|
Rapns::Daemon::Apns::DeliveryHandler.stub(:new => handler)
|
17
19
|
Rapns::Daemon::Apns::FeedbackReceiver.stub(:new => receiver)
|
18
20
|
Rapns::Daemon.stub(:config => config)
|
@@ -16,7 +16,8 @@ describe Rapns::Daemon::Apns::Delivery do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
before do
|
19
|
-
Rapns
|
19
|
+
Rapns.stub(:config => config)
|
20
|
+
Rapns::Daemon.stub(:logger => logger)
|
20
21
|
end
|
21
22
|
|
22
23
|
it "sends the binary version of the notification" do
|
@@ -95,9 +96,14 @@ describe Rapns::Daemon::Apns::Delivery do
|
|
95
96
|
end
|
96
97
|
|
97
98
|
it "logs the delivery error" do
|
98
|
-
error
|
99
|
-
|
100
|
-
|
99
|
+
# checking for the stubbed error doesn't work in jruby, but checking
|
100
|
+
# for the exception by class does.
|
101
|
+
|
102
|
+
#error = Rapns::DeliveryError.new(4, 12, "Missing payload")
|
103
|
+
#Rapns::DeliveryError.stub(:new => error)
|
104
|
+
#expect { delivery.perform }.to raise_error(error)
|
105
|
+
|
106
|
+
expect { delivery.perform }.to raise_error(Rapns::DeliveryError)
|
101
107
|
end
|
102
108
|
|
103
109
|
it "sets the notification error description" do
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'unit_spec_helper'
|
2
|
+
|
3
|
+
describe Rapns::Apns::DisconnectionError do
|
4
|
+
let(:error) { Rapns::Apns::DisconnectionError.new }
|
5
|
+
|
6
|
+
it 'returns a nil error code' do
|
7
|
+
error.code.should be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'contains an error description' do
|
11
|
+
error.description
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'returns a message' do
|
15
|
+
error.message
|
16
|
+
error.to_s
|
17
|
+
end
|
18
|
+
end
|
@@ -90,29 +90,29 @@ describe Rapns::Daemon::Apns::FeedbackReceiver, 'check_for_feedback' do
|
|
90
90
|
receiver.stop
|
91
91
|
end
|
92
92
|
|
93
|
-
it 'calls the
|
93
|
+
it 'calls the apns_feedback_callback when feedback is received and the callback is set' do
|
94
94
|
stub_connection_read_with_tuple
|
95
|
-
Rapns.
|
95
|
+
Rapns.config.apns_feedback_callback = Proc.new {}
|
96
96
|
feedback = Object.new
|
97
97
|
Rapns::Apns::Feedback.stub(:create! => feedback)
|
98
|
-
Rapns.
|
98
|
+
Rapns.config.apns_feedback_callback.should_receive(:call).with(feedback)
|
99
99
|
receiver.check_for_feedback
|
100
100
|
end
|
101
101
|
|
102
|
-
it 'catches exceptions in the
|
102
|
+
it 'catches exceptions in the apns_feedback_callback' do
|
103
103
|
error = StandardError.new('bork!')
|
104
104
|
stub_connection_read_with_tuple
|
105
105
|
callback = Proc.new { raise error }
|
106
|
-
Rapns
|
106
|
+
Rapns.config.on_apns_feedback &callback
|
107
107
|
expect { receiver.check_for_feedback }.not_to raise_error
|
108
108
|
end
|
109
109
|
|
110
|
-
it 'logs an exception from the
|
110
|
+
it 'logs an exception from the apns_feedback_callback' do
|
111
111
|
error = StandardError.new('bork!')
|
112
112
|
stub_connection_read_with_tuple
|
113
113
|
callback = Proc.new { raise error }
|
114
114
|
Rapns::Daemon.logger.should_receive(:error).with(error)
|
115
|
-
Rapns
|
115
|
+
Rapns.config.on_apns_feedback &callback
|
116
116
|
receiver.check_for_feedback
|
117
117
|
end
|
118
118
|
end
|