raad_totem 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +30 -0
- data/LICENSE.md +103 -0
- data/README.md +37 -0
- data/Rakefile +14 -0
- data/lib/raad_totem/bootstrap.rb +67 -0
- data/lib/raad_totem/env.rb +61 -0
- data/lib/raad_totem/runner.rb +195 -0
- data/lib/raad_totem/shell_cmds/service.rb +43 -0
- data/lib/raad_totem/signal_trampoline.rb +39 -0
- data/lib/raad_totem/spoon.rb +46 -0
- data/lib/raad_totem/unix_daemon.rb +131 -0
- data/lib/raad_totem/version.rb +3 -0
- data/lib/raad_totem.rb +9 -0
- data/raad.gemspec +26 -0
- data/spec/raad_totem/unit/bootstrap_spec.rb +11 -0
- data/spec/raad_totem/unit/env_spec.rb +21 -0
- data/spec/raad_totem/unit/signal_trampoline_spec.rb +22 -0
- data/spec/raad_totem/unit/spoon_spec.rb +13 -0
- data/spec/raad_totem/unit/unix_daemon_spec.rb +201 -0
- data/spec/raad_totem/unit/version_spec.rb +8 -0
- data/spec/spec_helper.rb +4 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: b6cc5ec492a3a7e4ff5ee49cf38c01826f0efb27
|
|
4
|
+
data.tar.gz: df041b77a669ce3a6abf6443f9a186ef215a775d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8aaf81a66fbe9eb507da57698f2d36f49507f49f3af90dfd3cda219d39222b2426f226a13660d242ae8c11242d122eef060822b87eb48da5e66882fb34cb4eed
|
|
7
|
+
data.tar.gz: 76cb583b22e8bbbc7981edf97b5566c3ebaf480e5d237c67eafc08dabb2f825f81b55cff52b96ab4a640eae8cf79b549ba7f6bc0d073cec2f43ac1e695c47ef3
|
data/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
.bundle
|
|
4
|
+
.config
|
|
5
|
+
.yardoc
|
|
6
|
+
Gemfile.lock
|
|
7
|
+
InstalledFiles
|
|
8
|
+
_yardoc
|
|
9
|
+
coverage
|
|
10
|
+
doc/
|
|
11
|
+
lib/bundler/man
|
|
12
|
+
pkg
|
|
13
|
+
rdoc
|
|
14
|
+
spec/reports
|
|
15
|
+
test/tmp
|
|
16
|
+
test/version_tmp
|
|
17
|
+
tmp
|
|
18
|
+
.ruby-version
|
|
19
|
+
.ruby-gemset
|
|
20
|
+
.rvmrc
|
|
21
|
+
test/validation/output/
|
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 0.0.1 03-14-2014
|
|
2
|
+
- Fork project and name it raad_totem.
|
|
3
|
+
- From this point forward, this Changelog will not be updated. Read git log instead.
|
|
4
|
+
|
|
5
|
+
# 0.3.1, 08-24-2011
|
|
6
|
+
- initial release. support for MRI 1.8 & 1.9 only
|
|
7
|
+
|
|
8
|
+
# 0.3.3, 08-26-2011
|
|
9
|
+
- reworked runner stop sequence & signals trapping
|
|
10
|
+
- force kill + pid removal bug
|
|
11
|
+
- wrong exit code bug
|
|
12
|
+
- comments & cosmetics
|
|
13
|
+
|
|
14
|
+
# 0.4.0, 09-13-2011
|
|
15
|
+
- using SignalTrampoline for safe signal handling
|
|
16
|
+
- JRuby support
|
|
17
|
+
- specs and validations for all supported rubies
|
|
18
|
+
- tested on MRI 1.8.7 & 1.9.2, REE 1.8.7, JRuby 1.6.4 under OSX 10.6.8 and Linux Ubuntu 10.04
|
|
19
|
+
|
|
20
|
+
# 0.4.1, 09-22-2011
|
|
21
|
+
- issue #1, allow arbitrary environment name, https://github.com/praized/raad/issues/1
|
|
22
|
+
- issue #3, avoid calling stop multiple times, https://github.com/praized/raad/issues/3
|
|
23
|
+
- added Raad.stopped?
|
|
24
|
+
|
|
25
|
+
# 0.5.0, 10-12-2011
|
|
26
|
+
- issue #4, allow logger usage in service initialize method, https://github.com/praized/raad/issues/4
|
|
27
|
+
- issue #7, Raad.env not correct in service.initialize, https://github.com/praized/raad/issues/7
|
|
28
|
+
- refactored Runner initialize/run/start_service methods
|
|
29
|
+
- custom options now uses options_parser class method in the service
|
|
30
|
+
- added examples/custom_options.rb
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
raad_totem (0.0.1)
|
|
5
|
+
totem (>= 0.0.5)
|
|
6
|
+
|
|
7
|
+
GEM
|
|
8
|
+
remote: http://rubygems.org/
|
|
9
|
+
specs:
|
|
10
|
+
diff-lcs (1.1.3)
|
|
11
|
+
rake (10.1.1)
|
|
12
|
+
rspec (2.8.0)
|
|
13
|
+
rspec-core (~> 2.8.0)
|
|
14
|
+
rspec-expectations (~> 2.8.0)
|
|
15
|
+
rspec-mocks (~> 2.8.0)
|
|
16
|
+
rspec-core (2.8.0)
|
|
17
|
+
rspec-expectations (2.8.0)
|
|
18
|
+
diff-lcs (~> 1.1.2)
|
|
19
|
+
rspec-mocks (2.8.0)
|
|
20
|
+
totem (0.0.5)
|
|
21
|
+
|
|
22
|
+
PLATFORMS
|
|
23
|
+
java
|
|
24
|
+
ruby
|
|
25
|
+
|
|
26
|
+
DEPENDENCIES
|
|
27
|
+
bundler (~> 1.5)
|
|
28
|
+
raad_totem!
|
|
29
|
+
rake
|
|
30
|
+
rspec (~> 2.8.0)
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Raad - Totem
|
|
2
|
+
Authored by Chad Remesch (chad@remesch.com) as a derivate of the original Raad Ruby gem.
|
|
3
|
+
|
|
4
|
+
Copyright 2014 Chad Remesch
|
|
5
|
+
|
|
6
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
you may not use this file except in compliance with the License.
|
|
8
|
+
You may obtain a copy of the License at
|
|
9
|
+
|
|
10
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
|
|
12
|
+
Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
See the License for the specific language governing permissions and
|
|
16
|
+
limitations under the License.
|
|
17
|
+
|
|
18
|
+
-------------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
Raad - Ruby as a Daemon lightweight service wrapper
|
|
21
|
+
Authored by Colin Surprenant, [@colinsurprenant][twitter], [colin.surprenant@needium.com][needium], [colin.surprenant@gmail.com][gmail], [http://github.com/colinsurprenant][github]
|
|
22
|
+
|
|
23
|
+
Copyright 2011 PraizedMedia Inc.
|
|
24
|
+
|
|
25
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
26
|
+
you may not use this file except in compliance with the License.
|
|
27
|
+
You may obtain a copy of the License at
|
|
28
|
+
|
|
29
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
30
|
+
|
|
31
|
+
Unless required by applicable law or agreed to in writing, software
|
|
32
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
33
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
34
|
+
See the License for the specific language governing permissions and
|
|
35
|
+
limitations under the License.
|
|
36
|
+
|
|
37
|
+
-------------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
Portions of this code are from the Thin project (https://github.com/macournoyer/thin) and under the following license:
|
|
40
|
+
Ruby License, http://www.ruby-lang.org/en/LICENSE.txt.
|
|
41
|
+
|
|
42
|
+
-------------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
Portions of this code are from the Goliath project (https://github.com/postrank-labs/goliath/) and under the following license:
|
|
45
|
+
Copyright (c) 2011 PostRank Inc.
|
|
46
|
+
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
48
|
+
of this software and associated documentation files (the "Software"), to
|
|
49
|
+
deal in the Software without restriction, including without limitation the
|
|
50
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
51
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
|
52
|
+
furnished to do so, subject to the following conditions:
|
|
53
|
+
|
|
54
|
+
The above copyright notice and this permission notice shall be included in
|
|
55
|
+
all copies or substantial portions of the Software.
|
|
56
|
+
|
|
57
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
58
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
59
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
60
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
61
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
62
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
63
|
+
|
|
64
|
+
-------------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
Portions of this code are from the Sinatra project (https://github.com/bmizerany/sinatra) and under the following license:
|
|
67
|
+
Copyright (c) 2007, 2008, 2009, 2010, 2011 Blake Mizerany
|
|
68
|
+
|
|
69
|
+
Permission is hereby granted, free of charge, to any person
|
|
70
|
+
obtaining a copy of this software and associated documentation
|
|
71
|
+
files (the "Software"), to deal in the Software without
|
|
72
|
+
restriction, including without limitation the rights to use,
|
|
73
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
74
|
+
copies of the Software, and to permit persons to whom the
|
|
75
|
+
Software is furnished to do so, subject to the following
|
|
76
|
+
conditions:
|
|
77
|
+
|
|
78
|
+
The above copyright notice and this permission notice shall be
|
|
79
|
+
included in all copies or substantial portions of the Software.
|
|
80
|
+
|
|
81
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
82
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
83
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
84
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
85
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
86
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
87
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
88
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
89
|
+
|
|
90
|
+
-------------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
Portions of this code are from the Spoon project https://github.com/headius/spoon) and under the following license:
|
|
93
|
+
|
|
94
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
95
|
+
you may not use this file except in compliance with the License.
|
|
96
|
+
You may obtain a copy of the License at
|
|
97
|
+
|
|
98
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
99
|
+
|
|
100
|
+
[needium]: colin.surprenant@needium.com
|
|
101
|
+
[gmail]: colin.surprenant@gmail.com
|
|
102
|
+
[twitter]: http://twitter.com/colinsurprenant
|
|
103
|
+
[github]: http://github.com/colinsurprenant
|
data/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Raad - Totem
|
|
2
|
+
|
|
3
|
+
This gem allows you to easily create daemons (also known as servers, services, or background processes) in a project generated with [Totem](https://github.com/chadrem/totem). Both MRI and Jruby are supported.
|
|
4
|
+
|
|
5
|
+
This is a fork of the [Raad](https://github.com/colinsurprenant/raad) gem that is modified for Totem projects. All credit goes to the original project as they did all the hard work.
|
|
6
|
+
|
|
7
|
+
## Changes from the original Raad v0.5.0:
|
|
8
|
+
|
|
9
|
+
- Removal of log4j since Totem already has a built in logger.
|
|
10
|
+
- Integration with Totem's environment setting.
|
|
11
|
+
- Change namespaces so as not to conflict with Raad if both gems are installed.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add this line to your application's Gemfile:
|
|
16
|
+
|
|
17
|
+
gem 'raad-totem'
|
|
18
|
+
|
|
19
|
+
And then execute:
|
|
20
|
+
|
|
21
|
+
$ bundle
|
|
22
|
+
|
|
23
|
+
Or install it yourself as:
|
|
24
|
+
|
|
25
|
+
$ gem install raad-totem
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Coming soon.
|
|
30
|
+
|
|
31
|
+
## Contributing
|
|
32
|
+
|
|
33
|
+
1. Fork it
|
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
37
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
|
|
3
|
+
task :default => :spec
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require 'rspec/core/rake_task'
|
|
7
|
+
desc 'run specs in the current ruby env'
|
|
8
|
+
task :spec do
|
|
9
|
+
sh 'ruby -v'
|
|
10
|
+
RSpec::Core::RakeTask.new
|
|
11
|
+
end
|
|
12
|
+
rescue NameError, LoadError => e
|
|
13
|
+
puts e
|
|
14
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module RaadTotem
|
|
2
|
+
|
|
3
|
+
# The bootstrap class for RaadTotem. This will execute in the at_exit
|
|
4
|
+
# handler to run the service.
|
|
5
|
+
class Bootstrap
|
|
6
|
+
|
|
7
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
|
8
|
+
/\/raad_totem(\/(bootstrap))?\.rb$/, # all raad code
|
|
9
|
+
/rubygems\/custom_require\.rb$/, # rubygems require hacks
|
|
10
|
+
/bundler(\/runtime)?\.rb/, # bundler require hacks
|
|
11
|
+
/<internal:/ # internal in ruby >= 1.9.2
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
|
|
15
|
+
|
|
16
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
|
17
|
+
# line / method information; the resulting array contains filenames only.
|
|
18
|
+
def self.caller_files
|
|
19
|
+
caller_locations.map { |file, line| file }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# like caller_files, but containing Arrays rather than strings with the
|
|
23
|
+
# first element being the file, and the second being the line.
|
|
24
|
+
def self.caller_locations
|
|
25
|
+
caller(1).
|
|
26
|
+
map { |line| line.split(/:(?=\d|in )/)[0,2] }.
|
|
27
|
+
reject { |file, line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# find the service_file that was used to execute the service
|
|
31
|
+
#
|
|
32
|
+
# @return [String] The service file
|
|
33
|
+
def self.service_file
|
|
34
|
+
c = caller_files.first
|
|
35
|
+
c = $0 if !c || c.empty?
|
|
36
|
+
c
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# execute the service
|
|
40
|
+
#
|
|
41
|
+
# @return [Nil]
|
|
42
|
+
def self.run!
|
|
43
|
+
service_class = Object.module_eval(camel_case(File.basename(service_file, '.rb')))
|
|
44
|
+
Runner.new(ARGV, service_class).run
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# convert a string to camel case
|
|
50
|
+
#
|
|
51
|
+
# @param str [String] The string to convert
|
|
52
|
+
# @return [String] The camel cased string
|
|
53
|
+
def self.camel_case(str)
|
|
54
|
+
return str if str !~ /_/ && str =~ /[A-Z]+.*/
|
|
55
|
+
|
|
56
|
+
str.split('_').map { |e| e.capitalize }.join
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
at_exit do
|
|
61
|
+
unless defined?($RAAD_TOTEM_NOT_RUN)
|
|
62
|
+
if $!.nil? && $0 == RaadTotem::Bootstrap.service_file
|
|
63
|
+
RaadTotem::Bootstrap.run!
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'rbconfig'
|
|
2
|
+
require 'thread'
|
|
3
|
+
|
|
4
|
+
module RaadTotem
|
|
5
|
+
|
|
6
|
+
@custom_options = {}
|
|
7
|
+
@stopped = false
|
|
8
|
+
@stop_lock = Mutex.new
|
|
9
|
+
|
|
10
|
+
# are we running inside jruby
|
|
11
|
+
#
|
|
12
|
+
# @return [Boolean] true if runnig inside jruby
|
|
13
|
+
def jruby?
|
|
14
|
+
!!(defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# absolute path of current interpreter
|
|
18
|
+
#
|
|
19
|
+
# @return [String] absolute path of current interpreter
|
|
20
|
+
def ruby_path
|
|
21
|
+
File.join(RbConfig::CONFIG["bindir"], RbConfig::CONFIG["RUBY_INSTALL_NAME"] + RbConfig::CONFIG["EXEEXT"])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# ruby interpreter command line options
|
|
25
|
+
#
|
|
26
|
+
# @return [Array] command line options list
|
|
27
|
+
def ruby_options
|
|
28
|
+
@ruby_options ||= []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# set ruby interpreter command line options
|
|
32
|
+
#
|
|
33
|
+
# @param [String] options_str space separated options list
|
|
34
|
+
def ruby_options=(options_str)
|
|
35
|
+
@ruby_options = options_str.split
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# a request to stop the service has been received (or the #start method has returned and, if defined, the service #stop method has been called by RaadTotem.
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean] true is the service has been stopped
|
|
41
|
+
def stopped?
|
|
42
|
+
@stop_lock.synchronize{@stopped}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# used internally to set the stopped flag
|
|
46
|
+
#
|
|
47
|
+
# @param [Boolean] state true to set the stopped flag
|
|
48
|
+
def stopped=(state)
|
|
49
|
+
@stop_lock.synchronize{@stopped = !!state}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# returns the custom options hash set in the service options_parser class method
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] custom options hash
|
|
55
|
+
def custom_options
|
|
56
|
+
@custom_options
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module_function :jruby?, :ruby_path, :ruby_options, :ruby_options=, :stopped?, :stopped=, :custom_options
|
|
60
|
+
|
|
61
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
require 'thread'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module RaadTotem
|
|
7
|
+
class Runner
|
|
8
|
+
include Daemonizable
|
|
9
|
+
|
|
10
|
+
SECOND = 1
|
|
11
|
+
STOP_TIMEOUT = 60 * SECOND
|
|
12
|
+
|
|
13
|
+
attr_accessor :pid_file
|
|
14
|
+
|
|
15
|
+
# Create a new Runner
|
|
16
|
+
#
|
|
17
|
+
# @param argv [Array] command line arguments
|
|
18
|
+
# @param service_class [Class] service class
|
|
19
|
+
def initialize(argv, service_class)
|
|
20
|
+
@argv = argv.dup # lets keep a copy for jruby double-launch
|
|
21
|
+
@service_class = service_class
|
|
22
|
+
@parsed_options = nil
|
|
23
|
+
|
|
24
|
+
@stop_lock = Mutex.new
|
|
25
|
+
@stop_signaled = false
|
|
26
|
+
|
|
27
|
+
# parse command line options and set @parsed_options
|
|
28
|
+
options_parser = service_class.respond_to?(:options_parser) ? service_class.options_parser(create_options_parser, RaadTotem.custom_options) : create_options_parser
|
|
29
|
+
begin
|
|
30
|
+
options_parser.parse!(argv)
|
|
31
|
+
rescue OptionParser::InvalidOption => e
|
|
32
|
+
puts(">> #{e.message}")
|
|
33
|
+
exit!(false)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# grab what's left after options, which should be the start/stop command
|
|
37
|
+
@parsed_options[:command] = argv[0].to_s.downcase
|
|
38
|
+
unless ['start', 'stop', 'post_fork'].include?(@parsed_options[:command])
|
|
39
|
+
puts(">> start|stop command is required")
|
|
40
|
+
exit!(false)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Totem.component = default_service_name(service_class)
|
|
44
|
+
|
|
45
|
+
# @pid_file is required to become Daemonizable
|
|
46
|
+
@pid_file = @parsed_options.delete(:pid_file) || default_pid_path
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(@pid_file))
|
|
48
|
+
|
|
49
|
+
# default stop timeout
|
|
50
|
+
@stop_timeout = (@parsed_options.delete(:stop_timeout) || STOP_TIMEOUT).to_i
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def run
|
|
54
|
+
# check for stop command, @pid_file must be set
|
|
55
|
+
if @parsed_options[:command] == 'stop'
|
|
56
|
+
puts(">> RaadTotem service wrapper v#{VERSION} stopping")
|
|
57
|
+
# first send the TERM signal which will invoke the daemon wait_or_will method which will timeout after @stop_timeout
|
|
58
|
+
# if still not stopped afer @stop_timeout + 2 seconds, KILL -9 will be sent.
|
|
59
|
+
success = send_signal('TERM', @stop_timeout + (2 * SECOND))
|
|
60
|
+
exit(success)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Dir.chdir(File.expand_path(File.dirname("./")))
|
|
64
|
+
|
|
65
|
+
if @parsed_options[:command] == 'post_fork'
|
|
66
|
+
# we've been spawned and re executed, finish setup
|
|
67
|
+
post_fork_setup(Totem.component, (@parsed_options[:redirect] || default_redirect_path))
|
|
68
|
+
start_service
|
|
69
|
+
else
|
|
70
|
+
puts(">> RaadTotem service wrapper v#{VERSION} starting")
|
|
71
|
+
@parsed_options[:daemonize] ? daemonize(@argv, Totem.component, @parsed_options[:redirect]) {start_service} : start_service
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def default_redirect_path
|
|
78
|
+
return File.join(Totem.root, 'log', "#{Totem.process_name}.stdout")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default_pid_path
|
|
82
|
+
return File.join(Totem.root, 'tmp', 'pid', "#{Totem.process_name}.pid")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def start_service
|
|
86
|
+
Totem.logger.debug("initializing #{Totem.component} service")
|
|
87
|
+
|
|
88
|
+
# create service instance
|
|
89
|
+
service = @service_class.new
|
|
90
|
+
|
|
91
|
+
# important to display this after service instantiation.
|
|
92
|
+
Totem.logger.info("starting #{Totem.component} service")
|
|
93
|
+
|
|
94
|
+
at_exit do
|
|
95
|
+
Totem.logger.info(">> RaadTotem service wrapper stopped")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# do not trap :QUIT because its not supported in jruby
|
|
99
|
+
[:INT, :TERM].each{|sig| SignalTrampoline.trap(sig) {stop_service(service)}}
|
|
100
|
+
|
|
101
|
+
# launch the service thread and call start. we expect start not to return
|
|
102
|
+
# unless it is done or has been stopped.
|
|
103
|
+
service_thread = Thread.new do
|
|
104
|
+
Thread.current.abort_on_exception = true
|
|
105
|
+
service.start
|
|
106
|
+
stop_service(service)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
result = wait_or_kill(service_thread)
|
|
110
|
+
# if not daemonized start a sentinel thread, if still alive after 2 seconds, do arakiri
|
|
111
|
+
Thread.new{sleep(2 * SECOND); Process.kill(:KILL, Process.pid)} unless @parsed_options[:daemonize]
|
|
112
|
+
# use exit and not exit! to make sure the at_exit hooks are called, like the pid cleanup, etc.
|
|
113
|
+
exit(result)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def stop_service(service)
|
|
117
|
+
return if @stop_lock.synchronize{s = @stop_signaled; @stop_signaled = true; s}
|
|
118
|
+
|
|
119
|
+
Totem.logger.info("stopping #{Totem.component} service")
|
|
120
|
+
service.stop if service.respond_to?(:stop)
|
|
121
|
+
RaadTotem.stopped = true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# try to do a timeout join periodically on the given thread. if the join succeed then the stop
|
|
125
|
+
# sequence is successful and return true.
|
|
126
|
+
# Otherwise, on timeout if stop has beed signaled, wait a maximum of @stop_timeout on the
|
|
127
|
+
# thread and kill it if the timeout is reached and return false in that case.
|
|
128
|
+
#
|
|
129
|
+
# @return [Boolean] true if the thread normally terminated, false if a kill was necessary
|
|
130
|
+
def wait_or_kill(thread)
|
|
131
|
+
while thread.join(SECOND).nil?
|
|
132
|
+
# the join has timed out, thread is still buzzy.
|
|
133
|
+
if @stop_lock.synchronize{@stop_signaled}
|
|
134
|
+
# but if stop has been signalled, start "the final countdown" ♫
|
|
135
|
+
try = 0; join = nil
|
|
136
|
+
while (try += 1) <= @stop_timeout && join.nil? do
|
|
137
|
+
join = thread.join(SECOND)
|
|
138
|
+
Totem.logger.debug("waiting for service to stop #{try}/#{@stop_timeout}") if join.nil?
|
|
139
|
+
end
|
|
140
|
+
if join.nil?
|
|
141
|
+
Totem.logger.error("stop timeout exhausted, killing service thread")
|
|
142
|
+
thread.kill
|
|
143
|
+
return false
|
|
144
|
+
end
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# convert the service class name from CameCase to underscore
|
|
152
|
+
#
|
|
153
|
+
# @return [String] underscored service class name
|
|
154
|
+
def default_service_name(clazz)
|
|
155
|
+
clazz.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase!
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Create the options parser
|
|
159
|
+
#
|
|
160
|
+
# @return [OptionParser] Creates the options parser for the runner with the default options
|
|
161
|
+
def create_options_parser
|
|
162
|
+
@parsed_options ||= {
|
|
163
|
+
:daemonize => false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
options_parser ||= OptionParser.new do |opts|
|
|
167
|
+
opts.banner = "USAGE: ruby <service>.rb [options] start|stop"
|
|
168
|
+
|
|
169
|
+
opts.separator ""
|
|
170
|
+
opts.separator "Service Options:"
|
|
171
|
+
|
|
172
|
+
opts.on('-d', '--daemonize', "run daemonized in the background (default: no)") { |v| @parsed_options[:daemonize] = v }
|
|
173
|
+
opts.on('-P', '--pid FILE', "pid file when daemonized (default: tmp/pids directory)") { |file| @parsed_options[:pid_file] = file }
|
|
174
|
+
opts.on('-r', '--redirect FILE', "redirect stdout when daemonized (default: log directory)") { |v| @parsed_options[:redirect] = v }
|
|
175
|
+
opts.on('-e', '--environment ENVIRONMENT', "Totem environment") { |v| Totem.env = v }
|
|
176
|
+
opts.on('-i', '--instance INSTANCE', "Totem instance") { |v| Totem.instance = v }
|
|
177
|
+
opts.on(nil, '--timeout SECONDS', "seconds to wait before force stopping the service (default: 60)") { |v| @parsed_options[:stop_timeout] = v }
|
|
178
|
+
opts.on(nil, '--ruby opts', "daemonized ruby interpreter specifc options") { |v| RaadTotem.ruby_options = v }
|
|
179
|
+
|
|
180
|
+
opts.on('-h', '--help', 'display help message') { show_options(options_parser) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
options_parser
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Output the servers options and exit Ruby
|
|
187
|
+
#
|
|
188
|
+
# @param opts [OptionsParser] The options parser
|
|
189
|
+
def show_options(opts)
|
|
190
|
+
puts(opts)
|
|
191
|
+
exit!(false)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module RaadTotem
|
|
2
|
+
module ShellCmds
|
|
3
|
+
class Service < Totem::ShellCmds::Base
|
|
4
|
+
def run
|
|
5
|
+
case @args[0]
|
|
6
|
+
when 'new' then new_s(@args[1])
|
|
7
|
+
else
|
|
8
|
+
puts_usage
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new_s(service)
|
|
13
|
+
return false unless require_arg(service, :service)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def puts_usage
|
|
19
|
+
puts "Usage:\n bundle exec totem service <command>"
|
|
20
|
+
puts
|
|
21
|
+
puts "Commands:\n"
|
|
22
|
+
puts " new <service> - Create a new service."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def puts_error(message)
|
|
26
|
+
puts "ERROR: #{message}"
|
|
27
|
+
puts
|
|
28
|
+
puts_usage
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def require_arg(val, name)
|
|
32
|
+
if val.nil? || val.length == 0
|
|
33
|
+
puts_error("You must provide a #{name}.")
|
|
34
|
+
return false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Totem::Shell.register_cmd(:service, RaadTotem::ShellCmds::Service)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'thread'
|
|
2
|
+
|
|
3
|
+
module SignalTrampoline
|
|
4
|
+
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
SIGNALS = {
|
|
8
|
+
:EXIT => 0, :HUP => 1, :INT => 2, :QUIT => 3, :ILL => 4, :TRAP => 5, :IOT => 6, :ABRT => 6, :FPE => 8, :KILL => 9,
|
|
9
|
+
:BUS => 7, :SEGV => 11, :SYS => 31, :PIPE => 13, :ALRM => 14, :TERM => 15, :URG => 23, :STOP => 19, :TSTP => 20,
|
|
10
|
+
:CONT => 18, :CHLD => 17, :CLD => 17, :TTIN => 21, :TTOU => 22, :IO => 29, :XCPU => 24, :XFSZ => 25, :VTALRM => 26,
|
|
11
|
+
:PROF => 27, :WINCH => 28, :USR1 => 10, :USR2 => 12, :PWR => 30, :POLL => 29
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@signal_q = Queue.new
|
|
15
|
+
@handlers = {}
|
|
16
|
+
@handler_thread = nil
|
|
17
|
+
|
|
18
|
+
# using threads to bounce signal using a thread-safe queue seem the most robust way to handle signals.
|
|
19
|
+
# it minimizes the code in the trap block and reissue the signal and its handling in the normal Ruby
|
|
20
|
+
# flow, within normal threads.
|
|
21
|
+
|
|
22
|
+
def trap(signal, &block)
|
|
23
|
+
raise("unknown signal") unless SIGNALS.has_key?(signal)
|
|
24
|
+
@handler_thread ||= detach_handler_thread
|
|
25
|
+
@handlers[signal] = block
|
|
26
|
+
Kernel.trap(signal) {Thread.new{@signal_q << signal}}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def detach_handler_thread
|
|
30
|
+
Thread.new do
|
|
31
|
+
Thread.current.abort_on_exception = true
|
|
32
|
+
loop do
|
|
33
|
+
s = @signal_q.pop
|
|
34
|
+
@handlers[s].call if @handlers[s]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'ffi'
|
|
2
|
+
|
|
3
|
+
# spoon code taken from Charles Oliver Nutter's spoon gem https://github.com/headius/spoon
|
|
4
|
+
# also see http://blog.headius.com/2009/05/fork-and-exec-on-jvm-jruby-to-rescue.html
|
|
5
|
+
|
|
6
|
+
module Spoon
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
ffi_lib 'c'
|
|
9
|
+
|
|
10
|
+
# int
|
|
11
|
+
# posix_spawn(pid_t *restrict pid, const char *restrict path,
|
|
12
|
+
# const posix_spawn_file_actions_t *file_actions,
|
|
13
|
+
# const posix_spawnattr_t *restrict attrp, char *const argv[restrict],
|
|
14
|
+
# char *const envp[restrict]);
|
|
15
|
+
|
|
16
|
+
attach_function :_posix_spawn, :posix_spawn, [:pointer, :string, :pointer, :pointer, :pointer, :pointer], :int
|
|
17
|
+
attach_function :_posix_spawnp, :posix_spawnp, [:pointer, :string, :pointer, :pointer, :pointer, :pointer], :int
|
|
18
|
+
|
|
19
|
+
def self.spawn(*args)
|
|
20
|
+
spawn_args = _prepare_spawn_args(args)
|
|
21
|
+
_posix_spawn(*spawn_args)
|
|
22
|
+
spawn_args[0].read_int
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.spawnp(*args)
|
|
26
|
+
spawn_args = _prepare_spawn_args(args)
|
|
27
|
+
_posix_spawnp(*spawn_args)
|
|
28
|
+
spawn_args[0].read_int
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def self._prepare_spawn_args(args)
|
|
34
|
+
pid_ptr = FFI::MemoryPointer.new(:pid_t, 1)
|
|
35
|
+
|
|
36
|
+
args_ary = FFI::MemoryPointer.new(:pointer, args.length + 1)
|
|
37
|
+
str_ptrs = args.map {|str| FFI::MemoryPointer.from_string(str)}
|
|
38
|
+
args_ary.put_array_of_pointer(0, str_ptrs)
|
|
39
|
+
|
|
40
|
+
env_ary = FFI::MemoryPointer.new(:pointer, ENV.length + 1)
|
|
41
|
+
env_ptrs = ENV.map {|key,value| FFI::MemoryPointer.from_string("#{key}=#{value}")}
|
|
42
|
+
env_ary.put_array_of_pointer(0, env_ptrs)
|
|
43
|
+
|
|
44
|
+
[pid_ptr, args[0], nil, nil, args_ary, env_ary]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require 'etc'
|
|
2
|
+
require 'timeout'
|
|
3
|
+
|
|
4
|
+
require 'raad/spoon' if RaadTotem.jruby?
|
|
5
|
+
|
|
6
|
+
module Process
|
|
7
|
+
|
|
8
|
+
def running?(pid)
|
|
9
|
+
Process.getpgid(pid) != -1
|
|
10
|
+
rescue Errno::EPERM
|
|
11
|
+
true
|
|
12
|
+
rescue Errno::ESRCH
|
|
13
|
+
false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module_function :running?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when the pid file already exist starting as a daemon.
|
|
20
|
+
class PidFileExist < RuntimeError; end
|
|
21
|
+
|
|
22
|
+
# module Daemonizable requires that the including class defines the @pid_file instance variable
|
|
23
|
+
module Daemonizable
|
|
24
|
+
|
|
25
|
+
def pid
|
|
26
|
+
File.exist?(@pid_file) ? open(@pid_file).read.to_i : nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def daemonize(argv, name, stdout_file = nil)
|
|
30
|
+
remove_stale_pid_file
|
|
31
|
+
pwd = Dir.pwd
|
|
32
|
+
|
|
33
|
+
if RaadTotem.jruby?
|
|
34
|
+
# in jruby the process is to posix-spawn a new process and re execute ourself using Spoon.
|
|
35
|
+
# swap command 'start' for 'post_fork' to signal the second exec
|
|
36
|
+
spanw_options = [RaadTotem.ruby_path].concat(RaadTotem.ruby_options)
|
|
37
|
+
spanw_options << $0
|
|
38
|
+
spanw_options.concat(argv.map{|arg| arg == 'start' ? 'post_fork' : arg})
|
|
39
|
+
Spoon.spawnp(*spanw_options)
|
|
40
|
+
else
|
|
41
|
+
# do the double fork dance
|
|
42
|
+
Process.fork do
|
|
43
|
+
Process.setsid
|
|
44
|
+
exit if fork # exit parent
|
|
45
|
+
|
|
46
|
+
Dir.chdir(pwd)
|
|
47
|
+
post_fork_setup(name, stdout_file)
|
|
48
|
+
|
|
49
|
+
yield
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def post_fork_setup(name, stdout_file = nil)
|
|
55
|
+
$0 = name # set process name, does not work with jruby
|
|
56
|
+
|
|
57
|
+
File.umask(0000) # file mode creation mask to 000 to allow creation of files with any required permission late
|
|
58
|
+
write_pid_file
|
|
59
|
+
|
|
60
|
+
# redirect stdin, stdout, stderr
|
|
61
|
+
STDIN.reopen('/dev/null')
|
|
62
|
+
stdout_file ? STDOUT.reopen(stdout_file, "a") : STDOUT.reopen('/dev/null', 'a')
|
|
63
|
+
STDERR.reopen(STDOUT)
|
|
64
|
+
|
|
65
|
+
at_exit do
|
|
66
|
+
remove_pid_file
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def send_signal(signal, timeout = 60)
|
|
71
|
+
if pid = read_pid_file
|
|
72
|
+
puts(">> sending #{signal} signal to process #{pid}")
|
|
73
|
+
Process.kill(signal, pid)
|
|
74
|
+
Timeout.timeout(timeout) do
|
|
75
|
+
sleep 0.1 while Process.running?(pid)
|
|
76
|
+
end
|
|
77
|
+
true
|
|
78
|
+
else
|
|
79
|
+
puts(">> can't stop process, no pid found in #{@pid_file}")
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
rescue Timeout::Error
|
|
83
|
+
force_kill_and_remove_pid_file(pid)
|
|
84
|
+
rescue Interrupt
|
|
85
|
+
force_kill_and_remove_pid_file(pid)
|
|
86
|
+
rescue Errno::ESRCH # No such process
|
|
87
|
+
puts(">> can't stop process, no such process #{pid}")
|
|
88
|
+
remove_pid_file
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def force_kill_and_remove_pid_file(pid)
|
|
93
|
+
puts(">> sending KILL signal to process #{pid}")
|
|
94
|
+
Process.kill("KILL", pid)
|
|
95
|
+
remove_pid_file
|
|
96
|
+
true
|
|
97
|
+
rescue Errno::ESRCH # No such process
|
|
98
|
+
puts(">> can't send KILL, no such process #{pid}")
|
|
99
|
+
remove_pid_file
|
|
100
|
+
false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def read_pid_file
|
|
104
|
+
if File.file?(@pid_file) && pid = File.read(@pid_file)
|
|
105
|
+
pid.to_i
|
|
106
|
+
else
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def remove_pid_file
|
|
112
|
+
File.delete(@pid_file) if @pid_file && File.exists?(@pid_file)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def write_pid_file
|
|
116
|
+
open(@pid_file,"w") { |f| f.write(Process.pid) }
|
|
117
|
+
File.chmod(0644, @pid_file)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def remove_stale_pid_file
|
|
121
|
+
if File.exist?(@pid_file)
|
|
122
|
+
if pid && Process.running?(pid)
|
|
123
|
+
raise PidFileExist, "#{@pid_file} exists and process #{pid} is runnig. stop the process or delete #{@pid_file}"
|
|
124
|
+
else
|
|
125
|
+
puts(">> deleting stale pid file #{@pid_file}")
|
|
126
|
+
remove_pid_file
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
end
|
data/lib/raad_totem.rb
ADDED
data/raad.gemspec
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'raad_totem/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = 'raad_totem'
|
|
8
|
+
spec.version = RaadTotem::VERSION
|
|
9
|
+
spec.authors = ['Colin Surprenant', 'Chad Remesch']
|
|
10
|
+
spec.email = ['colin.surprenant@gmail.com', 'chad@remesch.com']
|
|
11
|
+
spec.summary = %q{Easily create daemons in Totem projects (fork of Raad gem)}
|
|
12
|
+
spec.description = %q{Easily create daemons in Totem projects (fork of Raad gem)}
|
|
13
|
+
spec.homepage = 'https://github.com/chadrem/raad_totem'
|
|
14
|
+
spec.license = 'Apache'
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
|
|
21
|
+
spec.add_dependency('totem', '>= 0.0.7')
|
|
22
|
+
|
|
23
|
+
spec.add_development_dependency('bundler', '~> 1.5')
|
|
24
|
+
spec.add_development_dependency('rake')
|
|
25
|
+
spec.add_development_dependency "rspec", ['~> 2.8.0']
|
|
26
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
# @TODO workaround to bug with Kernel.caller between 1.8 and 1.9 need to dig in this.
|
|
4
|
+
$RAAD_TOTEM_NOT_RUN=true
|
|
5
|
+
require 'raad_totem/bootstrap'
|
|
6
|
+
|
|
7
|
+
describe RaadTotem::Bootstrap do
|
|
8
|
+
it "should work" do
|
|
9
|
+
true.should be_true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'raad_totem/env'
|
|
3
|
+
|
|
4
|
+
describe "RaadTotem env" do
|
|
5
|
+
it "should test for jruby" do
|
|
6
|
+
[true, false].should include(RaadTotem.jruby?)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it "should report ruby path" do
|
|
10
|
+
File.exist?(RaadTotem.ruby_path).should be_true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should default to empty ruby_options" do
|
|
14
|
+
RaadTotem.ruby_options.should == []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "should set ruby_options" do
|
|
18
|
+
RaadTotem.ruby_options = "a b"
|
|
19
|
+
RaadTotem.ruby_options.should == ['a', 'b']
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'raad_totem/signal_trampoline'
|
|
3
|
+
require 'thread'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
describe SignalTrampoline do
|
|
7
|
+
it "should trap signal" do
|
|
8
|
+
t = Thread.new{Thread.stop}
|
|
9
|
+
SignalTrampoline.trap(:USR2) {t.run}
|
|
10
|
+
t.alive?.should be_true
|
|
11
|
+
Timeout.timeout(5) {sleep(0.1) while !t.stop?} # avoid race condition
|
|
12
|
+
t.stop?.should be_true
|
|
13
|
+
Process.kill(:USR2, Process.pid)
|
|
14
|
+
Timeout.timeout(5) {sleep(0.1) while t.alive?}
|
|
15
|
+
t.alive?.should be_false
|
|
16
|
+
t.join(5).should == t
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "should raise on bad signal" do
|
|
20
|
+
lambda{SignalTrampoline.trap(:WAGADOUDOU) {}}.should raise_exception
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'raad_totem/env'
|
|
3
|
+
require 'raad_totem/unix_daemon'
|
|
4
|
+
require 'timeout'
|
|
5
|
+
|
|
6
|
+
class TestService
|
|
7
|
+
include Daemonizable
|
|
8
|
+
|
|
9
|
+
attr_accessor :pid_file
|
|
10
|
+
|
|
11
|
+
def initialize(pid_file)
|
|
12
|
+
@pid_file = pid_file
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Spoon
|
|
17
|
+
end unless RaadTotem.jruby?
|
|
18
|
+
|
|
19
|
+
describe 'UnixDaemon' do
|
|
20
|
+
|
|
21
|
+
before :all do
|
|
22
|
+
@l = 'test.log'
|
|
23
|
+
@p = 'test.pid'
|
|
24
|
+
File.delete(@l) if File.exist?(@l)
|
|
25
|
+
File.delete(@p) if File.exist?(@p)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
before :each do
|
|
29
|
+
File.delete(@l) if File.exist?(@l) rescue nil
|
|
30
|
+
File.delete(@p) if File.exist?(@p) rescue nil
|
|
31
|
+
@service = TestService.new(@p)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "non jruby daemonize" do
|
|
35
|
+
|
|
36
|
+
it 'should create a pid file' do
|
|
37
|
+
@service.should_receive(:remove_stale_pid_file).once
|
|
38
|
+
@service.daemonize([], 'test') do
|
|
39
|
+
Thread.new{Thread.stop}.join(5)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
Timeout.timeout(5) do
|
|
43
|
+
while !File.exist?(@service.pid_file); sleep(0.1); end
|
|
44
|
+
true
|
|
45
|
+
end.should == true
|
|
46
|
+
|
|
47
|
+
(pid = @service.pid).should > 0
|
|
48
|
+
Process.kill(:TERM, pid)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'should redirect stdio to a log file' do
|
|
52
|
+
@service.daemonize([], 'test', @l) do
|
|
53
|
+
puts("puts"); STDOUT.flush
|
|
54
|
+
STDERR.puts("STDERR.puts"); STDERR.flush
|
|
55
|
+
STDOUT.puts("STDOUT.puts"); STDOUT.flush
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Timeout.timeout(5) do
|
|
59
|
+
while !File.exist?(@l); sleep(0.1); end
|
|
60
|
+
true
|
|
61
|
+
end.should == true
|
|
62
|
+
|
|
63
|
+
f = File.new(@l, "r")
|
|
64
|
+
Timeout.timeout(5) do
|
|
65
|
+
while (l = f.readlines) != ["puts\n", "STDERR.puts\n", "STDOUT.puts\n"]; sleep(0.1); f.rewind; end
|
|
66
|
+
true
|
|
67
|
+
end.should == true
|
|
68
|
+
end
|
|
69
|
+
end unless RaadTotem.jruby?
|
|
70
|
+
|
|
71
|
+
describe "jruby daemonize (from any ruby)" do
|
|
72
|
+
|
|
73
|
+
# we don't have to use jruby for this test, Spoon is mocked when not jruby.
|
|
74
|
+
|
|
75
|
+
it "should swap start for post_fork and call spawnp with args" do
|
|
76
|
+
@service.should_receive(:remove_stale_pid_file).once
|
|
77
|
+
RaadTotem.should_receive(:jruby?).and_return(true)
|
|
78
|
+
Spoon.should_receive(:spawnp).with(RaadTotem.ruby_path, "-JXmx=256m", $0, "test", "post_fork")
|
|
79
|
+
|
|
80
|
+
RaadTotem.ruby_options = "-JXmx=256m"
|
|
81
|
+
@service.daemonize(["test", "start"], 'test')
|
|
82
|
+
RaadTotem.ruby_options = ""
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "jruby daemonize (only in jruby)" do
|
|
87
|
+
|
|
88
|
+
it "should daemonize" do
|
|
89
|
+
false.should == false
|
|
90
|
+
end
|
|
91
|
+
end if RaadTotem.jruby?
|
|
92
|
+
|
|
93
|
+
describe "post_fork_setup" do
|
|
94
|
+
|
|
95
|
+
it 'should create a pid file' do
|
|
96
|
+
STDIN.should_receive(:reopen)
|
|
97
|
+
STDOUT.should_receive(:reopen)
|
|
98
|
+
STDERR.should_receive(:reopen)
|
|
99
|
+
@service.post_fork_setup('test', nil)
|
|
100
|
+
File.exist?(@service.pid_file).should be_true
|
|
101
|
+
@service.pid.should == Process.pid
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'should redirect stdio to a log file' do
|
|
105
|
+
@service = TestService.new(@p)
|
|
106
|
+
|
|
107
|
+
STDIN.should_receive(:reopen).with("/dev/null")
|
|
108
|
+
STDOUT.should_receive(:reopen).with(@l, 'a')
|
|
109
|
+
STDERR.should_receive(:reopen).with(STDOUT)
|
|
110
|
+
|
|
111
|
+
@service.post_fork_setup('test', @l)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe 'read/write/remove pid file' do
|
|
116
|
+
|
|
117
|
+
it 'should write pid file' do
|
|
118
|
+
@service.write_pid_file
|
|
119
|
+
File.exist?(@service.pid_file).should be_true
|
|
120
|
+
File.read(@p).to_i.should == Process.pid
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'should read pid file' do
|
|
124
|
+
@service.write_pid_file
|
|
125
|
+
@service.read_pid_file.should == Process.pid
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'should remove pid file' do
|
|
129
|
+
@service.write_pid_file
|
|
130
|
+
File.exist?(@service.pid_file).should be_true
|
|
131
|
+
@service.remove_pid_file
|
|
132
|
+
File.exist?(@service.pid_file).should be_false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe 'send_signal' do
|
|
137
|
+
|
|
138
|
+
it 'should send signal and terminate process' do
|
|
139
|
+
@service.write_pid_file
|
|
140
|
+
t = Thread.new{Thread.stop}
|
|
141
|
+
Kernel.trap(:USR2) {Thread.new{t.run}}
|
|
142
|
+
Process.should_receive(:running?).once.and_return(false)
|
|
143
|
+
$stdout.should_receive(:write).twice # mute trace
|
|
144
|
+
@service.send_signal(:USR2, 5).should be_true
|
|
145
|
+
Timeout.timeout(5) {t.join}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'should force kill on Timeout::Error exception' do
|
|
149
|
+
@service.write_pid_file
|
|
150
|
+
Process.should_receive(:kill).and_raise(Timeout::Error)
|
|
151
|
+
@service.should_receive(:force_kill_and_remove_pid_file).and_return(true)
|
|
152
|
+
$stdout.should_receive(:write).twice # mute trace
|
|
153
|
+
@service.send_signal(:USR2, 5).should be_true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'should force kill on Interrupt exception' do
|
|
157
|
+
@service.write_pid_file
|
|
158
|
+
Process.should_receive(:kill).and_raise(Interrupt)
|
|
159
|
+
@service.should_receive(:force_kill_and_remove_pid_file).and_return(true)
|
|
160
|
+
$stdout.should_receive(:write).twice # mute trace
|
|
161
|
+
@service.send_signal(:USR2, 5).should be_true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'should remove pid file on Errno::ESRCH exception' do
|
|
165
|
+
@service.write_pid_file
|
|
166
|
+
Process.should_receive(:kill).and_raise(Errno::ESRCH)
|
|
167
|
+
$stdout.should_receive(:write).exactly(4).times # mute trace
|
|
168
|
+
@service.should_receive(:remove_pid_file)
|
|
169
|
+
@service.send_signal(:USR2, 5).should be_false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def force_kill_and_remove_pid_file(pid)
|
|
174
|
+
puts(">> sending KILL signal to process #{pid}")
|
|
175
|
+
Process.kill("KILL", pid)
|
|
176
|
+
remove_pid_file
|
|
177
|
+
true
|
|
178
|
+
rescue Errno::ESRCH # No such process
|
|
179
|
+
puts(">> can't send KILL, no such process #{pid}")
|
|
180
|
+
remove_pid_file
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe 'force_kill_and_remove_pid_file' do
|
|
185
|
+
|
|
186
|
+
it 'should send KILL signal and terminate process' do
|
|
187
|
+
@service.write_pid_file
|
|
188
|
+
Process.should_receive(:kill).with("KILL", 666).once
|
|
189
|
+
$stdout.should_receive(:write).twice # mute trace
|
|
190
|
+
@service.force_kill_and_remove_pid_file(666).should be_true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'should remove pid file on no such process exception' do
|
|
194
|
+
@service.write_pid_file
|
|
195
|
+
$stdout.should_receive(:write).exactly(4).times # mute trace
|
|
196
|
+
Process.should_receive(:kill).with("KILL", 666).once.and_raise(Errno::ESRCH)
|
|
197
|
+
@service.should_receive(:remove_pid_file)
|
|
198
|
+
@service.force_kill_and_remove_pid_file(666).should be_false
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: raad_totem
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Colin Surprenant
|
|
8
|
+
- Chad Remesch
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2014-03-15 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: totem
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ">="
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: 0.0.7
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - ">="
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: 0.0.7
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: bundler
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - "~>"
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '1.5'
|
|
35
|
+
type: :development
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - "~>"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '1.5'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: rake
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
type: :development
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: rspec
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - "~>"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 2.8.0
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 2.8.0
|
|
70
|
+
description: Easily create daemons in Totem projects (fork of Raad gem)
|
|
71
|
+
email:
|
|
72
|
+
- colin.surprenant@gmail.com
|
|
73
|
+
- chad@remesch.com
|
|
74
|
+
executables: []
|
|
75
|
+
extensions: []
|
|
76
|
+
extra_rdoc_files: []
|
|
77
|
+
files:
|
|
78
|
+
- ".gitignore"
|
|
79
|
+
- ".travis.yml"
|
|
80
|
+
- CHANGELOG.md
|
|
81
|
+
- Gemfile
|
|
82
|
+
- Gemfile.lock
|
|
83
|
+
- LICENSE.md
|
|
84
|
+
- README.md
|
|
85
|
+
- Rakefile
|
|
86
|
+
- lib/raad_totem.rb
|
|
87
|
+
- lib/raad_totem/bootstrap.rb
|
|
88
|
+
- lib/raad_totem/env.rb
|
|
89
|
+
- lib/raad_totem/runner.rb
|
|
90
|
+
- lib/raad_totem/shell_cmds/service.rb
|
|
91
|
+
- lib/raad_totem/signal_trampoline.rb
|
|
92
|
+
- lib/raad_totem/spoon.rb
|
|
93
|
+
- lib/raad_totem/unix_daemon.rb
|
|
94
|
+
- lib/raad_totem/version.rb
|
|
95
|
+
- raad.gemspec
|
|
96
|
+
- spec/raad_totem/unit/bootstrap_spec.rb
|
|
97
|
+
- spec/raad_totem/unit/env_spec.rb
|
|
98
|
+
- spec/raad_totem/unit/signal_trampoline_spec.rb
|
|
99
|
+
- spec/raad_totem/unit/spoon_spec.rb
|
|
100
|
+
- spec/raad_totem/unit/unix_daemon_spec.rb
|
|
101
|
+
- spec/raad_totem/unit/version_spec.rb
|
|
102
|
+
- spec/spec_helper.rb
|
|
103
|
+
homepage: https://github.com/chadrem/raad_totem
|
|
104
|
+
licenses:
|
|
105
|
+
- Apache
|
|
106
|
+
metadata: {}
|
|
107
|
+
post_install_message:
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubyforge_project:
|
|
123
|
+
rubygems_version: 2.2.2
|
|
124
|
+
signing_key:
|
|
125
|
+
specification_version: 4
|
|
126
|
+
summary: Easily create daemons in Totem projects (fork of Raad gem)
|
|
127
|
+
test_files:
|
|
128
|
+
- spec/raad_totem/unit/bootstrap_spec.rb
|
|
129
|
+
- spec/raad_totem/unit/env_spec.rb
|
|
130
|
+
- spec/raad_totem/unit/signal_trampoline_spec.rb
|
|
131
|
+
- spec/raad_totem/unit/spoon_spec.rb
|
|
132
|
+
- spec/raad_totem/unit/unix_daemon_spec.rb
|
|
133
|
+
- spec/raad_totem/unit/version_spec.rb
|
|
134
|
+
- spec/spec_helper.rb
|