apn_sender 1.0.6 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/README.md +20 -18
- data/lib/apn.rb +95 -12
- data/lib/apn/backend.rb +34 -0
- data/lib/apn/client.rb +65 -0
- data/lib/apn/connection.rb +39 -0
- data/lib/apn/feedback.rb +19 -23
- data/lib/apn/jobs/resque_notification_job.rb +15 -0
- data/lib/apn/jobs/sidekiq_notification_job.rb +15 -0
- data/lib/apn/notification.rb +13 -2
- data/lib/apn/railtie.rb +16 -0
- data/lib/apn/sender_daemon.rb +23 -29
- data/lib/apn/tasks.rb +11 -4
- data/lib/apn/version.rb +1 -1
- metadata +29 -38
- data/CHANGELOG +0 -5
- data/lib/apn/connection/base.rb +0 -127
- data/lib/apn/notification_job.rb +0 -24
- data/lib/apn/queue_manager.rb +0 -57
- data/lib/apn/queue_name.rb +0 -4
- data/lib/apn/sender.rb +0 -49
- data/lib/resque/hooks/before_unregister_worker.rb +0 -30
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d85dcde79eb0f21533547c631b827e73081d5167
|
4
|
+
data.tar.gz: a3bd028152b5f7ee6960b1516bf87b2fa7abdce8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 96a448c4a6d1ed2fcc9807617eea4171e876ef45f7dd94b41925ca84c81f656404ef526ab590f7d30612571bd3ecab4f47621aabe44aeb66c5c3909b89794da5
|
7
|
+
data.tar.gz: 12b386591581bdcb234d0dd90de44f0836ad27705aedeeefcc6e0333d5225207fdabd0e085c0cf2675abfbbed1b725d716611ca859ea06c60a6ffe9cb1385a46
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Version 2.0
|
2
|
+
## 2.0.0
|
3
|
+
- adding connection_pool for handle apple sockets
|
4
|
+
- removing resque hard dependency
|
5
|
+
- adding support for sending sync messages
|
6
|
+
- adding Thread support
|
7
|
+
- adding support to sidekiq (Caue Guerra)
|
8
|
+
- truncation messages when payload is greater than 256 option (Caue Guerra)
|
9
|
+
|
10
|
+
# Version 1.0
|
11
|
+
## 1.0.6
|
12
|
+
- Added support for password-protected .pem files
|
13
|
+
- Read feedback data in 38-byte chunks
|
14
|
+
- Support passing dictionary as :alert key
|
15
|
+
- Logging to STDOUT if no other loggers present
|
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
[![Code Climate](https://codeclimate.com/github/arthurnn/apn_sender.png)](https://codeclimate.com/github/arthurnn/apn_sender)
|
2
|
+
[![Build Status](https://travis-ci.org/arthurnn/apn_sender.png)](https://travis-ci.org/arthurnn/apn_sender)
|
3
|
+
|
1
4
|
## UPDATE May 3, 2013: current status
|
2
5
|
|
3
|
-
This project was not supported for a while, but we are going to support it again.
|
6
|
+
This project was not supported for a while, but we are going to support it again.
|
4
7
|
|
5
8
|
-----
|
6
9
|
|
@@ -19,7 +22,7 @@ The apn_sender gem includes a background daemon which processes background messa
|
|
19
22
|
Yup. There's some great code out there already, but we didn't like the idea of getting banned from the APN gateway for establishing a new connection each time we needed to send a batch of messages, and none of the libraries I found handled maintaining a persistent connection.
|
20
23
|
|
21
24
|
## Current Status
|
22
|
-
This gem has been in production on 500px,sending
|
25
|
+
This gem has been used in production, on 500px, sending millions of notifications.
|
23
26
|
|
24
27
|
## Usage
|
25
28
|
|
@@ -28,7 +31,7 @@ This gem has been in production on 500px,sending million of notifications.
|
|
28
31
|
To queue a message for sending through Apple's Push Notification service from your Rails application:
|
29
32
|
|
30
33
|
```
|
31
|
-
APN.
|
34
|
+
APN.notify_async(token, opts_hash)
|
32
35
|
```
|
33
36
|
|
34
37
|
where ```token``` is the unique identifier of the iPhone to receive the notification and ```opts_hash``` can have any of the following keys:
|
@@ -59,10 +62,9 @@ For production, you're probably better off running a dedicated daemon and settin
|
|
59
62
|
./script/generate apn_sender
|
60
63
|
|
61
64
|
# To run daemon. Pass --help to print all options
|
62
|
-
./script/apn_sender
|
65
|
+
./script/apn_sender start
|
63
66
|
```
|
64
67
|
|
65
|
-
Note the --environment must be explicitly set (separately from your <code>Rails.env</code>) to production in order to send messages via the production APN servers. Any other environment sends messages through Apple's sandbox servers at <code>gateway.sandbox.push.apple.com</code>.
|
66
68
|
|
67
69
|
Also, there are two similar options: ```:cert_path``` and ```:full_cert_path```. The former specifies the directory in which to find the .pem file (either apn_production.pem or apn_development.pem, depending on the environment). The latter specifies a .pem file explicitly, allowing customized certificate names if needed.
|
68
70
|
|
@@ -70,7 +72,7 @@ Check ```logs/apn_sender.log``` for debugging output. In addition to logging an
|
|
70
72
|
|
71
73
|
|
72
74
|
### 3. Checking Apple's Feedback Service
|
73
|
-
|
75
|
+
|
74
76
|
Since push notifications are a fire-and-forget sorta deal, where you get no indication if your message was received (or if the specified recipient even exists), Apple needed to come up with some other way to ensure their network isn't clogged with thousands of bogus messages (e.g. from developers sending messages to phones where their application <em>used</em> to be installed, but where the user has since removed it). Hence, the Feedback Service.
|
75
77
|
|
76
78
|
It's actually really simple - you connect to them periodically and they give you a big dump of tokens you shouldn't send to anymore. The gem wraps this up nicely -- just call:
|
@@ -78,9 +80,9 @@ It's actually really simple - you connect to them periodically and they give you
|
|
78
80
|
```
|
79
81
|
# APN::Feedback accepts the same optional :environment
|
80
82
|
# and :cert_path / :full_cert_path options as APN::Sender
|
81
|
-
feedback
|
83
|
+
feedback = APN::Feedback.new()
|
82
84
|
|
83
|
-
tokens
|
85
|
+
tokens = feedback.tokens # Array of device tokens
|
84
86
|
tokens.each do |token|
|
85
87
|
# ... custom logic here to stop you app from
|
86
88
|
# sending further notifications to this token
|
@@ -90,7 +92,7 @@ It's actually really simple - you connect to them periodically and they give you
|
|
90
92
|
If you're interested in knowing exactly <em>when</em> Apple determined each token was expired (which can be useful in determining if the application re-registered with your service since it first appeared in the expired queue):
|
91
93
|
|
92
94
|
```
|
93
|
-
items
|
95
|
+
items = feedback.data # Array of APN::FeedbackItem elements
|
94
96
|
items.each do |item|
|
95
97
|
item.token
|
96
98
|
item.timestamp
|
@@ -110,15 +112,15 @@ If you're sending notifications, you should definitely call one of the ```receiv
|
|
110
112
|
Just for the record, this is essentially what you want to have whenever run periodically for you:
|
111
113
|
```
|
112
114
|
def self.clear_uninstalled_applications
|
113
|
-
feedback_data
|
114
|
-
|
115
|
+
feedback_data = APN::Feedback.new(:environment #> :production).data
|
116
|
+
|
115
117
|
feedback_data.each do |item|
|
116
|
-
user
|
117
|
-
|
118
|
+
user = User.find_by_iphone_token( item.token )
|
119
|
+
|
118
120
|
if user.iphone_token_updated_at && user.iphone_token_updated_at > item.timestamp
|
119
121
|
return true # App has been reregistered since Apple determined it'd been uninstalled
|
120
122
|
else
|
121
|
-
user.update_attributes(:
|
123
|
+
user.update_attributes(iphone_token: nil, iphone_token_updated_at: Time.now)
|
122
124
|
end
|
123
125
|
end
|
124
126
|
end
|
@@ -141,12 +143,12 @@ In your Rails app, add (2.3.x):
|
|
141
143
|
|
142
144
|
```
|
143
145
|
config.gem 'apn_sender', :lib => 'apn'
|
144
|
-
```
|
146
|
+
```
|
145
147
|
or (3.x) to your Gemfile:
|
146
148
|
|
147
|
-
```
|
149
|
+
```
|
148
150
|
gem 'apn_sender', require: 'apn'
|
149
|
-
```
|
151
|
+
```
|
150
152
|
To add a few useful rake tasks for running workers, add the following line to your Rakefile:
|
151
153
|
|
152
154
|
```
|
@@ -155,4 +157,4 @@ To add a few useful rake tasks for running workers, add the following line to yo
|
|
155
157
|
|
156
158
|
## Copyright
|
157
159
|
|
158
|
-
Copyright (c)
|
160
|
+
Copyright (c) 2013 Arthur Nogueira Neves. See LICENSE for details.
|
data/lib/apn.rb
CHANGED
@@ -1,13 +1,96 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require '
|
6
|
-
|
7
|
-
require
|
8
|
-
require 'apn/
|
1
|
+
require "openssl"
|
2
|
+
require "socket"
|
3
|
+
require "active_support/core_ext"
|
4
|
+
require "active_support/json"
|
5
|
+
require 'connection_pool'
|
6
|
+
|
7
|
+
require "apn/version"
|
8
|
+
require 'apn/connection'
|
9
|
+
|
10
|
+
module APN
|
11
|
+
|
12
|
+
class << self
|
13
|
+
include APN::Connection
|
14
|
+
|
15
|
+
def notify_async(token, opts = {})
|
16
|
+
token = token.to_s.gsub(/\W/, '')
|
17
|
+
backend.notify(token, opts)
|
18
|
+
end
|
19
|
+
|
20
|
+
def notify_sync(token, opts)
|
21
|
+
token = token.to_s.gsub(/\W/, '')
|
22
|
+
msg = APN::Notification.new(token, opts)
|
23
|
+
raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
|
24
|
+
|
25
|
+
APN.with_connection do |client|
|
26
|
+
client.push(msg)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def backend=(backend)
|
31
|
+
@backend = backend
|
32
|
+
end
|
33
|
+
|
34
|
+
def backend
|
35
|
+
@backend ||=
|
36
|
+
if defined?(Sidekiq)
|
37
|
+
APN::Backend::Sidekiq.new
|
38
|
+
elsif defined?(Resque)
|
39
|
+
APN::Backend::Resque.new
|
40
|
+
else
|
41
|
+
APN::Backend::Simple.new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def logger=(logger)
|
46
|
+
@logger = logger
|
47
|
+
end
|
48
|
+
|
49
|
+
def logger
|
50
|
+
@logger ||= Logger.new(STDOUT)
|
51
|
+
end
|
52
|
+
|
53
|
+
def truncate_alert
|
54
|
+
@truncate_alert ||= false
|
55
|
+
end
|
56
|
+
|
57
|
+
def truncate_alert=(truncate)
|
58
|
+
@truncate_alert = truncate
|
59
|
+
end
|
60
|
+
|
61
|
+
# Log message to any logger provided by the user (e.g. the Rails logger).
|
62
|
+
# Accepts +log_level+, +message+, since that seems to make the most sense,
|
63
|
+
# and just +message+, to be compatible with Resque's log method and to enable
|
64
|
+
# sending verbose and very_verbose worker messages to e.g. the rails logger.
|
65
|
+
#
|
66
|
+
# Perhaps a method definition of +message, +level+ would make more sense, but
|
67
|
+
# that's also the complete opposite of what anyone comming from rails would expect.
|
68
|
+
def log(level, message = nil)
|
69
|
+
level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
|
70
|
+
|
71
|
+
return false unless logger && logger.respond_to?(level)
|
72
|
+
logger.send(level, "#{Time.now}: #{message}")
|
73
|
+
end
|
74
|
+
|
75
|
+
# Log the message first, to ensure it reports what went wrong if in daemon mode.
|
76
|
+
# Then die, because something went horribly wrong.
|
77
|
+
def log_and_die(msg)
|
78
|
+
logger.fatal(msg)
|
79
|
+
raise msg
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
9
84
|
require 'apn/notification'
|
10
|
-
require 'apn/
|
11
|
-
require 'apn/
|
12
|
-
|
13
|
-
|
85
|
+
require 'apn/client'
|
86
|
+
require 'apn/feedback'
|
87
|
+
|
88
|
+
module APN::Jobs
|
89
|
+
QUEUE_NAME = :apple_push_notifications
|
90
|
+
end
|
91
|
+
|
92
|
+
require 'apn/jobs/sidekiq_notification_job' if defined?(Sidekiq)
|
93
|
+
require 'apn/jobs/resque_notification_job' if defined?(Resque)
|
94
|
+
require "apn/railtie" if defined?(Rails)
|
95
|
+
|
96
|
+
require 'apn/backend'
|
data/lib/apn/backend.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module APN
|
2
|
+
module Backend
|
3
|
+
|
4
|
+
class Sidekiq
|
5
|
+
|
6
|
+
def notify(token, opts)
|
7
|
+
::Sidekiq::Client.enqueue(APN::Jobs::SidekiqNotificationJob, token, opts)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Resque
|
12
|
+
|
13
|
+
def notify(token, opts)
|
14
|
+
::Resque.enqueue(APN::Jobs::ResqueNotificationJob, token, opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Simple
|
19
|
+
|
20
|
+
def notify(token, opts)
|
21
|
+
Thread.new do
|
22
|
+
APN.notify_sync(token, opts)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Null
|
28
|
+
|
29
|
+
def notify(token, opts)
|
30
|
+
APN.log("Null Backend sending message to #{token}")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/apn/client.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module APN
|
2
|
+
class Client
|
3
|
+
|
4
|
+
DEFAULTS = {port: 2195, host: "gateway.push.apple.com"}
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
options = DEFAULTS.merge options.reject{|k,v| v.nil?}
|
8
|
+
@apn_cert, @cert_pass = options[:certificate], options[:password]
|
9
|
+
@host, @port = options[:host], options[:port]
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def push(message)
|
14
|
+
socket.write(message.to_s)
|
15
|
+
socket.flush
|
16
|
+
if IO.select([socket], nil, nil, 1) && error = socket.read(6)
|
17
|
+
error = error.unpack("ccN")
|
18
|
+
APN.log(:error, "Error on message: #{error}")
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
|
22
|
+
APN.log(:debug, "Message sent.")
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def feedback
|
27
|
+
if bunch = socket.read(38)
|
28
|
+
f = bunch.strip.unpack('N1n1H140')
|
29
|
+
APN::FeedbackItem.new(Time.at(f[0]), f[2])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def socket
|
34
|
+
@socket ||= setup_socket
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
# Open socket to Apple's servers
|
39
|
+
def setup_socket
|
40
|
+
ctx = setup_certificate
|
41
|
+
|
42
|
+
APN.log(:debug, "Connecting to #{@host}:#{@port}...")
|
43
|
+
|
44
|
+
socket_tcp = TCPSocket.new(@host, @port)
|
45
|
+
OpenSSL::SSL::SSLSocket.new(socket_tcp, ctx).tap do |s|
|
46
|
+
s.sync = true
|
47
|
+
s.connect
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def setup_certificate
|
52
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
53
|
+
ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
|
54
|
+
if @cert_pass
|
55
|
+
ctx.key = OpenSSL::PKey::RSA.new(@apn_cert, @cert_pass)
|
56
|
+
APN.log(:debug, "Setting up certificate using a password.")
|
57
|
+
|
58
|
+
else
|
59
|
+
ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
|
60
|
+
end
|
61
|
+
ctx
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module APN
|
2
|
+
module Connection
|
3
|
+
# APN::Connection::Base takes care of all the boring certificate loading, socket creating, and logging
|
4
|
+
# responsibilities so APN::Sender and APN::Feedback and focus on their respective specialties.
|
5
|
+
def connection_pool
|
6
|
+
@pool ||= ConnectionPool.new(size: (pool_size || 1), timeout: (pool_timeout || 5)) do
|
7
|
+
APN::Client.new(host: host,
|
8
|
+
port: port,
|
9
|
+
certificate: certificate,
|
10
|
+
password: password)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def with_connection(&block)
|
15
|
+
connection_pool.with(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# pool config
|
19
|
+
attr_accessor :pool_size, :pool_timeout
|
20
|
+
|
21
|
+
attr_accessor :host, :port, :root, :full_certificate_path, :password
|
22
|
+
|
23
|
+
def certificate
|
24
|
+
@apn_cert ||= File.read(certificate_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
def certificate_path
|
28
|
+
full_certificate_path || File.join(root, certificate_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def certificate_name
|
32
|
+
@cert_name || "apn_production.pem"
|
33
|
+
end
|
34
|
+
|
35
|
+
def certificate_name=(name)
|
36
|
+
@cert_name = name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/apn/feedback.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'apn/connection/base'
|
2
|
-
|
3
1
|
module APN
|
4
2
|
# Encapsulates data returned from the {APN Feedback Service}[http://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3].
|
5
3
|
# Possesses +timestamp+ and +token+ attributes.
|
@@ -22,7 +20,10 @@ module APN
|
|
22
20
|
#
|
23
21
|
# See README for usage and details.
|
24
22
|
class Feedback
|
25
|
-
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
@apn_host, @apn_port = options[:host], options[:port]
|
26
|
+
end
|
26
27
|
|
27
28
|
# Returns array of APN::FeedbackItem elements read from Apple. Connects to Apple once and caches the
|
28
29
|
# data, continues to returns cached data unless called with <code>data(true)</code>, which clears the
|
@@ -45,7 +46,7 @@ module APN
|
|
45
46
|
|
46
47
|
# Prettify to return meaningful status information when printed. Can't add these directly to connection/base, because Resque depends on decoding to_s
|
47
48
|
def to_s
|
48
|
-
"#{@socket ? 'Connected' : 'Connection not currently established'} to #{
|
49
|
+
"#{@socket ? 'Connected' : 'Connection not currently established'} to #{host} on #{port}"
|
49
50
|
end
|
50
51
|
|
51
52
|
protected
|
@@ -53,31 +54,26 @@ module APN
|
|
53
54
|
# Connects to Apple's Feedback Service and checks if there's anything there for us.
|
54
55
|
# Returns an array of APN::FeedbackItem pairs
|
55
56
|
def receive
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
setup_connection
|
60
|
-
|
61
|
-
# Unpacking code borrowed from http://github.com/jpoz/APNS/blob/master/lib/apns/core.rb
|
62
|
-
while bunch = socket.read(38) # Read data from the socket
|
63
|
-
f = bunch.strip.unpack('N1n1H140')
|
64
|
-
feedback << APN::FeedbackItem.new(Time.at(f[0]), f[2])
|
57
|
+
feedbacks = []
|
58
|
+
while f = client.feedback
|
59
|
+
feedbacks << f
|
65
60
|
end
|
66
|
-
|
67
|
-
# Bye Apple
|
68
|
-
teardown_connection
|
69
|
-
|
70
|
-
return feedback
|
61
|
+
return feedbacks
|
71
62
|
end
|
72
63
|
|
73
|
-
|
74
|
-
|
75
|
-
|
64
|
+
def client
|
65
|
+
@client ||= APN::Client.new(host: host,
|
66
|
+
port: port,
|
67
|
+
certificate: APN.certificate,
|
68
|
+
password: APN.password)
|
76
69
|
end
|
77
70
|
|
78
|
-
def
|
79
|
-
|
71
|
+
def host
|
72
|
+
@apn_host || "feedback.push.apple.com"
|
80
73
|
end
|
81
74
|
|
75
|
+
def port
|
76
|
+
@apn_port || 2196
|
77
|
+
end
|
82
78
|
end
|
83
79
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module APN::Jobs
|
2
|
+
# This is the class that's actually enqueued via Resque when user calls +APN.notify+.
|
3
|
+
# It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
|
4
|
+
# workers of the +APN::Sender+ class.
|
5
|
+
class ResqueNotificationJob
|
6
|
+
|
7
|
+
# Behind the scenes, this is the name of our Resque queue
|
8
|
+
@queue = QUEUE_NAME
|
9
|
+
|
10
|
+
# Build a notification from arguments and send to Apple
|
11
|
+
def self.perform(token, opts)
|
12
|
+
APN.notify_sync(token, opts)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module APN::Jobs
|
2
|
+
# This is the class that's actually enqueued via Resque when user calls +APN.notify+.
|
3
|
+
# It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
|
4
|
+
# workers of the +APN::Sender+ class.
|
5
|
+
class SidekiqNotificationJob
|
6
|
+
include Sidekiq::Worker
|
7
|
+
# Behind the scenes, this is the name of our Sidekiq queue
|
8
|
+
@queue = QUEUE_NAME
|
9
|
+
|
10
|
+
# Build a notification from arguments and send to Apple
|
11
|
+
def perform(token, opts)
|
12
|
+
APN.notify_sync(token, opts)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/apn/notification.rb
CHANGED
@@ -22,12 +22,15 @@ module APN
|
|
22
22
|
# Each iPhone Notification payload must be 256 or fewer characters. Encoding a null message has a 57
|
23
23
|
# character overhead, so there are 199 characters available for the alert string.
|
24
24
|
MAX_ALERT_LENGTH = 199
|
25
|
+
DATA_MAX_BYTES = 256
|
25
26
|
|
26
27
|
attr_accessor :options, :token
|
27
28
|
def initialize(token, opts)
|
28
29
|
@options = opts.is_a?(Hash) ? opts.symbolize_keys : {:alert => opts}
|
29
30
|
@token = token
|
30
31
|
|
32
|
+
truncate_alert! if APN.truncate_alert
|
33
|
+
|
31
34
|
raise "The maximum size allowed for a notification payload is 256 bytes." if packaged_notification.size.to_i > 256
|
32
35
|
end
|
33
36
|
|
@@ -41,8 +44,6 @@ module APN
|
|
41
44
|
false
|
42
45
|
end
|
43
46
|
|
44
|
-
protected
|
45
|
-
|
46
47
|
# Completed encoded notification, ready to send down the wire to Apple
|
47
48
|
def packaged_notification
|
48
49
|
pt = packaged_token
|
@@ -73,5 +74,15 @@ module APN
|
|
73
74
|
ActiveSupport::JSON::encode(hsh)
|
74
75
|
end
|
75
76
|
|
77
|
+
def truncate_alert!
|
78
|
+
while packaged_notification.size.to_i > DATA_MAX_BYTES
|
79
|
+
if @options[:alert].is_a? Hash
|
80
|
+
last = @options[:alert]['loc-args'].pop
|
81
|
+
@options[:alert]['loc-args'] << last[0..-2]
|
82
|
+
else
|
83
|
+
@options[:alert] = @options[:alert][0..-2]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
76
87
|
end
|
77
88
|
end
|
data/lib/apn/railtie.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module APN
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer "apn.setup" do |app|
|
4
|
+
|
5
|
+
APN.root = File.join(Rails.root, "config", "certs")
|
6
|
+
if Rails.env.development?
|
7
|
+
APN.certificate_name = "apn_development.pem"
|
8
|
+
APN.host = "gateway.sandbox.push.apple.com"
|
9
|
+
end
|
10
|
+
|
11
|
+
logger = Logger.new(File.join(Rails.root, 'log', 'apn_sender.log'))
|
12
|
+
APN.logger = logger
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/apn/sender_daemon.rb
CHANGED
@@ -3,6 +3,7 @@ require 'rubygems'
|
|
3
3
|
require 'daemons'
|
4
4
|
require 'optparse'
|
5
5
|
require 'logger'
|
6
|
+
require 'resque'
|
6
7
|
|
7
8
|
module APN
|
8
9
|
# A wrapper designed to daemonize an APN::Sender instance to keep in running in the background.
|
@@ -12,10 +13,10 @@ module APN
|
|
12
13
|
# Based off delayed_job's great example, except we can be much lighter by not loading the entire
|
13
14
|
# Rails environment. To use in a Rails app, <code>script/generate apn_sender</code>.
|
14
15
|
class SenderDaemon
|
15
|
-
|
16
|
+
|
16
17
|
def initialize(args)
|
17
|
-
@options = {:
|
18
|
-
|
18
|
+
@options = {worker_count: 1, delay: 5}
|
19
|
+
|
19
20
|
optparse = OptionParser.new do |opts|
|
20
21
|
opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
|
21
22
|
|
@@ -23,59 +24,52 @@ module APN
|
|
23
24
|
puts opts
|
24
25
|
exit 1
|
25
26
|
end
|
26
|
-
opts.on('-
|
27
|
-
@options[:
|
28
|
-
end
|
29
|
-
opts.on('--cert-path=NAME', 'Path to directory containing apn .pem certificates.') do |path|
|
30
|
-
@options[:cert_path] = path
|
27
|
+
opts.on('--cert-path=PATH', 'Path to directory containing apn .pem certificates.') do |path|
|
28
|
+
@options[:cert_root] = path
|
31
29
|
end
|
32
|
-
opts.on('c', '--full-cert-path=
|
30
|
+
opts.on('c', '--full-cert-path=PATH', 'Full path to desired .pem certificate.') do |path|
|
33
31
|
@options[:full_cert_path] = path
|
34
32
|
end
|
35
33
|
opts.on('--cert-pass=PASSWORD', 'Password for the apn .pem certificates.') do |pass|
|
36
34
|
@options[:cert_pass] = pass
|
37
35
|
end
|
36
|
+
opts.on('--cert-name=NAME', 'Certificate file name. Default: apn_production.pem') do |certificate_name|
|
37
|
+
@options[:certificate_name] = certificate_name
|
38
|
+
end
|
38
39
|
opts.on('-n', '--number-of-workers=WORKERS', "Number of unique workers to spawn") do |worker_count|
|
39
40
|
@options[:worker_count] = worker_count.to_i rescue 1
|
40
41
|
end
|
41
|
-
opts.on('-v', '--verbose', "Turn on verbose mode") do
|
42
|
-
@options[:verbose] = true
|
43
|
-
end
|
44
|
-
opts.on('-V', '--very-verbose', "Turn on very verbose mode") do
|
45
|
-
@options[:very_verbose] = true
|
46
|
-
end
|
47
42
|
opts.on('-d', '--delay=D', "Delay between rounds of work (seconds)") do |d|
|
48
43
|
@options[:delay] = d
|
49
44
|
end
|
50
45
|
end
|
51
|
-
|
46
|
+
|
52
47
|
# If no arguments, give help screen
|
53
48
|
@args = optparse.parse!(args.empty? ? ['-h'] : args)
|
54
|
-
@options[:verbose] = true if @options[:very_verbose]
|
55
49
|
end
|
56
|
-
|
50
|
+
|
57
51
|
def daemonize
|
58
52
|
@options[:worker_count].times do |worker_index|
|
59
53
|
process_name = @options[:worker_count] == 1 ? "apn_sender" : "apn_sender.#{worker_index}"
|
60
|
-
|
61
|
-
|
54
|
+
pids_dir = defined?(Rails) ? "#{::RAILS_ROOT}/tmp/pids" : "tmp/pids"
|
55
|
+
Daemons.run_proc(process_name, :dir => pids_dir, :dir_mode => :normal, :ARGV => @args) do |*args|
|
56
|
+
run(process_name)
|
62
57
|
end
|
63
58
|
end
|
64
59
|
end
|
65
|
-
|
60
|
+
|
66
61
|
def run(worker_name = nil)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
worker
|
62
|
+
APN.password = @options[:cert_pass]
|
63
|
+
APN.full_certificate_path = @options[:full_cert_path]
|
64
|
+
APN.root = @options[:cert_root]
|
65
|
+
APN.certificate_name = @options[:certificate_name]
|
66
|
+
|
67
|
+
worker = ::Resque::Worker.new(APN::Jobs::QUEUE_NAME)
|
73
68
|
worker.work(@options[:delay])
|
74
69
|
rescue => e
|
75
70
|
STDERR.puts e.message
|
76
|
-
logger.fatal(e) if logger && logger.respond_to?(:fatal)
|
77
71
|
exit 1
|
78
72
|
end
|
79
|
-
|
73
|
+
|
80
74
|
end
|
81
75
|
end
|
data/lib/apn/tasks.rb
CHANGED
@@ -8,9 +8,16 @@ namespace :apn do
|
|
8
8
|
task :sender => :setup do
|
9
9
|
require 'apn'
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
unless defined?(Resque)
|
12
|
+
puts "This rake task is only for resque workers"
|
13
|
+
return
|
14
|
+
end
|
15
|
+
|
16
|
+
APN.password = ENV['CERT_PASS']
|
17
|
+
APN.full_certificate_path = ENV['FULL_CERT_PATH']
|
18
|
+
APN.logger = Rails.logger
|
19
|
+
|
20
|
+
worker = ::Resque::Worker.new(APN::Jobs::QUEUE_NAME)
|
14
21
|
|
15
22
|
puts "*** Starting worker to send apple notifications in the background from #{worker}"
|
16
23
|
|
@@ -29,4 +36,4 @@ namespace :apn do
|
|
29
36
|
|
30
37
|
threads.each { |thread| thread.join }
|
31
38
|
end
|
32
|
-
end
|
39
|
+
end
|
data/lib/apn/version.rb
CHANGED
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apn_sender
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
5
|
-
prerelease:
|
4
|
+
version: 2.0.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Kali Donovan
|
@@ -13,102 +12,94 @@ cert_chain: []
|
|
13
12
|
date: 2011-05-15 00:00:00.000000000 Z
|
14
13
|
dependencies:
|
15
14
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
15
|
+
name: connection_pool
|
17
16
|
requirement: !ruby/object:Gem::Requirement
|
18
|
-
none: false
|
19
17
|
requirements:
|
20
|
-
- -
|
18
|
+
- - '>='
|
21
19
|
- !ruby/object:Gem::Version
|
22
20
|
version: '0'
|
23
21
|
type: :runtime
|
24
22
|
prerelease: false
|
25
23
|
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
none: false
|
27
24
|
requirements:
|
28
|
-
- -
|
25
|
+
- - '>='
|
29
26
|
- !ruby/object:Gem::Version
|
30
27
|
version: '0'
|
31
28
|
- !ruby/object:Gem::Dependency
|
32
|
-
name:
|
29
|
+
name: activesupport
|
33
30
|
requirement: !ruby/object:Gem::Requirement
|
34
|
-
none: false
|
35
31
|
requirements:
|
36
|
-
- -
|
32
|
+
- - '>='
|
37
33
|
- !ruby/object:Gem::Version
|
38
|
-
version: '
|
34
|
+
version: '3.1'
|
39
35
|
type: :runtime
|
40
36
|
prerelease: false
|
41
37
|
version_requirements: !ruby/object:Gem::Requirement
|
42
|
-
none: false
|
43
38
|
requirements:
|
44
|
-
- -
|
39
|
+
- - '>='
|
45
40
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
41
|
+
version: '3.1'
|
47
42
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
43
|
+
name: daemons
|
49
44
|
requirement: !ruby/object:Gem::Requirement
|
50
|
-
none: false
|
51
45
|
requirements:
|
52
|
-
- -
|
46
|
+
- - '>='
|
53
47
|
- !ruby/object:Gem::Version
|
54
48
|
version: '0'
|
55
49
|
type: :runtime
|
56
50
|
prerelease: false
|
57
51
|
version_requirements: !ruby/object:Gem::Requirement
|
58
|
-
none: false
|
59
52
|
requirements:
|
60
|
-
- -
|
53
|
+
- - '>='
|
61
54
|
- !ruby/object:Gem::Version
|
62
55
|
version: '0'
|
63
|
-
description:
|
64
|
-
|
65
|
-
|
66
|
-
send them.
|
56
|
+
description: Background worker to send Apple Push Notifications over a persistent
|
57
|
+
TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper
|
58
|
+
methods for enqueueing APN notifications, and a background daemon to send them.
|
67
59
|
email: arthurnn@gmail.com
|
68
60
|
executables: []
|
69
61
|
extensions: []
|
70
62
|
extra_rdoc_files: []
|
71
63
|
files:
|
72
|
-
- lib/apn/
|
64
|
+
- lib/apn/backend.rb
|
65
|
+
- lib/apn/client.rb
|
66
|
+
- lib/apn/connection.rb
|
73
67
|
- lib/apn/feedback.rb
|
68
|
+
- lib/apn/jobs/resque_notification_job.rb
|
69
|
+
- lib/apn/jobs/sidekiq_notification_job.rb
|
74
70
|
- lib/apn/notification.rb
|
75
|
-
- lib/apn/
|
76
|
-
- lib/apn/queue_manager.rb
|
77
|
-
- lib/apn/queue_name.rb
|
78
|
-
- lib/apn/sender.rb
|
71
|
+
- lib/apn/railtie.rb
|
79
72
|
- lib/apn/sender_daemon.rb
|
80
73
|
- lib/apn/tasks.rb
|
81
74
|
- lib/apn/version.rb
|
82
75
|
- lib/apn.rb
|
83
|
-
-
|
84
|
-
- CHANGELOG
|
76
|
+
- CHANGELOG.md
|
85
77
|
- LICENSE
|
86
78
|
- README.md
|
87
79
|
- Rakefile
|
88
80
|
homepage: http://github.com/arthurnn/apn_sender
|
89
81
|
licenses:
|
90
82
|
- MIT
|
83
|
+
metadata: {}
|
91
84
|
post_install_message:
|
92
85
|
rdoc_options: []
|
93
86
|
require_paths:
|
94
87
|
- lib
|
95
88
|
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
-
none: false
|
97
89
|
requirements:
|
98
|
-
- -
|
90
|
+
- - '>='
|
99
91
|
- !ruby/object:Gem::Version
|
100
92
|
version: '1.9'
|
101
93
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
-
none: false
|
103
94
|
requirements:
|
104
|
-
- -
|
95
|
+
- - '>='
|
105
96
|
- !ruby/object:Gem::Version
|
106
97
|
version: 1.3.6
|
107
98
|
requirements: []
|
108
99
|
rubyforge_project:
|
109
|
-
rubygems_version:
|
100
|
+
rubygems_version: 2.0.3
|
110
101
|
signing_key:
|
111
|
-
specification_version:
|
112
|
-
summary:
|
113
|
-
|
102
|
+
specification_version: 4
|
103
|
+
summary: Background worker to send Apple Push Notifications over a persistent TCP
|
104
|
+
socket.
|
114
105
|
test_files: []
|
data/CHANGELOG
DELETED
data/lib/apn/connection/base.rb
DELETED
@@ -1,127 +0,0 @@
|
|
1
|
-
require 'socket'
|
2
|
-
require 'openssl'
|
3
|
-
require 'resque'
|
4
|
-
|
5
|
-
module APN
|
6
|
-
module Connection
|
7
|
-
# APN::Connection::Base takes care of all the boring certificate loading, socket creating, and logging
|
8
|
-
# responsibilities so APN::Sender and APN::Feedback and focus on their respective specialties.
|
9
|
-
module Base
|
10
|
-
attr_accessor :opts, :logger
|
11
|
-
|
12
|
-
def initialize(opts = {})
|
13
|
-
@opts = opts
|
14
|
-
|
15
|
-
setup_logger
|
16
|
-
log(:info, "APN::Sender initializing. Establishing connections first...") if @opts[:verbose]
|
17
|
-
setup_paths
|
18
|
-
|
19
|
-
super( APN::QUEUE_NAME ) if self.class.ancestors.include?(Resque::Worker)
|
20
|
-
end
|
21
|
-
|
22
|
-
# Lazy-connect the socket once we try to access it in some way
|
23
|
-
def socket
|
24
|
-
setup_connection unless @socket
|
25
|
-
return @socket
|
26
|
-
end
|
27
|
-
|
28
|
-
protected
|
29
|
-
|
30
|
-
# Default to Rails or Merg logger, if available
|
31
|
-
def setup_logger
|
32
|
-
@logger = if defined?(Merb::Logger)
|
33
|
-
Merb.logger
|
34
|
-
elsif defined?(::Rails.logger)
|
35
|
-
::Rails.logger
|
36
|
-
end
|
37
|
-
@logger ||= Logger.new(STDOUT)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Log message to any logger provided by the user (e.g. the Rails logger).
|
41
|
-
# Accepts +log_level+, +message+, since that seems to make the most sense,
|
42
|
-
# and just +message+, to be compatible with Resque's log method and to enable
|
43
|
-
# sending verbose and very_verbose worker messages to e.g. the rails logger.
|
44
|
-
#
|
45
|
-
# Perhaps a method definition of +message, +level+ would make more sense, but
|
46
|
-
# that's also the complete opposite of what anyone comming from rails would expect.
|
47
|
-
alias_method(:resque_log, :log) if defined?(log)
|
48
|
-
def log(level, message = nil)
|
49
|
-
level, message = 'info', level if message.nil? # Handle only one argument if called from Resque, which expects only message
|
50
|
-
|
51
|
-
resque_log(message) if defined?(resque_log)
|
52
|
-
return false unless self.logger && self.logger.respond_to?(level)
|
53
|
-
self.logger.send(level, "#{Time.now}: #{message}")
|
54
|
-
end
|
55
|
-
|
56
|
-
# Log the message first, to ensure it reports what went wrong if in daemon mode.
|
57
|
-
# Then die, because something went horribly wrong.
|
58
|
-
def log_and_die(msg)
|
59
|
-
log(:fatal, msg)
|
60
|
-
raise msg
|
61
|
-
end
|
62
|
-
|
63
|
-
def apn_production?
|
64
|
-
@opts[:environment] && @opts[:environment] != '' && :production == @opts[:environment].to_sym
|
65
|
-
end
|
66
|
-
|
67
|
-
# Get a fix on the .pem certificate we'll be using for SSL
|
68
|
-
def setup_paths
|
69
|
-
@opts[:environment] ||= ::Rails.env if defined?(::Rails.env)
|
70
|
-
|
71
|
-
# Accept a complete :full_cert_path allowing arbitrary certificate names, or create a default from the Rails env
|
72
|
-
cert_path = @opts[:full_cert_path] || begin
|
73
|
-
# Note that RAILS_ROOT is still here not from Rails, but to handle passing in root from sender_daemon
|
74
|
-
@opts[:root_path] ||= defined?(::Rails.root) ? ::Rails.root.to_s : (defined?(RAILS_ROOT) ? RAILS_ROOT : '/')
|
75
|
-
@opts[:cert_path] ||= File.join(File.expand_path(@opts[:root_path]), "config", "certs")
|
76
|
-
@opts[:cert_name] ||= apn_production? ? "apn_production.pem" : "apn_development.pem"
|
77
|
-
|
78
|
-
File.join(@opts[:cert_path], @opts[:cert_name])
|
79
|
-
end
|
80
|
-
|
81
|
-
@apn_cert = File.read(cert_path) if File.exists?(cert_path)
|
82
|
-
log_and_die("Please specify correct :full_cert_path. No apple push notification certificate found in: #{cert_path}") unless @apn_cert
|
83
|
-
end
|
84
|
-
|
85
|
-
# Open socket to Apple's servers
|
86
|
-
def setup_connection
|
87
|
-
log_and_die("Missing apple push notification certificate") unless @apn_cert
|
88
|
-
return true if @socket && @socket_tcp
|
89
|
-
log_and_die("Trying to open half-open connection") if @socket || @socket_tcp
|
90
|
-
|
91
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
92
|
-
ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
|
93
|
-
|
94
|
-
if @opts[:cert_pass]
|
95
|
-
ctx.key = OpenSSL::PKey::RSA.new(@apn_cert, @opts[:cert_pass])
|
96
|
-
else
|
97
|
-
ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
|
98
|
-
end
|
99
|
-
|
100
|
-
@socket_tcp = TCPSocket.new(apn_host, apn_port)
|
101
|
-
@socket = OpenSSL::SSL::SSLSocket.new(@socket_tcp, ctx)
|
102
|
-
@socket.sync = true
|
103
|
-
@socket.connect
|
104
|
-
rescue SocketError => error
|
105
|
-
log_and_die("Error with connection to #{apn_host}: #{error}")
|
106
|
-
end
|
107
|
-
|
108
|
-
# Close open sockets
|
109
|
-
def teardown_connection
|
110
|
-
log(:info, "Closing connections...") if @opts[:verbose]
|
111
|
-
|
112
|
-
begin
|
113
|
-
@socket.close if @socket
|
114
|
-
rescue Exception => e
|
115
|
-
log(:error, "Error closing SSL Socket: #{e}")
|
116
|
-
end
|
117
|
-
|
118
|
-
begin
|
119
|
-
@socket_tcp.close if @socket_tcp
|
120
|
-
rescue Exception => e
|
121
|
-
log(:error, "Error closing TCP Socket: #{e}")
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
data/lib/apn/notification_job.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
module APN
|
2
|
-
# This is the class that's actually enqueued via Resque when user calls +APN.notify+.
|
3
|
-
# It gets added to the +apple_server_notifications+ Resque queue, which should only be operated on by
|
4
|
-
# workers of the +APN::Sender+ class.
|
5
|
-
class NotificationJob
|
6
|
-
# Behind the scenes, this is the name of our Resque queue
|
7
|
-
@queue = APN::QUEUE_NAME
|
8
|
-
|
9
|
-
# Build a notification from arguments and send to Apple
|
10
|
-
def self.perform(token, opts)
|
11
|
-
msg = APN::Notification.new(token, opts)
|
12
|
-
raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
|
13
|
-
|
14
|
-
raise "APN::NotificationJob was picked up by a non-APN:Sender resque worker. Aborting." unless worker
|
15
|
-
worker.send_to_apple(msg)
|
16
|
-
end
|
17
|
-
|
18
|
-
|
19
|
-
# Only execute this job in specialized APN::Sender workers, since
|
20
|
-
# standard Resque workers don't maintain the persistent TCP connection.
|
21
|
-
extend Resque::Plugins::AccessWorkerFromJob
|
22
|
-
self.required_worker_class = 'APN::Sender'
|
23
|
-
end
|
24
|
-
end
|
data/lib/apn/queue_manager.rb
DELETED
@@ -1,57 +0,0 @@
|
|
1
|
-
# Extending Resque to respond to the +before_unregister_worker+ hook. Note this requires a matching
|
2
|
-
# monkeypatch in the Resque::Worker class. See +resque/hooks/before_unregister_worker.rb+ for an
|
3
|
-
# example implementation
|
4
|
-
|
5
|
-
module APN
|
6
|
-
# Enqueues a notification to be sent in the background via the persistent TCP socket, assuming apn_sender is running (or will be soon)
|
7
|
-
def self.notify(token, opts = {})
|
8
|
-
token = token.to_s.gsub(/\W/, '')
|
9
|
-
APN::QueueManager.enqueue(APN::NotificationJob, token, opts)
|
10
|
-
end
|
11
|
-
|
12
|
-
# Extends Resque, allowing us to add all the callbacks to Resque we desire without affecting the expected
|
13
|
-
# functionality in the parent app, if we're included in e.g. a Rails application.
|
14
|
-
class QueueManager
|
15
|
-
extend Resque
|
16
|
-
|
17
|
-
def self.before_unregister_worker(&block)
|
18
|
-
block ? (@before_unregister_worker = block) : @before_unregister_worker
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.before_unregister_worker=(before_unregister_worker)
|
22
|
-
@before_unregister_worker = before_unregister_worker
|
23
|
-
end
|
24
|
-
|
25
|
-
def self.to_s
|
26
|
-
"APN::QueueManager (Resque Client) connected to #{redis.server}"
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
# Ensures we close any open sockets when the worker exits
|
33
|
-
APN::QueueManager.before_unregister_worker do |worker|
|
34
|
-
worker.send(:teardown_connection) if worker.respond_to?(:teardown_connection)
|
35
|
-
end
|
36
|
-
|
37
|
-
|
38
|
-
# # Run N jobs per fork, rather than creating a new fork for each notification
|
39
|
-
# # By defunkt - http://gist.github.com/349376
|
40
|
-
# APN::QueueManager.after_fork do |job|
|
41
|
-
# # How many jobs should we process in each fork?
|
42
|
-
# jobs_per_fork = 10
|
43
|
-
#
|
44
|
-
# # Set hook to nil to prevent running this hook over
|
45
|
-
# # and over while processing more jobs in this fork.
|
46
|
-
# Resque.after_fork = nil
|
47
|
-
#
|
48
|
-
# # Make sure we process jobs in the right order.
|
49
|
-
# job.worker.process(job)
|
50
|
-
#
|
51
|
-
# # One less than specified because the child will run a
|
52
|
-
# # final job after exiting this hook.
|
53
|
-
# (jobs_per_fork.to_i - 1).times do
|
54
|
-
# job.worker.process
|
55
|
-
# end
|
56
|
-
# end
|
57
|
-
|
data/lib/apn/queue_name.rb
DELETED
data/lib/apn/sender.rb
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
module APN
|
2
|
-
# Subclass of Resque::Worker which initializes a single TCP socket on creation to communicate with Apple's Push Notification servers.
|
3
|
-
# Shares this socket with each child process forked off by Resque to complete a job. Socket is closed in the before_unregister_worker
|
4
|
-
# callback, which gets called on normal or exceptional exits.
|
5
|
-
#
|
6
|
-
# End result: single persistent TCP connection to Apple, so they don't ban you for frequently opening and closing connections,
|
7
|
-
# which they apparently view as a DOS attack.
|
8
|
-
#
|
9
|
-
# Accepts <code>:environment</code> (production vs anything else), <code>:cert_pass</code> and <code>:cert_path</code> options on initialization. If called in a
|
10
|
-
# Rails context, will default to RAILS_ENV and RAILS_ROOT/config/certs. :environment will default to development.
|
11
|
-
# APN::Sender expects two files to exist in the specified <code>:cert_path</code> directory:
|
12
|
-
# <code>apn_production.pem</code> and <code>apn_development.pem</code>.
|
13
|
-
#
|
14
|
-
# Use the <code>:cert_pass</code> option if your certificates require a password
|
15
|
-
#
|
16
|
-
# If a socket error is encountered, will teardown the connection and retry again twice before admitting defeat.
|
17
|
-
class Sender < ::Resque::Worker
|
18
|
-
include APN::Connection::Base
|
19
|
-
TIMES_TO_RETRY_SOCKET_ERROR = 2
|
20
|
-
|
21
|
-
# Send a raw string over the socket to Apple's servers (presumably already formatted by APN::Notification)
|
22
|
-
def send_to_apple( notification, attempt = 0 )
|
23
|
-
if attempt > TIMES_TO_RETRY_SOCKET_ERROR
|
24
|
-
log_and_die("Error with connection to #{apn_host} (retried #{TIMES_TO_RETRY_SOCKET_ERROR} times): #{error}")
|
25
|
-
end
|
26
|
-
|
27
|
-
self.socket.write( notification.to_s )
|
28
|
-
rescue SocketError => error
|
29
|
-
log(:error, "Error with connection to #{apn_host} (attempt #{attempt}): #{error}")
|
30
|
-
|
31
|
-
# Try reestablishing the connection
|
32
|
-
teardown_connection
|
33
|
-
setup_connection
|
34
|
-
send_to_apple(notification, attempt + 1)
|
35
|
-
end
|
36
|
-
|
37
|
-
protected
|
38
|
-
|
39
|
-
def apn_host
|
40
|
-
@apn_host ||= apn_production? ? "gateway.push.apple.com" : "gateway.sandbox.push.apple.com"
|
41
|
-
end
|
42
|
-
|
43
|
-
def apn_port
|
44
|
-
2195
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# Adding a +before_unregister_worker+ hook Resque::Worker. To be used, must be matched by a similar monkeypatch
|
2
|
-
# for Resque class itself, or else a class that extends Resque. See apple_push_notification/queue_manager.rb for
|
3
|
-
# an implementation.
|
4
|
-
module Resque
|
5
|
-
class Worker
|
6
|
-
alias_method :unregister_worker_without_before_hook, :unregister_worker
|
7
|
-
|
8
|
-
# Wrapper for original unregister_worker method which adds a before hook +before_unregister_worker+
|
9
|
-
# to be executed if present.
|
10
|
-
def unregister_worker
|
11
|
-
run_hook(:before_unregister_worker, self)
|
12
|
-
unregister_worker_without_before_hook
|
13
|
-
end
|
14
|
-
|
15
|
-
|
16
|
-
# Unforunately have to override Resque::Worker's +run_hook+ method to call hook on
|
17
|
-
# APN::QueueManager rather on Resque directly. Any suggestions on
|
18
|
-
# how to make this more flexible are more than welcome.
|
19
|
-
def run_hook(name, *args)
|
20
|
-
# return unless hook = Resque.send(name)
|
21
|
-
return unless hook = APN::QueueManager.send(name)
|
22
|
-
msg = "Running #{name} hook"
|
23
|
-
msg << " with #{args.inspect}" if args.any?
|
24
|
-
log msg
|
25
|
-
|
26
|
-
args.any? ? hook.call(*args) : hook.call
|
27
|
-
end
|
28
|
-
|
29
|
-
end
|
30
|
-
end
|