rspec-background-process 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +76 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +41 -0
- data/VERSION +1 -0
- data/lib/rspec-background-process.rb +4 -0
- data/lib/rspec-background-process/background_process.rb +416 -0
- data/lib/rspec-background-process/background_process_helpers.rb +95 -0
- data/lib/rspec-background-process/process_pool.rb +356 -0
- data/lib/rspec-background-process/readiness_checks.rb +48 -0
- data/lib/rspec-background-process/refresh_actions.rb +15 -0
- data/lib/rspec-background-process/server.rb +49 -0
- data/rspec-background-process.gemspec +96 -0
- data/spec/background_process_helpers_spec.rb +32 -0
- data/spec/background_process_spec.rb +90 -0
- data/spec/features/cwd_spec.rb +51 -0
- data/spec/features/dead_detection_spec.rb +81 -0
- data/spec/features/exec_and_loading_spec.rb +26 -0
- data/spec/features/pool_lru_spec.rb +23 -0
- data/spec/features/ready_test_spec.rb +48 -0
- data/spec/features/refresh_spec.rb +46 -0
- data/spec/features/reuse_spec.rb +81 -0
- data/spec/features/server_spec.rb +35 -0
- data/spec/features/variables_replacement_spec.rb +68 -0
- data/spec/process_definition_spec.rb +182 -0
- data/spec/spec_helper.rb +102 -0
- data/spec/support/test_die +3 -0
- data/spec/support/test_http_server +28 -0
- data/spec/support/test_process +16 -0
- data/spec/support/test_slow_die +7 -0
- metadata +232 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem 'faraday', '>= 0.8'
|
5
|
+
gem 'rspec', '~> 3.1'
|
6
|
+
gem 'jeweler', '~> 1.8.4'
|
7
|
+
gem 'bundler', '~> 1.0'
|
8
|
+
gem 'daemon', '~> 1.2'
|
9
|
+
gem 'micromachine', '~> 1.1'
|
10
|
+
gem 'rufus-lru', '~> 1.0'
|
11
|
+
gem 'file-tail', '~> 1.0'
|
12
|
+
gem 'retries', '~> 0.0.5'
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.3.6)
|
5
|
+
builder (3.2.2)
|
6
|
+
daemon (1.2.0)
|
7
|
+
diff-lcs (1.2.5)
|
8
|
+
faraday (0.8.9)
|
9
|
+
multipart-post (~> 1.2.0)
|
10
|
+
file-tail (1.0.12)
|
11
|
+
tins (~> 0.5)
|
12
|
+
git (1.2.8)
|
13
|
+
github_api (0.10.1)
|
14
|
+
addressable
|
15
|
+
faraday (~> 0.8.1)
|
16
|
+
hashie (>= 1.2)
|
17
|
+
multi_json (~> 1.4)
|
18
|
+
nokogiri (~> 1.5.2)
|
19
|
+
oauth2
|
20
|
+
hashie (3.3.1)
|
21
|
+
highline (1.6.21)
|
22
|
+
jeweler (1.8.8)
|
23
|
+
builder
|
24
|
+
bundler (~> 1.0)
|
25
|
+
git (>= 1.2.5)
|
26
|
+
github_api (= 0.10.1)
|
27
|
+
highline (>= 1.6.15)
|
28
|
+
nokogiri (= 1.5.10)
|
29
|
+
rake
|
30
|
+
rdoc
|
31
|
+
json (1.8.1)
|
32
|
+
jwt (1.0.0)
|
33
|
+
micromachine (1.1.0)
|
34
|
+
multi_json (1.10.1)
|
35
|
+
multi_xml (0.5.5)
|
36
|
+
multipart-post (1.2.0)
|
37
|
+
nokogiri (1.5.10)
|
38
|
+
oauth2 (1.0.0)
|
39
|
+
faraday (>= 0.8, < 0.10)
|
40
|
+
jwt (~> 1.0)
|
41
|
+
multi_json (~> 1.3)
|
42
|
+
multi_xml (~> 0.5)
|
43
|
+
rack (~> 1.2)
|
44
|
+
rack (1.5.2)
|
45
|
+
rake (10.3.2)
|
46
|
+
rdoc (4.1.1)
|
47
|
+
json (~> 1.4)
|
48
|
+
retries (0.0.5)
|
49
|
+
rspec (3.1.0)
|
50
|
+
rspec-core (~> 3.1.0)
|
51
|
+
rspec-expectations (~> 3.1.0)
|
52
|
+
rspec-mocks (~> 3.1.0)
|
53
|
+
rspec-core (3.1.2)
|
54
|
+
rspec-support (~> 3.1.0)
|
55
|
+
rspec-expectations (3.1.0)
|
56
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
57
|
+
rspec-support (~> 3.1.0)
|
58
|
+
rspec-mocks (3.1.0)
|
59
|
+
rspec-support (~> 3.1.0)
|
60
|
+
rspec-support (3.1.0)
|
61
|
+
rufus-lru (1.0.5)
|
62
|
+
tins (0.13.2)
|
63
|
+
|
64
|
+
PLATFORMS
|
65
|
+
ruby
|
66
|
+
|
67
|
+
DEPENDENCIES
|
68
|
+
bundler (~> 1.0)
|
69
|
+
daemon (~> 1.2)
|
70
|
+
faraday (>= 0.8)
|
71
|
+
file-tail (~> 1.0)
|
72
|
+
jeweler (~> 1.8.4)
|
73
|
+
micromachine (~> 1.1)
|
74
|
+
retries (~> 0.0.5)
|
75
|
+
rspec (~> 3.1)
|
76
|
+
rufus-lru (~> 1.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2014 Global Medical Treatment Ltd trading as WhatClinic.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
= rspec-background-process
|
2
|
+
|
3
|
+
See features.
|
4
|
+
|
5
|
+
== Contributing to rspec-background-process
|
6
|
+
|
7
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
8
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
9
|
+
* Fork the project.
|
10
|
+
* Start a feature/bugfix branch.
|
11
|
+
* Commit and push until you are happy with your contribution.
|
12
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
13
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2014 Jakub Pastuszek. See LICENSE.txt for
|
18
|
+
further details.
|
19
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "rspec-background-process"
|
18
|
+
gem.homepage = "http://github.com/jpastuszek/rspec-background-process"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = "RSpec and Cucumber DSL library that helps managing background processes during test runs"
|
21
|
+
gem.description = "RSpec and Cucumber DSL that allows definition of processes with their arguments, working directory, time outs, port numbers etc. and start/stop them during test runs. Processes with same definitions can pooled and reused between example runs to save time on startup/shutdown. Pooling supports process number limiting with LRU to limit memory used."
|
22
|
+
gem.email = "jpastuszek@gmail.com"
|
23
|
+
gem.authors = ["Jakub Pastuszek"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec)
|
30
|
+
|
31
|
+
task :default => :spec
|
32
|
+
|
33
|
+
require 'rdoc/task'
|
34
|
+
Rake::RDocTask.new do |rdoc|
|
35
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
36
|
+
|
37
|
+
rdoc.rdoc_dir = 'rdoc'
|
38
|
+
rdoc.title = "rspec-background-process #{version}"
|
39
|
+
rdoc.rdoc_files.include('README*')
|
40
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
41
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,416 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'pathname'
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'daemon'
|
5
|
+
require 'shellwords'
|
6
|
+
require 'thwait'
|
7
|
+
require 'micromachine'
|
8
|
+
|
9
|
+
module RSpecBackgroundProcess
|
10
|
+
class BackgroundProcess
|
11
|
+
class ProcessExitedError < RuntimeError
|
12
|
+
def initialize(process, exit_code)
|
13
|
+
super "process #{process} exited with exit code: #{exit_code}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class ProcessReadyFailedError < RuntimeError
|
18
|
+
def initialize(process)
|
19
|
+
super "process #{process} readiness check failed"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ProcessReadyTimeOutError < Timeout::Error
|
24
|
+
def initialize(process)
|
25
|
+
super "process #{process} failed to start in time"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ProcessRunAwayError < RuntimeError
|
30
|
+
def initialize(process, pid)
|
31
|
+
super "process #{process} could not be stopped; pid: #{pid}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class StateError < RuntimeError
|
36
|
+
def initialize(process, action, state)
|
37
|
+
super "process #{process} can't #{action} when in state: #{state}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(name, cmd, args = [], working_directory = nil, options = {})
|
42
|
+
@name = name
|
43
|
+
|
44
|
+
@exec = (Pathname.new(Dir.pwd) + cmd).cleanpath.to_s
|
45
|
+
@args = args.map(&:to_s)
|
46
|
+
|
47
|
+
@pid = nil
|
48
|
+
@process = nil
|
49
|
+
|
50
|
+
@state_log = []
|
51
|
+
|
52
|
+
case working_directory
|
53
|
+
when Array
|
54
|
+
working_directory = Dir.mktmpdir(working_directory)
|
55
|
+
when nil
|
56
|
+
working_directory = Dir.mktmpdir(name.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
@working_directory = Pathname.new(working_directory.to_s)
|
60
|
+
@working_directory.directory? or @working_directory.mkdir
|
61
|
+
|
62
|
+
@pid_file = @working_directory + "#{@name}.pid"
|
63
|
+
@log_file = @working_directory + "out.log"
|
64
|
+
|
65
|
+
@options = options
|
66
|
+
reset_options(options)
|
67
|
+
|
68
|
+
@fsm_lock = Mutex.new
|
69
|
+
|
70
|
+
@_fsm = MicroMachine.new(:not_running)
|
71
|
+
|
72
|
+
@state_change_time = Time.now.to_f
|
73
|
+
@after_state_change = []
|
74
|
+
|
75
|
+
@_fsm.on(:any) do
|
76
|
+
@state_change_time = Time.now.to_f
|
77
|
+
puts "process is now #{@_fsm.state}"
|
78
|
+
@after_state_change.each{|callback| callback.call(@_fsm.state)}
|
79
|
+
end
|
80
|
+
|
81
|
+
@_fsm.when(:starting,
|
82
|
+
not_running: :starting
|
83
|
+
)
|
84
|
+
|
85
|
+
@_fsm.on(:starting) do
|
86
|
+
puts "starting: `#{command}`"
|
87
|
+
puts "working directory: #{@working_directory}"
|
88
|
+
puts "log file: #{@log_file}"
|
89
|
+
end
|
90
|
+
|
91
|
+
@_fsm.when(:started,
|
92
|
+
starting: :running
|
93
|
+
)
|
94
|
+
@_fsm.on(:running) do
|
95
|
+
puts "running with pid: #{@pid}"
|
96
|
+
end
|
97
|
+
|
98
|
+
@_fsm.when(:stopped,
|
99
|
+
running: :not_running,
|
100
|
+
ready: :not_running
|
101
|
+
)
|
102
|
+
|
103
|
+
@_fsm.when(:died,
|
104
|
+
starting: :dead,
|
105
|
+
running: :dead,
|
106
|
+
ready: :dead
|
107
|
+
)
|
108
|
+
|
109
|
+
# it is topped before marked failed
|
110
|
+
@_fsm.when(:failed,
|
111
|
+
not_running: :failed
|
112
|
+
)
|
113
|
+
|
114
|
+
@_fsm.when(:verified,
|
115
|
+
running: :ready,
|
116
|
+
ready: :ready,
|
117
|
+
)
|
118
|
+
@_fsm.when(:run_away,
|
119
|
+
running: :jammed,
|
120
|
+
ready: :jammed
|
121
|
+
)
|
122
|
+
|
123
|
+
@template_renderer = options[:template_renderer]
|
124
|
+
|
125
|
+
# make sure we stop on exit
|
126
|
+
my_pid = Process.pid
|
127
|
+
at_exit do
|
128
|
+
stop if Process.pid == my_pid and running? #only run in master process
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def render(str)
|
133
|
+
if @template_renderer
|
134
|
+
@template_renderer.call(template_variables, str)
|
135
|
+
else
|
136
|
+
str
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def template_variables
|
141
|
+
{
|
142
|
+
/working directory/ => -> { working_directory },
|
143
|
+
/pid file/ => -> { pid_file },
|
144
|
+
/log file/ => -> { log_file },
|
145
|
+
/name/ => -> { name },
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
def command
|
150
|
+
# update arguments with actual port numbers, working directories etc. (see template variables)
|
151
|
+
Shellwords.join([@exec, *@args.map{|arg| render(arg)}])
|
152
|
+
end
|
153
|
+
|
154
|
+
attr_reader :name
|
155
|
+
attr_reader :working_directory
|
156
|
+
attr_reader :pid_file
|
157
|
+
attr_reader :log_file
|
158
|
+
attr_reader :ready_timeout
|
159
|
+
attr_reader :term_timeout
|
160
|
+
attr_reader :kill_timeout
|
161
|
+
attr_reader :state_change_time
|
162
|
+
attr_reader :state_log
|
163
|
+
|
164
|
+
def reset_options(opts)
|
165
|
+
@logging = opts[:logging]
|
166
|
+
|
167
|
+
@ready_timeout = opts[:ready_timeout] || 10
|
168
|
+
@term_timeout = opts[:term_timeout] || 10
|
169
|
+
@kill_timeout = opts[:kill_timeout] || 10
|
170
|
+
|
171
|
+
@ready_test = opts[:ready_test] || ->(_){true}
|
172
|
+
@refresh_action = opts[:refresh_action] || ->(_){restart}
|
173
|
+
end
|
174
|
+
|
175
|
+
def pid
|
176
|
+
@pid if starting? or running?
|
177
|
+
end
|
178
|
+
|
179
|
+
def exit_code
|
180
|
+
@process.value.exitstatus if not running? and @process
|
181
|
+
end
|
182
|
+
|
183
|
+
def running?
|
184
|
+
trigger? :stopped # if it can be stopped it must be running :D
|
185
|
+
end
|
186
|
+
|
187
|
+
def starting?
|
188
|
+
state == :starting
|
189
|
+
end
|
190
|
+
|
191
|
+
def ready?
|
192
|
+
state == :ready
|
193
|
+
end
|
194
|
+
|
195
|
+
def dead?
|
196
|
+
state == :dead
|
197
|
+
end
|
198
|
+
|
199
|
+
def failed?
|
200
|
+
state == :failed
|
201
|
+
end
|
202
|
+
|
203
|
+
def jammed?
|
204
|
+
state == :jammed
|
205
|
+
end
|
206
|
+
|
207
|
+
def state
|
208
|
+
lock_fsm{|fsm| fsm.state }
|
209
|
+
end
|
210
|
+
|
211
|
+
def refresh
|
212
|
+
puts 'refreshing'
|
213
|
+
cwd = Dir.pwd
|
214
|
+
begin
|
215
|
+
Dir.chdir(@working_directory.to_s)
|
216
|
+
@refresh_action.call(self)
|
217
|
+
ensure
|
218
|
+
Dir.chdir(cwd)
|
219
|
+
end
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
def restart
|
224
|
+
puts 'restarting'
|
225
|
+
stop
|
226
|
+
start
|
227
|
+
end
|
228
|
+
|
229
|
+
def start
|
230
|
+
return self if trigger? :stopped
|
231
|
+
trigger? :starting or raise StateError.new(self, 'start', state)
|
232
|
+
|
233
|
+
trigger :starting
|
234
|
+
@pid, @process = spawn
|
235
|
+
|
236
|
+
fail "expected 2 values from #spawn, got: #{@pid}, #{@process}" unless @pid and @process
|
237
|
+
|
238
|
+
@process_watcher = Thread.new do
|
239
|
+
@process.join
|
240
|
+
trigger :died
|
241
|
+
end
|
242
|
+
|
243
|
+
trigger :started
|
244
|
+
self
|
245
|
+
end
|
246
|
+
|
247
|
+
def stop
|
248
|
+
return if trigger? :started
|
249
|
+
trigger? :stopped or raise StateError.new(self, 'stop', state)
|
250
|
+
|
251
|
+
# get rid of the watcher thread
|
252
|
+
@process_watcher and @process_watcher.kill and @process_watcher.join
|
253
|
+
|
254
|
+
catch :done do
|
255
|
+
begin
|
256
|
+
if @term_timeout > 0
|
257
|
+
puts "terminating process: #{@pid}"
|
258
|
+
Process.kill("TERM", @pid)
|
259
|
+
@process.join(@term_timeout) and throw :done
|
260
|
+
puts "process #{@pid} did not terminate in time"
|
261
|
+
end
|
262
|
+
|
263
|
+
if @kill_timeout > 0
|
264
|
+
puts "killing process: #{@pid}"
|
265
|
+
Process.kill("KILL", @pid)
|
266
|
+
@process.join(@kill_timeout) and throw :done
|
267
|
+
puts "process #{@pid} could not be killed!!!"
|
268
|
+
end
|
269
|
+
rescue Errno::ESRCH
|
270
|
+
throw :done
|
271
|
+
end
|
272
|
+
|
273
|
+
trigger :run_away
|
274
|
+
raise ProcessRunAwayError.new(self, @pid)
|
275
|
+
end
|
276
|
+
|
277
|
+
trigger :stopped
|
278
|
+
self
|
279
|
+
end
|
280
|
+
|
281
|
+
def wait_ready
|
282
|
+
trigger? :verified or raise StateError.new(self, 'wait ready', state)
|
283
|
+
|
284
|
+
puts 'waiting ready'
|
285
|
+
|
286
|
+
status = while_running do
|
287
|
+
begin
|
288
|
+
Timeout.timeout(@ready_timeout) do
|
289
|
+
@ready_test.call(self) ? :ready : :failed
|
290
|
+
end
|
291
|
+
rescue Timeout::Error
|
292
|
+
:ready_timeout
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
case status
|
297
|
+
when :failed
|
298
|
+
puts "process failed to pass it's readiness test"
|
299
|
+
stop
|
300
|
+
trigger :failed
|
301
|
+
raise ProcessReadyFailedError.new(self)
|
302
|
+
when :ready_timeout
|
303
|
+
puts "process not ready in time; see #{log_file} for detail"
|
304
|
+
stop
|
305
|
+
trigger :failed
|
306
|
+
raise ProcessReadyTimeOutError.new(self)
|
307
|
+
when Exception
|
308
|
+
puts "process readiness check raised error: #{status}; see #{log_file} for detail"
|
309
|
+
stop
|
310
|
+
trigger :failed
|
311
|
+
raise status
|
312
|
+
else
|
313
|
+
trigger :verified
|
314
|
+
self
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def after_state_change(&callback)
|
319
|
+
@after_state_change << callback
|
320
|
+
end
|
321
|
+
|
322
|
+
def puts(message)
|
323
|
+
message = "#{name}: #{message}"
|
324
|
+
@state_log << message
|
325
|
+
super message if @logging
|
326
|
+
end
|
327
|
+
|
328
|
+
def to_s
|
329
|
+
"#{name}[#{@exec}](#{state})"
|
330
|
+
end
|
331
|
+
|
332
|
+
private
|
333
|
+
|
334
|
+
def lock_fsm
|
335
|
+
@fsm_lock.synchronize{yield @_fsm}
|
336
|
+
end
|
337
|
+
|
338
|
+
def trigger(change)
|
339
|
+
lock_fsm{|fsm| fsm.trigger(change)}
|
340
|
+
end
|
341
|
+
|
342
|
+
def trigger?(change)
|
343
|
+
lock_fsm{|fsm| fsm.trigger?(change)}
|
344
|
+
end
|
345
|
+
|
346
|
+
def spawn
|
347
|
+
daemonize('exec') do |command|
|
348
|
+
# TODO: looks like exec is eating pending TERM (or other) signal and .start.stop may time out on TERM if signal was delivered before exec?
|
349
|
+
Kernel.exec(command)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def while_running
|
354
|
+
action = Thread.new do
|
355
|
+
begin
|
356
|
+
yield
|
357
|
+
rescue => error
|
358
|
+
error
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
value = ThreadsWait.new.join(action, @process).value
|
363
|
+
case value
|
364
|
+
when Process::Status
|
365
|
+
puts "process exited; see #{log_file} for detail"
|
366
|
+
trigger :died
|
367
|
+
raise ProcessExitedError.new(self, exit_code)
|
368
|
+
end
|
369
|
+
|
370
|
+
value
|
371
|
+
end
|
372
|
+
|
373
|
+
def daemonize(type = 'exec')
|
374
|
+
Daemon.daemonize(@pid_file, @log_file) do |log|
|
375
|
+
_command = command # render command
|
376
|
+
|
377
|
+
log.truncate(0)
|
378
|
+
Dir.chdir(@working_directory.to_s)
|
379
|
+
|
380
|
+
# useful for testing
|
381
|
+
ENV['PROCESS_SPAWN_TYPE'] = type
|
382
|
+
|
383
|
+
yield _command
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
class LoadedBackgroundProcess < BackgroundProcess
|
389
|
+
private
|
390
|
+
|
391
|
+
# cmd will be loaded in forked ruby interpreter and arguments passed via ENV['ARGS']
|
392
|
+
# This way starting new process will be much faster since ruby VM is already loaded
|
393
|
+
def spawn
|
394
|
+
puts "loading ruby script: #{@exec}"
|
395
|
+
daemonize('load') do |command|
|
396
|
+
cmd = Shellwords.split(command)
|
397
|
+
file = cmd.shift
|
398
|
+
|
399
|
+
# reset ARGV
|
400
|
+
Object.instance_eval{ remove_const(:ARGV) }
|
401
|
+
Object.const_set(:ARGV, cmd)
|
402
|
+
|
403
|
+
# reset $0
|
404
|
+
$0 = file
|
405
|
+
|
406
|
+
# reset $*
|
407
|
+
$*.replace(cmd)
|
408
|
+
|
409
|
+
load file
|
410
|
+
|
411
|
+
# make sure we exit if loaded file won't
|
412
|
+
exit 0
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|