raad 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +2 -0
- data/LICENSE.md +77 -0
- data/README.md +98 -0
- data/lib/raad.rb +7 -0
- data/lib/raad/configuration.rb +46 -0
- data/lib/raad/env.rb +54 -0
- data/lib/raad/logger.rb +64 -0
- data/lib/raad/runner.rb +185 -0
- data/lib/raad/service.rb +70 -0
- data/lib/raad/unix_daemon.rb +111 -0
- data/lib/raad/version.rb +3 -0
- metadata +97 -0
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Raad
|
2
|
+
|
3
|
+
Raad - Ruby as a Daemon lightweight service wrapper
|
4
|
+
Authored by Colin Surprenant, [@colinsurprenant][twitter], [colin.surprenant@needium.com][needium], [colin.surprenant@gmail.com][gmail], [http://github.com/colinsurprenant][github]
|
5
|
+
|
6
|
+
Copyright 2011 PraizedMedia Inc.
|
7
|
+
|
8
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
you may not use this file except in compliance with the License.
|
10
|
+
You may obtain a copy of the License at
|
11
|
+
|
12
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
|
14
|
+
Unless required by applicable law or agreed to in writing, software
|
15
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
17
|
+
See the License for the specific language governing permissions and
|
18
|
+
limitations under the License.
|
19
|
+
|
20
|
+
-------------------------------------------------------------------------------
|
21
|
+
|
22
|
+
Portions of this code are from the Thin project (https://github.com/macournoyer/thin) and under the following license:
|
23
|
+
Ruby License, http://www.ruby-lang.org/en/LICENSE.txt.
|
24
|
+
|
25
|
+
-------------------------------------------------------------------------------
|
26
|
+
|
27
|
+
Portions of this code are from the Goliath project (https://github.com/postrank-labs/goliath/) and under the following license:
|
28
|
+
Copyright (c) 2011 PostRank Inc.
|
29
|
+
|
30
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
31
|
+
of this software and associated documentation files (the "Software"), to
|
32
|
+
deal in the Software without restriction, including without limitation the
|
33
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
34
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
35
|
+
furnished to do so, subject to the following conditions:
|
36
|
+
|
37
|
+
The above copyright notice and this permission notice shall be included in
|
38
|
+
all copies or substantial portions of the Software.
|
39
|
+
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
42
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
43
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
44
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
45
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
46
|
+
|
47
|
+
-------------------------------------------------------------------------------
|
48
|
+
|
49
|
+
Portions of this code are from the Sinatra project (https://github.com/bmizerany/sinatra) and under the following license:
|
50
|
+
Copyright (c) 2007, 2008, 2009, 2010, 2011 Blake Mizerany
|
51
|
+
|
52
|
+
Permission is hereby granted, free of charge, to any person
|
53
|
+
obtaining a copy of this software and associated documentation
|
54
|
+
files (the "Software"), to deal in the Software without
|
55
|
+
restriction, including without limitation the rights to use,
|
56
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
57
|
+
copies of the Software, and to permit persons to whom the
|
58
|
+
Software is furnished to do so, subject to the following
|
59
|
+
conditions:
|
60
|
+
|
61
|
+
The above copyright notice and this permission notice shall be
|
62
|
+
included in all copies or substantial portions of the Software.
|
63
|
+
|
64
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
65
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
66
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
67
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
68
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
69
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
70
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
71
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
72
|
+
|
73
|
+
|
74
|
+
[needium]: colin.surprenant@needium.com
|
75
|
+
[gmail]: colin.surprenant@gmail.com
|
76
|
+
[twitter]: http://twitter.com/colinsurprenant
|
77
|
+
[github]: http://github.com/colinsurprenant
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Raad v0.3.1
|
2
|
+
|
3
|
+
Raad - Ruby as a Daemon lightweight service wrapper.
|
4
|
+
|
5
|
+
Raad is a non-intrusive, lightweight, simple Ruby daemon maker. Basically A simple class which implements
|
6
|
+
the start and stop methods, can be used seemslessy as a daemon or a normal console app.
|
7
|
+
|
8
|
+
Raad provides daemon control using the start/stop commands. Your code can optionnally use the Raad
|
9
|
+
logging module.
|
10
|
+
|
11
|
+
## Example
|
12
|
+
Create a class with a start and a stop method. Just by requiring 'raad', your class will be
|
13
|
+
wrapped by Raad and daemonizable.
|
14
|
+
|
15
|
+
require 'raad'
|
16
|
+
|
17
|
+
class SimpleDaemon
|
18
|
+
def start
|
19
|
+
@stopped = false
|
20
|
+
while !@stopped
|
21
|
+
Raad::Logger.info("simple_daemon running")
|
22
|
+
sleep(1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def stop
|
27
|
+
@stopped = true
|
28
|
+
Raad::Logger.info("simple_daemon stopped")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
run it in console mode, ^C will stop it, calling the stop method
|
33
|
+
$ ruby simple_daemon.rb start
|
34
|
+
|
35
|
+
run it daemonized, by default ./simple_daemon.log and ./simple_daemon.pid will be created
|
36
|
+
$ ruby simple_daemon.rb -d start
|
37
|
+
|
38
|
+
stop daemon, removing ./simple_daemon.pid
|
39
|
+
$ ruby simple_daemon.rb stop
|
40
|
+
|
41
|
+
## Documentation
|
42
|
+
|
43
|
+
### Supported rubies
|
44
|
+
Raad has only been tested on MRI 1.8 and 1.9.
|
45
|
+
|
46
|
+
### Command line options
|
47
|
+
usage: ruby <service>.rb [options] start|stop
|
48
|
+
|
49
|
+
Raad common options:
|
50
|
+
-e, --environment NAME set the execution environment (default: development)
|
51
|
+
-l, --log FILE log to file (default: in console mode: no, daemonized: <service>.log)
|
52
|
+
-s, --stdout log to stdout (default: in console mode: true, daemonized: false)
|
53
|
+
-c, --config FILE config file (default: ./config/<service>.rb)
|
54
|
+
-d, --daemonize run daemonized in the background (default: false)
|
55
|
+
-P, --pid FILE pid file when daemonized (default: <service>.pid)
|
56
|
+
-r, --redirect FILE redirect stdout to FILE when daemonized (default: no)
|
57
|
+
-n, --name NAME daemon process name (default: <service>)
|
58
|
+
-v, --verbose enable verbose logging (default: false)
|
59
|
+
-h, --help display help message
|
60
|
+
|
61
|
+
Note that the command line options will always override any config file settings if present.
|
62
|
+
### Config file usage
|
63
|
+
tbd.
|
64
|
+
|
65
|
+
### Logging
|
66
|
+
tbd.
|
67
|
+
|
68
|
+
### Adding custom command line options
|
69
|
+
tbd.
|
70
|
+
|
71
|
+
### Stop sequence details
|
72
|
+
tbd.
|
73
|
+
|
74
|
+
## TODO
|
75
|
+
- better doc, more examples
|
76
|
+
- specs
|
77
|
+
- JRuby support
|
78
|
+
|
79
|
+
## Dependencies
|
80
|
+
The Log4r gem (~> 1.1.9) is required.
|
81
|
+
|
82
|
+
## Author
|
83
|
+
Authored by Colin Surprenant, [@colinsurprenant][twitter], [colin.surprenant@needium.com][needium], [colin.surprenant@gmail.com][gmail], [http://github.com/colinsurprenant][github]
|
84
|
+
|
85
|
+
## Acknoledgements
|
86
|
+
Thanks to the Thin ([https://github.com/macournoyer/thin][thin]), Goliath ([https://github.com/postrank-labs/goliath/][goliath])
|
87
|
+
and Sinatra ([https://github.com/bmizerany/sinatra][sinatra]) projects for providing inspiration and/or code!
|
88
|
+
|
89
|
+
## License
|
90
|
+
Raada is distributed under the Apache License, Version 2.0. See the LICENSE.md file.
|
91
|
+
|
92
|
+
[needium]: colin.surprenant@needium.com
|
93
|
+
[gmail]: colin.surprenant@gmail.com
|
94
|
+
[twitter]: http://twitter.com/colinsurprenant
|
95
|
+
[github]: http://github.com/colinsurprenant
|
96
|
+
[thin]: https://github.com/macournoyer/thin
|
97
|
+
[goliath]: https://github.com/postrank-labs/goliath/
|
98
|
+
[sinatra]: https://github.com/bmizerany/sinatra
|
data/lib/raad.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Raad
|
2
|
+
module Configuration
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def self.init(&block)
|
6
|
+
instance_eval(&block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](key)
|
10
|
+
config[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
# Loads a configuration file and eval its content in the service object context
|
14
|
+
#
|
15
|
+
# @param file [String] The file to load, if not set will use ./config/{servive_name}
|
16
|
+
# @return [Nil]
|
17
|
+
def load(file = nil)
|
18
|
+
return unless File.exists?(file)
|
19
|
+
self.instance_eval(IO.read(file))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# cosmetic alias for config dsl
|
25
|
+
def configuration(&block)
|
26
|
+
Configuration.init(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def set(key, value)
|
30
|
+
config[key] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
@config ||= Hash.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(sym, *args)
|
38
|
+
if sym.to_s =~ /(.+)=$/
|
39
|
+
config[$1] = args.first
|
40
|
+
else
|
41
|
+
config[sym]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/raad/env.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module Raad
|
2
|
+
|
3
|
+
@env = :development
|
4
|
+
|
5
|
+
# Retrieves the current environment
|
6
|
+
#
|
7
|
+
# @return [Symbol] the current environment
|
8
|
+
def env
|
9
|
+
@env
|
10
|
+
end
|
11
|
+
|
12
|
+
# Sets the current environment
|
13
|
+
#
|
14
|
+
# @param [String or Symbol] env the environment [development|production|stage|test]
|
15
|
+
def env=(env)
|
16
|
+
case(env.to_s)
|
17
|
+
when 'dev', 'development' then @env = :development
|
18
|
+
when 'prod', 'production' then @env = :production
|
19
|
+
when 'stage', 'staging' then @env = :stage
|
20
|
+
when 'test' then @env = :test
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Determines if we are in the production environment
|
25
|
+
#
|
26
|
+
# @return [Boolean] true if current environemnt is production, false otherwise
|
27
|
+
def production?
|
28
|
+
@env == :production
|
29
|
+
end
|
30
|
+
|
31
|
+
# Determines if we are in the development environment
|
32
|
+
#
|
33
|
+
# @return [Boolean] true if current environemnt is development, false otherwise
|
34
|
+
def development?
|
35
|
+
@env == :development
|
36
|
+
end
|
37
|
+
|
38
|
+
# Determines if we are in the staging environment
|
39
|
+
#
|
40
|
+
# @return [Boolean] true if current environemnt is staging, false otherwise
|
41
|
+
def stage?
|
42
|
+
@env == :stage
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determines if we are in the test environment
|
46
|
+
#
|
47
|
+
# @return [Boolean] true if current environemnt is test, false otherwise
|
48
|
+
def test?
|
49
|
+
@env == :test
|
50
|
+
end
|
51
|
+
|
52
|
+
module_function :env, :env=, :production?, :development?, :stage?, :test?
|
53
|
+
|
54
|
+
end
|
data/lib/raad/logger.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'log4r'
|
2
|
+
|
3
|
+
module Raad
|
4
|
+
module Logger
|
5
|
+
|
6
|
+
extend self
|
7
|
+
|
8
|
+
# Sets up the logging for the runner
|
9
|
+
# @return [Logger] The logger object
|
10
|
+
def setup(options = {})
|
11
|
+
@log = Log4r::Logger.new('raad')
|
12
|
+
|
13
|
+
log_format = Log4r::PatternFormatter.new(:pattern => "[#{Process.pid}:%l] %d :: %m")
|
14
|
+
setup_file_logger(@log, log_format, options[:file]) if options[:file]
|
15
|
+
setup_stdout_logger(@log, log_format) if options[:stdout]
|
16
|
+
|
17
|
+
@verbose = !!options[:verbose]
|
18
|
+
|
19
|
+
@log.level = @verbose ? Log4r::DEBUG : Log4r::INFO
|
20
|
+
@log
|
21
|
+
end
|
22
|
+
|
23
|
+
def level=(l)
|
24
|
+
levels = {
|
25
|
+
:debug => Log4r::DEBUG,
|
26
|
+
:info => Log4r::INFO,
|
27
|
+
:warn => Log4r::WARN,
|
28
|
+
:error => Log4r::ERROR,
|
29
|
+
}
|
30
|
+
@log.level = @verbose ? Log4r::DEBUG : levels[l]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# setup file logging
|
36
|
+
#
|
37
|
+
# @param log [Logger] The logger to add file logging too
|
38
|
+
# @param log_format [Log4r::Formatter] The log format to use
|
39
|
+
# @return [Nil]
|
40
|
+
def setup_file_logger(log, log_format, file)
|
41
|
+
FileUtils.mkdir_p(File.dirname(file))
|
42
|
+
|
43
|
+
@log.add(Log4r::FileOutputter.new('fileOutput', {
|
44
|
+
:filename => file,
|
45
|
+
:trunc => false,
|
46
|
+
:formatter => log_format
|
47
|
+
}))
|
48
|
+
end
|
49
|
+
|
50
|
+
# setup stdout logging
|
51
|
+
#
|
52
|
+
# @param log [Logger] The logger to add stdout logging too
|
53
|
+
# @param log_format [Log4r::Formatter] The log format to use
|
54
|
+
# @return [Nil]
|
55
|
+
def setup_stdout_logger(log, log_format)
|
56
|
+
@log.add(Log4r::StdoutOutputter.new('console', :formatter => log_format))
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(sym, *args)
|
60
|
+
@log.send(sym, *args)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/raad/runner.rb
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Raad
|
5
|
+
class Runner
|
6
|
+
include Daemonizable
|
7
|
+
|
8
|
+
SECOND = 1
|
9
|
+
SOFT_STOP_TIMEOUT = 58 * SECOND
|
10
|
+
HARD_STOP_TIMEOUT = 60 * SECOND
|
11
|
+
|
12
|
+
# The pid file for the server
|
13
|
+
# @return [String] The file to write the servers pid file into
|
14
|
+
attr_accessor :pid_file
|
15
|
+
|
16
|
+
# The application
|
17
|
+
# @return [Object] The service to execute
|
18
|
+
attr_accessor :service
|
19
|
+
|
20
|
+
# The parsed options
|
21
|
+
# @return [Hash] The options parsed by the runner
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# Create a new Runner
|
25
|
+
#
|
26
|
+
# @param argv [Array] The command line arguments
|
27
|
+
# @param service [Object] The service to execute
|
28
|
+
def initialize(argv, service)
|
29
|
+
create_options_parser(service).parse!(argv)
|
30
|
+
|
31
|
+
options[:command] = argv[0].to_s.downcase
|
32
|
+
unless ['start', 'stop'].include?(options[:command])
|
33
|
+
puts(">> start|stop command is required")
|
34
|
+
exit!
|
35
|
+
end
|
36
|
+
|
37
|
+
@service = service
|
38
|
+
@service_name = nil
|
39
|
+
@logger_options = nil
|
40
|
+
@pid_file = nil
|
41
|
+
@service_thread = nil
|
42
|
+
@stopped = false
|
43
|
+
end
|
44
|
+
|
45
|
+
def run
|
46
|
+
# first load config if present
|
47
|
+
Configuration.load(options[:config] || File.expand_path("./config/#{default_service_name}.rb"))
|
48
|
+
|
49
|
+
# then set vars which depends on configuration
|
50
|
+
@service_name = options[:name] || Configuration.daemon_name || default_service_name
|
51
|
+
|
52
|
+
# ajust "dynamic defaults"
|
53
|
+
unless options[:log_file]
|
54
|
+
options[:log_file] = (options[:daemonize] ? File.expand_path("#{@service_name}.log") : nil)
|
55
|
+
end
|
56
|
+
unless options[:log_stdout]
|
57
|
+
options[:log_stdout] = !options[:daemonize]
|
58
|
+
end
|
59
|
+
|
60
|
+
@logger_options = {
|
61
|
+
:file => options.delete(:log_file),
|
62
|
+
:stdout => options.delete(:log_stdout),
|
63
|
+
:verbose => options.delete(:verbose),
|
64
|
+
}
|
65
|
+
@pid_file = options.delete(:pid_file) || "./#{@service_name}.pid"
|
66
|
+
|
67
|
+
# setup logging
|
68
|
+
Logger.setup(@logger_options)
|
69
|
+
Logger.level = Configuration.log_level if Configuration.log_level
|
70
|
+
|
71
|
+
if options[:command] == 'stop'
|
72
|
+
puts(">> Raad service wrapper v#{VERSION} stopping")
|
73
|
+
send_signal('TERM', HARD_STOP_TIMEOUT) # if not stopped afer HARD_STOP_TIMEOUT, SIGKILL will be sent
|
74
|
+
exit!
|
75
|
+
end
|
76
|
+
puts(">> Raad service wrapper v#{VERSION} starting")
|
77
|
+
|
78
|
+
Dir.chdir(File.expand_path(File.dirname("./"))) unless Raad.test?
|
79
|
+
|
80
|
+
jruby = (defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby')
|
81
|
+
raise("daemonize not supported in JRuby") if options[:daemonize] && jruby
|
82
|
+
|
83
|
+
options[:daemonize] ? daemonize(@service_name, options[:redirect]) {run_service} : run_service
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def default_service_name
|
89
|
+
service.class.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase!
|
90
|
+
end
|
91
|
+
|
92
|
+
# Create the options parser
|
93
|
+
#
|
94
|
+
# @return [OptionParser] Creates the options parser for the runner with the default options
|
95
|
+
def create_options_parser(service)
|
96
|
+
@options ||= {
|
97
|
+
:daemonize => false,
|
98
|
+
:verbose => false,
|
99
|
+
}
|
100
|
+
|
101
|
+
options_parser ||= OptionParser.new do |opts|
|
102
|
+
opts.banner = "usage: ruby <service>.rb [options] start|stop"
|
103
|
+
|
104
|
+
opts.separator ""
|
105
|
+
opts.separator "Raad common options:"
|
106
|
+
|
107
|
+
opts.on('-e', '--environment NAME', "set the execution environment (default: #{Raad.env.to_s})") { |val| Raad.env = val }
|
108
|
+
|
109
|
+
opts.on('-l', '--log FILE', "log to file (default: in console mode: no, daemonized: <service>.log)") { |file| @options[:log_file] = file }
|
110
|
+
opts.on('-s', '--stdout', "log to stdout (default: in console mode: true, daemonized: false)") { |v| @options[:log_stdout] = v }
|
111
|
+
|
112
|
+
opts.on('-c', '--config FILE', "config file (default: ./config/<service>.rb)") { |v| @options[:config] = v }
|
113
|
+
opts.on('-d', '--daemonize', "run daemonized in the background (default: #{@options[:daemonize]})") { |v| @options[:daemonize] = v }
|
114
|
+
opts.on('-P', '--pid FILE', "pid file when daemonized (default: <service>.pid)") { |file| @options[:pid_file] = file }
|
115
|
+
opts.on('-r', '--redirect FILE', "redirect stdout to FILE when daemonized (default: no)") { |v| @options[:redirect] = v }
|
116
|
+
opts.on('-n', '--name NAME', "daemon process name (default: <service>)") { |v| @options[:name] = v }
|
117
|
+
opts.on('-v', '--verbose', "enable verbose logging (default: #{@options[:verbose]})") { |v| @options[:verbose] = v }
|
118
|
+
|
119
|
+
opts.on('-h', '--help', 'display help message') { show_options(opts) }
|
120
|
+
end
|
121
|
+
service.respond_to?(:options_parser) ? service.options_parser(options_parser) : options_parser
|
122
|
+
end
|
123
|
+
|
124
|
+
# Output the servers options
|
125
|
+
#
|
126
|
+
# @param opts [OptionsParser] The options parser
|
127
|
+
# @return [exit] This will exit Ruby
|
128
|
+
def show_options(opts)
|
129
|
+
puts(opts)
|
130
|
+
exit!
|
131
|
+
end
|
132
|
+
|
133
|
+
# Run the server
|
134
|
+
#
|
135
|
+
# @return [Nil]
|
136
|
+
def run_service
|
137
|
+
Logger.info("starting #{@service_name} service in #{Raad.env.to_s} mode")
|
138
|
+
|
139
|
+
at_exit do
|
140
|
+
Logger.info(">> Raad service wrapper stopped")
|
141
|
+
end
|
142
|
+
|
143
|
+
# by default exit on SIGTERM and SIGINT
|
144
|
+
[:INT, :TERM, :QUIT].each do |sig|
|
145
|
+
trap(sig) {stop_service}
|
146
|
+
end
|
147
|
+
|
148
|
+
@service_thread = Thread.new do
|
149
|
+
Thread.current.abort_on_exception = true
|
150
|
+
service.start
|
151
|
+
end
|
152
|
+
while @service_thread.join(SECOND).nil?
|
153
|
+
if @stopped
|
154
|
+
Logger.info("stopping #{@service_name} service")
|
155
|
+
service.stop if service.respond_to?(:stop)
|
156
|
+
wait_or_kill_service
|
157
|
+
return
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
unless @stopped
|
162
|
+
Logger.info("stopping #{@service_name} service")
|
163
|
+
service.stop if service.respond_to?(:stop)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def stop_service
|
168
|
+
@stopped = true
|
169
|
+
end
|
170
|
+
|
171
|
+
def wait_or_kill_service
|
172
|
+
try = 0; join = nil
|
173
|
+
while (try += 1) <= SOFT_STOP_TIMEOUT && join.nil? do
|
174
|
+
join = @service_thread.join(SECOND)
|
175
|
+
Logger.debug("waiting for service to stop #{try}/#{SOFT_STOP_TIMEOUT}") if join.nil?
|
176
|
+
end
|
177
|
+
if join.nil?
|
178
|
+
Logger.error("stop timeout exhausted, killing service thread")
|
179
|
+
@service_thread.kill
|
180
|
+
@service_thread.join
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
data/lib/raad/service.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Raad
|
2
|
+
|
3
|
+
# The main execution class for Raad. This will execute in the at_exit
|
4
|
+
# handler to run the server.
|
5
|
+
class Service
|
6
|
+
|
7
|
+
# Set of caller regex's to be skippe when looking for our API file
|
8
|
+
CALLERS_TO_IGNORE = [ # :nodoc:
|
9
|
+
/\/raad(\/(service))?\.rb$/, # all raad code
|
10
|
+
/rubygems\/custom_require\.rb$/, # rubygems require hacks
|
11
|
+
/bundler(\/runtime)?\.rb/, # bundler require hacks
|
12
|
+
/<internal:/ # internal in ruby >= 1.9.2
|
13
|
+
]
|
14
|
+
|
15
|
+
# @todo add rubinius (and hopefully other VM impls) ignore patterns ...
|
16
|
+
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
|
17
|
+
|
18
|
+
# Like Kernel#caller but excluding certain magic entries and without
|
19
|
+
# line / method information; the resulting array contains filenames only.
|
20
|
+
def self.caller_files
|
21
|
+
caller_locations.map { |file, line| file }
|
22
|
+
end
|
23
|
+
|
24
|
+
# Like caller_files, but containing Arrays rather than strings with the
|
25
|
+
# first element being the file, and the second being the line.
|
26
|
+
def self.caller_locations
|
27
|
+
caller(1).
|
28
|
+
map { |line| line.split(/:(?=\d|in )/)[0,2] }.
|
29
|
+
reject { |file, line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Find the service_file that was used to execute the service
|
33
|
+
#
|
34
|
+
# @return [String] The service file
|
35
|
+
def self.service_file
|
36
|
+
c = caller_files.first
|
37
|
+
c = $0 if !c || c.empty?
|
38
|
+
c
|
39
|
+
end
|
40
|
+
|
41
|
+
# Execute the service
|
42
|
+
#
|
43
|
+
# @return [Nil]
|
44
|
+
def self.run!
|
45
|
+
file = File.basename(service_file, '.rb')
|
46
|
+
service = Object.module_eval(camel_case(file)).new
|
47
|
+
|
48
|
+
runner = Raad::Runner.new(ARGV, service)
|
49
|
+
runner.run
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Convert a string to camel case
|
55
|
+
#
|
56
|
+
# @param str [String] The string to convert
|
57
|
+
# @return [String] The camel cased string
|
58
|
+
def self.camel_case(str)
|
59
|
+
return str if str !~ /_/ && str =~ /[A-Z]+.*/
|
60
|
+
|
61
|
+
str.split('_').map { |e| e.capitalize }.join
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
at_exit do
|
66
|
+
if $!.nil? && $0 == Raad::Service.service_file
|
67
|
+
Service.run!
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Process
|
5
|
+
|
6
|
+
def running?(pid)
|
7
|
+
Process.getpgid(pid) != -1
|
8
|
+
rescue Errno::EPERM
|
9
|
+
true
|
10
|
+
rescue Errno::ESRCH
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
module_function :running?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Raised when the pid file already exist starting as a daemon.
|
18
|
+
class PidFileExist < RuntimeError; end
|
19
|
+
|
20
|
+
# module Daemonizable requires that the including class defines the @pid_file instance variable
|
21
|
+
module Daemonizable
|
22
|
+
|
23
|
+
def pid
|
24
|
+
File.exist?(@pid_file) ? open(@pid_file).read.to_i : nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def daemonize(name, stdout_file)
|
28
|
+
remove_stale_pid_file
|
29
|
+
pwd = Dir.pwd
|
30
|
+
|
31
|
+
# do the double fork dance
|
32
|
+
Process.fork do
|
33
|
+
Process.setsid
|
34
|
+
exit if fork
|
35
|
+
$0 = name # set process name
|
36
|
+
|
37
|
+
File.umask(0000) # file mode creation mask to 000 to allow creation of files with any required permission late
|
38
|
+
Dir.chdir(pwd)
|
39
|
+
write_pid_file
|
40
|
+
|
41
|
+
# redirect stdout into a log
|
42
|
+
STDIN.reopen('/dev/null')
|
43
|
+
stdout_file ? STDOUT.reopen(stdout_file, "a") : STDOUT.reopen('/dev/null', 'a')
|
44
|
+
STDERR.reopen(STDOUT)
|
45
|
+
|
46
|
+
at_exit do
|
47
|
+
remove_pid_file
|
48
|
+
end
|
49
|
+
|
50
|
+
yield
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def send_signal(signal, timeout = 60)
|
55
|
+
if pid = read_pid_file
|
56
|
+
puts(">> sending #{signal} signal to process #{pid}")
|
57
|
+
Process.kill(signal, pid)
|
58
|
+
Timeout.timeout(timeout) do
|
59
|
+
sleep 0.1 while Process.running?(pid)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
puts(">> can't stop process, no pid found in #{@pid_file}")
|
63
|
+
end
|
64
|
+
rescue Timeout::Error
|
65
|
+
force_kill
|
66
|
+
rescue Interrupt
|
67
|
+
force_kill
|
68
|
+
rescue Errno::ESRCH # No such process
|
69
|
+
force_kill
|
70
|
+
end
|
71
|
+
|
72
|
+
def force_kill
|
73
|
+
if pid = read_pid_file
|
74
|
+
puts(">> sending KILL signal to process #{pid}")
|
75
|
+
Process.kill("KILL", pid)
|
76
|
+
File.delete(@pid_file) if File.exist?(@pid_file)
|
77
|
+
else
|
78
|
+
puts(">> can't stop process, no pid found in #{@pid_file}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def read_pid_file
|
83
|
+
if File.file?(@pid_file) && pid = File.read(@pid_file)
|
84
|
+
pid.to_i
|
85
|
+
else
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def remove_pid_file
|
91
|
+
File.delete(@pid_file) if @pid_file && File.exists?(@pid_file)
|
92
|
+
end
|
93
|
+
|
94
|
+
def write_pid_file
|
95
|
+
puts(">> writing pid to #{@pid_file}")
|
96
|
+
open(@pid_file,"w") { |f| f.write(Process.pid) }
|
97
|
+
File.chmod(0644, @pid_file)
|
98
|
+
end
|
99
|
+
|
100
|
+
def remove_stale_pid_file
|
101
|
+
if File.exist?(@pid_file)
|
102
|
+
if pid && Process.running?(pid)
|
103
|
+
raise PidFileExist, "#{@pid_file} exists and process #{pid} is runnig. stop the process or delete #{@pid_file}"
|
104
|
+
else
|
105
|
+
puts(">> deleting stale pid file #{@pid_file}")
|
106
|
+
remove_pid_file
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/lib/raad/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: raad
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.3.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Colin Surprenant
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-24 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rubyforge
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :development
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rspec
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ~>
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 2.5.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: log4r
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 1.1.9
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id003
|
48
|
+
description: Ruby as a Daemon lightweight service wrapper
|
49
|
+
email:
|
50
|
+
- colin.surprenant@gmail.com
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- lib/raad/configuration.rb
|
59
|
+
- lib/raad/env.rb
|
60
|
+
- lib/raad/logger.rb
|
61
|
+
- lib/raad/runner.rb
|
62
|
+
- lib/raad/service.rb
|
63
|
+
- lib/raad/unix_daemon.rb
|
64
|
+
- lib/raad/version.rb
|
65
|
+
- lib/raad.rb
|
66
|
+
- README.md
|
67
|
+
- CHANGELOG.md
|
68
|
+
- LICENSE.md
|
69
|
+
homepage: http://github.com/praized/raad
|
70
|
+
licenses: []
|
71
|
+
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.3.0
|
89
|
+
requirements: []
|
90
|
+
|
91
|
+
rubyforge_project: raad
|
92
|
+
rubygems_version: 1.8.9
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Ruby as a Daemon
|
96
|
+
test_files: []
|
97
|
+
|