apn_sender 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Kali Donovan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,60 @@
1
+ = apn_sender
2
+
3
+ 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. 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] (<em>Apple Dev account required</em>) that the Apple's servers may treat non-persistent connections as a Denial of Service attack, and things started looking more complicated.
4
+
5
+ If this doesn't sound familiar, this gem probably isn't anything you need. If it does, though...
6
+
7
+ 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 helper methods for enqueueing your jobs and a sample monit config to make sure the background worker is around when you need it.
8
+
9
+
10
+ == Usage
11
+
12
+ === Queueing Messages From Your Application
13
+
14
+ To queue a message for sending through Apple's Push Notification service from your Rails application:
15
+
16
+ APN.notify(token, opts_hash)
17
+
18
+ where +token+ is the unique identifier of the iPhone to receive the notification and +opts_hash+ can have any of the following keys:
19
+
20
+ # :alert #=> The alert to send
21
+ # :badge #=> The badge number to send
22
+ # :sound #=> The sound file to play on receipt, or true to play the default sound installed with your app
23
+ # :custom #=> Hash of application-specific custom data to send along with the notification
24
+
25
+ === Getting Messages Actually Sent to Apple
26
+
27
+ Put your <code>apn_development.pem</code> and <code>apn_production.pem</code> certificates from Apple in your <code>RAILS_ROOT/config/certs</code> directory.
28
+
29
+ Once this is done, you can fire off a background worker with
30
+
31
+ $ rake apn:sender
32
+
33
+ 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:
34
+
35
+ # To run standard daemon
36
+ ./script/apn_sender
37
+
38
+ # To pass in options
39
+ ./script/apn_sender -- --cert-path=PATH_TO_NONSTANDARD_FOLDER_WITH_PEM_FILES --environment=production
40
+
41
+ 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>.
42
+
43
+ === Keeping Your Workers Working
44
+
45
+ There's also an included sample <code>apn_sender.monitrc</code> file in the <code>contrib/</code> folder to help monit handle server restarts and unexpected disasters.
46
+
47
+
48
+ == Installation
49
+
50
+ APN is built on top of {Resque}[http://github.com/defunkt/resque] (an awesome {Redis}[http://code.google.com/p/redis/]-based background runner similar to {delayed_job}[http://github.com/collectiveidea/delayed_job]). Read through the {Resque README}[http://github.com/defunkt/resque#readme] to get a feel for what's going on, follow the installation instructions there, and then run:
51
+
52
+ $ sudo gem install apple_push_notification
53
+
54
+ In your Rails app, add
55
+
56
+ config.gem 'apple_push_notification'
57
+
58
+ == Copyright
59
+
60
+ Copyright (c) 2010 Kali Donovan. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ load 'lib/apn/tasks.rb'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "apn_sender"
10
+ gem.summary = %Q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket.}
11
+ gem.description = %Q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper methods for enqueueing APN notifications, and a background daemon to send them.}
12
+ gem.email = "kali.donovan@gmail.com"
13
+ gem.homepage = "http://github.com/kdonovan/apple_push_notification"
14
+ gem.authors = ["Kali Donovan"]
15
+ gem.add_dependency 'resque'
16
+ gem.add_dependency 'resque-access_worker_from_job'
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/rdoctask'
49
+ Rake::RDocTask.new do |rdoc|
50
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "apple_push_notification #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,69 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{apn_sender}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Kali Donovan"]
12
+ s.date = %q{2010-04-25}
13
+ s.description = %q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper methods for enqueueing APN notifications, and a background daemon to send them.}
14
+ s.email = %q{kali.donovan@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "apn_sender.gemspec",
27
+ "contrib/apn_sender.monitrc",
28
+ "generators/apple_push_notification_generator.rb",
29
+ "generators/templates/script",
30
+ "init.rb",
31
+ "lib/apn.rb",
32
+ "lib/apn/notification.rb",
33
+ "lib/apn/notification_job.rb",
34
+ "lib/apn/queue_manager.rb",
35
+ "lib/apn/sender.rb",
36
+ "lib/apn/sender_daemon.rb",
37
+ "lib/apn/tasks.rb",
38
+ "lib/resque/hooks/before_unregister_worker.rb",
39
+ "rails/init.rb",
40
+ "test/helper.rb",
41
+ "test/test_apple_push_notification.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/kdonovan/apple_push_notification}
44
+ s.rdoc_options = ["--charset=UTF-8"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.3.5}
47
+ s.summary = %q{Resque-based background worker to send Apple Push Notifications over a persistent TCP socket.}
48
+ s.test_files = [
49
+ "test/helper.rb",
50
+ "test/test_apple_push_notification.rb"
51
+ ]
52
+
53
+ if s.respond_to? :specification_version then
54
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
55
+ s.specification_version = 3
56
+
57
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
58
+ s.add_runtime_dependency(%q<resque>, [">= 0"])
59
+ s.add_runtime_dependency(%q<resque-access_worker_from_job>, [">= 0"])
60
+ else
61
+ s.add_dependency(%q<resque>, [">= 0"])
62
+ s.add_dependency(%q<resque-access_worker_from_job>, [">= 0"])
63
+ end
64
+ else
65
+ s.add_dependency(%q<resque>, [">= 0"])
66
+ s.add_dependency(%q<resque-access_worker_from_job>, [">= 0"])
67
+ end
68
+ end
69
+
@@ -0,0 +1,13 @@
1
+ # an example Monit configuration file for running the apple_push_notification background sender
2
+ #
3
+ # To use:
4
+ # 1. copy to /var/www/apps/{app_name}/shared/apn_sender.monitrc
5
+ # 2. replace {app_name} as appropriate
6
+ # 3. add this to your /etc/monit/monitrc
7
+ #
8
+ # include /var/www/apps/{app_name}/shared/apn_sender.monitrc
9
+
10
+ check process apn_sender
11
+ with pidfile /var/www/apps/{app_name}/shared/pids/apn_sender.pid
12
+ start program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/apn_sender start"
13
+ stop program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/apn_sender stop"
@@ -0,0 +1,9 @@
1
+ class ApplePushNotificationGenerator < Rails::Generator::Base
2
+
3
+ def manifest
4
+ record do |m|
5
+ m.template 'script', 'script/apn_sender', :chmod => 0755
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Daemons sets pwd to /, so we have to explicitly set RAILS_ROOT
4
+ RAILS_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
5
+
6
+ require File.join(File.dirname(__FILE__), *%w(.. vendor plugins apple_push_notification lib apple_push_notification sender_daemon))
7
+ APN::SenderDaemon.new(ARGV).daemonize
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init.rb"
@@ -0,0 +1,65 @@
1
+ module APN
2
+ # Encapsulates the logic necessary to convert an iPhone token and an array of options into a string of the format required
3
+ # by Apple's servers to send the notification. Much of the processing code here copied with many thanks from
4
+ # http://github.com/samsoffes/apple_push_notification/blob/master/lib/apple_push_notification.rb
5
+ #
6
+ # APN::Notification.new's first argument is the token of the iPhone which should receive the notification. The second argument
7
+ # is a hash with any of :alert, :badge, and :sound keys. All three accept string arguments, while :sound can also be set to +true+
8
+ # to play the default sound installed with the application. At least one of these keys must exist. The hash also accepts a
9
+ # :custom key to send a hash of custom application data:
10
+ #
11
+ # APN::Notification.new(token, {:alert => 'Stuff', :custom => {:code => 23}})
12
+ # # Writes this JSON to servers: {"code":23,"aps":{"alert":"Stuff"}}
13
+ #
14
+ # As a shortcut, APN::Notification.new also accepts a string as the second argument, which it converts into the alert to send. The
15
+ # following two lines are equivalent:
16
+ #
17
+ # APN::Notification.new(token, 'Some Alert')
18
+ # APN::Notification.new(token, {:alert => 'Some Alert'})
19
+ #
20
+ class Notification
21
+ attr_accessor :message, :options, :json
22
+ def initialize(token, opts)
23
+ @options = hash_as_symbols(opts.is_a?(Hash) ? opts : {:alert => opts})
24
+ @json = generate_apple_json
25
+ hex_token = [token.delete(' ')].pack('H*')
26
+ @message = "\0\0 #{hex_token}\0#{json.length.chr}#{json}"
27
+ raise "The maximum size allowed for a notification payload is 256 bytes." if @message.size.to_i > 256
28
+ end
29
+
30
+ def to_s
31
+ @message
32
+ end
33
+
34
+ def valid?
35
+ return true if %w(alert badge sound).any?{|key| options.keys.include?(key.to_sym) }
36
+ false
37
+ end
38
+
39
+ protected
40
+
41
+ # Convert the supplied options into the JSON needed for Apple's push notification servers
42
+ def generate_apple_json
43
+ hsh = {'aps' => {}}
44
+ hsh['aps']['alert'] = @options[:alert].to_s if @options[:alert]
45
+ hsh['aps']['badge'] = @options[:badge].to_i if @options[:badge]
46
+ hsh['aps']['sound'] = @options[:sound] if @options[:sound] and @options[:sound].is_a? String
47
+ hsh['aps']['sound'] = 'default' if @options[:sound] and @options[:sound].is_a? TrueClass
48
+ hsh.merge!(@options[:custom]) if @options[:custom]
49
+ hsh.to_json
50
+ end
51
+
52
+ # Symbolize keys, using ActiveSupport if available
53
+ def hash_as_symbols(hash)
54
+ if hash.respond_to?(:symbolize_keys)
55
+ return hash.symbolize_keys
56
+ else
57
+ hash.inject({}) do |opt, (key, value)|
58
+ opt[(key.to_sym rescue key) || key] = value
59
+ opt
60
+ end
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,22 @@
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: #{opts.inspect}" unless msg.valid?
13
+ worker.send_to_apple( msg )
14
+ end
15
+
16
+
17
+ # Only execute this job in specialized APN::Sender workers, since
18
+ # standard Resque workers don't maintain the persistent TCP connection.
19
+ extend Resque::Plugins::AccessWorkerFromJob
20
+ self.required_worker_class = 'APN::Sender'
21
+ end
22
+ end
@@ -0,0 +1,51 @@
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
+ # Extends Resque, allowing us to add all the callbacks to Resque we desire without affecting the expected
7
+ # functionality in the parent app, if we're included in e.g. a Rails application.
8
+ class QueueManager
9
+ extend Resque
10
+
11
+ def self.before_unregister_worker(&block)
12
+ block ? (@before_unregister_worker = block) : @before_unregister_worker
13
+ end
14
+
15
+ def self.before_unregister_worker=(before_unregister_worker)
16
+ @before_unregister_worker = before_unregister_worker
17
+ end
18
+
19
+ def self.to_s
20
+ "APN::QueueManager (Resque Client) connected to #{redis.server}"
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ # Ensures we close any open sockets when the worker exits
27
+ APN::QueueManager.before_unregister_worker do |worker|
28
+ worker.send(:teardown_connection) if worker.respond_to?(:teardown_connection)
29
+ end
30
+
31
+
32
+ # # Run N jobs per fork, rather than creating a new fork for each notification
33
+ # # By defunkt - http://gist.github.com/349376
34
+ # APN::QueueManager.after_fork do |job|
35
+ # # How many jobs should we process in each fork?
36
+ # jobs_per_fork = 10
37
+ #
38
+ # # Set hook to nil to prevent running this hook over
39
+ # # and over while processing more jobs in this fork.
40
+ # Resque.after_fork = nil
41
+ #
42
+ # # Make sure we process jobs in the right order.
43
+ # job.worker.process(job)
44
+ #
45
+ # # One less than specified because the child will run a
46
+ # # final job after exiting this hook.
47
+ # (jobs_per_fork.to_i - 1).times do
48
+ # job.worker.process
49
+ # end
50
+ # end
51
+
data/lib/apn/sender.rb ADDED
@@ -0,0 +1,133 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+ require 'resque'
4
+
5
+ module APN
6
+ # Subclass of Resque::Worker which initializes a single TCP socket on creation to communicate with Apple's Push Notification servers.
7
+ # Shares this socket with each child process forked off by Resque to complete a job. Socket is closed in the before_unregister_worker
8
+ # callback, which gets called on normal or exceptional exits.
9
+ #
10
+ # End result: single persistent TCP connection to Apple, so they don't ban you for frequently opening and closing connections,
11
+ # which they apparently view as a DOS attack.
12
+ #
13
+ # Accepts <code>:environment</code> (production vs anything else) and <code>:cert_path</code> options on initialization. If called in a
14
+ # Rails context, will default to RAILS_ENV and RAILS_ROOT/config/certs. :environment will default to development.
15
+ # APN::Sender expects two files to exist in the specified <code>:cert_path</code> directory:
16
+ # <code>apn_production.pem</code> and <code>apn_development.pem</code>.
17
+ class Sender < ::Resque::Worker
18
+ APN_PORT = 2195
19
+ attr_accessor :apn_cert, :apn_host, :socket, :socket_tcp, :opts
20
+
21
+ class << self
22
+ attr_accessor :logger
23
+ end
24
+
25
+ self.logger = if defined?(Merb::Logger)
26
+ Merb.logger
27
+ elsif defined?(RAILS_DEFAULT_LOGGER)
28
+ RAILS_DEFAULT_LOGGER
29
+ end
30
+
31
+ def initialize(opts = {})
32
+ @opts = opts
33
+
34
+ # Set option defaults
35
+ @opts[:cert_path] ||= File.join(File.expand_path(RAILS_ROOT), "config", "certs") if defined?(RAILS_ROOT)
36
+ @opts[:environment] ||= RAILS_ENV if defined?(RAILS_ENV)
37
+
38
+ logger.info "APN::Sender initializing. Establishing connections first..." if @opts[:verbose]
39
+ setup_paths
40
+ setup_connection
41
+
42
+ super( APN::QUEUE_NAME )
43
+ end
44
+
45
+ # Send a raw string over the socket to Apple's servers (presumably already formatted by APN::Notification)
46
+ def send_to_apple(msg)
47
+ @socket.write( msg.to_s )
48
+ rescue SocketError => error
49
+ logger.error("Error with connection to #{@apn_host}: #{error}")
50
+ raise "Error with connection to #{@apn_host}: #{error}"
51
+ end
52
+
53
+ protected
54
+
55
+ def apn_production?
56
+ @opts[:environment] && @opts[:environment] != '' && :production == @opts[:environment].to_sym
57
+ end
58
+
59
+ # Get a fix on the .pem certificate we'll be using for SSL
60
+ def setup_paths
61
+ raise "Missing certificate path. Please specify :cert_path when initializing class." unless @opts[:cert_path]
62
+ @apn_host = apn_production? ? "gateway.push.apple.com" : "gateway.sandbox.push.apple.com"
63
+ cert_name = apn_production? ? "apn_production.pem" : "apn_development.pem"
64
+ cert_path = File.join(@opts[:cert_path], cert_name)
65
+
66
+ @apn_cert = File.exists?(cert_path) ? File.read(cert_path) : nil
67
+ raise "Missing apple push notification certificate in #{cert_path}" unless @apn_cert
68
+ end
69
+
70
+ # Open socket to Apple's servers
71
+ def setup_connection
72
+ raise "Missing apple push notification certificate" unless @apn_cert
73
+ raise "Trying to open already-open socket" if @socket || @socket_tcp
74
+
75
+ ctx = OpenSSL::SSL::SSLContext.new
76
+ ctx.cert = OpenSSL::X509::Certificate.new(@apn_cert)
77
+ ctx.key = OpenSSL::PKey::RSA.new(@apn_cert)
78
+
79
+ @socket_tcp = TCPSocket.new(@apn_host, APN_PORT)
80
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket_tcp, ctx)
81
+ @socket.sync = true
82
+ @socket.connect
83
+ rescue SocketError => error
84
+ logger.error("Error with connection to #{@apn_host}: #{error}")
85
+ raise "Error with connection to #{@apn_host}: #{error}"
86
+ end
87
+
88
+ # Close open sockets
89
+ def teardown_connection
90
+ logger.info "Closing connections..." if @opts[:verbose]
91
+
92
+ begin
93
+ @socket.close if @socket
94
+ rescue Exception => e
95
+ logger.error("Error closing SSL Socket: #{e}")
96
+ end
97
+
98
+ begin
99
+ @socket_tcp.close if @socket_tcp
100
+ rescue Exception => e
101
+ logger.error("Error closing TCP Socket: #{e}")
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end
108
+
109
+
110
+ __END__
111
+
112
+ # irb -r 'lib/apple_push_notification'
113
+
114
+ ## To enqueue test job
115
+ Resque.enqueue APN::NotificationJob, 'ceecdc18 ef17b2d0 745475e0 0a6cd5bf 54534184 ac2649eb 40873c81 ae76dbe8', {:alert => 'Resque Test'}
116
+
117
+
118
+ ## To run worker from rake task
119
+ CERT_PATH=/Users/kali/Code/insurrection/certs/ ENVIRONMENT=production rake apn:work
120
+
121
+ ## To run worker from IRB
122
+ Resque.workers.map(&:unregister_worker)
123
+ require 'ruby-debug'
124
+ worker = APN::Sender.new(:cert_path => '/Users/kali/Code/insurrection/certs/', :environment => :production)
125
+ worker.very_verbose = true
126
+ worker.work(5)
127
+
128
+ ## To run worker as daemon - NOT YET TESTED
129
+ APN::SenderDaemon.new(ARGV).daemonize
130
+
131
+ ## TESTING - check the broken pipe errors
132
+
133
+ ## TESTING - then check implementation of resque-web
@@ -0,0 +1,65 @@
1
+ # Modified slightly from delayed_job's delayed/command.rb
2
+ require 'rubygems'
3
+ require 'daemons'
4
+ require 'optparse'
5
+
6
+ module APN
7
+ class SenderDaemon
8
+ attr_accessor :worker_count
9
+
10
+ def initialize(args)
11
+ @options = {:quiet => true}
12
+ @worker_count = 1
13
+
14
+ opts = OptionParser.new do |opts|
15
+ opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
16
+
17
+ opts.on('-h', '--help', 'Show this message') do
18
+ puts opts
19
+ exit 1
20
+ end
21
+ opts.on('-e', '--environment=NAME', 'Specifies the environment to run this apn_sender under ([development]/production).') do |e|
22
+ @options[:environment] = e
23
+ end
24
+ opts.on('--cert-path=NAME', '--certificate-path=NAME', 'Path to directory containing apn .pem certificates.') do |path|
25
+ @options[:cert_path] = path
26
+ end
27
+ opts.on('-n', '--number_of_workers=workers', "Number of unique workers to spawn") do |worker_count|
28
+ @worker_count = worker_count.to_i rescue 1
29
+ end
30
+ end
31
+ @args = opts.parse!(args)
32
+ end
33
+
34
+ def daemonize
35
+ worker_count.times do |worker_index|
36
+ process_name = worker_count == 1 ? "apn_sender" : "apn_sender.#{worker_index}"
37
+ Daemons.run_proc(process_name, :dir => "#{::RAILS_ROOT}/tmp/pids", :dir_mode => :normal, :ARGV => @args) do |*args|
38
+ run process_name
39
+ end
40
+ end
41
+ end
42
+
43
+ def run(worker_name = nil)
44
+ Dir.chdir(::RAILS_ROOT)
45
+ require File.join(::RAILS_ROOT, 'config', 'environment')
46
+
47
+ # Replace the default logger
48
+ logger = Logger.new(File.join(::RAILS_ROOT, 'log', 'apn_sender.log'))
49
+ logger.level = ActiveRecord::Base.logger.level
50
+ ActiveRecord::Base.logger = logger
51
+ ActiveRecord::Base.clear_active_connections!
52
+ APN::Sender.logger = logger
53
+
54
+ worker = APN::Sender.new(@options)
55
+ worker.verbose = @options[:verbose]
56
+ worker.very_verbose = @options[:very_verbose]
57
+ worker.work(@options[:delay] || 5)
58
+ rescue => e
59
+ logger.fatal e
60
+ STDERR.puts e.message
61
+ exit 1
62
+ end
63
+
64
+ end
65
+ end
data/lib/apn/tasks.rb ADDED
@@ -0,0 +1,39 @@
1
+ # Slight modifications from the default Resque tasks
2
+ namespace :apn do
3
+ task :setup
4
+ task :sender => :work
5
+ task :senders => :workers
6
+
7
+ desc "Start an APN worker"
8
+ task :work => :setup do
9
+ require 'lib/apple_push_notification'
10
+
11
+ worker = nil
12
+
13
+ begin
14
+ worker = APN::Sender.new(:cert_path => ENV['CERT_PATH'], :environment => ENV['ENVIRONMENT'])
15
+ worker.verbose = ENV['LOGGING'] || ENV['VERBOSE']
16
+ worker.very_verbose = ENV['VVERBOSE']
17
+ rescue Exception => e
18
+ raise e
19
+ # abort "set QUEUE env var, e.g. $ QUEUE=critical,high rake resque:work"
20
+ end
21
+
22
+ puts "*** Starting worker to send apple notifications in the background from #{worker}"
23
+
24
+ worker.work(ENV['INTERVAL'] || 5) # interval, will block
25
+ end
26
+
27
+ desc "Start multiple APN workers. Should only be used in dev mode."
28
+ task :workers do
29
+ threads = []
30
+
31
+ ENV['COUNT'].to_i.times do
32
+ threads << Thread.new do
33
+ system "rake apn:work"
34
+ end
35
+ end
36
+
37
+ threads.each { |thread| thread.join }
38
+ end
39
+ end
data/lib/apn.rb ADDED
@@ -0,0 +1,28 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'resque'
5
+ require 'resque/plugins/access_worker_from_job'
6
+ require 'resque/hooks/before_unregister_worker'
7
+
8
+ begin
9
+ require 'yajl/json_gem'
10
+ rescue LoadError
11
+ require 'json'
12
+ end
13
+
14
+ require 'apn/queue_manager'
15
+
16
+ module APN
17
+ # Change this to modify the queue notification jobs are pushed to and pulled from
18
+ QUEUE_NAME = :apple_push_notifications
19
+
20
+ # Enqueues a notification to be sent in the background via the persistent TCP socket, assuming apn_sender is running (or will be soon)
21
+ def self.notify(token, opts = {})
22
+ APN::QueueManager.enqueue(APN::NotificationJob, token, opts)
23
+ end
24
+ end
25
+
26
+ require 'apn/notification'
27
+ require 'apn/notification_job'
28
+ require 'apn/sender'
@@ -0,0 +1,30 @@
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
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "apn"
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'apple_push_notification'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestAPN < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apn_sender
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Kali Donovan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-04-25 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: resque
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: resque-access_worker_from_job
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: Resque-based background worker to send Apple Push Notifications over a persistent TCP socket. Includes Resque tweaks to allow persistent sockets between jobs, helper methods for enqueueing APN notifications, and a background daemon to send them.
36
+ email: kali.donovan@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE
43
+ - README.rdoc
44
+ files:
45
+ - .document
46
+ - .gitignore
47
+ - LICENSE
48
+ - README.rdoc
49
+ - Rakefile
50
+ - VERSION
51
+ - apn_sender.gemspec
52
+ - contrib/apn_sender.monitrc
53
+ - generators/apple_push_notification_generator.rb
54
+ - generators/templates/script
55
+ - init.rb
56
+ - lib/apn.rb
57
+ - lib/apn/notification.rb
58
+ - lib/apn/notification_job.rb
59
+ - lib/apn/queue_manager.rb
60
+ - lib/apn/sender.rb
61
+ - lib/apn/sender_daemon.rb
62
+ - lib/apn/tasks.rb
63
+ - lib/resque/hooks/before_unregister_worker.rb
64
+ - rails/init.rb
65
+ - test/helper.rb
66
+ - test/test_apple_push_notification.rb
67
+ has_rdoc: true
68
+ homepage: http://github.com/kdonovan/apple_push_notification
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options:
73
+ - --charset=UTF-8
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.5
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: Resque-based background worker to send Apple Push Notifications over a persistent TCP socket.
95
+ test_files:
96
+ - test/helper.rb
97
+ - test/test_apple_push_notification.rb