rapns 3.0.0.beta.1 → 3.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/README.md +75 -106
  2. data/bin/rapns +14 -14
  3. data/config/database.yml +8 -0
  4. data/lib/generators/rapns_generator.rb +5 -1
  5. data/lib/generators/templates/add_gcm.rb +8 -0
  6. data/lib/generators/templates/rapns.rb +39 -0
  7. data/lib/rapns.rb +1 -0
  8. data/lib/rapns/apns/notification.rb +2 -0
  9. data/lib/rapns/apns/required_fields_validator.rb +14 -0
  10. data/lib/rapns/configuration.rb +24 -33
  11. data/lib/rapns/daemon.rb +18 -14
  12. data/lib/rapns/daemon/apns/app_runner.rb +1 -1
  13. data/lib/rapns/daemon/apns/delivery.rb +1 -1
  14. data/lib/rapns/daemon/apns/feedback_receiver.rb +1 -1
  15. data/lib/rapns/daemon/app_runner.rb +13 -7
  16. data/lib/rapns/daemon/delivery_handler.rb +0 -4
  17. data/lib/rapns/daemon/feeder.rb +1 -5
  18. data/lib/rapns/daemon/logger.rb +22 -9
  19. data/lib/rapns/version.rb +1 -1
  20. data/lib/tasks/cane.rake +2 -3
  21. data/lib/tasks/test.rake +1 -2
  22. data/spec/unit/apns/notification_spec.rb +12 -2
  23. data/spec/unit/configuration_spec.rb +38 -0
  24. data/spec/unit/daemon/apns/app_runner_spec.rb +2 -0
  25. data/spec/unit/daemon/apns/delivery_spec.rb +10 -4
  26. data/spec/unit/daemon/apns/disconnection_error_spec.rb +18 -0
  27. data/spec/unit/daemon/apns/feedback_receiver_spec.rb +7 -7
  28. data/spec/unit/daemon/app_runner_spec.rb +51 -0
  29. data/spec/unit/daemon/database_reconnectable_spec.rb +2 -0
  30. data/spec/unit/daemon/delivery_error_spec.rb +2 -2
  31. data/spec/unit/daemon/delivery_handler_shared.rb +10 -1
  32. data/spec/unit/daemon/gcm/app_runner_spec.rb +3 -1
  33. data/spec/unit/daemon/logger_spec.rb +10 -2
  34. data/spec/unit/daemon_spec.rb +31 -14
  35. data/spec/unit_spec_helper.rb +3 -7
  36. metadata +10 -4
data/README.md CHANGED
@@ -1,27 +1,24 @@
1
1
  [![Build Status](https://secure.travis-ci.org/ileitch/rapns.png?branch=master)](http://travis-ci.org/ileitch/rapns)
2
2
 
3
- # Features
3
+ ### Rapns - Professional grade APNs and GCM daemon
4
4
 
5
- * Works with Rails 3 and Ruby 1.9 & 1.8.
6
- * Supports multiple iOS apps.
7
- * [Add & remove apps](#hot-app-updates) without restarting or affecting the delivery of notifications to other apps.
8
- * Uses a daemon process to keep open persistent connections to the APNs, as recommended by Apple.
9
- * Uses the enhanced binary format so that [delivery errors can be reported](#delivery-failures).
10
- * Records feedback from [The Feedback Service](#delivery-failures).
11
- * [Airbrake](http://airbrakeapp.com/) (Hoptoad) integration.
12
- * Support for [dictionary `alert` properties](#assigning-a-hash-to-alert).
13
- * [Mobile Device Management (MDM)](#mobile-device-management)
14
- * Stable. Reconnects to the APNs and your database if connections are lost.
5
+ * Supports both APNs (iOS) and GCM (Google Cloud Messaging, Android).
6
+ * Seamless Rails integration.
7
+ * Scalable - choose the number of threads each app spawns.
8
+ * Designed for uptime - signal -HUP to add, update apps.
9
+ * Stable - reconnects database and network connections when lost.
10
+ * Works with MRI, JRuby, Rubinius 1.8 and 1.9.
11
+ * [Airbrake](http://airbrakeapp.com/) integration.
15
12
 
16
- ### Who uses rapns?
13
+ ### Who uses Rapns?
17
14
 
18
15
  [GateGuru](http://gateguruapp.com) and [Desk.com](http://desk.com), among others!
19
16
 
20
- *I'd love to hear if you use rapns - @ileitch on twitter.*
17
+ *I'd love to hear if you use Rapns - @ileitch on twitter.*
21
18
 
22
19
  ## Getting Started
23
20
 
24
- Add rapns to your Gemfile:
21
+ Add Rapns to your Gemfile:
25
22
 
26
23
  gem 'rapns'
27
24
 
@@ -44,107 +41,75 @@ Generate the migration, rapns.yml and migrate:
44
41
 
45
42
  ## Create an App
46
43
 
47
- app = Rapns::App.new
48
- app.key = "my_app"
49
- app.certificate = File.read("/path/to/development.pem")
50
- app.environment = "development"
51
- app.password = "certificate password"
52
- app.connections = 1
53
- app.save!
54
-
55
- * `key` is a symbolic name to tie this app to notifications.
56
- * `certificate` is the contents of your PEM certificate, NOT its path on disk.
57
- * `environment` the certificate type, either `development` or `production`.
58
- * `password` should be left blank if you did not password protect your certificate.
59
- * `connections` (default: 1) the number of connections to keep open to the APNs. Consider increasing this if you are sending a large number of notifications to this app.
60
-
61
- ## Starting the rapns Daemon
44
+ #### APNs
45
+ ```ruby
46
+ app = Rapns::Apns::App.new
47
+ app.name = "ios_app"
48
+ app.certificate = File.read("/path/to/development.pem")
49
+ app.environment = "development"
50
+ app.password = "certificate password"
51
+ app.connections = 1
52
+ app.save!
53
+ ```
54
+
55
+ #### GCM
56
+ ```ruby
57
+ app = Rapns::Gcm::App.new
58
+ app.name = "android_app"
59
+ app.auth_key = "..."
60
+ app.connections = 1
61
+ app.save!
62
+ ```
63
+
64
+ ## Create a Notification
65
+
66
+ #### APNs
67
+ ```ruby
68
+ n = Rapns::Apns::Notification.new
69
+ n.app = Rapns::Apns::App.find_by_name("ios_app")
70
+ n.device_token = "..."
71
+ n.alert = "hi mom!"
72
+ n.attributes_for_device = {:foo => :bar}
73
+ n.save!
74
+ ```
75
+
76
+ #### GCM
77
+ ```ruby
78
+ n = Rapns::Gcm::Notification.new
79
+ n.app = Rapns::Gcm::App.find_by_name("android_app")
80
+ n.registration_ids = ["..."]
81
+ n.data = {:message => "hi mom!"}
82
+ n.save!
83
+ ```
84
+
85
+ ## Starting Rapns
62
86
 
63
87
  cd /path/to/rails/app
64
- bundle exec rapns <Rails environment> [options]
65
-
66
- ### Options
67
-
68
- * `-f` `--foreground` Prevent rapns from forking into a daemon.
69
- * `-P N` `--db-poll N` Frequency in seconds to check for new notifications. Default: 2.
70
- * `-F N` `--feedback-poll N` Frequency in seconds to check for feedback. Default: 60.
71
- * `-e` `--no-error-checks` Disables [error checking](#immediately-when-processing-a-notification-for-delivery) after notification delivery. You may want to disable this if you are sending a very high number of notifications.
72
- * `-n` `--no-airbrake-notify` Disables error notifications via Airbrake.
73
- * `-p PATH` `--pid-file PATH` Path to write PID file. Relative to Rails root unless absolute.
74
- * `-b N` `--batch-size N` ActiveRecord batch size of notifications. Increase for possible higher throughput but higher memory footprint. Default: 5000.
75
-
76
- ## Sending a Notification
77
-
78
- n = Rapns::Notification.new
79
- n.app = "my_app"
80
- n.device_token = "934f7a..."
81
- n.alert = "This is the message shown on the device."
82
- n.badge = 1
83
- n.sound = "1.aiff"
84
- n.expiry = 1.day.to_i
85
- n.attributes_for_device = {"question" => nil, "answer" => 42}
86
- n.deliver_after = 1.hour.from_now
87
- n.save!
88
-
89
- * `app` must match `key` on an `Rapns::App`.
90
- * `sound` defaults to `1.aiff`. You can either set it to a custom .aiff file, or `nil` for no sound.
91
- * `expiry` is the time in seconds the APNs (not rapns) 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.
92
- * `attributes_for_device` is the `NSDictionary` argument passed to your iOS app in either `didFinishLaunchingWithOptions` or `didReceiveRemoteNotification`.
93
- * `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.
94
-
95
- ### Mobile Device Management
96
-
97
- n = Rapns::Notification.new
98
- n.mdm = "magic"
99
- n.save!
100
-
101
- ### Assigning a Hash to alert
102
-
103
- 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).
104
-
105
- ## Hot App Updates
106
-
107
- If you signal the rapns process with `HUP` it will synchronize with the current `Rapns::App` configurations. This includes adding an app, removing and increasing/decreasing the number of connections an app uses.
108
-
109
- This synchronization process does not pause the delivery of notifications to other apps.
88
+ rapns <Rails environment> [options]
110
89
 
111
- ## Logging
90
+ See [Configuration](wiki/Configuration) for a list of options, or run `rapns --help`.
112
91
 
113
- rapns logs activity to `rapns.log` in your Rails log directory. This is also printed to STDOUT when running in the foreground. When running as a daemon rapns does not print to STDOUT or STDERR.
114
-
115
- ## Delivery Failures
116
-
117
- The APNs provides two mechanism for delivery failure notification:
118
-
119
- ### Immediately, when processing a notification for delivery.
120
-
121
- 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):
122
-
123
- `failed` flag is set to true.
124
- `failed_at` is set to the time of failure.
125
- `error` is set to Apple's code for the error.
126
- `error_description` is set to a (somewhat brief) description of the error.
127
-
128
- rapns will not attempt to deliver the notification again.
129
-
130
- ### Via the Feedback Service.
131
-
132
- rapns checks for feedback periodically and stores results in the `Rapns::Feedback` model. Each record contains the device token and a timestamp of when the APNs determined that the app no longer exists on the device.
133
-
134
- It is your responsibility to avoid creating new notifications for devices that no longer have your app installed. rapns does not and will not check `Rapns::Feedback` before sending notifications.
135
-
136
- *Note: In my testing and from other reports on the Internet, it appears you may not receive feedback when using the APNs sandbox environment.*
137
-
138
- ## Updating rapns
92
+ ## Updating Rapns
139
93
 
140
94
  After updating you should run `rails g rapns` to check for any new migrations.
141
95
 
142
96
  ## Wiki
143
97
 
144
- * [Deploying to Heroku](https://github.com/ileitch/rapns/wiki/Heroku)
145
- * [Why open multiple connections to the APNs?](https://github.com/ileitch/rapns/wiki/Why-open-multiple-connections-to-the-APNs%3F)
98
+ ### General
99
+ * [Configuration](wiki/Configuration)
100
+ * [Upgrading from 2.x to 3.0](wiki/Upgrading-from-version-2.x-to-3.0)
101
+ * [Deploying to Heroku](wiki/Heroku)
102
+ * [Hot App Updates](wiki/Hot-App-Updates)
146
103
 
147
- ## Contributing to rapns
104
+ ### APNs
105
+ * [Advanced APNs Features](wiki/Advanced-APNs-Features)
106
+ * [APNs Delivery Failure Handling](wiki/APNs-Delivery-Failure-Handling)
107
+ * [Why open multiple connections to the APNs?](wiki/Why-open-multiple-connections-to-the-APNs%3F)
108
+ * [Silent failures might be dropped connections](wiki/Dropped-connections)
109
+
110
+ ### GCM
111
+
112
+ ## Contributing
148
113
 
149
114
  Fork as usual and go crazy!
150
115
 
@@ -152,9 +117,13 @@ When running specs, please note that the ActiveRecord adapter can be changed by
152
117
 
153
118
  Available adapters for testing are `mysql`, `mysql2` and `postgresql`.
154
119
 
120
+ Note that the database username is changed at runtime to be the currently logged in user's name. So if you're testing
121
+ with mysql and you're using a user named 'bob', you will need to grant a mysql user 'bob' access to the 'rapns_test'
122
+ mysql database.
123
+
155
124
  ### Contributors
156
125
 
157
- Thank you to the following wonderful people for contributing to rapns:
126
+ Thank you to the following wonderful people for contributing:
158
127
 
159
128
  * [@blakewatters](https://github.com/blakewatters)
160
129
  * [@forresty](https://github.com/forresty)
data/bin/rapns CHANGED
@@ -5,16 +5,23 @@ require 'rapns'
5
5
 
6
6
  environment = ARGV[0]
7
7
 
8
- config = Rapns.configuration
9
-
10
8
  banner = 'Usage: rapns <Rails environment> [options]'
9
+ if environment.nil? || environment =~ /^-/
10
+ puts banner
11
+ exit 1
12
+ end
13
+
14
+ # A mock configuration to be used before the Rails environment is loaded.
15
+ class CommandlineConfig < Struct.new(*Rapns::CONFIG_ATTRS); end
16
+ config = CommandlineConfig.new
17
+
11
18
  ARGV.options do |opts|
12
19
  opts.banner = banner
13
20
  opts.on('-f', '--foreground', 'Run in the foreground.') { config.foreground = true }
14
21
  opts.on('-P N', '--db-poll N', Integer, "Frequency in seconds to check for new notifications. Default: #{config.push_poll}.") { |n| config.push_poll = n }
15
22
  opts.on('-F N', '--feedback-poll N', Integer, "Frequency in seconds to check for feedback. Default: #{config.feedback_poll}.") { |n| config.feedback_poll = n }
16
- opts.on('-e', '--no-error-checks', 'Disable error checking after notification delivery.') { config.airbrake_notify = false }
17
- opts.on('-n', '--no-airbrake-notify', 'Disables error notifications via Airbrake.') { config.check_for_errors = false }
23
+ opts.on('-e', '--no-error-checks', 'Disable APNs error checking after notification delivery.') { config.check_for_errors = false }
24
+ opts.on('-n', '--no-airbrake-notify', 'Disables error notifications via Airbrake.') { config.airbrake_notify = false }
18
25
  opts.on('-p PATH', '--pid-file PATH', String, 'Path to write PID file. Relative to Rails root unless absolute.') { |path| config.pid_file = path }
19
26
  opts.on('-b N', '--batch-size N', Integer, 'ActiveRecord notifications batch size.') { |n| config.batch_size = n }
20
27
  opts.on('-v', '--version', 'Print this version of rapns.') { puts "rapns #{Rapns::VERSION}"; exit }
@@ -22,20 +29,13 @@ ARGV.options do |opts|
22
29
  opts.parse!
23
30
  end
24
31
 
25
- if environment.nil? || environment =~ /^-/
26
- puts banner
27
- exit 1
28
- end
29
-
30
32
  ENV['RAILS_ENV'] = environment
31
33
  load 'config/environment.rb'
32
34
  load 'config/initializers/rapns.rb' if File.exist?('config/initializers/rapns.rb')
33
35
 
36
+ Rapns.config.update(config)
37
+
34
38
  require 'rapns/daemon'
35
39
  require 'rapns/patches'
36
40
 
37
- if config.pid_file && !Pathname.new(config.pid_file).absolute?
38
- config.pid_file = File.join(Rails.root, config.pid_file)
39
- end
40
-
41
- Rapns::Daemon.start(config)
41
+ Rapns::Daemon.start
@@ -29,3 +29,11 @@ mysql2:
29
29
  username: rapns_test
30
30
  password: ""
31
31
  encoding: utf8
32
+
33
+ jdbcmysql:
34
+ adapter: jdbcmysql
35
+ database: rapns_test
36
+ host: localhost
37
+ username: rapns_test
38
+ password: ""
39
+ encoding: utf8
@@ -18,6 +18,10 @@ class RapnsGenerator < Rails::Generators::Base
18
18
  add_rapns_migration('add_gcm')
19
19
  end
20
20
 
21
+ def copy_config
22
+ copy_file 'rapns.rb', 'config/initializers/rapns.rb'
23
+ end
24
+
21
25
  protected
22
26
 
23
27
  def add_rapns_migration(template)
@@ -27,4 +31,4 @@ class RapnsGenerator < Rails::Generators::Base
27
31
  migration_template "#{template}.rb", "db/migrate/#{template}.rb"
28
32
  end
29
33
  end
30
- end
34
+ end
@@ -25,6 +25,7 @@ class AddGcm < ActiveRecord::Migration
25
25
  change_column_null :rapns_apps, :certificate, true
26
26
 
27
27
  change_column :rapns_notifications, :error_description, :text
28
+ change_column :rapns_notifications, :sound, :string, :default => 'default'
28
29
 
29
30
  rename_column :rapns_notifications, :attributes_for_device, :data
30
31
  rename_column :rapns_apps, :key, :name
@@ -46,6 +47,9 @@ class AddGcm < ActiveRecord::Migration
46
47
 
47
48
  change_column_null :rapns_notifications, :app_id, false
48
49
  remove_column :rapns_notifications, :app
50
+
51
+ remove_index :rapns_notifications, :name => "index_rapns_notifications_multi"
52
+ add_index :rapns_notifications, [:app_id, :delivered, :failed, :deliver_after], :name => "index_rapns_notifications_multi"
49
53
  end
50
54
 
51
55
  def self.down
@@ -60,6 +64,7 @@ class AddGcm < ActiveRecord::Migration
60
64
  change_column_null :rapns_apps, :certificate, false
61
65
 
62
66
  change_column :rapns_notifications, :error_description, :string
67
+ change_column :rapns_notifications, :sound, :string, :default => '1.aiff'
63
68
 
64
69
  rename_column :rapns_notifications, :data, :attributes_for_device
65
70
  rename_column :rapns_apps, :name, :key
@@ -82,5 +87,8 @@ class AddGcm < ActiveRecord::Migration
82
87
 
83
88
  change_column_null :rapns_notifications, :key, false
84
89
  remove_column :rapns_notifications, :app_id
90
+
91
+ remove_index :rapns_notifications, :name => "index_rapns_notifications_multi"
92
+ add_index :rapns_notifications, [:delivered, :failed, :deliver_after], :name => "index_rapns_notifications_multi"
85
93
  end
86
94
  end
@@ -0,0 +1,39 @@
1
+ # Rapns configuration. Options set here are overridden by command-line options.
2
+
3
+ Rapns.configure do |config|
4
+
5
+ # Run in the foreground?
6
+ # config.foreground = false
7
+
8
+ # Frequency in seconds to check for new notifications.
9
+ # config.push_poll = 2
10
+
11
+ # Frequency in seconds to check for feedback
12
+ # config.feedback_poll = 60
13
+
14
+ # Enable/Disable error notifications via Airbrake.
15
+ # config.airbrake_notify = true
16
+
17
+ # Disable APNs error checking after notification delivery.
18
+ # config.check_for_errors = true
19
+
20
+ # ActiveRecord notifications batch size.
21
+ # config.batch_size = 5000
22
+
23
+ # Path to write PID file. Relative to Rails root unless absolute.
24
+ # config.pid_file = '/path/to/rapns.pid'
25
+
26
+ # Define a block that will be called with a Rapns::Apns::Feedback instance
27
+ # when feedback is received from the APNs that a notification has
28
+ # failed to be delivered. Further notifications should not be sent to the device.
29
+ #
30
+ # Example:
31
+ # config.on_apns_feedback do |feedback|
32
+ # device = Device.find_by_device_token(feedback.device_token)
33
+ # if device
34
+ # device.active = false
35
+ # device.save!
36
+ # end
37
+ # end
38
+
39
+ end
@@ -9,6 +9,7 @@ require 'rapns/configuration'
9
9
 
10
10
  require 'rapns/apns/binary_notification_validator'
11
11
  require 'rapns/apns/device_token_format_validator'
12
+ require 'rapns/apns/required_fields_validator'
12
13
  require 'rapns/apns/notification'
13
14
  require 'rapns/apns/feedback'
14
15
  require 'rapns/apns/app'
@@ -8,6 +8,7 @@ module Rapns
8
8
 
9
9
  validates_with Rapns::Apns::DeviceTokenFormatValidator
10
10
  validates_with Rapns::Apns::BinaryNotificationValidator
11
+ validates_with Rapns::Apns::RequiredFieldsValidator
11
12
 
12
13
  alias_method :attributes_for_device=, :data=
13
14
  alias_method :attributes_for_device, :data
@@ -79,6 +80,7 @@ module Rapns
79
80
  id_for_pack = options[:for_validation] ? 0 : id
80
81
  [1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*")
81
82
  end
83
+
82
84
  end
83
85
  end
84
86
  end
@@ -0,0 +1,14 @@
1
+ module Rapns
2
+ module Apns
3
+ class RequiredFieldsValidator < ActiveModel::Validator
4
+
5
+ # Notifications must contain one of alert, badge or sound as per:
6
+ # https://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html
7
+ def validate(record)
8
+ if record.alert.nil? && record.badge.nil? && record.sound.nil?
9
+ record.errors[:base] << "APN Notification must contain one of alert, badge, or sound"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,28 +1,21 @@
1
1
  module Rapns
2
-
3
- # A globally accessible instance of Rapns::Configuration
4
- def self.configuration
5
- @configuration ||= Rapns::Configuration.new
2
+ def self.config
3
+ @config ||= Rapns::Configuration.new
6
4
  end
7
5
 
8
- # Call the given block yielding to it the global Rapns::Configuration instance for setting
9
- # configuration values / callbacks.
10
- #
11
- # Typically this would be used in your Rails application's config/initializers/rapns.rb file
12
6
  def self.configure
13
- yield configuration if block_given?
7
+ yield config if block_given?
14
8
  end
15
9
 
16
- # A class to hold Rapns configuration settings and callbacks.
17
- class Configuration < Struct.new(:foreground, :push_poll, :feedback_poll, :airbrake_notify, :check_for_errors, :pid_file, :batch_size)
10
+ CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll,
11
+ :airbrake_notify, :check_for_errors, :pid_file, :batch_size]
18
12
 
19
- attr_accessor :feedback_callback
13
+ class Configuration < Struct.new(*CONFIG_ATTRS)
14
+ attr_accessor :apns_feedback_callback
20
15
 
21
- # Initialize the Config with default values
22
16
  def initialize
23
17
  super
24
18
 
25
- # defaults:
26
19
  self.foreground = false
27
20
  self.push_poll = 2
28
21
  self.feedback_poll = 60
@@ -31,25 +24,23 @@ module Rapns
31
24
  self.batch_size = 5000
32
25
  end
33
26
 
34
- # Define a block that will be executed with a Rapns::Feedback instance when feedback has been received from the
35
- # push notification servers that a notification has failed to be delivered. Further notifications should not
36
- # be sent to this device token.
37
- #
38
- # Example usage (in config/initializers/rapns.rb):
39
- #
40
- # Rapns.configure do |config|
41
- # config.on_feedback do |feedback|
42
- # device = Device.find_by_device_token feedback.device_token
43
- # if device
44
- # device.active = false
45
- # device.save
46
- # end
47
- # end
48
- # end
49
- #
50
- # Where `Device` is a model specific to your Rails app that has a `device_token` field.
51
- def on_feedback(&block)
52
- self.feedback_callback = block
27
+ def update(other)
28
+ CONFIG_ATTRS.each do |attr|
29
+ other_value = other.send(attr)
30
+ send("#{attr}=", other_value) unless other_value.nil?
31
+ end
32
+ end
33
+
34
+ def on_apns_feedback(&block)
35
+ self.apns_feedback_callback = block
36
+ end
37
+
38
+ def pid_file=(path)
39
+ if path && !Pathname.new(path).absolute?
40
+ super(File.join(Rails.root, path))
41
+ else
42
+ super
43
+ end
53
44
  end
54
45
  end
55
46
  end