apn_sender 0.0.7 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +21 -15
- data/VERSION +1 -1
- data/apn_sender.gemspec +1 -1
- data/contrib/apn_sender.monitrc +18 -10
- data/lib/apn/connection/base.rb +3 -0
- data/lib/apn/notification.rb +5 -4
- data/lib/apn/notification_job.rb +1 -1
- data/lib/apn/sender.rb +5 -5
- data/lib/apn/sender_daemon.rb +8 -4
- metadata +1 -1
data/README.rdoc
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
|
1
|
+
== Synopsis
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
Need to send background notifications to an iPhone application over a <em>persistent</em> connection in Ruby? Keep reading...
|
4
|
+
|
5
|
+
== The Story
|
6
|
+
|
7
|
+
So you're building the server component of an iPhone application in Ruby. And you want to send background notifications through the Apple Push Notification servers, which doesn't seem too bad at first. But then you read in the {Apple Documentation}[https://developer.apple.com/iphone/library/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/WhatAreRemoteNotif/WhatAreRemoteNotif.html#//apple_ref/doc/uid/TP40008194-CH102-SW7] that Apple's servers may treat non-persistent connections as a Denial of Service attack, and you realize that Rails has no easy way to maintain a persistent connection internally, and things started looking more complicated.
|
8
|
+
|
9
|
+
The apn_sender gem includes a background daemon which processes background messages from your application and sends them along to Apple <em>over a single, persistent socket</em>. It also includes the ability to query the Feedback service, helper methods for enqueueing your jobs, and a sample monit config to make sure the background worker is around when you need it.
|
7
10
|
|
8
|
-
|
11
|
+
== Yet another ApplePushNotification interface?
|
9
12
|
|
10
|
-
|
13
|
+
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.
|
11
14
|
|
12
|
-
|
15
|
+
== Current Status
|
13
16
|
|
14
|
-
|
17
|
+
This gem has been in production use since early May, 2010. There are no guarantees of complete functionality, of course, but it's working for us. :)
|
15
18
|
|
16
19
|
|
17
20
|
== Usage
|
@@ -39,14 +42,17 @@ Once this is done, you can fire off a background worker with
|
|
39
42
|
|
40
43
|
For production, you're probably better off running a dedicated daemon and setting up monit to watch over it for you. Luckily, that's pretty easy:
|
41
44
|
|
42
|
-
# To
|
43
|
-
./script/apn_sender
|
44
|
-
|
45
|
-
# To
|
46
|
-
./script/apn_sender -- --
|
45
|
+
# To generate daemon
|
46
|
+
./script/generate apn_sender
|
47
|
+
|
48
|
+
# To run daemon. Pass --help to print all options
|
49
|
+
./script/apn_sender --environment=production --verbose start
|
47
50
|
|
48
51
|
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>.
|
49
52
|
|
53
|
+
Check <code>RAILS_ROOT/logs/apn_sender.log</code> for debugging output. In addition to logging any major errors there, apn_sender hooks into the Resque::Worker logging to display any verbose or very_verbose worker output in apn_sender.log file as well.
|
54
|
+
|
55
|
+
|
50
56
|
=== 3. Checking Apple's Feedback Service
|
51
57
|
|
52
58
|
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.
|
@@ -71,7 +77,7 @@ If you're interested in knowing exactly <em>when</em> Apple determined each toke
|
|
71
77
|
# ... custom logic here
|
72
78
|
end
|
73
79
|
|
74
|
-
The Feedback Service works as a big queue
|
80
|
+
The Feedback Service works as a big queue. When you connect it pops off all its data and sends it over the wire at once, which means connecting a second time will return an empty array, so for ease of use a call to either +tokens+ or +data+ will connect once and cache the data. If you call either one again it'll continue to use its cached version (rather than connecting to Apple a second time to retrieve an empty array, which is probably not what you want).
|
75
81
|
|
76
82
|
Forcing a reconnect is as easy as calling either method with the single parameter +true+, but be sure you've already used the existing data because you'll never get it back.
|
77
83
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
1.0.0
|
data/apn_sender.gemspec
CHANGED
data/contrib/apn_sender.monitrc
CHANGED
@@ -1,13 +1,21 @@
|
|
1
|
-
#
|
1
|
+
# An example Monit configuration file for running the apn_sender background daemon
|
2
2
|
#
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
# 1. Replace #{app_name} with the path to your configuration
|
4
|
+
# 2. Add any arguments between apn_sender the and start/stop command
|
5
|
+
# 3. Install as a monitrc file
|
6
|
+
|
7
|
+
check process redis
|
8
|
+
with pidfile /tmp/redis.pid
|
9
|
+
group apn_sender
|
10
|
+
start program = "/usr/bin/redis-server /etc/redis.conf"
|
11
|
+
stop program = "/bin/echo SHUTDOWN | nc localhost 6379"
|
12
|
+
if failed host 127.0.0.1 port 6379 then restart
|
13
|
+
if 5 restarts within 5 cycles then timeout
|
14
|
+
|
9
15
|
|
10
16
|
check process apn_sender
|
11
|
-
with pidfile /var/www/apps
|
12
|
-
|
13
|
-
|
17
|
+
with pidfile /var/www/apps/#{app_name}/shared/pids/apn_sender.pid
|
18
|
+
group apn_sender
|
19
|
+
start program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/apn_sender --environment=production start"
|
20
|
+
stop program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/apn_sender --environment=production stop"
|
21
|
+
depends_on redis
|
data/lib/apn/connection/base.rb
CHANGED
@@ -4,6 +4,8 @@ require 'resque'
|
|
4
4
|
|
5
5
|
module APN
|
6
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.
|
7
9
|
module Base
|
8
10
|
attr_accessor :opts, :logger
|
9
11
|
|
@@ -25,6 +27,7 @@ module APN
|
|
25
27
|
|
26
28
|
protected
|
27
29
|
|
30
|
+
# Default to Rails or Merg logger, if available
|
28
31
|
def setup_logger
|
29
32
|
@logger = if defined?(Merb::Logger)
|
30
33
|
Merb.logger
|
data/lib/apn/notification.rb
CHANGED
@@ -59,13 +59,14 @@ module APN
|
|
59
59
|
# Extracts :alert, :badge, and :sound keys into the 'aps' hash, merges any other hash data
|
60
60
|
# into the root of the hash to encode and send to apple.
|
61
61
|
def packaged_message
|
62
|
+
opts = @options.clone # Don't destroy our pristine copy
|
62
63
|
hsh = {'aps' => {}}
|
63
|
-
hsh['aps']['alert'] =
|
64
|
-
hsh['aps']['badge'] =
|
65
|
-
if sound =
|
64
|
+
hsh['aps']['alert'] = opts.delete(:alert).to_s if opts[:alert]
|
65
|
+
hsh['aps']['badge'] = opts.delete(:badge).to_i if opts[:badge]
|
66
|
+
if sound = opts.delete(:sound)
|
66
67
|
hsh['aps']['sound'] = sound.is_a?(TrueClass) ? 'default' : sound.to_s
|
67
68
|
end
|
68
|
-
hsh.merge!(
|
69
|
+
hsh.merge!(opts)
|
69
70
|
hsh.to_json
|
70
71
|
end
|
71
72
|
|
data/lib/apn/notification_job.rb
CHANGED
@@ -9,7 +9,7 @@ module APN
|
|
9
9
|
# Build a notification from arguments and send to Apple
|
10
10
|
def self.perform(token, opts)
|
11
11
|
msg = APN::Notification.new(token, opts)
|
12
|
-
raise "Invalid notification options: #{opts.inspect}" unless msg.valid?
|
12
|
+
raise "Invalid notification options (did you provide :alert, :badge, or :sound?): #{opts.inspect}" unless msg.valid?
|
13
13
|
|
14
14
|
worker.send_to_apple( msg )
|
15
15
|
end
|
data/lib/apn/sender.rb
CHANGED
@@ -54,23 +54,23 @@ __END__
|
|
54
54
|
## To enqueue test job
|
55
55
|
k = 'ceecdc18 ef17b2d0 745475e0 0a6cd5bf 54534184 ac2649eb 40873c81 ae76dbe8'
|
56
56
|
c = '0f58e3e2 77237b8f f8213851 c835dee0 376b7a31 9e0484f7 06fe3035 7c5dda2f'
|
57
|
-
Resque.enqueue APN::NotificationJob, k, {:alert => 'Resque Test'}
|
58
57
|
APN.notify k, 'Resque Test'
|
59
58
|
|
60
59
|
# If you need to really force quit some screwed up workers
|
61
60
|
Resque.workers.map{|w| Resque.redis.srem(:workers, w)}
|
62
61
|
|
63
|
-
|
62
|
+
# To run worker from rake task
|
64
63
|
CERT_PATH=/Users/kali/Code/insurrection/certs/ ENVIRONMENT=production rake apn:work
|
65
64
|
|
66
|
-
|
65
|
+
# To run worker from IRB
|
67
66
|
Resque.workers.map(&:unregister_worker)
|
68
67
|
require 'ruby-debug'
|
69
68
|
worker = APN::Sender.new(:cert_path => '/Users/kali/Code/insurrection/certs/', :environment => :production)
|
70
69
|
worker.very_verbose = true
|
71
70
|
worker.work(5)
|
72
71
|
|
73
|
-
|
74
|
-
|
72
|
+
# To run worker as daemon
|
73
|
+
args = ['--environment=production', '--cert-path=/Users/kali/Code/insurrection/certs/']
|
74
|
+
APN::SenderDaemon.new(args).daemonize
|
75
75
|
|
76
76
|
|
data/lib/apn/sender_daemon.rb
CHANGED
@@ -4,12 +4,17 @@ require 'daemons'
|
|
4
4
|
require 'optparse'
|
5
5
|
|
6
6
|
module APN
|
7
|
+
# A wrapper designed to daemonize an APN::Sender instance to keep in running in the background.
|
8
|
+
# Connects worker's output to the Rails logger, if available. Creates a pid file suitable for
|
9
|
+
# monitoring with {monit}[http://mmonit.com/monit/].
|
10
|
+
#
|
11
|
+
# Based off delayed_job's great example. To use in a Rails app, <code>script/generate apn_sender</code>.
|
7
12
|
class SenderDaemon
|
8
13
|
|
9
14
|
def initialize(args)
|
10
|
-
@options = {:quiet => true, :worker_count => 1, :environment => :development}
|
15
|
+
@options = {:quiet => true, :worker_count => 1, :environment => :development, :delay => 5}
|
11
16
|
|
12
|
-
|
17
|
+
optparse = OptionParser.new do |opts|
|
13
18
|
opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
|
14
19
|
|
15
20
|
opts.on('-h', '--help', 'Show this message') do
|
@@ -38,8 +43,7 @@ module APN
|
|
38
43
|
|
39
44
|
# If no arguments, give help screen
|
40
45
|
@args = optparse.parse!(args.empty? ? ['-h'] : args)
|
41
|
-
|
42
|
-
puts "OPTS: #{@options.inspect}"
|
46
|
+
@options[:verbose] = true if @options[:very_verbose]
|
43
47
|
end
|
44
48
|
|
45
49
|
def daemonize
|