raad 0.3.1 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,2 +1,8 @@
1
1
  # 0.3.1, 08-24-2011
2
- - initial release. support for MRI 1.8 & 1.9 only
2
+ - initial release. support for MRI 1.8 & 1.9 only
3
+
4
+ # 0.3.3, 08-26-2011
5
+ - reworked runner stop sequence & signals trapping
6
+ - force kill + pid removal bug
7
+ - wrong exit code bug
8
+ - comments & cosmetics
data/README.md CHANGED
@@ -1,13 +1,25 @@
1
- # Raad v0.3.1
1
+ # Raad v0.3.3
2
2
 
3
- Raad - Ruby as a Daemon lightweight service wrapper.
3
+ Raad - Ruby as a daemon lightweight service wrapper.
4
4
 
5
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.
6
+ the start and stop methods, can be used seamlessly as a daemon or a normal console app.
7
7
 
8
8
  Raad provides daemon control using the start/stop commands. Your code can optionnally use the Raad
9
9
  logging module.
10
10
 
11
+ ## Installation
12
+
13
+ ### Gem
14
+ gem install raad
15
+
16
+ ### Bundler
17
+ #### Latest from github
18
+ gem "raad", :git => "git://github.com/praized/raad.git", :branch => "master"
19
+
20
+ #### Released gem
21
+ gem "raad", "~> 0.3.3"
22
+
11
23
  ## Example
12
24
  Create a class with a start and a stop method. Just by requiring 'raad', your class will be
13
25
  wrapped by Raad and daemonizable.
@@ -82,12 +94,12 @@ The Log4r gem (~> 1.1.9) is required.
82
94
  ## Author
83
95
  Authored by Colin Surprenant, [@colinsurprenant][twitter], [colin.surprenant@needium.com][needium], [colin.surprenant@gmail.com][gmail], [http://github.com/colinsurprenant][github]
84
96
 
85
- ## Acknoledgements
97
+ ## Acknowledgements
86
98
  Thanks to the Thin ([https://github.com/macournoyer/thin][thin]), Goliath ([https://github.com/postrank-labs/goliath/][goliath])
87
99
  and Sinatra ([https://github.com/bmizerany/sinatra][sinatra]) projects for providing inspiration and/or code!
88
100
 
89
101
  ## License
90
- Raada is distributed under the Apache License, Version 2.0. See the LICENSE.md file.
102
+ Raad is distributed under the Apache License, Version 2.0. See the LICENSE.md file.
91
103
 
92
104
  [needium]: colin.surprenant@needium.com
93
105
  [gmail]: colin.surprenant@gmail.com
data/lib/raad/runner.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'optparse'
2
2
  require 'thread'
3
+ require 'monitor'
3
4
 
4
5
  module Raad
5
6
  class Runner
@@ -9,54 +10,46 @@ module Raad
9
10
  SOFT_STOP_TIMEOUT = 58 * SECOND
10
11
  HARD_STOP_TIMEOUT = 60 * SECOND
11
12
 
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
13
+ attr_accessor :service, :pid_file, :options
23
14
 
24
15
  # Create a new Runner
25
16
  #
26
- # @param argv [Array] The command line arguments
27
- # @param service [Object] The service to execute
17
+ # @param argv [Array] command line arguments
18
+ # @param service [Object] service to execute
28
19
  def initialize(argv, service)
29
20
  create_options_parser(service).parse!(argv)
30
21
 
31
- options[:command] = argv[0].to_s.downcase
22
+ # start/stop
23
+ @options[:command] = argv[0].to_s.downcase
32
24
  unless ['start', 'stop'].include?(options[:command])
33
25
  puts(">> start|stop command is required")
34
- exit!
26
+ exit!(false)
35
27
  end
36
28
 
37
29
  @service = service
38
30
  @service_name = nil
39
31
  @logger_options = nil
40
32
  @pid_file = nil
41
- @service_thread = nil
42
- @stopped = false
33
+
34
+ # signals handling
35
+ @signals = []
36
+ @monitor = Monitor.new
37
+ @stop_signaled = false
43
38
  end
44
39
 
45
40
  def run
46
41
  # first load config if present
47
42
  Configuration.load(options[:config] || File.expand_path("./config/#{default_service_name}.rb"))
48
43
 
49
- # then set vars which depends on configuration
44
+ # settings which depends on configuration
50
45
  @service_name = options[:name] || Configuration.daemon_name || default_service_name
51
46
 
52
- # ajust "dynamic defaults"
53
47
  unless options[:log_file]
54
48
  options[:log_file] = (options[:daemonize] ? File.expand_path("#{@service_name}.log") : nil)
55
49
  end
56
50
  unless options[:log_stdout]
57
51
  options[:log_stdout] = !options[:daemonize]
58
52
  end
59
-
60
53
  @logger_options = {
61
54
  :file => options.delete(:log_file),
62
55
  :stdout => options.delete(:log_stdout),
@@ -64,15 +57,17 @@ module Raad
64
57
  }
65
58
  @pid_file = options.delete(:pid_file) || "./#{@service_name}.pid"
66
59
 
60
+ # check for stop command, @pid_file must be set
61
+ if options[:command] == 'stop'
62
+ puts(">> Raad service wrapper v#{VERSION} stopping")
63
+ success = send_signal('TERM', HARD_STOP_TIMEOUT) # if not stopped afer HARD_STOP_TIMEOUT, SIGKILL will be sent
64
+ exit!(success)
65
+ end
66
+
67
67
  # setup logging
68
68
  Logger.setup(@logger_options)
69
69
  Logger.level = Configuration.log_level if Configuration.log_level
70
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
71
  puts(">> Raad service wrapper v#{VERSION} starting")
77
72
 
78
73
  Dir.chdir(File.expand_path(File.dirname("./"))) unless Raad.test?
@@ -85,6 +80,87 @@ module Raad
85
80
 
86
81
  private
87
82
 
83
+ # Run the service
84
+ #
85
+ # @return nil
86
+ def run_service
87
+ Logger.info("starting #{@service_name} service in #{Raad.env.to_s} mode")
88
+
89
+ at_exit do
90
+ Logger.info(">> Raad service wrapper stopped")
91
+ end
92
+
93
+ # store received signals into the @signal queue. here we want to avoid
94
+ # doing anything complex from within the trap block.
95
+ [:INT, :TERM, :QUIT].each do |sig|
96
+ trap(sig) {@monitor.synchronize{@signals << :STOP}}
97
+ end
98
+
99
+ # launch the signal handler thread. the idea here is to handle signal outside the
100
+ # trap block. the trap block will simply store the signal which this thread will
101
+ # retrieve and do whatever is required.
102
+ signals_thread = Thread.new do
103
+ Thread.current.abort_on_exception = true
104
+ loop do
105
+ signals = @monitor.synchronize{s = @signals.dup; @signals.clear; s}
106
+
107
+ if signals.include?(:STOP)
108
+ @stop_signaled = true
109
+ stop_service
110
+ break
111
+ end
112
+
113
+ sleep(0.5)
114
+ end
115
+ end
116
+
117
+ # launch the service thread and call start. we expect start not to return
118
+ # unless it is done or has been stopped.
119
+ service_thread = Thread.new do
120
+ Thread.current.abort_on_exception = true
121
+ service.start
122
+ stop_service unless @stop_signaled # don't stop twice if already called from the signals_thread
123
+ end
124
+
125
+ # use exit and not exit! to make sure the at_exit hooks are called, like the pid cleanup, etc.
126
+ exit(wait_or_kill(service_thread))
127
+ end
128
+
129
+ def stop_service
130
+ Logger.info("stopping #{@service_name} service")
131
+ service.stop if service.respond_to?(:stop)
132
+ end
133
+
134
+ # try to do a timeout join periodically on the given thread. if the join succeed then the stop
135
+ # sequence is successful and return true.
136
+ # Otherwise, on timeout if stop has beed signaled, wait a maximum of SOFT_STOP_TIMEOUT on the
137
+ # thread and kill it if the timeout is reached and return false in that case.
138
+ #
139
+ # @return [Boolean] true if the thread normally terminated, false if a kill was necessary
140
+ def wait_or_kill(thread)
141
+ while thread.join(SECOND).nil?
142
+ # the join has timed out, thread is still buzzy.
143
+ if @stop_signaled
144
+ # but if stop has been signalled, start "the final countdown" ♫
145
+ try = 0; join = nil
146
+ while (try += 1) <= SOFT_STOP_TIMEOUT && join.nil? do
147
+ join = thread.join(SECOND)
148
+ Logger.debug("waiting for service to stop #{try}/#{SOFT_STOP_TIMEOUT}") if join.nil?
149
+ end
150
+ if join.nil?
151
+ Logger.error("stop timeout exhausted, killing service thread")
152
+ thread.kill
153
+ return false
154
+ end
155
+ return true
156
+ end
157
+ end
158
+ true
159
+ end
160
+
161
+ # convert the service class name from CameCase to underscore
162
+ #
163
+ # @return [String] underscored service class name
88
164
  def default_service_name
89
165
  service.class.to_s.split('::').last.gsub(/(.)([A-Z])/,'\1_\2').downcase!
90
166
  end
@@ -121,64 +197,12 @@ module Raad
121
197
  service.respond_to?(:options_parser) ? service.options_parser(options_parser) : options_parser
122
198
  end
123
199
 
124
- # Output the servers options
200
+ # Output the servers options and exit Ruby
125
201
  #
126
202
  # @param opts [OptionsParser] The options parser
127
- # @return [exit] This will exit Ruby
128
203
  def show_options(opts)
129
204
  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
205
+ exit!(false)
182
206
  end
183
207
 
184
208
  end
@@ -58,25 +58,36 @@ module Daemonizable
58
58
  Timeout.timeout(timeout) do
59
59
  sleep 0.1 while Process.running?(pid)
60
60
  end
61
+ true
61
62
  else
62
63
  puts(">> can't stop process, no pid found in #{@pid_file}")
64
+ false
63
65
  end
64
66
  rescue Timeout::Error
65
- force_kill
67
+ force_kill_and_remove_pid_file
66
68
  rescue Interrupt
67
- force_kill
69
+ force_kill_and_remove_pid_file
68
70
  rescue Errno::ESRCH # No such process
69
- force_kill
71
+ puts(">> can't stop process, no such process #{pid}")
72
+ remove_pid_file
73
+ false
70
74
  end
71
75
 
72
- def force_kill
73
- if pid = read_pid_file
76
+ def force_kill_and_remove_pid_file
77
+ success = if (pid = read_pid_file)
74
78
  puts(">> sending KILL signal to process #{pid}")
75
79
  Process.kill("KILL", pid)
76
- File.delete(@pid_file) if File.exist?(@pid_file)
80
+ true
77
81
  else
78
82
  puts(">> can't stop process, no pid found in #{@pid_file}")
83
+ false
79
84
  end
85
+ remove_pid_file
86
+ success
87
+ rescue Errno::ESRCH # No such process
88
+ puts(">> can't stop process, no such process #{pid}")
89
+ remove_pid_file
90
+ false
80
91
  end
81
92
 
82
93
  def read_pid_file
data/lib/raad/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Raad
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.3"
3
3
  end unless defined?(Raad::VERSION)
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: raad
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.3.1
5
+ version: 0.3.3
6
6
  platform: ruby
7
7
  authors:
8
8
  - Colin Surprenant
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-08-24 00:00:00 Z
13
+ date: 2011-08-26 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rubyforge