rapns 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +105 -0
- data/bin/rapns +26 -0
- data/lib/generators/rapns_generator.rb +16 -0
- data/lib/generators/templates/create_rapns_notifications.rb +26 -0
- data/lib/generators/templates/rapns.yml +17 -0
- data/lib/rapns/binary_notification_validator.rb +10 -0
- data/lib/rapns/daemon/certificate.rb +27 -0
- data/lib/rapns/daemon/configuration.rb +69 -0
- data/lib/rapns/daemon/connection.rb +99 -0
- data/lib/rapns/daemon/connection_pool.rb +31 -0
- data/lib/rapns/daemon/delivery_error.rb +15 -0
- data/lib/rapns/daemon/delivery_handler.rb +53 -0
- data/lib/rapns/daemon/delivery_handler_pool.rb +24 -0
- data/lib/rapns/daemon/feeder.rb +31 -0
- data/lib/rapns/daemon/logger.rb +49 -0
- data/lib/rapns/daemon/pool.rb +41 -0
- data/lib/rapns/daemon.rb +76 -0
- data/lib/rapns/notification.rb +44 -0
- data/lib/rapns/version.rb +3 -0
- data/lib/rapns.rb +5 -0
- data/spec/rapns/daemon/certificate_spec.rb +16 -0
- data/spec/rapns/daemon/configuration_spec.rb +125 -0
- data/spec/rapns/daemon/connection_pool_spec.rb +40 -0
- data/spec/rapns/daemon/connection_spec.rb +247 -0
- data/spec/rapns/daemon/delivery_error_spec.rb +11 -0
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +26 -0
- data/spec/rapns/daemon/delivery_handler_spec.rb +110 -0
- data/spec/rapns/daemon/feeder_spec.rb +61 -0
- data/spec/rapns/daemon/logger_spec.rb +96 -0
- data/spec/rapns/daemon_spec.rb +141 -0
- data/spec/rapns/notification_spec.rb +112 -0
- data/spec/spec_helper.rb +25 -0
- metadata +91 -0
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# rapns [![Build Status](https://secure.travis-ci.org/ileitch/rapns.png)](http://travis-ci.org/ileitch/rapns)
|
2
|
+
|
3
|
+
Easy to use library for Apple's Push Notification Service with Rails 3.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
* Works with Rails 3 and Ruby 1.9.
|
8
|
+
* Uses a daemon process to keep open a persistent connection to the Push Notification Service, as recommended by Apple.
|
9
|
+
* Uses the [enhanced binary format](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingWIthAPS/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW4) (Figure 5-2) so that delivery errors can be reported.
|
10
|
+
* [Airbrake](http://airbrakeapp.com/) (Hoptoad) integration.
|
11
|
+
* Support for [dictionary `alert` properties](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Table 3-2).
|
12
|
+
|
13
|
+
## Getting Started
|
14
|
+
|
15
|
+
Add rapns to your Gemfile:
|
16
|
+
|
17
|
+
gem 'rapns'
|
18
|
+
|
19
|
+
Generate the migration, rapns.yml and migrate:
|
20
|
+
|
21
|
+
rails g rapns
|
22
|
+
rake db:migrate
|
23
|
+
|
24
|
+
## Generating Certificates
|
25
|
+
|
26
|
+
1. Open up Keychain Access and select the `Certificates` category in the sidebar.
|
27
|
+
2. Expand the disclosure arrow next to the iOS Push Services certificate you want to export.
|
28
|
+
3. Select both the certificate and private key.
|
29
|
+
4. Right click and select `Export 2 items...`.
|
30
|
+
5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
|
31
|
+
6. If you decide to set a password for your exported certificate, please read the Configuration section below.
|
32
|
+
7. Convert the certificate to a .pem, where `<environment>` should be `development` or `production`, depending on the certificate you exported.
|
33
|
+
|
34
|
+
`openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
|
35
|
+
|
36
|
+
8. Move the .pem file into your Rails application under `config/rapns`.
|
37
|
+
|
38
|
+
## Configuration
|
39
|
+
|
40
|
+
Environment configuration lives in `config/rapns/rapns.yml`. For common setups you probably wont need to change this file.
|
41
|
+
|
42
|
+
If you want to use rapns in environments other than development or production, you will need to create an entry for it. Simply duplicate the configuration for development or production, depending on which iOS Push Certificate you wish to use.
|
43
|
+
|
44
|
+
### Options:
|
45
|
+
|
46
|
+
* `host` the APNs host to connect to, either `gateway.sandbox.push.apple.com` or `gateway.sandbox.push.apple.com`.
|
47
|
+
* `port` the APNs port. Currently 2195 for both hosts.
|
48
|
+
* `certificate` The path to your .pem certificate, `config/rapns` is automatically checked if a relative path is given.
|
49
|
+
* `certificate_password` (default: blank) the password you used when exporting your certificate, if any.
|
50
|
+
* `airbrake_notify` (default: true) Enables/disables error notifications via Airbrake.
|
51
|
+
* `poll` (default: 2) Frequency in seconds to check for new notifications to deliver.
|
52
|
+
* `connections` (default: 3) the number of connections to keep open to the APNs. Consider increasing this if you are sending a very large number of notifications.
|
53
|
+
|
54
|
+
## Starting the rapns Daemon
|
55
|
+
|
56
|
+
cd /path/to/rails/app
|
57
|
+
bundle exec rapns <Rails environment>
|
58
|
+
|
59
|
+
### Options
|
60
|
+
|
61
|
+
* `--foreground` will prevent rapns from forking into a daemon. Activity information will be printed to the screen.
|
62
|
+
|
63
|
+
## Sending a Notification
|
64
|
+
|
65
|
+
n = Rapns::Notification.new
|
66
|
+
n.device_token = "934f7a..."
|
67
|
+
n.alert = "This is the message shown on the device."
|
68
|
+
n.badge = 1
|
69
|
+
n.sound = "1.aiff"
|
70
|
+
n.expiry = 1.day.to_i
|
71
|
+
n.attributes_for_device = {"question" => nil, "answer" => 42}
|
72
|
+
n.deliver_after = 1.hour.from_now
|
73
|
+
n.save!
|
74
|
+
|
75
|
+
* `sound` defaults to `1.aiff`. You can either set it to a custom .aiff file, or `nil` for no sound.
|
76
|
+
* `expiry` is the time in seconds the APNs will spend trying to deliver the notification to the device. The notification is discarded if it has not been delivered in this time. Default is 1 day.
|
77
|
+
* `attributes_for_device` is the `NSDictionary` argument passed to your iOS app in either `didFinishLaunchingWithOptions` or `didReceiveRemoteNotification`.
|
78
|
+
* `deliver_after` is not required, but may be set if you'd like to delay delivery of the notification to a specific time in the future.
|
79
|
+
|
80
|
+
### Assigning a Hash to alert
|
81
|
+
|
82
|
+
Please refer to Apple's [documentation](http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html#//apple_ref/doc/uid/TP40008194-CH100-SW1) (Tables 3-1 and 3-2).
|
83
|
+
|
84
|
+
Not yet implemented!
|
85
|
+
|
86
|
+
## Delivery Failures
|
87
|
+
|
88
|
+
The APN service provides two mechanism for delivery failure notification:
|
89
|
+
|
90
|
+
### Immediately, when processing a notification for delivery.
|
91
|
+
|
92
|
+
Although rapns makes such errors highly unlikely due to validation, the APNs reports processing errors immediately after being sent a notification. These errors are all centred around the well-formedness of the notification payload. Should a notification be rejected due to such an error, rapns will update the following attributes on the notification and send a notification via Airbrake/Hoptoad (if enabled):
|
93
|
+
|
94
|
+
`failed` flag is set to true.
|
95
|
+
`failed_at` is set to the time of failure.
|
96
|
+
`error` is set to Apple's code for the error.
|
97
|
+
`error_description` is set to a (somewhat brief) description of the error.
|
98
|
+
|
99
|
+
rapns will not attempt to deliver the notification again.
|
100
|
+
|
101
|
+
### Via the Feedback Service.
|
102
|
+
|
103
|
+
Not implemented yet!
|
104
|
+
|
105
|
+
|
data/bin/rapns
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "rapns"
|
5
|
+
require "rapns/daemon"
|
6
|
+
|
7
|
+
foreground = false
|
8
|
+
environment = ARGV[0]
|
9
|
+
banner = "Usage: rapns <Rails environment> [options]"
|
10
|
+
ARGV.options do |opts|
|
11
|
+
opts.banner = banner
|
12
|
+
opts.on("-f", "--foreground", "Run in the foreground.") { foreground = true }
|
13
|
+
opts.on("-v", "--version", "Print this version of rapns.") { puts "rapns #{Rapns::VERSION}"; exit }
|
14
|
+
opts.on("-h", "--help", "You're looking at it.") { puts opts; exit }
|
15
|
+
opts.parse!
|
16
|
+
end
|
17
|
+
|
18
|
+
if environment.nil?
|
19
|
+
puts banner
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
ENV["RAILS_ENV"] = environment
|
24
|
+
load "config/environment.rb"
|
25
|
+
|
26
|
+
Rapns::Daemon.start(environment, foreground)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class RapnsGenerator < Rails::Generators::Base
|
2
|
+
include Rails::Generators::Migration
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def self.next_migration_number(path)
|
6
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
7
|
+
end
|
8
|
+
|
9
|
+
def copy_migration
|
10
|
+
migration_template "create_rapns_notifications.rb", "db/migrate/create_rapns_notifications.rb"
|
11
|
+
end
|
12
|
+
|
13
|
+
def copy_config
|
14
|
+
copy_file "rapns.yml", "config/rapns/rapns.yml"
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class CreateRapnsNotifications < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :rapns_notifications do |t|
|
4
|
+
t.integer :badge, :null => true
|
5
|
+
t.string :device_token, :null => false, :limit => 64
|
6
|
+
t.string :sound, :null => true, :default => "1.aiff"
|
7
|
+
t.string :alert, :null => true
|
8
|
+
t.text :attributes_for_device, :null => true
|
9
|
+
t.integer :expiry, :null => false, :default => 1.day.to_i
|
10
|
+
t.boolean :delivered, :null => false, :default => false
|
11
|
+
t.timestamp :delivered_at, :null => true
|
12
|
+
t.boolean :failed, :null => false, :default => false
|
13
|
+
t.timestamp :failed_at, :null => true
|
14
|
+
t.integer :error_code, :null => true
|
15
|
+
t.string :error_description, :null => true
|
16
|
+
t.timestamp :deliver_after, :null => true
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
|
20
|
+
add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_on_delivered_failed_deliver_after"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.down
|
24
|
+
drop_table :rapns_notifications
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
development:
|
2
|
+
host: gateway.sandbox.push.apple.com
|
3
|
+
port: 2195
|
4
|
+
certificate: development.pem
|
5
|
+
certificate_password:
|
6
|
+
airbrake_notify: true
|
7
|
+
poll: 2
|
8
|
+
connections: 3
|
9
|
+
|
10
|
+
production:
|
11
|
+
host: gateway.push.apple.com
|
12
|
+
port: 2195
|
13
|
+
certificate: production.pem
|
14
|
+
certificate_password:
|
15
|
+
airbrake_notify: true
|
16
|
+
poll: 2
|
17
|
+
connections: 3
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Rapns
|
2
|
+
class BinaryNotificationValidator < ActiveModel::Validator
|
3
|
+
|
4
|
+
def validate(record)
|
5
|
+
if record.to_binary(:for_validation => true).size > 256
|
6
|
+
record.errors[:base] << "APN notification cannot be larger than 256 bytes. Try condensing your alert and device attributes."
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rapns
|
2
|
+
class CertificateError < StandardError; end
|
3
|
+
|
4
|
+
module Daemon
|
5
|
+
class Certificate
|
6
|
+
attr_accessor :certificate
|
7
|
+
|
8
|
+
def initialize(certificate_path)
|
9
|
+
@certificate_path = certificate_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
@certificate = read_certificate
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def read_certificate
|
19
|
+
if !File.exists?(@certificate_path)
|
20
|
+
raise CertificateError, "#{@certificate_path} does not exist. The certificate location can be configured in config/rapns/rapns.yml."
|
21
|
+
else
|
22
|
+
File.read(@certificate_path)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Rapns
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
module Daemon
|
7
|
+
class Configuration
|
8
|
+
attr_accessor :host, :port, :certificate, :certificate_password, :poll, :airbrake_notify, :connections
|
9
|
+
alias_method :airbrake_notify?, :airbrake_notify
|
10
|
+
|
11
|
+
def initialize(environment, config_path)
|
12
|
+
@environment = environment
|
13
|
+
@config_path = config_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def load
|
17
|
+
config = read_config
|
18
|
+
ensure_environment_configured(config)
|
19
|
+
config = config[@environment]
|
20
|
+
set_variable(:host, config)
|
21
|
+
set_variable(:port, config)
|
22
|
+
set_variable(:certificate, config)
|
23
|
+
set_variable(:airbrake_notify, config, :optional => true, :default => true)
|
24
|
+
set_variable(:certificate_password, config, :optional => true, :default => "")
|
25
|
+
set_variable(:poll, config, :optional => true, :default => 2)
|
26
|
+
set_variable(:connections, config, :optional => true, :default => 3)
|
27
|
+
end
|
28
|
+
|
29
|
+
def certificate
|
30
|
+
if Pathname.new(@certificate).absolute?
|
31
|
+
@certificate
|
32
|
+
else
|
33
|
+
File.join(Rails.root, "config", "rapns", @certificate)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def read_config
|
40
|
+
ensure_config_exists
|
41
|
+
File.open(@config_path) { |fd| YAML.load(fd) }
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_variable(key, config, options = {})
|
45
|
+
if !config.key?(key.to_s) || config[key.to_s].to_s.strip == ""
|
46
|
+
if options[:optional]
|
47
|
+
instance_variable_set("@#{key}", options[:default])
|
48
|
+
else
|
49
|
+
raise Rapns::ConfigurationError, "'#{key}' not defined for environment '#{@environment}' in #{@config_path}"
|
50
|
+
end
|
51
|
+
else
|
52
|
+
instance_variable_set("@#{key}", config[key.to_s])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def ensure_config_exists
|
57
|
+
if !File.exists?(@config_path)
|
58
|
+
raise Rapns::ConfigurationError, "#{@config_path} does not exist. Have you run 'rails g rapns'?"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def ensure_environment_configured(config)
|
63
|
+
if !config.key?(@environment)
|
64
|
+
raise Rapns::ConfigurationError, "Configuration for environment '#{@environment}' not defined in #{@config_path}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class ConnectionError < StandardError; end
|
4
|
+
|
5
|
+
class Connection
|
6
|
+
SELECT_TIMEOUT = 0.5
|
7
|
+
ERROR_PACKET_BYTES = 6
|
8
|
+
APN_ERRORS = {
|
9
|
+
1 => "Processing error",
|
10
|
+
2 => "Missing device token",
|
11
|
+
3 => "Missing topic",
|
12
|
+
4 => "Missing payload",
|
13
|
+
5 => "Missing token size",
|
14
|
+
6 => "Missing topic size",
|
15
|
+
7 => "Missing payload size",
|
16
|
+
8 => "Invalid token",
|
17
|
+
255 => "None (unknown error)"
|
18
|
+
}
|
19
|
+
|
20
|
+
def initialize(name)
|
21
|
+
@name = name
|
22
|
+
end
|
23
|
+
|
24
|
+
def connect
|
25
|
+
@ssl_context = setup_ssl_context
|
26
|
+
@tcp_socket, @ssl_socket = connect_socket
|
27
|
+
end
|
28
|
+
|
29
|
+
def close
|
30
|
+
@ssl_socket.close if @ssl_socket
|
31
|
+
@tcp_socket.close if @tcp_socket
|
32
|
+
end
|
33
|
+
|
34
|
+
def write(data)
|
35
|
+
retry_count = 0
|
36
|
+
|
37
|
+
begin
|
38
|
+
@ssl_socket.write(data)
|
39
|
+
@ssl_socket.flush
|
40
|
+
|
41
|
+
check_for_error
|
42
|
+
rescue Errno::EPIPE => e
|
43
|
+
Rapns::Daemon.logger.warn("[#{@name}] Lost connection to #{Rapns::Daemon.configuration.host}:#{Rapns::Daemon.configuration.port}, reconnecting...")
|
44
|
+
@tcp_socket, @ssl_socket = connect_socket
|
45
|
+
|
46
|
+
retry_count += 1
|
47
|
+
|
48
|
+
if retry_count < 3
|
49
|
+
sleep 1
|
50
|
+
retry
|
51
|
+
else
|
52
|
+
raise ConnectionError, "#{@name} tried #{retry_count} times to reconnect but failed: #{e.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def check_for_error
|
60
|
+
if IO.select([@ssl_socket], nil, nil, SELECT_TIMEOUT)
|
61
|
+
delivery_error = nil
|
62
|
+
|
63
|
+
if error = @ssl_socket.read(ERROR_PACKET_BYTES)
|
64
|
+
cmd, status, notification_id = error.unpack("ccN")
|
65
|
+
|
66
|
+
if cmd == 8 && status != 0
|
67
|
+
description = APN_ERRORS[status] || "Unknown error. Possible rapns bug?"
|
68
|
+
delivery_error = Rapns::DeliveryError.new(status, description, notification_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
begin
|
73
|
+
Rapns::Daemon.logger.warn("[#{@name}] Error received, reconnecting...")
|
74
|
+
close
|
75
|
+
@tcp_socket, @ssl_socket = connect_socket
|
76
|
+
ensure
|
77
|
+
raise delivery_error if delivery_error
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup_ssl_context
|
83
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
84
|
+
ssl_context.key = OpenSSL::PKey::RSA.new(Rapns::Daemon.certificate.certificate, Rapns::Daemon.configuration.certificate_password)
|
85
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(Rapns::Daemon.certificate.certificate)
|
86
|
+
ssl_context
|
87
|
+
end
|
88
|
+
|
89
|
+
def connect_socket
|
90
|
+
tcp_socket = TCPSocket.new(Rapns::Daemon.configuration.host, Rapns::Daemon.configuration.port)
|
91
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl_context)
|
92
|
+
ssl_socket.sync = true
|
93
|
+
ssl_socket.connect
|
94
|
+
Rapns::Daemon.logger.info("[#{@name}] Connected to #{Rapns::Daemon.configuration.host}:#{Rapns::Daemon.configuration.port}")
|
95
|
+
[tcp_socket, ssl_socket]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class ConnectionPool < Pool
|
4
|
+
|
5
|
+
def claim_connection
|
6
|
+
connection = nil
|
7
|
+
begin
|
8
|
+
connection = @queue.pop
|
9
|
+
yield connection
|
10
|
+
ensure
|
11
|
+
@queue.push(connection) if connection
|
12
|
+
end
|
13
|
+
connection
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def new_object_for_pool(i)
|
19
|
+
Connection.new("Connection #{i}")
|
20
|
+
end
|
21
|
+
|
22
|
+
def object_added_to_pool(object)
|
23
|
+
object.connect
|
24
|
+
end
|
25
|
+
|
26
|
+
def object_removed_from_pool(object)
|
27
|
+
object.close
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rapns
|
2
|
+
class DeliveryError < StandardError
|
3
|
+
attr_reader :code, :description, :notification_id
|
4
|
+
|
5
|
+
def initialize(code, description, notification_id)
|
6
|
+
@code = code
|
7
|
+
@description = description
|
8
|
+
@notification_id = notification_id
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"Unable to deliver notification #{notification_id}, received APN error #{code} (#{description})"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class DeliveryHandler
|
4
|
+
STOP = 0x666
|
5
|
+
|
6
|
+
def start
|
7
|
+
@thread = Thread.new do
|
8
|
+
loop do
|
9
|
+
break if @stop
|
10
|
+
handle_next_notification
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop
|
16
|
+
@stop = true
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def handle_next_notification
|
22
|
+
begin
|
23
|
+
notification = Rapns::Daemon.delivery_queue.pop
|
24
|
+
return if notification == STOP
|
25
|
+
|
26
|
+
Rapns::Daemon.connection_pool.claim_connection do |connection|
|
27
|
+
begin
|
28
|
+
connection.write(notification.to_binary)
|
29
|
+
|
30
|
+
notification.delivered = true
|
31
|
+
notification.delivered_at = Time.now
|
32
|
+
notification.save!(:validate => false)
|
33
|
+
|
34
|
+
Rapns::Daemon.logger.info("Notification #{notification.id} delivered to #{notification.device_token}")
|
35
|
+
rescue Rapns::DeliveryError => error
|
36
|
+
Rapns::Daemon.logger.error(error)
|
37
|
+
|
38
|
+
notification.delivered = false
|
39
|
+
notification.delivered_at = nil
|
40
|
+
notification.failed = true
|
41
|
+
notification.failed_at = Time.now
|
42
|
+
notification.error_code = error.code
|
43
|
+
notification.error_description = error.description
|
44
|
+
notification.save!(:validate => false)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
rescue StandardError => e
|
48
|
+
Rapns::Daemon.logger.error(e)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class DeliveryHandlerPool < Pool
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def new_object_for_pool(i)
|
8
|
+
DeliveryHandler.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def object_added_to_pool(object)
|
12
|
+
object.start
|
13
|
+
end
|
14
|
+
|
15
|
+
def object_removed_from_pool(object)
|
16
|
+
object.stop
|
17
|
+
end
|
18
|
+
|
19
|
+
def drain_started
|
20
|
+
@num_objects.times { Rapns::Daemon.delivery_queue.push(Rapns::Daemon::DeliveryHandler::STOP) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class Feeder
|
4
|
+
def self.start
|
5
|
+
@thread = Thread.new do
|
6
|
+
loop do
|
7
|
+
break if @stop
|
8
|
+
enqueue_notifications
|
9
|
+
end
|
10
|
+
end
|
11
|
+
@thread.join
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.enqueue_notifications
|
15
|
+
begin
|
16
|
+
Rapns::Notification.ready_for_delivery.each do |notification|
|
17
|
+
Rapns::Daemon.delivery_queue.push(notification)
|
18
|
+
end
|
19
|
+
rescue StandardError => e
|
20
|
+
Rapns::Daemon.logger.error(e)
|
21
|
+
end
|
22
|
+
|
23
|
+
sleep Rapns::Daemon.configuration.poll
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.stop
|
27
|
+
@stop = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class Logger
|
4
|
+
def initialize(options)
|
5
|
+
@options = options
|
6
|
+
log_path = File.join(Rails.root, "log", "rapns.log")
|
7
|
+
@logger = ActiveSupport::BufferedLogger.new(log_path, Rails.logger.level)
|
8
|
+
@logger.auto_flushing = Rails.logger.auto_flushing
|
9
|
+
end
|
10
|
+
|
11
|
+
def info(msg)
|
12
|
+
log(:info, msg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def error(msg)
|
16
|
+
airbrake_notify(msg) if msg.is_a?(Exception)
|
17
|
+
log(:error, msg, "ERROR")
|
18
|
+
end
|
19
|
+
|
20
|
+
def warn(msg)
|
21
|
+
log(:warn, msg, "WARNING")
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def log(where, msg, prefix = nil)
|
27
|
+
if msg.is_a?(Exception)
|
28
|
+
msg = "#{msg.class.name}, #{msg.message}"
|
29
|
+
end
|
30
|
+
|
31
|
+
formatted_msg = "[#{Time.now.to_s(:db)}] "
|
32
|
+
formatted_msg << "[#{prefix}] " if prefix
|
33
|
+
formatted_msg << msg
|
34
|
+
puts formatted_msg if @options[:foreground]
|
35
|
+
@logger.send(where, formatted_msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
def airbrake_notify(e)
|
39
|
+
return unless @options[:airbrake_notify] == true
|
40
|
+
|
41
|
+
if defined?(Airbrake)
|
42
|
+
Airbrake.notify(e)
|
43
|
+
elsif defined?(HoptoadNotifier)
|
44
|
+
HoptoadNotifier.notify(e)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Rapns
|
2
|
+
module Daemon
|
3
|
+
class Pool
|
4
|
+
def initialize(num_objects)
|
5
|
+
@num_objects = num_objects
|
6
|
+
@queue = Queue.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def populate
|
10
|
+
@num_objects.times do |i|
|
11
|
+
object = new_object_for_pool(i)
|
12
|
+
@queue.push(object)
|
13
|
+
object_added_to_pool(object)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def drain
|
18
|
+
drain_started
|
19
|
+
|
20
|
+
while !@queue.empty?
|
21
|
+
object = @queue.pop
|
22
|
+
object_removed_from_pool(object)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def new_object_for_pool(i)
|
29
|
+
end
|
30
|
+
|
31
|
+
def object_added_to_pool(object)
|
32
|
+
end
|
33
|
+
|
34
|
+
def object_removed_from_pool(object)
|
35
|
+
end
|
36
|
+
|
37
|
+
def drain_started
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|