push-core 0.0.1.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
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.md ADDED
@@ -0,0 +1,3 @@
1
+ # Push
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/bin/push ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'push'
5
+
6
+ foreground = false
7
+ environment = ARGV[0]
8
+ banner = 'Usage: push <Rails environment> [options]'
9
+ ARGV.options do |opts|
10
+ opts.banner = banner
11
+ opts.on('-f', '--foreground', 'Run in the foreground.') { foreground = true }
12
+ opts.on('-v', '--version', 'Print this version of push.') { puts "push #{Push::VERSION}"; exit }
13
+ opts.on('-h', '--help', 'You\'re looking at it.') { puts opts; exit }
14
+ opts.parse!
15
+ end
16
+
17
+ if environment.nil?
18
+ puts banner
19
+ exit 1
20
+ end
21
+
22
+ ENV['RAILS_ENV'] = environment
23
+ load 'config/environment.rb'
24
+
25
+ require 'push/daemon'
26
+
27
+ Push::Daemon.start(environment, foreground)
@@ -0,0 +1,22 @@
1
+ class PushGenerator < 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_dir = File.expand_path("db/migrate")
11
+
12
+ if !self.class.migration_exists?(migration_dir, 'create_push')
13
+ migration_template "create_push.rb", "db/migrate/create_push.rb"
14
+ end
15
+ end
16
+
17
+ def copy_config
18
+ copy_file "development.rb", "config/push/development.rb"
19
+ copy_file "staging.rb", "config/push/staging.rb"
20
+ copy_file "production.rb", "config/push/production.rb"
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ class CreatePush < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :push_messages do |t|
4
+ t.string :device, :null => false
5
+ t.string :type, :null => false
6
+ t.text :properties, :null => true
7
+ t.boolean :delivered, :null => false, :default => false
8
+ t.timestamp :delivered_at, :null => true
9
+ t.boolean :failed, :null => false, :default => false
10
+ t.timestamp :failed_at, :null => true
11
+ t.integer :error_code, :null => true
12
+ t.string :error_description, :null => true
13
+ t.timestamp :deliver_after, :null => true
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :push_messages, [:delivered, :failed, :deliver_after]
18
+
19
+ create_table :push_feedback do |t|
20
+ t.string :device, :null => false
21
+ t.string :type, :null => false
22
+ t.timestamp :failed_at, :null => false
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :push_feedback, :device
27
+ end
28
+
29
+ def self.down
30
+ drop_table :push_feedback
31
+ drop_table :push_messages
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ Push::Daemon::Builder.new do
2
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
+
4
+ provider :apns,
5
+ {
6
+ :certificate => "development.pem",
7
+ :certificate_password => "",
8
+ :sandbox => true,
9
+ :connections => 3,
10
+ :feedback_poll => 60
11
+ }
12
+
13
+ provider :c2dm,
14
+ {
15
+ :connections => 2,
16
+ :email => "",
17
+ :password => ""
18
+ }
19
+ end
@@ -0,0 +1,19 @@
1
+ Push::Daemon::Builder.new do
2
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
+
4
+ provider :apns,
5
+ {
6
+ :certificate => "production.pem",
7
+ :certificate_password => "",
8
+ :sandbox => false,
9
+ :connections => 3,
10
+ :feedback_poll => 60
11
+ }
12
+
13
+ provider :c2dm,
14
+ {
15
+ :connections => 2,
16
+ :email => "",
17
+ :password => ""
18
+ }
19
+ end
@@ -0,0 +1,19 @@
1
+ Push::Daemon::Builder.new do
2
+ daemon({ :poll => 2, :pid_file => "tmp/pids/push.pid", :airbrake_notify => false })
3
+
4
+ provider :apns,
5
+ {
6
+ :certificate => "staging.pem",
7
+ :certificate_password => "",
8
+ :sandbox => true,
9
+ :connections => 3,
10
+ :feedback_poll => 60
11
+ }
12
+
13
+ provider :c2dm,
14
+ {
15
+ :connections => 2,
16
+ :email => "",
17
+ :password => ""
18
+ }
19
+ end
data/lib/push.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'active_record'
2
+ require 'push/version'
3
+ require 'push/message'
4
+ require 'push/feedback'
@@ -0,0 +1,124 @@
1
+ require 'thread'
2
+ require 'push/daemon/builder'
3
+ require 'push/daemon/interruptible_sleep'
4
+ require 'push/daemon/delivery_error'
5
+ require 'push/daemon/disconnection_error'
6
+ require 'push/daemon/pool'
7
+ require 'push/daemon/connection_pool'
8
+ require 'push/daemon/database_reconnectable'
9
+ require 'push/daemon/delivery_queue'
10
+ require 'push/daemon/delivery_handler'
11
+ require 'push/daemon/delivery_handler_pool'
12
+ require 'push/daemon/feeder'
13
+ require 'push/daemon/logger'
14
+
15
+ module Push
16
+ module Daemon
17
+ class << self
18
+ attr_accessor :logger, :configuration, :delivery_queue,
19
+ :connection_pool, :delivery_handler_pool, :foreground, :providers
20
+ end
21
+
22
+ def self.start(environment, foreground)
23
+ self.providers = []
24
+ @foreground = foreground
25
+ setup_signal_hooks
26
+
27
+ require File.join(Rails.root, 'config', 'push', environment + '.rb')
28
+
29
+ self.logger = Logger.new(:foreground => foreground, :airbrake_notify => configuration[:airbrake_notify])
30
+
31
+ self.delivery_queue = DeliveryQueue.new
32
+
33
+ daemonize unless foreground
34
+
35
+ write_pid_file
36
+
37
+ dbconnections = 0
38
+ self.connection_pool = ConnectionPool.new
39
+ self.providers.each do |provider|
40
+ self.connection_pool.populate(provider)
41
+ dbconnections += provider.totalconnections
42
+ end
43
+
44
+ rescale_poolsize(dbconnections)
45
+
46
+ self.delivery_handler_pool = DeliveryHandlerPool.new(connection_pool.size)
47
+ delivery_handler_pool.populate
48
+
49
+ logger.info('[Daemon] Ready')
50
+
51
+ Push::Daemon::Feeder.start(foreground)
52
+ end
53
+
54
+ protected
55
+
56
+ def self.rescale_poolsize(size)
57
+ # 1 feeder + providers
58
+ size = 1 + size
59
+
60
+ h = ActiveRecord::Base.connection_config
61
+ h[:pool] = size
62
+ ActiveRecord::Base.establish_connection(h)
63
+ logger.info("[Daemon] Rescaled ActiveRecord ConnectionPool size to #{size}")
64
+ end
65
+
66
+ def self.setup_signal_hooks
67
+ @shutting_down = false
68
+
69
+ ['SIGINT', 'SIGTERM'].each do |signal|
70
+ Signal.trap(signal) do
71
+ handle_shutdown_signal
72
+ end
73
+ end
74
+ end
75
+
76
+ def self.handle_shutdown_signal
77
+ exit 1 if @shutting_down
78
+ @shutting_down = true
79
+ shutdown
80
+ end
81
+
82
+ def self.shutdown
83
+ puts "\nShutting down..."
84
+ Push::Daemon::Feeder.stop
85
+ Push::Daemon.delivery_handler_pool.drain if Push::Daemon.delivery_handler_pool
86
+
87
+ self.providers.each do |provider|
88
+ provider.stop
89
+ end
90
+
91
+ delete_pid_file
92
+ end
93
+
94
+ def self.daemonize
95
+ exit if pid = fork
96
+ Process.setsid
97
+ exit if pid = fork
98
+
99
+ Dir.chdir '/'
100
+ File.umask 0000
101
+
102
+ STDIN.reopen '/dev/null'
103
+ STDOUT.reopen '/dev/null', 'a'
104
+ STDERR.reopen STDOUT
105
+ end
106
+
107
+ def self.write_pid_file
108
+ if !configuration[:pid_file].blank?
109
+ begin
110
+ File.open(configuration[:pid_file], 'w') do |f|
111
+ f.puts $$
112
+ end
113
+ rescue SystemCallError => e
114
+ logger.error("Failed to write PID to '#{configuration[:pid_file]}': #{e.inspect}")
115
+ end
116
+ end
117
+ end
118
+
119
+ def self.delete_pid_file
120
+ pid_file = configuration[:pid_file]
121
+ File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,23 @@
1
+ module Push
2
+ module Daemon
3
+ class Builder
4
+ def initialize(&block)
5
+ instance_eval(&block) if block_given?
6
+ end
7
+
8
+ def daemon(options)
9
+ Push::Daemon.configuration = options
10
+ end
11
+
12
+ def provider(klass, options)
13
+ begin
14
+ middleware = Push::Daemon.const_get("#{klass}".camelize)
15
+ rescue NameError
16
+ raise LoadError, "Could not find matching push provider for #{klass.inspect}. You may need to install an additional gem (such as push-#{klass})."
17
+ end
18
+
19
+ Push::Daemon.providers << middleware.new(options)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module Push
2
+ module Daemon
3
+ class ConnectionPool
4
+ def initialize()
5
+ @connections = Hash.new
6
+ end
7
+
8
+ def populate(provider)
9
+ @connections[provider.connectiontype.to_s] = Queue.new
10
+ provider.pushconnections.times do |i|
11
+ c = provider.connectiontype.new(provider, i+1)
12
+ c.connect
13
+ checkin(c)
14
+ end
15
+ end
16
+
17
+ def checkin(connection)
18
+ @connections[connection.class.to_s].push(connection)
19
+ end
20
+
21
+ def checkout(notification_type)
22
+ @connections[notification_type.to_s].pop
23
+ end
24
+
25
+ def size
26
+ @connections.size
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,51 @@
1
+ class PGError < StandardError; end if !defined?(PGError)
2
+ module Mysql2; class Error < StandardError; end; end if !defined?(Mysql2)
3
+
4
+ module Push
5
+ module Daemon
6
+ module DatabaseReconnectable
7
+ ADAPTER_ERRORS = [ActiveRecord::StatementInvalid, PGError, Mysql2::Error]
8
+
9
+ def with_database_reconnect_and_retry(name)
10
+ begin
11
+ yield
12
+ rescue *ADAPTER_ERRORS => e
13
+ Push::Daemon.logger.error(e)
14
+ database_connection_lost(name)
15
+ retry
16
+ end
17
+ end
18
+
19
+ def database_connection_lost(name)
20
+ Push::Daemon.logger.warn("[#{name}] Lost connection to database, reconnecting...")
21
+ attempts = 0
22
+ loop do
23
+ begin
24
+ Push::Daemon.logger.warn("[#{name}] Attempt #{attempts += 1}")
25
+ reconnect_database
26
+ check_database_is_connected
27
+ break
28
+ rescue *ADAPTER_ERRORS => e
29
+ Push::Daemon.logger.error(e, :airbrake_notify => false)
30
+ sleep_to_avoid_thrashing
31
+ end
32
+ end
33
+ Push::Daemon.logger.warn("[#{name}] Database reconnected")
34
+ end
35
+
36
+ def reconnect_database
37
+ ActiveRecord::Base.clear_all_connections!
38
+ ActiveRecord::Base.establish_connection
39
+ end
40
+
41
+ def check_database_is_connected
42
+ # Simply asking the adapter for the connection state is not sufficient.
43
+ Push::Message.count
44
+ end
45
+
46
+ def sleep_to_avoid_thrashing
47
+ sleep 2
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ module Push
2
+ class DeliveryError < StandardError
3
+ attr_reader :code, :description
4
+
5
+ def initialize(code, message_id, description, source)
6
+ @code = code
7
+ @message_id = message_id
8
+ @description = description
9
+ @source = source
10
+ end
11
+
12
+ def message
13
+ "Unable to deliver message #{@message_id}, received #{@source} error #{@code} (#{@description})"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,48 @@
1
+ module Push
2
+ module Daemon
3
+ class DeliveryHandler
4
+ include DatabaseReconnectable
5
+
6
+ attr_reader :name
7
+ STOP = 0x666
8
+
9
+ def initialize(i)
10
+ @name = "DeliveryHandler #{i}"
11
+ end
12
+
13
+ def start
14
+ Thread.new do
15
+ loop do
16
+ break if @stop
17
+ handle_next_notification
18
+ end
19
+ end
20
+ end
21
+
22
+ def stop
23
+ @stop = true
24
+ Push::Daemon.delivery_queue.push(STOP)
25
+ end
26
+
27
+ protected
28
+
29
+ def handle_next_notification
30
+ notification = Push::Daemon.delivery_queue.pop
31
+
32
+ if notification == STOP
33
+ return
34
+ end
35
+
36
+ begin
37
+ connection = Push::Daemon.connection_pool.checkout(notification.use_connection)
38
+ notification.deliver(connection)
39
+ rescue StandardError => e
40
+ Push::Daemon.logger.error(e)
41
+ ensure
42
+ Push::Daemon.connection_pool.checkin(connection)
43
+ Push::Daemon.delivery_queue.notification_processed
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ module Push
2
+ module Daemon
3
+ class DeliveryHandlerPool < Pool
4
+
5
+ protected
6
+
7
+ def new_object_for_pool(i)
8
+ DeliveryHandler.new(i)
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
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ module Push
2
+ module Daemon
3
+ class DeliveryQueue
4
+ def initialize
5
+ @mutex = Mutex.new
6
+ @num_notifications = 0
7
+ @queue = Queue.new
8
+ end
9
+
10
+ def push(notification)
11
+ @mutex.synchronize { @num_notifications += 1 }
12
+ @queue.push(notification)
13
+ end
14
+
15
+ def pop
16
+ @queue.pop
17
+ end
18
+
19
+ def notification_processed
20
+ @mutex.synchronize { @num_notifications -= 1 }
21
+ end
22
+
23
+ def notifications_processed?
24
+ @mutex.synchronize { @num_notifications == 0 }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module Push
2
+ class DisconnectionError < StandardError
3
+ attr_reader :code, :description
4
+
5
+ def initialize
6
+ @code = nil
7
+ @description = "APNs disconnected without returning an error."
8
+ end
9
+
10
+ def message
11
+ "The APNs disconnected without returning an error. This may indicate you are using an invalid certificate for the host."
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ module Push
2
+ module Daemon
3
+ class Feeder
4
+ extend DatabaseReconnectable
5
+ extend InterruptibleSleep
6
+
7
+ def self.name
8
+ "Feeder"
9
+ end
10
+
11
+ def self.start(foreground)
12
+ reconnect_database unless foreground
13
+
14
+ loop do
15
+ break if @stop
16
+ enqueue_notifications
17
+ interruptible_sleep Push::Daemon.configuration[:poll]
18
+ end
19
+ end
20
+
21
+ def self.stop
22
+ @stop = true
23
+ interrupt_sleep
24
+ end
25
+
26
+ protected
27
+
28
+ def self.enqueue_notifications
29
+ begin
30
+ with_database_reconnect_and_retry(name) do
31
+ if Push::Daemon.delivery_queue.notifications_processed?
32
+ Push::Message.ready_for_delivery.each do |notification|
33
+ Push::Daemon.delivery_queue.push(notification)
34
+ end
35
+ end
36
+ end
37
+ rescue StandardError => e
38
+ Push::Daemon.logger.error(e)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,18 @@
1
+ module Push
2
+ module Daemon
3
+ module InterruptibleSleep
4
+ def interruptible_sleep(seconds)
5
+ @_sleep_check, @_sleep_interrupt = IO.pipe
6
+ IO.select([@_sleep_check], nil, nil, seconds)
7
+ @_sleep_check.close rescue IOError
8
+ @_sleep_interrupt.close rescue IOError
9
+ end
10
+
11
+ def interrupt_sleep
12
+ if @_sleep_interrupt
13
+ @_sleep_interrupt.close rescue IOError
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ module Push
2
+ module Daemon
3
+ class Logger
4
+ def initialize(options)
5
+ @options = options
6
+ log_path = File.join(Rails.root, 'log', 'push.log')
7
+ @logger = ActiveSupport::BufferedLogger.new(log_path, Rails.logger.level)
8
+ @logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
9
+ end
10
+
11
+ def info(msg)
12
+ log(:info, msg)
13
+ end
14
+
15
+ def error(msg, options = {})
16
+ airbrake_notify(msg) if notify_via_airbrake?(msg, options)
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}: #{msg.backtrace.join("\n")}"
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_or_ignore(e)
43
+ elsif defined?(HoptoadNotifier)
44
+ HoptoadNotifier.notify_or_ignore(e)
45
+ end
46
+ end
47
+
48
+ def notify_via_airbrake?(msg, options)
49
+ msg.is_a?(Exception) && options[:airbrake_notify] != false
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ module Push
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
+ while !@queue.empty?
19
+ object = @queue.pop
20
+ object_removed_from_pool(object)
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ def new_object_for_pool(i)
27
+ end
28
+
29
+ def object_added_to_pool(object)
30
+ end
31
+
32
+ def object_removed_from_pool(object)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,8 @@
1
+ module Push
2
+ class Feedback < ActiveRecord::Base
3
+ self.table_name = 'push_feedback'
4
+
5
+ validates :device, :presence => true
6
+ validates :failed_at, :presence => true
7
+ end
8
+ end
@@ -0,0 +1,47 @@
1
+ require 'active_record'
2
+ require 'active_record/errors'
3
+ require 'push/daemon/database_reconnectable'
4
+ module Push
5
+ class Message < ActiveRecord::Base
6
+ include Push::Daemon::DatabaseReconnectable
7
+ self.table_name = "push_messages"
8
+
9
+ validates :device, :presence => true
10
+
11
+ scope :ready_for_delivery, lambda { where('delivered = ? AND failed = ? AND (deliver_after IS NULL OR deliver_after < ?)', false, false, Time.now) }
12
+
13
+ def deliver(connection)
14
+ begin
15
+ connection.write(self.to_message)
16
+ check_for_error(connection)
17
+
18
+ # this makes no sense in the rails environment, but it does in the daemon
19
+ with_database_reconnect_and_retry(connection.name) do
20
+ self.delivered = true
21
+ self.delivered_at = Time.now
22
+ self.save!(:validate => false)
23
+ end
24
+
25
+ Push::Daemon.logger.info("Message #{id} delivered to #{device}")
26
+ rescue Push::DeliveryError, Push::DisconnectionError => error
27
+ handle_delivery_error(error, connection)
28
+ raise
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def handle_delivery_error(error, connection)
35
+ # this code makes no sense in the rails environment, but it does in the daemon
36
+ with_database_reconnect_and_retry(connection.name) do
37
+ self.delivered = false
38
+ self.delivered_at = nil
39
+ self.failed = true
40
+ self.failed_at = Time.now
41
+ self.error_code = error.code
42
+ self.error_description = error.description
43
+ self.save!(:validate => false)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,3 @@
1
+ module Push
2
+ VERSION = "0.0.1.pre"
3
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: push-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.pre
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Tom Pesman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70154092596240 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70154092596240
25
+ - !ruby/object:Gem::Dependency
26
+ name: sqlite3
27
+ requirement: &70154092595780 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70154092595780
36
+ description: Push daemon for push notification services like APNS and C2DM.
37
+ email:
38
+ - tom@tnux.net
39
+ executables:
40
+ - push
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - lib/generators/push_generator.rb
45
+ - lib/generators/templates/create_push.rb
46
+ - lib/generators/templates/development.rb
47
+ - lib/generators/templates/production.rb
48
+ - lib/generators/templates/staging.rb
49
+ - lib/push.rb
50
+ - lib/push/daemon.rb
51
+ - lib/push/daemon/builder.rb
52
+ - lib/push/daemon/connection_pool.rb
53
+ - lib/push/daemon/database_reconnectable.rb
54
+ - lib/push/daemon/delivery_error.rb
55
+ - lib/push/daemon/delivery_handler.rb
56
+ - lib/push/daemon/delivery_handler_pool.rb
57
+ - lib/push/daemon/delivery_queue.rb
58
+ - lib/push/daemon/disconnection_error.rb
59
+ - lib/push/daemon/feeder.rb
60
+ - lib/push/daemon/interruptible_sleep.rb
61
+ - lib/push/daemon/logger.rb
62
+ - lib/push/daemon/pool.rb
63
+ - lib/push/feedback.rb
64
+ - lib/push/message.rb
65
+ - lib/push/version.rb
66
+ - README.md
67
+ - MIT-LICENSE
68
+ - bin/push
69
+ homepage: https://github.com/tompesman/push-core
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>'
85
+ - !ruby/object:Gem::Version
86
+ version: 1.3.1
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.5
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Core of the modular push daemon.
93
+ test_files: []