rapns 3.0.0-java
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.
- data/CHANGELOG.md +31 -0
- data/LICENSE +7 -0
- data/README.md +138 -0
- data/bin/rapns +41 -0
- data/config/database.yml +39 -0
- data/lib/generators/rapns_generator.rb +34 -0
- data/lib/generators/templates/add_alert_is_json_to_rapns_notifications.rb +9 -0
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/add_gcm.rb +94 -0
- data/lib/generators/templates/create_rapns_apps.rb +16 -0
- data/lib/generators/templates/create_rapns_feedback.rb +15 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.rb +39 -0
- data/lib/rapns/apns/app.rb +8 -0
- data/lib/rapns/apns/binary_notification_validator.rb +12 -0
- data/lib/rapns/apns/device_token_format_validator.rb +12 -0
- data/lib/rapns/apns/feedback.rb +14 -0
- data/lib/rapns/apns/notification.rb +86 -0
- data/lib/rapns/apns/required_fields_validator.rb +14 -0
- data/lib/rapns/app.rb +29 -0
- data/lib/rapns/configuration.rb +46 -0
- data/lib/rapns/daemon/apns/app_runner.rb +36 -0
- data/lib/rapns/daemon/apns/connection.rb +113 -0
- data/lib/rapns/daemon/apns/delivery.rb +63 -0
- data/lib/rapns/daemon/apns/delivery_handler.rb +21 -0
- data/lib/rapns/daemon/apns/disconnection_error.rb +20 -0
- data/lib/rapns/daemon/apns/feedback_receiver.rb +74 -0
- data/lib/rapns/daemon/app_runner.rb +135 -0
- data/lib/rapns/daemon/database_reconnectable.rb +57 -0
- data/lib/rapns/daemon/delivery.rb +43 -0
- data/lib/rapns/daemon/delivery_error.rb +19 -0
- data/lib/rapns/daemon/delivery_handler.rb +46 -0
- data/lib/rapns/daemon/delivery_queue.rb +42 -0
- data/lib/rapns/daemon/delivery_queue_18.rb +44 -0
- data/lib/rapns/daemon/delivery_queue_19.rb +42 -0
- data/lib/rapns/daemon/feeder.rb +37 -0
- data/lib/rapns/daemon/gcm/app_runner.rb +13 -0
- data/lib/rapns/daemon/gcm/delivery.rb +206 -0
- data/lib/rapns/daemon/gcm/delivery_handler.rb +20 -0
- data/lib/rapns/daemon/interruptible_sleep.rb +18 -0
- data/lib/rapns/daemon/logger.rb +68 -0
- data/lib/rapns/daemon.rb +136 -0
- data/lib/rapns/gcm/app.rb +7 -0
- data/lib/rapns/gcm/expiry_collapse_key_mutual_inclusion_validator.rb +11 -0
- data/lib/rapns/gcm/notification.rb +31 -0
- data/lib/rapns/gcm/payload_size_validator.rb +13 -0
- data/lib/rapns/multi_json_helper.rb +16 -0
- data/lib/rapns/notification.rb +54 -0
- data/lib/rapns/patches/rails/3.1.0/postgresql_adapter.rb +12 -0
- data/lib/rapns/patches/rails/3.1.1/postgresql_adapter.rb +17 -0
- data/lib/rapns/patches.rb +6 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns.rb +21 -0
- data/lib/tasks/cane.rake +18 -0
- data/lib/tasks/test.rake +33 -0
- data/spec/acceptance/gcm_upgrade_spec.rb +34 -0
- data/spec/acceptance_spec_helper.rb +85 -0
- data/spec/support/simplecov_helper.rb +13 -0
- data/spec/support/simplecov_quality_formatter.rb +8 -0
- data/spec/unit/apns/app_spec.rb +15 -0
- data/spec/unit/apns/feedback_spec.rb +12 -0
- data/spec/unit/apns/notification_spec.rb +198 -0
- data/spec/unit/app_spec.rb +18 -0
- data/spec/unit/configuration_spec.rb +38 -0
- data/spec/unit/daemon/apns/app_runner_spec.rb +39 -0
- data/spec/unit/daemon/apns/connection_spec.rb +234 -0
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +48 -0
- data/spec/unit/daemon/apns/delivery_spec.rb +160 -0
- data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +118 -0
- data/spec/unit/daemon/app_runner_shared.rb +66 -0
- data/spec/unit/daemon/app_runner_spec.rb +129 -0
- data/spec/unit/daemon/database_reconnectable_spec.rb +109 -0
- data/spec/unit/daemon/delivery_error_spec.rb +13 -0
- data/spec/unit/daemon/delivery_handler_shared.rb +28 -0
- data/spec/unit/daemon/delivery_queue_spec.rb +29 -0
- data/spec/unit/daemon/feeder_spec.rb +95 -0
- data/spec/unit/daemon/gcm/app_runner_spec.rb +17 -0
- data/spec/unit/daemon/gcm/delivery_handler_spec.rb +36 -0
- data/spec/unit/daemon/gcm/delivery_spec.rb +236 -0
- data/spec/unit/daemon/interruptible_sleep_spec.rb +40 -0
- data/spec/unit/daemon/logger_spec.rb +156 -0
- data/spec/unit/daemon_spec.rb +139 -0
- data/spec/unit/gcm/app_spec.rb +5 -0
- data/spec/unit/gcm/notification_spec.rb +55 -0
- data/spec/unit/notification_shared.rb +38 -0
- data/spec/unit/notification_spec.rb +6 -0
- data/spec/unit_spec_helper.rb +145 -0
- metadata +240 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class DeliveryHandler
|
|
4
|
+
attr_accessor :queue
|
|
5
|
+
|
|
6
|
+
def start
|
|
7
|
+
@thread = Thread.new do
|
|
8
|
+
loop do
|
|
9
|
+
handle_next_notification
|
|
10
|
+
break if @stop
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stop
|
|
16
|
+
@stop = true
|
|
17
|
+
if @thread
|
|
18
|
+
queue.wakeup(@thread)
|
|
19
|
+
@thread.join
|
|
20
|
+
end
|
|
21
|
+
stopped
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
protected
|
|
25
|
+
|
|
26
|
+
def stopped
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def handle_next_notification
|
|
30
|
+
begin
|
|
31
|
+
notification = queue.pop
|
|
32
|
+
rescue DeliveryQueue::WakeupError
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
deliver(notification)
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
Rapns::Daemon.logger.error(e)
|
|
40
|
+
ensure
|
|
41
|
+
queue.notification_processed
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
if RUBY_VERSION < '1.9'
|
|
4
|
+
require 'rapns/daemon/delivery_queue_18'
|
|
5
|
+
ancestor_class = DeliveryQueue18
|
|
6
|
+
else
|
|
7
|
+
require 'rapns/daemon/delivery_queue_19'
|
|
8
|
+
ancestor_class = DeliveryQueue19
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class DeliveryQueue < ancestor_class
|
|
12
|
+
class WakeupError < StandardError; end
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@num_notifications = 0
|
|
16
|
+
@queue = []
|
|
17
|
+
@waiting = []
|
|
18
|
+
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def wakeup(thread)
|
|
23
|
+
synchronize do
|
|
24
|
+
t = @waiting.delete(thread)
|
|
25
|
+
t.raise WakeupError if t
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def size
|
|
30
|
+
synchronize { @queue.size }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def notification_processed
|
|
34
|
+
synchronize { @num_notifications -= 1 }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def notifications_processed?
|
|
38
|
+
synchronize { @num_notifications == 0 }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class DeliveryQueue18
|
|
4
|
+
def push(obj)
|
|
5
|
+
Thread.critical = true
|
|
6
|
+
@queue.push obj
|
|
7
|
+
@num_notifications += 1
|
|
8
|
+
begin
|
|
9
|
+
t = @waiting.shift
|
|
10
|
+
t.wakeup if t
|
|
11
|
+
rescue ThreadError
|
|
12
|
+
retry
|
|
13
|
+
ensure
|
|
14
|
+
Thread.critical = false
|
|
15
|
+
end
|
|
16
|
+
begin
|
|
17
|
+
t.run if t
|
|
18
|
+
rescue ThreadError
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pop
|
|
23
|
+
while (Thread.critical = true; @queue.empty?)
|
|
24
|
+
@waiting.push Thread.current
|
|
25
|
+
Thread.stop
|
|
26
|
+
end
|
|
27
|
+
@queue.shift
|
|
28
|
+
ensure
|
|
29
|
+
Thread.critical = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
protected
|
|
33
|
+
|
|
34
|
+
def synchronize
|
|
35
|
+
Thread.critical = true
|
|
36
|
+
begin
|
|
37
|
+
yield
|
|
38
|
+
ensure
|
|
39
|
+
Thread.critical = false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class DeliveryQueue19
|
|
4
|
+
def initialize
|
|
5
|
+
@mutex = Mutex.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def push(notification)
|
|
9
|
+
@mutex.synchronize do
|
|
10
|
+
@num_notifications += 1
|
|
11
|
+
@queue.push(notification)
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
t = @waiting.shift
|
|
15
|
+
t.wakeup if t
|
|
16
|
+
rescue ThreadError
|
|
17
|
+
retry
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pop
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
while true
|
|
25
|
+
if @queue.empty?
|
|
26
|
+
@waiting.push Thread.current
|
|
27
|
+
@mutex.sleep
|
|
28
|
+
else
|
|
29
|
+
return @queue.shift
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
def synchronize(&blk)
|
|
38
|
+
@mutex.synchronize(&blk)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class Feeder
|
|
4
|
+
extend InterruptibleSleep
|
|
5
|
+
extend DatabaseReconnectable
|
|
6
|
+
|
|
7
|
+
def self.start(poll)
|
|
8
|
+
loop do
|
|
9
|
+
enqueue_notifications
|
|
10
|
+
interruptible_sleep poll
|
|
11
|
+
break if @stop
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.stop
|
|
16
|
+
@stop = true
|
|
17
|
+
interrupt_sleep
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
def self.enqueue_notifications
|
|
23
|
+
begin
|
|
24
|
+
with_database_reconnect_and_retry do
|
|
25
|
+
batch_size = Rapns.config.batch_size
|
|
26
|
+
idle = Rapns::Daemon::AppRunner.idle.map(&:app)
|
|
27
|
+
Rapns::Notification.ready_for_delivery.for_apps(idle).limit(batch_size).each do |notification|
|
|
28
|
+
Rapns::Daemon::AppRunner.enqueue(notification)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Rapns::Daemon.logger.error(e)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Gcm
|
|
4
|
+
# http://developer.android.com/guide/google/gcm/gcm.html#response
|
|
5
|
+
class Delivery < Rapns::Daemon::Delivery
|
|
6
|
+
include Rapns::MultiJsonHelper
|
|
7
|
+
|
|
8
|
+
GCM_URI = URI.parse('https://android.googleapis.com/gcm/send')
|
|
9
|
+
UNAVAILABLE_STATES = ['Unavailable', 'InternalServerError']
|
|
10
|
+
|
|
11
|
+
def initialize(app, http, notification)
|
|
12
|
+
@app = app
|
|
13
|
+
@http = http
|
|
14
|
+
@notification = notification
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def perform
|
|
18
|
+
begin
|
|
19
|
+
handle_response(do_post)
|
|
20
|
+
rescue Rapns::DeliveryError => error
|
|
21
|
+
mark_failed(error.code, error.description)
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def handle_response(response)
|
|
29
|
+
case response.code.to_i
|
|
30
|
+
when 200
|
|
31
|
+
ok(response)
|
|
32
|
+
when 400
|
|
33
|
+
bad_request(response)
|
|
34
|
+
when 401
|
|
35
|
+
unauthorized(response)
|
|
36
|
+
when 500
|
|
37
|
+
internal_server_error(response)
|
|
38
|
+
when 503
|
|
39
|
+
service_unavailable(response)
|
|
40
|
+
else
|
|
41
|
+
raise Rapns::DeliveryError.new(response.code, @notification.id, HTTP_STATUS_CODES[response.code.to_i])
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ok(response)
|
|
46
|
+
body = multi_json_load(response.body)
|
|
47
|
+
|
|
48
|
+
if body['failure'].to_i == 0
|
|
49
|
+
mark_delivered
|
|
50
|
+
Rapns::Daemon.logger.info("[#{@app.name}] #{@notification.id} sent to #{@notification.registration_ids.join(', ')}")
|
|
51
|
+
else
|
|
52
|
+
handle_errors(response, body)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_errors(response, body)
|
|
57
|
+
errors = {}
|
|
58
|
+
|
|
59
|
+
body['results'].each_with_index do |result, i|
|
|
60
|
+
errors[i] = result['error'] if result['error']
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if body['success'].to_i == 0 && errors.values.all? { |error| error.in?(UNAVAILABLE_STATES) }
|
|
64
|
+
all_devices_unavailable(response)
|
|
65
|
+
elsif errors.values.any? { |error| error.in?(UNAVAILABLE_STATES) }
|
|
66
|
+
some_devices_unavailable(response, errors)
|
|
67
|
+
else
|
|
68
|
+
raise Rapns::DeliveryError.new(nil, @notification.id, describe_errors(errors))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def bad_request(response)
|
|
73
|
+
raise Rapns::DeliveryError.new(400, @notification.id, 'GCM failed to parse the JSON request. Possibly an rapns bug, please open an issue.')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def unauthorized(response)
|
|
77
|
+
raise Rapns::DeliveryError.new(401, @notification.id, 'Unauthorized, check your App auth_key.')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def internal_server_error(response)
|
|
81
|
+
retry_delivery(@notification, response)
|
|
82
|
+
Rapns::Daemon.logger.warn("GCM responded with an Internal Error. " + retry_message)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def service_unavailable(response)
|
|
86
|
+
retry_delivery(@notification, response)
|
|
87
|
+
Rapns::Daemon.logger.warn("GCM responded with an Service Unavailable Error. " + retry_message)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def all_devices_unavailable(response)
|
|
91
|
+
retry_delivery(@notification, response)
|
|
92
|
+
Rapns::Daemon.logger.warn("All recipients unavailable. " + retry_message)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def some_devices_unavailable(response, errors)
|
|
96
|
+
unavailable_idxs = errors.find_all { |i, error| error.in?(UNAVAILABLE_STATES) }.map(&:first)
|
|
97
|
+
new_notification = build_new_notification(response, unavailable_idxs)
|
|
98
|
+
with_database_reconnect_and_retry { new_notification.save! }
|
|
99
|
+
raise Rapns::DeliveryError.new(nil, @notification.id,
|
|
100
|
+
describe_errors(errors) + " #{unavailable_idxs.join(', ')} will be retried as notification #{new_notification.id}.")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_new_notification(response, idxs)
|
|
104
|
+
notification = Rapns::Gcm::Notification.new
|
|
105
|
+
notification.assign_attributes(@notification.attributes.slice('app_id', 'collapse_key', 'delay_while_idle'))
|
|
106
|
+
notification.data = @notification.data
|
|
107
|
+
notification.registration_ids = idxs.map { |i| @notification.registration_ids[i] }
|
|
108
|
+
notification.deliver_after = deliver_after_header(response)
|
|
109
|
+
notification
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def deliver_after_header(response)
|
|
113
|
+
if response.header['retry-after']
|
|
114
|
+
retry_after = if response.header['retry-after'].to_s =~ /^[0-9]+$/
|
|
115
|
+
Time.now + response.header['retry-after'].to_i
|
|
116
|
+
else
|
|
117
|
+
Time.httpdate(response.header['retry-after'])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def retry_delivery(notification, response)
|
|
123
|
+
if time = deliver_after_header(response)
|
|
124
|
+
retry_after(notification, time)
|
|
125
|
+
else
|
|
126
|
+
retry_exponentially(notification)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def describe_errors(errors)
|
|
131
|
+
description = if errors.size == @notification.registration_ids.size
|
|
132
|
+
"Failed to deliver to all recipients. Errors: #{errors.values.join(', ')}."
|
|
133
|
+
else
|
|
134
|
+
"Failed to deliver to recipients #{errors.keys.join(', ')}. Errors: #{errors.values.join(', ')}."
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def retry_message
|
|
139
|
+
"Notification #{@notification.id} will be retired after #{@notification.deliver_after.strftime("%Y-%m-%d %H:%M:%S")} (retry #{@notification.retries})."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def do_post
|
|
143
|
+
post = Net::HTTP::Post.new(GCM_URI.path, initheader = {'Content-Type' => 'application/json',
|
|
144
|
+
'Authorization' => "key=#{@notification.app.auth_key}"})
|
|
145
|
+
post.body = @notification.as_json.to_json
|
|
146
|
+
@http.request(GCM_URI, post)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
HTTP_STATUS_CODES = {
|
|
150
|
+
100 => 'Continue',
|
|
151
|
+
101 => 'Switching Protocols',
|
|
152
|
+
102 => 'Processing',
|
|
153
|
+
200 => 'OK',
|
|
154
|
+
201 => 'Created',
|
|
155
|
+
202 => 'Accepted',
|
|
156
|
+
203 => 'Non-Authoritative Information',
|
|
157
|
+
204 => 'No Content',
|
|
158
|
+
205 => 'Reset Content',
|
|
159
|
+
206 => 'Partial Content',
|
|
160
|
+
207 => 'Multi-Status',
|
|
161
|
+
226 => 'IM Used',
|
|
162
|
+
300 => 'Multiple Choices',
|
|
163
|
+
301 => 'Moved Permanently',
|
|
164
|
+
302 => 'Found',
|
|
165
|
+
303 => 'See Other',
|
|
166
|
+
304 => 'Not Modified',
|
|
167
|
+
305 => 'Use Proxy',
|
|
168
|
+
306 => 'Reserved',
|
|
169
|
+
307 => 'Temporary Redirect',
|
|
170
|
+
400 => 'Bad Request',
|
|
171
|
+
401 => 'Unauthorized',
|
|
172
|
+
402 => 'Payment Required',
|
|
173
|
+
403 => 'Forbidden',
|
|
174
|
+
404 => 'Not Found',
|
|
175
|
+
405 => 'Method Not Allowed',
|
|
176
|
+
406 => 'Not Acceptable',
|
|
177
|
+
407 => 'Proxy Authentication Required',
|
|
178
|
+
408 => 'Request Timeout',
|
|
179
|
+
409 => 'Conflict',
|
|
180
|
+
410 => 'Gone',
|
|
181
|
+
411 => 'Length Required',
|
|
182
|
+
412 => 'Precondition Failed',
|
|
183
|
+
413 => 'Request Entity Too Large',
|
|
184
|
+
414 => 'Request-URI Too Long',
|
|
185
|
+
415 => 'Unsupported Media Type',
|
|
186
|
+
416 => 'Requested Range Not Satisfiable',
|
|
187
|
+
417 => 'Expectation Failed',
|
|
188
|
+
418 => "I'm a Teapot",
|
|
189
|
+
422 => 'Unprocessable Entity',
|
|
190
|
+
423 => 'Locked',
|
|
191
|
+
424 => 'Failed Dependency',
|
|
192
|
+
426 => 'Upgrade Required',
|
|
193
|
+
500 => 'Internal Server Error',
|
|
194
|
+
501 => 'Not Implemented',
|
|
195
|
+
502 => 'Bad Gateway',
|
|
196
|
+
503 => 'Service Unavailable',
|
|
197
|
+
504 => 'Gateway Timeout',
|
|
198
|
+
505 => 'HTTP Version Not Supported',
|
|
199
|
+
506 => 'Variant Also Negotiates',
|
|
200
|
+
507 => 'Insufficient Storage',
|
|
201
|
+
510 => 'Not Extended',
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module Gcm
|
|
4
|
+
class DeliveryHandler < Rapns::Daemon::DeliveryHandler
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
@http = Net::HTTP::Persistent.new('rapns')
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def deliver(notification)
|
|
11
|
+
Rapns::Daemon::Gcm::Delivery.perform(@app, @http, notification)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def stopped
|
|
15
|
+
@http.shutdown
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
module InterruptibleSleep
|
|
4
|
+
def interruptible_sleep(seconds)
|
|
5
|
+
@_sleep_check, @_sleep_interrupt = IO.pipe
|
|
6
|
+
IO.select([@_sleep_check], nil, nil, seconds)
|
|
7
|
+
@_sleep_check.close rescue IOError
|
|
8
|
+
@_sleep_interrupt.close rescue IOError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def interrupt_sleep
|
|
12
|
+
if @_sleep_interrupt
|
|
13
|
+
@_sleep_interrupt.close rescue IOError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Daemon
|
|
3
|
+
class Logger
|
|
4
|
+
def initialize(options)
|
|
5
|
+
@options = options
|
|
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
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def info(msg)
|
|
20
|
+
log(:info, msg)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def error(msg, options = {})
|
|
24
|
+
airbrake_notify(msg) if notify_via_airbrake?(msg, options)
|
|
25
|
+
log(:error, msg, 'ERROR', STDERR)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def warn(msg)
|
|
29
|
+
log(:warn, msg, 'WARNING', STDERR)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def log(where, msg, prefix = nil, io = STDOUT)
|
|
35
|
+
if msg.is_a?(Exception)
|
|
36
|
+
formatted_backtrace = msg.backtrace.join("\n")
|
|
37
|
+
msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
formatted_msg = "[#{Time.now.to_s(:db)}] "
|
|
41
|
+
formatted_msg << "[#{prefix}] " if prefix
|
|
42
|
+
formatted_msg << msg
|
|
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
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def airbrake_notify(e)
|
|
54
|
+
return unless @options[:airbrake_notify] == true
|
|
55
|
+
|
|
56
|
+
if defined?(Airbrake)
|
|
57
|
+
Airbrake.notify_or_ignore(e)
|
|
58
|
+
elsif defined?(HoptoadNotifier)
|
|
59
|
+
HoptoadNotifier.notify_or_ignore(e)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def notify_via_airbrake?(msg, options)
|
|
64
|
+
msg.is_a?(Exception) && options[:airbrake_notify] != false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/lib/rapns/daemon.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
require 'net/http/persistent'
|
|
7
|
+
|
|
8
|
+
require 'rapns/daemon/interruptible_sleep'
|
|
9
|
+
require 'rapns/daemon/delivery_error'
|
|
10
|
+
require 'rapns/daemon/database_reconnectable'
|
|
11
|
+
require 'rapns/daemon/delivery'
|
|
12
|
+
require 'rapns/daemon/delivery_queue'
|
|
13
|
+
require 'rapns/daemon/feeder'
|
|
14
|
+
require 'rapns/daemon/logger'
|
|
15
|
+
require 'rapns/daemon/app_runner'
|
|
16
|
+
require 'rapns/daemon/delivery_handler'
|
|
17
|
+
|
|
18
|
+
require 'rapns/daemon/apns/delivery'
|
|
19
|
+
require 'rapns/daemon/apns/disconnection_error'
|
|
20
|
+
require 'rapns/daemon/apns/connection'
|
|
21
|
+
require 'rapns/daemon/apns/app_runner'
|
|
22
|
+
require 'rapns/daemon/apns/delivery_handler'
|
|
23
|
+
require 'rapns/daemon/apns/feedback_receiver'
|
|
24
|
+
|
|
25
|
+
require 'rapns/daemon/gcm/delivery'
|
|
26
|
+
require 'rapns/daemon/gcm/app_runner'
|
|
27
|
+
require 'rapns/daemon/gcm/delivery_handler'
|
|
28
|
+
|
|
29
|
+
module Rapns
|
|
30
|
+
module Daemon
|
|
31
|
+
extend DatabaseReconnectable
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
attr_accessor :logger
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.start
|
|
38
|
+
self.logger = Logger.new(:foreground => Rapns.config.foreground,
|
|
39
|
+
:airbrake_notify => Rapns.config.airbrake_notify)
|
|
40
|
+
setup_signal_hooks
|
|
41
|
+
|
|
42
|
+
unless Rapns.config.foreground
|
|
43
|
+
daemonize
|
|
44
|
+
reconnect_database
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
write_pid_file
|
|
48
|
+
ensure_upgraded
|
|
49
|
+
AppRunner.sync
|
|
50
|
+
Feeder.start(Rapns.config.push_poll)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
def self.ensure_upgraded
|
|
56
|
+
count = 0
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
count = Rapns::App.count
|
|
60
|
+
rescue ActiveRecord::StatementInvalid
|
|
61
|
+
puts "!!!! RAPNS NOT STARTED !!!!"
|
|
62
|
+
puts
|
|
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 app."
|
|
65
|
+
puts "See https://github.com/ileitch/rapns for further instructions."
|
|
66
|
+
puts
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if count == 0
|
|
71
|
+
logger.warn("You have not created an app yet. See https://github.com/ileitch/rapns for instructions.")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if File.exists?(File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
|
|
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
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.setup_signal_hooks
|
|
84
|
+
@shutting_down = false
|
|
85
|
+
|
|
86
|
+
Signal.trap('SIGHUP') { AppRunner.sync }
|
|
87
|
+
Signal.trap('SIGUSR2') { AppRunner.debug }
|
|
88
|
+
|
|
89
|
+
['SIGINT', 'SIGTERM'].each do |signal|
|
|
90
|
+
Signal.trap(signal) { handle_shutdown_signal }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.handle_shutdown_signal
|
|
95
|
+
exit 1 if @shutting_down
|
|
96
|
+
@shutting_down = true
|
|
97
|
+
shutdown
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.shutdown
|
|
101
|
+
puts "\nShutting down..."
|
|
102
|
+
Feeder.stop
|
|
103
|
+
AppRunner.stop
|
|
104
|
+
delete_pid_file
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.write_pid_file
|
|
108
|
+
if !Rapns.config.pid_file.blank?
|
|
109
|
+
begin
|
|
110
|
+
File.open(Rapns.config.pid_file, 'w') { |f| f.puts Process.pid }
|
|
111
|
+
rescue SystemCallError => e
|
|
112
|
+
logger.error("Failed to write PID to '#{Rapns.config.pid_file}': #{e.inspect}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.delete_pid_file
|
|
118
|
+
pid_file = Rapns.config.pid_file
|
|
119
|
+
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# :nocov:
|
|
123
|
+
def self.daemonize
|
|
124
|
+
exit if pid = fork
|
|
125
|
+
Process.setsid
|
|
126
|
+
exit if pid = fork
|
|
127
|
+
|
|
128
|
+
Dir.chdir '/'
|
|
129
|
+
File.umask 0000
|
|
130
|
+
|
|
131
|
+
STDIN.reopen '/dev/null'
|
|
132
|
+
STDOUT.reopen '/dev/null', 'a'
|
|
133
|
+
STDERR.reopen STDOUT
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Rapns
|
|
2
|
+
module Gcm
|
|
3
|
+
class ExpiryCollapseKeyMutualInclusionValidator < ActiveModel::Validator
|
|
4
|
+
def validate(record)
|
|
5
|
+
if record.collapse_key && !record.expiry
|
|
6
|
+
record.errors[:expiry] << "must be set when using a collapse_key"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|