rapns 1.0.7 → 2.0.0rc1
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/CHANGELOG.md +7 -0
- data/LICENSE +7 -0
- data/README.md +58 -41
- data/bin/rapns +23 -5
- data/lib/generators/rapns_generator.rb +2 -4
- data/lib/generators/templates/add_app_to_rapns.rb +11 -0
- data/lib/generators/templates/create_rapns_apps.rb +15 -0
- data/lib/rapns/app.rb +9 -0
- data/lib/rapns/daemon/app_runner.rb +131 -0
- data/lib/rapns/daemon/connection.rb +5 -3
- data/lib/rapns/daemon/delivery_handler.rb +13 -15
- data/lib/rapns/daemon/delivery_handler_pool.rb +8 -10
- data/lib/rapns/daemon/delivery_queue.rb +36 -4
- data/lib/rapns/daemon/feedback_receiver.rb +19 -12
- data/lib/rapns/daemon/feeder.rb +8 -10
- data/lib/rapns/daemon/logger.rb +5 -3
- data/lib/rapns/daemon.rb +52 -38
- data/lib/rapns/notification.rb +16 -5
- data/lib/rapns/patches.rb +2 -2
- data/lib/rapns/version.rb +1 -1
- data/lib/rapns.rb +2 -1
- data/spec/rapns/daemon/app_runner_spec.rb +207 -0
- data/spec/rapns/daemon/connection_spec.rb +177 -236
- data/spec/rapns/daemon/delivery_handler_pool_spec.rb +10 -14
- data/spec/rapns/daemon/delivery_handler_spec.rb +92 -79
- data/spec/rapns/daemon/feedback_receiver_spec.rb +29 -23
- data/spec/rapns/daemon/feeder_spec.rb +40 -44
- data/spec/rapns/daemon/logger_spec.rb +21 -3
- data/spec/rapns/daemon_spec.rb +65 -125
- data/spec/rapns/notification_spec.rb +16 -0
- data/spec/spec_helper.rb +4 -1
- metadata +14 -15
- data/History.md +0 -5
- data/lib/generators/templates/rapns.yml +0 -31
- data/lib/rapns/daemon/certificate.rb +0 -27
- data/lib/rapns/daemon/configuration.rb +0 -98
- data/lib/rapns/daemon/pool.rb +0 -36
- data/spec/rapns/daemon/certificate_spec.rb +0 -22
- data/spec/rapns/daemon/configuration_spec.rb +0 -231
data/lib/rapns/daemon/feeder.rb
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
module Rapns
|
2
2
|
module Daemon
|
3
3
|
class Feeder
|
4
|
-
extend DatabaseReconnectable
|
5
4
|
extend InterruptibleSleep
|
5
|
+
extend DatabaseReconnectable
|
6
6
|
|
7
7
|
def self.name
|
8
|
-
|
8
|
+
'Feeder'
|
9
9
|
end
|
10
10
|
|
11
|
-
def self.start(
|
12
|
-
reconnect_database unless foreground
|
13
|
-
|
11
|
+
def self.start(poll)
|
14
12
|
loop do
|
15
13
|
break if @stop
|
16
14
|
enqueue_notifications
|
17
|
-
interruptible_sleep
|
15
|
+
interruptible_sleep poll
|
18
16
|
end
|
19
17
|
end
|
20
18
|
|
@@ -28,10 +26,10 @@ module Rapns
|
|
28
26
|
def self.enqueue_notifications
|
29
27
|
begin
|
30
28
|
with_database_reconnect_and_retry do
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
ready_apps = Rapns::Daemon::AppRunner.ready
|
30
|
+
batch_size = Rapns::Daemon.config.batch_size
|
31
|
+
Rapns::Notification.ready_for_delivery.find_each(:batch_size => batch_size) do |notification|
|
32
|
+
Rapns::Daemon::AppRunner.deliver(notification) if ready_apps.include?(notification.app)
|
35
33
|
end
|
36
34
|
end
|
37
35
|
rescue StandardError => e
|
data/lib/rapns/daemon/logger.rb
CHANGED
@@ -3,8 +3,9 @@ module Rapns
|
|
3
3
|
class Logger
|
4
4
|
def initialize(options)
|
5
5
|
@options = options
|
6
|
-
|
7
|
-
|
6
|
+
log = File.open(File.join(Rails.root, 'log', 'rapns.log'), 'w')
|
7
|
+
log.sync = true
|
8
|
+
@logger = ActiveSupport::BufferedLogger.new(log, Rails.logger.level)
|
8
9
|
@logger.auto_flushing = Rails.logger.respond_to?(:auto_flushing) ? Rails.logger.auto_flushing : true
|
9
10
|
end
|
10
11
|
|
@@ -25,7 +26,8 @@ module Rapns
|
|
25
26
|
|
26
27
|
def log(where, msg, prefix = nil)
|
27
28
|
if msg.is_a?(Exception)
|
28
|
-
|
29
|
+
formatted_backtrace = msg.backtrace.join("\n")
|
30
|
+
msg = "#{msg.class.name}, #{msg.message}\n#{formatted_backtrace}"
|
29
31
|
end
|
30
32
|
|
31
33
|
formatted_msg = "[#{Time.now.to_s(:db)}] "
|
data/lib/rapns/daemon.rb
CHANGED
@@ -3,64 +3,81 @@ require 'socket'
|
|
3
3
|
require 'pathname'
|
4
4
|
|
5
5
|
require 'rapns/daemon/interruptible_sleep'
|
6
|
-
require 'rapns/daemon/configuration'
|
7
|
-
require 'rapns/daemon/certificate'
|
8
6
|
require 'rapns/daemon/delivery_error'
|
9
7
|
require 'rapns/daemon/disconnection_error'
|
10
|
-
require 'rapns/daemon/pool'
|
11
8
|
require 'rapns/daemon/connection'
|
12
9
|
require 'rapns/daemon/database_reconnectable'
|
13
10
|
require 'rapns/daemon/delivery_queue'
|
14
11
|
require 'rapns/daemon/delivery_handler'
|
15
12
|
require 'rapns/daemon/delivery_handler_pool'
|
16
13
|
require 'rapns/daemon/feedback_receiver'
|
14
|
+
require 'rapns/daemon/app_runner'
|
17
15
|
require 'rapns/daemon/feeder'
|
18
16
|
require 'rapns/daemon/logger'
|
19
17
|
|
20
18
|
module Rapns
|
21
19
|
module Daemon
|
20
|
+
extend DatabaseReconnectable
|
21
|
+
|
22
22
|
class << self
|
23
|
-
attr_accessor :logger, :
|
24
|
-
:delivery_queue, :delivery_handler_pool, :foreground
|
25
|
-
alias_method :foreground?, :foreground
|
23
|
+
attr_accessor :logger, :config
|
26
24
|
end
|
27
25
|
|
28
|
-
def self.start(environment,
|
29
|
-
|
26
|
+
def self.start(environment, config)
|
27
|
+
self.config = config
|
28
|
+
self.logger = Logger.new(:foreground => config.foreground, :airbrake_notify => config.airbrake_notify)
|
30
29
|
setup_signal_hooks
|
31
30
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
self.certificate = Certificate.new(configuration.certificate)
|
38
|
-
certificate.load
|
39
|
-
|
40
|
-
self.delivery_queue = DeliveryQueue.new
|
41
|
-
|
42
|
-
daemonize unless foreground?
|
31
|
+
unless config.foreground
|
32
|
+
daemonize
|
33
|
+
reconnect_database
|
34
|
+
end
|
43
35
|
|
44
36
|
write_pid_file
|
37
|
+
ensure_upgraded
|
38
|
+
AppRunner.sync
|
39
|
+
Feeder.start(config.push_poll)
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
45
43
|
|
46
|
-
|
47
|
-
|
44
|
+
def self.ensure_upgraded
|
45
|
+
count = 0
|
46
|
+
|
47
|
+
begin
|
48
|
+
count = Rapns::App.count
|
49
|
+
rescue ActiveRecord::StatementInvalid
|
50
|
+
puts "!!!! RAPNS NOT STARTED !!!!"
|
51
|
+
puts
|
52
|
+
puts "As of version v2.0.0 apps are configured in the database instead of rapns.yml."
|
53
|
+
puts "Please run 'rails g rapns' to generate the new migrations and create your apps with Rapns::App."
|
54
|
+
puts "See https://github.com/ileitch/rapns for further instructions."
|
55
|
+
puts
|
56
|
+
exit 1
|
57
|
+
end
|
48
58
|
|
49
|
-
|
59
|
+
if count == 0
|
60
|
+
puts "!!!! RAPNS NOT STARTED !!!!"
|
61
|
+
puts
|
62
|
+
puts "You must create an Rapns::App."
|
63
|
+
puts "See https://github.com/ileitch/rapns for instructions."
|
64
|
+
puts
|
65
|
+
exit 1
|
66
|
+
end
|
50
67
|
|
51
|
-
|
52
|
-
|
68
|
+
if File.exists?(File.join(Rails.root, 'config', 'rapns', 'rapns.yml'))
|
69
|
+
logger.warn("Since 2.0.0 rapns uses command-line options instead of a configuration file. Please remove config/rapns/rapns.yml.")
|
70
|
+
end
|
53
71
|
end
|
54
72
|
|
55
|
-
protected
|
56
|
-
|
57
73
|
def self.setup_signal_hooks
|
58
74
|
@shutting_down = false
|
59
75
|
|
76
|
+
Signal.trap('SIGHUP') { AppRunner.sync }
|
77
|
+
Signal.trap('SIGUSR1') { AppRunner.debug }
|
78
|
+
|
60
79
|
['SIGINT', 'SIGTERM'].each do |signal|
|
61
|
-
Signal.trap(signal)
|
62
|
-
handle_shutdown_signal
|
63
|
-
end
|
80
|
+
Signal.trap(signal) { handle_shutdown_signal }
|
64
81
|
end
|
65
82
|
end
|
66
83
|
|
@@ -72,9 +89,8 @@ module Rapns
|
|
72
89
|
|
73
90
|
def self.shutdown
|
74
91
|
puts "\nShutting down..."
|
75
|
-
|
76
|
-
|
77
|
-
Rapns::Daemon.delivery_handler_pool.drain if Rapns::Daemon.delivery_handler_pool
|
92
|
+
Feeder.stop
|
93
|
+
AppRunner.stop
|
78
94
|
delete_pid_file
|
79
95
|
end
|
80
96
|
|
@@ -92,19 +108,17 @@ module Rapns
|
|
92
108
|
end
|
93
109
|
|
94
110
|
def self.write_pid_file
|
95
|
-
if !
|
111
|
+
if !config.pid_file.blank?
|
96
112
|
begin
|
97
|
-
File.open(
|
98
|
-
f.puts $$
|
99
|
-
end
|
113
|
+
File.open(config.pid_file, 'w') { |f| f.puts Process.pid }
|
100
114
|
rescue SystemCallError => e
|
101
|
-
logger.error("Failed to write PID to '#{
|
115
|
+
logger.error("Failed to write PID to '#{config.pid_file}': #{e.inspect}")
|
102
116
|
end
|
103
117
|
end
|
104
118
|
end
|
105
119
|
|
106
120
|
def self.delete_pid_file
|
107
|
-
pid_file =
|
121
|
+
pid_file = config.pid_file
|
108
122
|
File.delete(pid_file) if !pid_file.blank? && File.exists?(pid_file)
|
109
123
|
end
|
110
124
|
end
|
data/lib/rapns/notification.rb
CHANGED
@@ -48,13 +48,24 @@ module Rapns
|
|
48
48
|
ActiveSupport::JSON.decode(read_attribute(:attributes_for_device)) if read_attribute(:attributes_for_device)
|
49
49
|
end
|
50
50
|
|
51
|
+
MDM_OVERIDE_KEY = '__rapns_mdm__'
|
52
|
+
def mdm=(magic)
|
53
|
+
self.attributes_for_device = {MDM_OVERIDE_KEY => magic}
|
54
|
+
end
|
55
|
+
|
51
56
|
def as_json
|
52
57
|
json = ActiveSupport::OrderedHash.new
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
|
59
|
+
if attributes_for_device && attributes_for_device.key?(MDM_OVERIDE_KEY)
|
60
|
+
json['mdm'] = attributes_for_device[MDM_OVERIDE_KEY]
|
61
|
+
else
|
62
|
+
json['aps'] = ActiveSupport::OrderedHash.new
|
63
|
+
json['aps']['alert'] = alert if alert
|
64
|
+
json['aps']['badge'] = badge if badge
|
65
|
+
json['aps']['sound'] = sound if sound
|
66
|
+
attributes_for_device.each { |k, v| json[k.to_s] = v.to_s } if attributes_for_device
|
67
|
+
end
|
68
|
+
|
58
69
|
json
|
59
70
|
end
|
60
71
|
|
data/lib/rapns/patches.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
if
|
2
|
-
if
|
1
|
+
if Rails::VERSION::STRING == '3.1.0' || Rails::VERSION::STRING == '3.1.1'
|
2
|
+
if ActiveRecord::Base.configurations[Rails.env]['adapter'] == 'postgresql'
|
3
3
|
STDERR.puts "[WARNING] Detected Rails #{Rails::VERSION::STRING}, patching PostgreSQLAdapter to fix reconnection bug: https://github.com/rails/rails/issues/3160"
|
4
4
|
require "rapns/patches/rails/#{Rails::VERSION::STRING}/postgresql_adapter.rb"
|
5
5
|
end
|
data/lib/rapns/version.rb
CHANGED
data/lib/rapns.rb
CHANGED
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rapns::Daemon::AppRunner do
|
4
|
+
let(:app) { stub(:key => 'app', :certificate => 'cert', :password => '', :connections => 1) }
|
5
|
+
let(:queue) { stub(:notifications_processed? => true, :push => nil) }
|
6
|
+
let(:receiver) { stub(:start => nil, :stop => nil) }
|
7
|
+
let(:handler) { stub(:start => nil, :stop => nil) }
|
8
|
+
let(:push_config) { stub(:host => 'gateway.push.apple.com', :port => 2195) }
|
9
|
+
let(:feedback_config) { stub(:host => 'feedback.push.apple.com', :port => 2196, :poll => 60) }
|
10
|
+
let(:runner) { Rapns::Daemon::AppRunner.new(app, push_config.host, push_config.port,
|
11
|
+
feedback_config.host, feedback_config.port, feedback_config.poll) }
|
12
|
+
|
13
|
+
before do
|
14
|
+
Rapns::Daemon::DeliveryQueue.stub(:new => queue)
|
15
|
+
Rapns::Daemon::FeedbackReceiver.stub(:new => receiver)
|
16
|
+
Rapns::Daemon::DeliveryHandler.stub(:new => handler)
|
17
|
+
end
|
18
|
+
|
19
|
+
after { Rapns::Daemon::AppRunner.all.clear }
|
20
|
+
|
21
|
+
describe 'start' do
|
22
|
+
it 'starts a feedback receiver' do
|
23
|
+
Rapns::Daemon::FeedbackReceiver.should_receive(:new).with(app.key, feedback_config.host, feedback_config.port, feedback_config.poll, app.certificate, app.password)
|
24
|
+
receiver.should_receive(:start)
|
25
|
+
runner.start
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'starts a delivery handler for each connection' do
|
29
|
+
Rapns::Daemon::DeliveryHandler.should_receive(:new).with(queue, app.key, push_config.host,
|
30
|
+
push_config.port, app.certificate, app.password)
|
31
|
+
handler.should_receive(:start)
|
32
|
+
runner.start
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'deliver' do
|
37
|
+
let(:notification) { stub }
|
38
|
+
|
39
|
+
it 'enqueues the notification' do
|
40
|
+
queue.should_receive(:push).with(notification)
|
41
|
+
runner.deliver(notification)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'stop' do
|
46
|
+
before { runner.start }
|
47
|
+
|
48
|
+
it 'stops the delivery handlers' do
|
49
|
+
handler.should_receive(:stop)
|
50
|
+
runner.stop
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'stops the feedback receiver' do
|
54
|
+
receiver.should_receive(:stop)
|
55
|
+
runner.stop
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'ready?' do
|
60
|
+
it 'is ready if all notifications have been processed' do
|
61
|
+
queue.stub(:notifications_processed? => true)
|
62
|
+
runner.ready?.should be_true
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'is not ready if not all notifications have been processed' do
|
66
|
+
queue.stub(:notifications_processed? => false)
|
67
|
+
runner.ready?.should be_false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'sync' do
|
72
|
+
let(:new_app) { stub(:key => 'app', :certificate => 'cert', :password => '', :connections => 1) }
|
73
|
+
before { runner.start }
|
74
|
+
|
75
|
+
it 'reduces the number of handlers if needed' do
|
76
|
+
handler.should_receive(:stop)
|
77
|
+
new_app.stub(:connections => app.connections - 1)
|
78
|
+
runner.sync(new_app)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'increases the number of handlers if needed' do
|
82
|
+
new_handler = stub
|
83
|
+
Rapns::Daemon::DeliveryHandler.should_receive(:new).and_return(new_handler)
|
84
|
+
new_handler.should_receive(:start)
|
85
|
+
new_app.stub(:connections => app.connections + 1)
|
86
|
+
runner.sync(new_app)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe Rapns::Daemon::AppRunner, 'stop' do
|
92
|
+
let(:runner) { stub }
|
93
|
+
before { Rapns::Daemon::AppRunner.all['app'] = runner }
|
94
|
+
after { Rapns::Daemon::AppRunner.all.clear }
|
95
|
+
|
96
|
+
it 'stops all runners' do
|
97
|
+
runner.should_receive(:stop)
|
98
|
+
Rapns::Daemon::AppRunner.stop
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe Rapns::Daemon::AppRunner, 'deliver' do
|
103
|
+
let(:runner) { stub }
|
104
|
+
let(:notification) { stub(:app => 'app') }
|
105
|
+
let(:logger) { stub(:error => nil) }
|
106
|
+
|
107
|
+
before do
|
108
|
+
Rapns::Daemon.stub(:logger => logger)
|
109
|
+
Rapns::Daemon::AppRunner.all['app'] = runner
|
110
|
+
end
|
111
|
+
|
112
|
+
after { Rapns::Daemon::AppRunner.all.clear }
|
113
|
+
|
114
|
+
it 'delivers the notification' do
|
115
|
+
runner.should_receive(:deliver).with(notification)
|
116
|
+
Rapns::Daemon::AppRunner.deliver(notification)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'logs an error if there is no runner to deliver the notification' do
|
120
|
+
notification.stub(:app => 'unknonw', :id => 123)
|
121
|
+
logger.should_receive(:error).with("No such app '#{notification.app}' for notification #{notification.id}.")
|
122
|
+
Rapns::Daemon::AppRunner.deliver(notification)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe Rapns::Daemon::AppRunner, 'ready' do
|
127
|
+
let(:runner1) { stub(:ready? => true) }
|
128
|
+
let(:runner2) { stub(:ready? => false) }
|
129
|
+
|
130
|
+
before do
|
131
|
+
Rapns::Daemon::AppRunner.all['app1'] = runner1
|
132
|
+
Rapns::Daemon::AppRunner.all['app2'] = runner2
|
133
|
+
end
|
134
|
+
|
135
|
+
after { Rapns::Daemon::AppRunner.all.clear }
|
136
|
+
|
137
|
+
it 'returns apps that are ready for more notifications' do
|
138
|
+
Rapns::Daemon::AppRunner.ready.should == ['app1']
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe Rapns::Daemon::AppRunner, 'sync' do
|
143
|
+
let(:app) { stub(:key => 'app') }
|
144
|
+
let(:new_app) { stub(:key => 'new_app') }
|
145
|
+
let(:runner) { stub(:sync => nil, :stop => nil) }
|
146
|
+
let(:new_runner) { stub }
|
147
|
+
let(:logger) { stub(:error => nil) }
|
148
|
+
let(:config) { stub(:feedback_poll => 60) }
|
149
|
+
|
150
|
+
before do
|
151
|
+
Rapns::Daemon.stub(:config => config, :logger => logger)
|
152
|
+
Rapns::Daemon::AppRunner.all['app'] = runner
|
153
|
+
Rapns::App.stub(:all => [app])
|
154
|
+
end
|
155
|
+
|
156
|
+
after { Rapns::Daemon::AppRunner.all.clear }
|
157
|
+
|
158
|
+
it 'loads all apps' do
|
159
|
+
Rapns::App.should_receive(:all)
|
160
|
+
Rapns::Daemon::AppRunner.sync
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'instructs existing runners to sync' do
|
164
|
+
runner.should_receive(:sync).with(app)
|
165
|
+
Rapns::Daemon::AppRunner.sync
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'starts a runner for a new app with a production certificate' do
|
169
|
+
new_app.stub(:certificate => 'Apple Production IOS Push Services')
|
170
|
+
Rapns::App.stub(:all => [new_app])
|
171
|
+
new_runner = stub
|
172
|
+
Rapns::Daemon::AppRunner.should_receive(:new).with(new_app, 'gateway.push.apple.com', 2195,
|
173
|
+
'feedback.push.apple.com', 2196, config.feedback_poll).and_return(new_runner)
|
174
|
+
new_runner.should_receive(:start)
|
175
|
+
Rapns::Daemon::AppRunner.sync
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'starts a runner for a new app with a development certificate' do
|
179
|
+
new_app.stub(:certificate => 'Apple Development IOS Push Services')
|
180
|
+
Rapns::App.stub(:all => [new_app])
|
181
|
+
new_runner = stub
|
182
|
+
Rapns::Daemon::AppRunner.should_receive(:new).with(new_app, 'gateway.sandbox.push.apple.com', 2195,
|
183
|
+
'feedback.sandbox.push.apple.com', 2196, config.feedback_poll).and_return(new_runner)
|
184
|
+
new_runner.should_receive(:start)
|
185
|
+
Rapns::Daemon::AppRunner.sync
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'logs an error if the environment cannot be determined from the certificate' do
|
189
|
+
new_app.stub(:certificate => 'wat')
|
190
|
+
Rapns::App.stub(:all => [new_app])
|
191
|
+
Rapns::Daemon.logger.should_receive(:error).with("Could not detect environment for app 'new_app'.")
|
192
|
+
Rapns::Daemon::AppRunner.sync
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'does not attempt to start an AppRunner if the environment could not be detected' do
|
196
|
+
new_app.stub(:certificate => 'wat')
|
197
|
+
Rapns::App.stub(:all => [new_app])
|
198
|
+
Rapns::Daemon::AppRunner.should_not_receive(:new)
|
199
|
+
Rapns::Daemon::AppRunner.sync
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'deletes old apps' do
|
203
|
+
Rapns::App.stub(:all => [])
|
204
|
+
runner.should_receive(:stop)
|
205
|
+
Rapns::Daemon::AppRunner.sync
|
206
|
+
end
|
207
|
+
end
|