rapns 3.0.0-java
Sign up to get free protection for your applications and to get access to all the features.
- 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
|