rapns 3.0.1-java → 3.1.0-java
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 +10 -2
- data/README.md +26 -3
- data/bin/rapns +2 -7
- data/lib/generators/templates/add_gcm.rb +3 -1
- data/lib/generators/templates/rapns.rb +42 -11
- data/lib/rapns.rb +11 -1
- data/lib/rapns/apns/notification.rb +8 -2
- data/lib/rapns/app.rb +3 -3
- data/lib/rapns/configuration.rb +46 -13
- data/lib/rapns/daemon.rb +33 -22
- data/lib/rapns/daemon/apns/connection.rb +12 -9
- data/lib/rapns/daemon/apns/delivery_handler.rb +1 -1
- data/lib/rapns/daemon/apns/feedback_receiver.rb +6 -2
- data/lib/rapns/daemon/app_runner.rb +23 -7
- data/lib/rapns/daemon/delivery.rb +5 -1
- data/lib/rapns/daemon/delivery_handler.rb +4 -0
- data/lib/rapns/daemon/feeder.rb +26 -5
- data/lib/rapns/daemon/reflectable.rb +13 -0
- data/lib/rapns/embed.rb +28 -0
- data/lib/rapns/gcm/notification.rb +7 -2
- data/lib/rapns/gcm/payload_data_size_validator.rb +13 -0
- data/lib/rapns/gcm/registration_ids_count_validator.rb +13 -0
- data/lib/rapns/push.rb +12 -0
- data/lib/rapns/reflection.rb +44 -0
- data/lib/rapns/version.rb +1 -1
- data/spec/support/cert_with_password.pem +90 -0
- data/spec/support/cert_without_password.pem +59 -0
- data/spec/unit/apns/app_spec.rb +15 -1
- data/spec/unit/apns/notification_spec.rb +16 -1
- data/spec/unit/configuration_spec.rb +10 -1
- data/spec/unit/daemon/apns/connection_spec.rb +11 -2
- data/spec/unit/daemon/apns/delivery_handler_spec.rb +1 -1
- data/spec/unit/daemon/apns/delivery_spec.rb +10 -0
- data/spec/unit/daemon/apns/feedback_receiver_spec.rb +16 -7
- data/spec/unit/daemon/delivery_handler_shared.rb +8 -0
- data/spec/unit/daemon/feeder_spec.rb +37 -6
- data/spec/unit/daemon/gcm/delivery_spec.rb +33 -1
- data/spec/unit/daemon/reflectable_spec.rb +27 -0
- data/spec/unit/daemon_spec.rb +55 -9
- data/spec/unit/embed_spec.rb +44 -0
- data/spec/unit/gcm/notification_spec.rb +9 -3
- data/spec/unit/push_spec.rb +28 -0
- data/spec/unit/reflection_spec.rb +34 -0
- data/spec/unit_spec_helper.rb +4 -62
- metadata +22 -5
- data/lib/rapns/gcm/payload_size_validator.rb +0 -13
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
-
## 3.0
|
1
|
+
## 3.1.0 (Jan 26, 2013)
|
2
|
+
* Rapns.reflect API for fine-grained introspection.
|
3
|
+
* Rapns.embed API for embedding Rapns into an existing process.
|
4
|
+
* Rapns.push API for using Rapns in scheduled jobs.
|
5
|
+
* Fix issue with integration with ActiveScaffold (#98) (@jeffarena).
|
6
|
+
* Fix content-available setter for APNs (#95) (@dup2).
|
7
|
+
* GCM validation fixes (#96) (@DianthuDia).
|
8
|
+
|
9
|
+
## 3.0.1 (Dec 16, 2012)
|
2
10
|
* Fix compatibility with Rails 3.0.x. Fixes #89.
|
3
11
|
|
4
|
-
## 3.0.0 (
|
12
|
+
## 3.0.0 (Dec 15, 2012)
|
5
13
|
* Add support for Google Cloud Messaging.
|
6
14
|
* Fix Heroku logging issue.
|
7
15
|
|
data/README.md
CHANGED
@@ -1,14 +1,18 @@
|
|
1
1
|
[](http://travis-ci.org/ileitch/rapns)
|
2
2
|
|
3
|
-
### Rapns - Professional grade APNs and GCM
|
3
|
+
### Rapns - Professional grade APNs and GCM for Ruby.
|
4
4
|
|
5
5
|
* Supports both APNs (iOS) and GCM (Google Cloud Messaging, Android).
|
6
6
|
* Seamless Rails integration.
|
7
7
|
* Scalable - choose the number of threads each app spawns.
|
8
8
|
* Designed for uptime - signal -HUP to add, update apps.
|
9
9
|
* Stable - reconnects database and network connections when lost.
|
10
|
+
* Run as a daemon or inside an existing process.
|
11
|
+
* Use in a scheduler for low-workload deployments ([Push API](rapns/wiki/Push-API)).
|
12
|
+
* Reflection API for fine-grained instrumentation ([Reflection API](rapns/wiki/Relfection-API)).
|
10
13
|
* Works with MRI, JRuby, Rubinius 1.8 and 1.9.
|
11
14
|
* [Airbrake](http://airbrakeapp.com/) integration.
|
15
|
+
* Built with a love for Open Source :)
|
12
16
|
|
13
17
|
#### 2.x users please read [upgrading from 2.x to 3.0](rapns/wiki/Upgrading-from-version-2.x-to-3.0)
|
14
18
|
|
@@ -36,11 +40,16 @@ Generate the migrations, rapns.yml and migrate:
|
|
36
40
|
3. Select both the certificate and private key.
|
37
41
|
4. Right click and select `Export 2 items...`.
|
38
42
|
5. Save the file as `cert.p12`, make sure the File Format is `Personal Information Exchange (p12)`.
|
39
|
-
6.
|
40
|
-
|
43
|
+
6. Convert the certificate to a .pem, where `<environment>` should be `development` or `production`, depending on the certificate you exported.
|
44
|
+
|
45
|
+
Without a password:
|
41
46
|
|
42
47
|
`openssl pkcs12 -nodes -clcerts -in cert.p12 -out <environment>.pem`
|
43
48
|
|
49
|
+
With a password:
|
50
|
+
|
51
|
+
`openssl pkcs12 -clcerts -in cert.p12 -out <environment>.pem`
|
52
|
+
|
44
53
|
## Create an App
|
45
54
|
|
46
55
|
#### APNs
|
@@ -86,9 +95,17 @@ n.save!
|
|
86
95
|
|
87
96
|
## Starting Rapns
|
88
97
|
|
98
|
+
As a daemon:
|
99
|
+
|
89
100
|
cd /path/to/rails/app
|
90
101
|
rapns <Rails environment> [options]
|
91
102
|
|
103
|
+
Inside an existing process:
|
104
|
+
|
105
|
+
Rapns.embed
|
106
|
+
|
107
|
+
*Please note that only ever a single instance of Rapns should be running.*
|
108
|
+
|
92
109
|
See [Configuration](rapns/wiki/Configuration) for a list of options, or run `rapns --help`.
|
93
110
|
|
94
111
|
## Updating Rapns
|
@@ -102,6 +119,9 @@ After updating you should run `rails g rapns` to check for any new migrations.
|
|
102
119
|
* [Upgrading from 2.x to 3.0](rapns/wiki/Upgrading-from-version-2.x-to-3.0)
|
103
120
|
* [Deploying to Heroku](rapns/wiki/Heroku)
|
104
121
|
* [Hot App Updates](rapns/wiki/Hot-App-Updates)
|
122
|
+
* [Reflection API](rapns/wiki/Reflection-API)
|
123
|
+
* [Push API](rapns/wiki/Push-API)
|
124
|
+
* [Embedding API](rapns/wiki/Embedding-API)
|
105
125
|
|
106
126
|
### APNs
|
107
127
|
* [Advanced APNs Features](rapns/wiki/Advanced-APNs-Features)
|
@@ -138,3 +158,6 @@ Thank you to the following wonderful people for contributing:
|
|
138
158
|
* [@adorr](https://github.com/adorr)
|
139
159
|
* [@mattconnolly](https://github.com/mattconnolly)
|
140
160
|
* [@emeitch](https://github.com/emeitch)
|
161
|
+
* [@jeffarena](https://github.com/jeffarena)
|
162
|
+
* [@DianthuDia](https://github.com/DianthuDia)
|
163
|
+
* [@dup2](https://github.com/dup2)
|
data/bin/rapns
CHANGED
@@ -11,9 +11,7 @@ if environment.nil? || environment =~ /^-/
|
|
11
11
|
exit 1
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
class CommandlineConfig < Struct.new(*Rapns::CONFIG_ATTRS); end
|
16
|
-
config = CommandlineConfig.new
|
14
|
+
config = Rapns::ConfigurationWithoutDefaults.new
|
17
15
|
|
18
16
|
ARGV.options do |opts|
|
19
17
|
opts.banner = banner
|
@@ -34,8 +32,5 @@ load 'config/environment.rb'
|
|
34
32
|
load 'config/initializers/rapns.rb' if File.exist?('config/initializers/rapns.rb')
|
35
33
|
|
36
34
|
Rapns.config.update(config)
|
37
|
-
|
38
|
-
require 'rapns/daemon'
|
39
|
-
require 'rapns/patches'
|
40
|
-
|
35
|
+
Rapns.require_for_daemon
|
41
36
|
Rapns::Daemon.start
|
@@ -34,7 +34,9 @@ class AddGcm < ActiveRecord::Migration
|
|
34
34
|
|
35
35
|
add_column :rapns_notifications, :collapse_key, :string, :null => true
|
36
36
|
add_column :rapns_notifications, :delay_while_idle, :boolean, :null => false, :default => false
|
37
|
-
|
37
|
+
|
38
|
+
reg_ids_type = ActiveRecord::Base.connection.adapter_name.include?('Mysql') ? :mediumtext : :text
|
39
|
+
add_column :rapns_notifications, :registration_ids, reg_ids_type, :null => true
|
38
40
|
add_column :rapns_notifications, :app_id, :integer, :null => true
|
39
41
|
add_column :rapns_notifications, :retries, :integer, :null => true, :default => 0
|
40
42
|
|
@@ -23,17 +23,48 @@
|
|
23
23
|
# Path to write PID file. Relative to Rails root unless absolute.
|
24
24
|
# config.pid_file = '/path/to/rapns.pid'
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
end
|
27
|
+
|
28
|
+
Rapns.reflect do |on|
|
29
|
+
|
30
|
+
# Called with a Rapns::Apns::Feedback instance when feedback is received
|
31
|
+
# from the APNs that a notification has failed to be delivered.
|
32
|
+
# Further notifications should not be sent to the device.
|
33
|
+
# on.apns_feedback do |feedback|
|
34
|
+
# end
|
35
|
+
|
36
|
+
# Called when a notification is queued internally for delivery.
|
37
|
+
# The internal queue for each app runner can be inspected:
|
29
38
|
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# end
|
39
|
+
# Rapns::Daemon::AppRunner.runners.each do |app_id, runner|
|
40
|
+
# runner.app
|
41
|
+
# runner.queue_size
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# on.notification_enqueued do |notification|
|
37
45
|
# end
|
38
46
|
|
39
|
-
|
47
|
+
# Called when a notification is successfully delivered.
|
48
|
+
# on.notification_delivered do |notification|
|
49
|
+
# end
|
50
|
+
|
51
|
+
# Called when notification delivery failed.
|
52
|
+
# Call 'error_code' and 'error_description' on the notification for the cause.
|
53
|
+
# on.notification_failed do |notification|
|
54
|
+
# end
|
55
|
+
|
56
|
+
# Called when a notification will be retried at a later date.
|
57
|
+
# Call 'deliver_after' on the notification for the next delivery date
|
58
|
+
# and 'retries' for the number of times this notification has been retried.
|
59
|
+
# on.notification_will_retry do |notification|
|
60
|
+
# end
|
61
|
+
|
62
|
+
# Called when an APNs connection is lost and will be reconnected.
|
63
|
+
# on.apns_connection_lost do |app, error|
|
64
|
+
# end
|
65
|
+
|
66
|
+
# Called when an exception is raised.
|
67
|
+
# on.error do |error|
|
68
|
+
# end
|
69
|
+
|
70
|
+
end
|
data/lib/rapns.rb
CHANGED
@@ -8,6 +8,9 @@ require 'rapns/multi_json_helper'
|
|
8
8
|
require 'rapns/notification'
|
9
9
|
require 'rapns/app'
|
10
10
|
require 'rapns/configuration'
|
11
|
+
require 'rapns/reflection'
|
12
|
+
require 'rapns/embed'
|
13
|
+
require 'rapns/push'
|
11
14
|
|
12
15
|
require 'rapns/apns/binary_notification_validator'
|
13
16
|
require 'rapns/apns/device_token_format_validator'
|
@@ -17,7 +20,14 @@ require 'rapns/apns/feedback'
|
|
17
20
|
require 'rapns/apns/app'
|
18
21
|
|
19
22
|
require 'rapns/gcm/expiry_collapse_key_mutual_inclusion_validator'
|
20
|
-
require 'rapns/gcm/
|
23
|
+
require 'rapns/gcm/payload_data_size_validator'
|
24
|
+
require 'rapns/gcm/registration_ids_count_validator'
|
21
25
|
require 'rapns/gcm/notification'
|
22
26
|
require 'rapns/gcm/app'
|
23
27
|
|
28
|
+
module Rapns
|
29
|
+
def self.require_for_daemon
|
30
|
+
require 'rapns/daemon'
|
31
|
+
require 'rapns/patches'
|
32
|
+
end
|
33
|
+
end
|
@@ -43,13 +43,13 @@ module Rapns
|
|
43
43
|
|
44
44
|
MDM_KEY = '__rapns_mdm__'
|
45
45
|
def mdm=(magic)
|
46
|
-
self.attributes_for_device = { MDM_KEY => magic }
|
46
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ MDM_KEY => magic })
|
47
47
|
end
|
48
48
|
|
49
49
|
CONTENT_AVAILABLE_KEY = '__rapns_content_available__'
|
50
50
|
def content_available=(bool)
|
51
51
|
return unless bool
|
52
|
-
self.attributes_for_device = { CONTENT_AVAILABLE_KEY => true }
|
52
|
+
self.attributes_for_device = (attributes_for_device || {}).merge({ CONTENT_AVAILABLE_KEY => true })
|
53
53
|
end
|
54
54
|
|
55
55
|
def as_json
|
@@ -81,6 +81,12 @@ module Rapns
|
|
81
81
|
[1, id_for_pack, expiry, 0, 32, device_token, payload_size, payload].pack("cNNccH*na*")
|
82
82
|
end
|
83
83
|
|
84
|
+
def data=(attrs)
|
85
|
+
return unless attrs
|
86
|
+
raise ArgumentError, "must be a Hash" if !attrs.is_a?(Hash)
|
87
|
+
super attrs.merge(data || {})
|
88
|
+
end
|
89
|
+
|
84
90
|
end
|
85
91
|
end
|
86
92
|
end
|
data/lib/rapns/app.rb
CHANGED
@@ -4,7 +4,7 @@ module Rapns
|
|
4
4
|
|
5
5
|
attr_accessible :name, :environment, :certificate, :password, :connections, :auth_key
|
6
6
|
|
7
|
-
has_many :notifications
|
7
|
+
has_many :notifications, :class_name => 'Rapns::Notification'
|
8
8
|
|
9
9
|
validates :name, :presence => true, :uniqueness => { :scope => [:type, :environment] }
|
10
10
|
validates_numericality_of :connections, :greater_than => 0, :only_integer => true
|
@@ -16,8 +16,8 @@ module Rapns
|
|
16
16
|
def certificate_has_matching_private_key
|
17
17
|
result = false
|
18
18
|
if certificate.present?
|
19
|
-
x509 = OpenSSL::X509::Certificate.new
|
20
|
-
pkey = OpenSSL::PKey::RSA.new
|
19
|
+
x509 = OpenSSL::X509::Certificate.new(certificate) rescue nil
|
20
|
+
pkey = OpenSSL::PKey::RSA.new(certificate, password) rescue nil
|
21
21
|
result = !x509.nil? && !pkey.nil?
|
22
22
|
unless result
|
23
23
|
errors.add :certificate, 'Certificate value must contain a certificate and a private key.'
|
data/lib/rapns/configuration.rb
CHANGED
@@ -7,21 +7,21 @@ module Rapns
|
|
7
7
|
yield config if block_given?
|
8
8
|
end
|
9
9
|
|
10
|
-
CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll,
|
11
|
-
:airbrake_notify, :check_for_errors, :pid_file, :batch_size
|
10
|
+
CONFIG_ATTRS = [:foreground, :push_poll, :feedback_poll, :embedded,
|
11
|
+
:airbrake_notify, :check_for_errors, :pid_file, :batch_size,
|
12
|
+
:push]
|
13
|
+
|
14
|
+
class ConfigurationWithoutDefaults < Struct.new(*CONFIG_ATTRS)
|
15
|
+
end
|
12
16
|
|
13
17
|
class Configuration < Struct.new(*CONFIG_ATTRS)
|
18
|
+
include Deprecatable
|
19
|
+
|
14
20
|
attr_accessor :apns_feedback_callback
|
15
21
|
|
16
22
|
def initialize
|
17
23
|
super
|
18
|
-
|
19
|
-
self.foreground = false
|
20
|
-
self.push_poll = 2
|
21
|
-
self.feedback_poll = 60
|
22
|
-
self.airbrake_notify = true
|
23
|
-
self.check_for_errors = true
|
24
|
-
self.batch_size = 5000
|
24
|
+
set_defaults
|
25
25
|
end
|
26
26
|
|
27
27
|
def update(other)
|
@@ -31,10 +31,6 @@ module Rapns
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def on_apns_feedback(&block)
|
35
|
-
self.apns_feedback_callback = block
|
36
|
-
end
|
37
|
-
|
38
34
|
def pid_file=(path)
|
39
35
|
if path && !Pathname.new(path).absolute?
|
40
36
|
super(File.join(Rails.root, path))
|
@@ -42,5 +38,42 @@ module Rapns
|
|
42
38
|
super
|
43
39
|
end
|
44
40
|
end
|
41
|
+
|
42
|
+
def foreground=(bool)
|
43
|
+
if defined? JRUBY_VERSION
|
44
|
+
# The JVM does not support fork().
|
45
|
+
super(true)
|
46
|
+
else
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_apns_feedback(&block)
|
52
|
+
self.apns_feedback_callback = block
|
53
|
+
end
|
54
|
+
deprecated(:on_apns_feedback, 3.2, "Please use the Rapns.reflect API instead.")
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def set_defaults
|
59
|
+
if defined? JRUBY_VERSION
|
60
|
+
# The JVM does not support fork().
|
61
|
+
self.foreground = true
|
62
|
+
else
|
63
|
+
self.foreground = false
|
64
|
+
end
|
65
|
+
|
66
|
+
self.push_poll = 2
|
67
|
+
self.feedback_poll = 60
|
68
|
+
self.airbrake_notify = true
|
69
|
+
self.check_for_errors = true
|
70
|
+
self.batch_size = 5000
|
71
|
+
self.pid_file = nil
|
72
|
+
self.apns_feedback_callback = nil
|
73
|
+
|
74
|
+
# Internal options.
|
75
|
+
self.embedded = false
|
76
|
+
self.push = false
|
77
|
+
end
|
45
78
|
end
|
46
79
|
end
|
data/lib/rapns/daemon.rb
CHANGED
@@ -5,6 +5,7 @@ require 'openssl'
|
|
5
5
|
|
6
6
|
require 'net/http/persistent'
|
7
7
|
|
8
|
+
require 'rapns/daemon/reflectable'
|
8
9
|
require 'rapns/daemon/interruptible_sleep'
|
9
10
|
require 'rapns/daemon/delivery_error'
|
10
11
|
require 'rapns/daemon/database_reconnectable'
|
@@ -37,9 +38,10 @@ module Rapns
|
|
37
38
|
def self.start
|
38
39
|
self.logger = Logger.new(:foreground => Rapns.config.foreground,
|
39
40
|
:airbrake_notify => Rapns.config.airbrake_notify)
|
40
|
-
setup_signal_hooks
|
41
41
|
|
42
|
-
|
42
|
+
setup_signal_traps if trap_signals?
|
43
|
+
|
44
|
+
if daemonize?
|
43
45
|
daemonize
|
44
46
|
reconnect_database
|
45
47
|
end
|
@@ -47,11 +49,22 @@ module Rapns
|
|
47
49
|
write_pid_file
|
48
50
|
ensure_upgraded
|
49
51
|
AppRunner.sync
|
50
|
-
Feeder.start
|
52
|
+
Feeder.start
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.shutdown(quiet = false)
|
56
|
+
puts "\nShutting down..." unless quiet
|
57
|
+
Feeder.stop
|
58
|
+
AppRunner.stop
|
59
|
+
delete_pid_file
|
51
60
|
end
|
52
61
|
|
53
62
|
protected
|
54
63
|
|
64
|
+
def self.daemonize?
|
65
|
+
!(Rapns.config.foreground || Rapns.config.embedded || Rapns.config.push || defined?(JRUBY_VERSION))
|
66
|
+
end
|
67
|
+
|
55
68
|
def self.ensure_upgraded
|
56
69
|
count = 0
|
57
70
|
|
@@ -64,7 +77,7 @@ module Rapns
|
|
64
77
|
puts "Please run 'rails g rapns' to generate the new migrations and create your app."
|
65
78
|
puts "See https://github.com/ileitch/rapns for further instructions."
|
66
79
|
puts
|
67
|
-
exit 1
|
80
|
+
exit 1 unless Rapns.config.embedded || Rapns.config.push
|
68
81
|
end
|
69
82
|
|
70
83
|
if count == 0
|
@@ -80,7 +93,11 @@ Remove config/rapns/rapns.yml to avoid this warning.
|
|
80
93
|
end
|
81
94
|
end
|
82
95
|
|
83
|
-
def self.
|
96
|
+
def self.trap_signals?
|
97
|
+
!(Rapns.config.embedded || Rapns.config.push)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.setup_signal_traps
|
84
101
|
@shutting_down = false
|
85
102
|
|
86
103
|
Signal.trap('SIGHUP') { AppRunner.sync }
|
@@ -97,13 +114,6 @@ Remove config/rapns/rapns.yml to avoid this warning.
|
|
97
114
|
shutdown
|
98
115
|
end
|
99
116
|
|
100
|
-
def self.shutdown
|
101
|
-
puts "\nShutting down..."
|
102
|
-
Feeder.stop
|
103
|
-
AppRunner.stop
|
104
|
-
delete_pid_file
|
105
|
-
end
|
106
|
-
|
107
117
|
def self.write_pid_file
|
108
118
|
if !Rapns.config.pid_file.blank?
|
109
119
|
begin
|
@@ -121,16 +131,17 @@ Remove config/rapns/rapns.yml to avoid this warning.
|
|
121
131
|
|
122
132
|
# :nocov:
|
123
133
|
def self.daemonize
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
+
if RUBY_VERSION < "1.9"
|
135
|
+
exit if fork
|
136
|
+
Process.setsid
|
137
|
+
exit if fork
|
138
|
+
Dir.chdir "/"
|
139
|
+
STDIN.reopen "/dev/null"
|
140
|
+
STDOUT.reopen "/dev/null", "a"
|
141
|
+
STDERR.reopen "/dev/null", "a"
|
142
|
+
else
|
143
|
+
Process.daemon
|
144
|
+
end
|
134
145
|
end
|
135
146
|
end
|
136
147
|
end
|