hamster-the-process-watcher 1.1.0
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/.document +5 -0
- data/.rvmrc +1 -0
- data/Gemfile +5 -0
- data/LICENSE +15 -0
- data/README.rdoc +21 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/hamster +39 -0
- data/bin/test_daemon +4 -0
- data/hamster-the-process-watcher.gemspec +62 -0
- data/lib/hamster.rb +2 -0
- data/lib/process_watch.rb +56 -0
- data/test/helper.rb +11 -0
- data/test/test_process_watch.rb +92 -0
- metadata +137 -0
data/.document
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ree@hampster --create
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
Copyright (C) 2009,2010 Curtis Schofield
|
3
|
+
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
5
|
+
it under the terms of the GNU General Public License as published by
|
6
|
+
the Free Software Foundation, either version 3 of the License, or
|
7
|
+
(at your option) any later version.
|
8
|
+
|
9
|
+
This program is distributed in the hope that it will be useful,
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
GNU General Public License for more details.
|
13
|
+
|
14
|
+
You should have received a copy of the GNU General Public License
|
15
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/README.rdoc
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
= Hampster
|
2
|
+
|
3
|
+
Simple process monitor -
|
4
|
+
Used to check if a process is running - if it insn't execute some ruby code
|
5
|
+
|
6
|
+
see ./bin/hampster --help for all the arguments that it takes
|
7
|
+
|
8
|
+
= Example
|
9
|
+
|
10
|
+
hampster --callback "%x{echo "Rails is Dead" | mail -s rails_is_dead me@ram9.cc}" --watch 'Rails:' --callback-message 'emailing you some fyi' --delay 1
|
11
|
+
|
12
|
+
samples.
|
13
|
+
|
14
|
+
Put this in a cronjob to run every minute and you have a very simple, robust way to detect missing jobs
|
15
|
+
|
16
|
+
This is not a replacement for more robust excellent froody things like Daemontools - but it is a simple way to
|
17
|
+
make sure a important background job is running in userland - without muking about with root
|
18
|
+
|
19
|
+
= Fin
|
20
|
+
|
21
|
+
Copyright (c) 2011 Curtis Schofield. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "hamster-the-process-watcher"
|
8
|
+
gem.summary = %Q{Hamster : The process runner}
|
9
|
+
gem.description = %Q{Dedicated to hamsters running in wheels everywhere}
|
10
|
+
gem.email = "github.com@robotarmyma.de"
|
11
|
+
gem.homepage = "http://github.com/robotarmy/hamster"
|
12
|
+
gem.authors = ["Curtis Schofield"]
|
13
|
+
gem.add_development_dependency "bundler", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
task :check_dependencies do
|
41
|
+
puts '..bundler'
|
42
|
+
`bundle check || bundle install`
|
43
|
+
end
|
44
|
+
|
45
|
+
task :test => :check_dependencies
|
46
|
+
|
47
|
+
|
48
|
+
task :default => :test
|
49
|
+
|
50
|
+
require 'rake/rdoctask'
|
51
|
+
Rake::RDocTask.new do |rdoc|
|
52
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
53
|
+
|
54
|
+
rdoc.rdoc_dir = 'rdoc'
|
55
|
+
rdoc.title = "weasel #{version}"
|
56
|
+
rdoc.rdoc_files.include('README*')
|
57
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
58
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.1.0
|
data/bin/hamster
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby -rubygems
|
2
|
+
$:.push File.join(File.dirname(__FILE__),'..','lib')
|
3
|
+
require 'optparse'
|
4
|
+
require 'hamster'
|
5
|
+
options = {}
|
6
|
+
OptionParser.new do |opts|
|
7
|
+
opts.banner = "Usage: #{__FILE__} [options]"
|
8
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
9
|
+
puts opts
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
opts.on("--command shellcode",String, "shell code to execute if process missing") do |v|
|
14
|
+
options[:command] = v
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on("--callback rubycode",String, "ruby code to execute if process missing") do |v|
|
18
|
+
options[:callback] = lambda { # may be evil
|
19
|
+
eval(v)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
opts.on("--callback_message string",String, "message to write when callback fired") do |v|
|
23
|
+
options[:callback_message] = v
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on("--watch S",String, "process identifying string") do |v|
|
27
|
+
options[:watch] = v
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on("--num_cycles N", "total executions before exit") do |v|
|
31
|
+
options[:num_cycles] = v.to_i
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("--delay F",Float, "delay between cycles") do |v|
|
35
|
+
options[:delay] = v
|
36
|
+
end
|
37
|
+
|
38
|
+
end.parse!
|
39
|
+
ProcessWatch.new(options).cycle
|
data/bin/test_daemon
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{hamster-the-process-watcher}
|
8
|
+
s.version = "1.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Curtis Schofield"]
|
12
|
+
s.date = %q{2011-07-07}
|
13
|
+
s.description = %q{Dedicated to hamsters running in wheels everywhere}
|
14
|
+
s.email = %q{github.com@robotarmyma.de}
|
15
|
+
s.executables = ["test_daemon", "hamster"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"LICENSE",
|
18
|
+
"README.rdoc"
|
19
|
+
]
|
20
|
+
s.files = [
|
21
|
+
".document",
|
22
|
+
".rvmrc",
|
23
|
+
"Gemfile",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"bin/hamster",
|
29
|
+
"bin/test_daemon",
|
30
|
+
"hamster-the-process-watcher.gemspec",
|
31
|
+
"lib/hamster.rb",
|
32
|
+
"lib/process_watch.rb",
|
33
|
+
"test/helper.rb",
|
34
|
+
"test/test_process_watch.rb"
|
35
|
+
]
|
36
|
+
s.homepage = %q{http://github.com/robotarmy/hamster}
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.6.1}
|
39
|
+
s.summary = %q{Hamster : The process runner}
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_runtime_dependency(%q<wrong>, [">= 0"])
|
46
|
+
s.add_runtime_dependency(%q<jeweler>, [">= 0"])
|
47
|
+
s.add_runtime_dependency(%q<daemons>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<bundler>, [">= 0"])
|
49
|
+
else
|
50
|
+
s.add_dependency(%q<wrong>, [">= 0"])
|
51
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
52
|
+
s.add_dependency(%q<daemons>, [">= 0"])
|
53
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
54
|
+
end
|
55
|
+
else
|
56
|
+
s.add_dependency(%q<wrong>, [">= 0"])
|
57
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
58
|
+
s.add_dependency(%q<daemons>, [">= 0"])
|
59
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/lib/hamster.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
class ProcessWatch
|
2
|
+
attr_accessor :watch_string,
|
3
|
+
:delay,
|
4
|
+
:num_cycles,
|
5
|
+
:command,
|
6
|
+
:callback,
|
7
|
+
:callback_message
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
self.command = options[:command] || nil
|
11
|
+
self.callback = options[:callback] ||
|
12
|
+
lambda { p 'default callback does nothing'}
|
13
|
+
self.callback_message = (options[:callback_message] ||
|
14
|
+
"Callback Triggered").strip
|
15
|
+
self.watch_string = (options[:watch] ||
|
16
|
+
"this poem is a pomme").strip
|
17
|
+
self.delay = options[:delay] || 60
|
18
|
+
self.num_cycles = options[:num_cycles] || 3
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def find(watch_string,ps_args = "")
|
24
|
+
cmd = %%ps -x#{ps_args} |grep '#{watch_string}' |grep -v grep | grep -v 'watch #{watch_string}' | awk '{print $1}'%
|
25
|
+
pids = %x{#{cmd}}.split()
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
public
|
30
|
+
def cycle
|
31
|
+
self.num_cycles.times do
|
32
|
+
_check
|
33
|
+
sleep self.delay
|
34
|
+
end
|
35
|
+
end
|
36
|
+
private
|
37
|
+
def _trigger_callback
|
38
|
+
_fork
|
39
|
+
ensure
|
40
|
+
self.callback.call
|
41
|
+
puts self.callback_message
|
42
|
+
end
|
43
|
+
def _check
|
44
|
+
if self.class.find(self.watch_string).empty?
|
45
|
+
_trigger_callback
|
46
|
+
end
|
47
|
+
end
|
48
|
+
def _fork
|
49
|
+
Daemonize.call_as_daemon(lambda {
|
50
|
+
p ENV['PWD']
|
51
|
+
Dir.chdir(ENV['PWD'])
|
52
|
+
::Process.exec(self.command)
|
53
|
+
},"hamster.daemon.log",self.command)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require "bundler/setup"
|
4
|
+
require 'wrong/adapters/test_unit'
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
8
|
+
require 'hamster'
|
9
|
+
|
10
|
+
class Test::Unit::TestCase
|
11
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'helper'
|
2
|
+
class TestProcessWatch < Test::Unit::TestCase
|
3
|
+
def test_find_running_daemon
|
4
|
+
cmd = %%./bin/test_daemon ID=1%
|
5
|
+
Process.detach(d_pid = fork {
|
6
|
+
Process.exec cmd
|
7
|
+
})
|
8
|
+
assert {
|
9
|
+
ProcessWatch.find('test_daemon ID=1').size == 1
|
10
|
+
}
|
11
|
+
%x{kill -9 #{d_pid}}
|
12
|
+
end
|
13
|
+
def test_pid_of_new_process_is_sibling
|
14
|
+
cmd = %%ruby ./bin/hamster --watch 'test_daemon ID=1' --command "./bin/test_daemon ID=1" --callback_message o_o__poot --delay 4 --num_cycles 45 %
|
15
|
+
Process.detach(h_pid = fork {
|
16
|
+
Process.exec cmd
|
17
|
+
})
|
18
|
+
sleep 3 # let it start
|
19
|
+
pid = ProcessWatch.find('test_daemon ID=1').first
|
20
|
+
|
21
|
+
assert {
|
22
|
+
lookup_parent_pid_via_ps('ruby ./bin/test_daemon ID=1') == lookup_parent_pid_via_ps('o_o__poot')
|
23
|
+
}
|
24
|
+
# clean up hamster and the test_daemon
|
25
|
+
%x{kill -9 #{pid}}
|
26
|
+
%x{kill -9 #{h_pid}}
|
27
|
+
end
|
28
|
+
def test_running_already_exit_without_action
|
29
|
+
cmd = %%./bin/test_daemon ID=1%
|
30
|
+
Process.detach(d_pid = fork {
|
31
|
+
Process.exec cmd
|
32
|
+
})
|
33
|
+
cmd = %%ruby ./bin/hamster --watch 'test_daemon ID=1' --command "./bin/test_daemon ID=1" --callback_message poot --delay 1 --num_cycles 2 %
|
34
|
+
Process.detach(h_pid = fork {
|
35
|
+
%x{#{cmd}}
|
36
|
+
})
|
37
|
+
pid, status = Process.waitpid2(h_pid)
|
38
|
+
assert {
|
39
|
+
status.exitstatus == 0
|
40
|
+
}
|
41
|
+
%x{kill -9 #{d_pid}}
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_watches_self
|
45
|
+
callback_called = false
|
46
|
+
pw = ProcessWatch.new(:watch => "nothing", :delay => 1, :cycle => 1,
|
47
|
+
:callback => lambda {callback_called = true})
|
48
|
+
pw.cycle
|
49
|
+
assert {
|
50
|
+
callback_called
|
51
|
+
}
|
52
|
+
end
|
53
|
+
def test_watches_but_not_the_watch_string_exits
|
54
|
+
cmd = %%./bin/hamster --watch hamster --callback "puts 'toop'" --callback_message poot --delay 1 --num_cycles 2 %
|
55
|
+
out = %x{#{cmd}}
|
56
|
+
assert {
|
57
|
+
out =~ /poot/ && out =~ /toop/
|
58
|
+
}
|
59
|
+
end
|
60
|
+
def test_runs_test_daemon_command_if_not_present
|
61
|
+
cmd = %%ruby ./bin/hamster --watch 'test_daemon ID=1' --command "./bin/test_daemon ID=1" --callback_message poot --delay 1 --num_cycles 2 %
|
62
|
+
Process.detach(pid = fork {
|
63
|
+
Process.exec cmd
|
64
|
+
})
|
65
|
+
sleep 3 # let it start
|
66
|
+
assert {
|
67
|
+
ProcessWatch.find('test_daemon ID=1').size == 1
|
68
|
+
}
|
69
|
+
%x{kill -9 #{ProcessWatch.find('test_daemon ID=1').first}}
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_daemon_keeps_running
|
73
|
+
cmd = %%ruby ./bin/hamster --watch 'test_daemon ID=1' --command "./bin/test_daemon ID=1" --callback_message poot --delay 5 --num_cycles 10 %
|
74
|
+
Process.detach(pid = fork {
|
75
|
+
Process.exec cmd
|
76
|
+
})
|
77
|
+
sleep 3 # let it start
|
78
|
+
%x{kill -9 #{pid}}
|
79
|
+
#child should live on if hampster is killed
|
80
|
+
sleep 3 # wait..for it
|
81
|
+
assert {
|
82
|
+
ProcessWatch.find('test_daemon ID=1').size == 1
|
83
|
+
}
|
84
|
+
%x{kill -9 #{ProcessWatch.find('test_daemon ID=1').first}}
|
85
|
+
end
|
86
|
+
|
87
|
+
def lookup_parent_pid_via_ps(match)
|
88
|
+
ProcessWatch.find(match,"f").first
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hamster-the-process-watcher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 1.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Curtis Schofield
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-07 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 3
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
version_requirements: *id001
|
32
|
+
name: wrong
|
33
|
+
prerelease: false
|
34
|
+
type: :runtime
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
hash: 3
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
version: "0"
|
45
|
+
version_requirements: *id002
|
46
|
+
name: jeweler
|
47
|
+
prerelease: false
|
48
|
+
type: :runtime
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
hash: 3
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
version_requirements: *id003
|
60
|
+
name: daemons
|
61
|
+
prerelease: false
|
62
|
+
type: :runtime
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
version_requirements: *id004
|
74
|
+
name: bundler
|
75
|
+
prerelease: false
|
76
|
+
type: :development
|
77
|
+
description: Dedicated to hamsters running in wheels everywhere
|
78
|
+
email: github.com@robotarmyma.de
|
79
|
+
executables:
|
80
|
+
- test_daemon
|
81
|
+
- hamster
|
82
|
+
extensions: []
|
83
|
+
|
84
|
+
extra_rdoc_files:
|
85
|
+
- LICENSE
|
86
|
+
- README.rdoc
|
87
|
+
files:
|
88
|
+
- .document
|
89
|
+
- .rvmrc
|
90
|
+
- Gemfile
|
91
|
+
- LICENSE
|
92
|
+
- README.rdoc
|
93
|
+
- Rakefile
|
94
|
+
- VERSION
|
95
|
+
- bin/hamster
|
96
|
+
- bin/test_daemon
|
97
|
+
- hamster-the-process-watcher.gemspec
|
98
|
+
- lib/hamster.rb
|
99
|
+
- lib/process_watch.rb
|
100
|
+
- test/helper.rb
|
101
|
+
- test/test_process_watch.rb
|
102
|
+
has_rdoc: true
|
103
|
+
homepage: http://github.com/robotarmy/hamster
|
104
|
+
licenses: []
|
105
|
+
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
hash: 3
|
117
|
+
segments:
|
118
|
+
- 0
|
119
|
+
version: "0"
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
hash: 3
|
126
|
+
segments:
|
127
|
+
- 0
|
128
|
+
version: "0"
|
129
|
+
requirements: []
|
130
|
+
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 1.6.1
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: "Hamster : The process runner"
|
136
|
+
test_files: []
|
137
|
+
|