raad 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md 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