assassin 0.4.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.
- data/LICENSE +1 -0
- data/README.md +44 -0
- data/Rakefile +394 -0
- data/assassin.gemspec +40 -0
- data/lib/assassin.rb +89 -0
- data/test/assassin_test.rb +100 -0
- data/test/child.rb +9 -0
- data/test/lib/testing.rb +74 -0
- data/test/parent.rb +25 -0
- metadata +55 -0
data/LICENSE
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Ruby
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
NAME
|
2
|
+
----
|
3
|
+
assassin.rb
|
4
|
+
|
5
|
+
SYNOPSIS
|
6
|
+
--------
|
7
|
+
no zombies ever, not even on `exit!` or `kill -9`
|
8
|
+
|
9
|
+
USAGE
|
10
|
+
-----
|
11
|
+
pipe = IO.popen 'program-that-must-not-be-zombied'
|
12
|
+
|
13
|
+
Assassin.at_exit_kill(pipe.pid)
|
14
|
+
|
15
|
+
also see lib/assassin.rb and test/assassin_test.rb
|
16
|
+
|
17
|
+
DESCRIPTION
|
18
|
+
-----------
|
19
|
+
assassin.rb is a small (~ 60 loc), simple, reliable methodology of ensuring
|
20
|
+
that child processes are *always* cleaned up, regardless of the manner in
|
21
|
+
which the parent program is shut down.
|
22
|
+
|
23
|
+
the basic concept it to generate, start, and detach another script that
|
24
|
+
monitors the parent process and, when that process no longer exists, ensures
|
25
|
+
through escalation of signals that a child process is shut down and does not
|
26
|
+
become a zombie.
|
27
|
+
|
28
|
+
this becomes espcially important for libraries which spawn processes, such
|
29
|
+
as via `IO.popen` that need to ensure those children are cleaned up, but
|
30
|
+
which cannot control whether client code may call `exit`. this approach
|
31
|
+
also handles being `kill -9`d - something no `at_exit{}` handler can
|
32
|
+
promise.
|
33
|
+
|
34
|
+
INSTALL
|
35
|
+
-------
|
36
|
+
gem install assassin
|
37
|
+
|
38
|
+
URI
|
39
|
+
---
|
40
|
+
http://github.com/ahoward/assassin
|
41
|
+
|
42
|
+
HISTORY
|
43
|
+
-------
|
44
|
+
- 0.4.2. initial release
|
data/Rakefile
ADDED
@@ -0,0 +1,394 @@
|
|
1
|
+
This.rubyforge_project = 'codeforpeople'
|
2
|
+
This.author = "Ara T. Howard"
|
3
|
+
This.email = "ara.t.howard@gmail.com"
|
4
|
+
This.homepage = "https://github.com/ahoward/#{ This.lib }"
|
5
|
+
|
6
|
+
task :license do
|
7
|
+
open('LICENSE', 'w'){|fd| fd.puts "same as ruby's"}
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default do
|
11
|
+
puts((Rake::Task.tasks.map{|task| task.name.gsub(/::/,':')} - ['default']).sort)
|
12
|
+
end
|
13
|
+
|
14
|
+
task :test do
|
15
|
+
run_tests!
|
16
|
+
end
|
17
|
+
|
18
|
+
namespace :test do
|
19
|
+
task(:unit){ run_tests!(:unit) }
|
20
|
+
task(:functional){ run_tests!(:functional) }
|
21
|
+
task(:integration){ run_tests!(:integration) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def run_tests!(which = nil)
|
25
|
+
which ||= '**'
|
26
|
+
test_dir = File.join(This.dir, "test")
|
27
|
+
test_glob ||= File.join(test_dir, "#{ which }/**_test.rb")
|
28
|
+
test_rbs = Dir.glob(test_glob).sort
|
29
|
+
|
30
|
+
div = ('=' * 119)
|
31
|
+
line = ('-' * 119)
|
32
|
+
|
33
|
+
test_rbs.each_with_index do |test_rb, index|
|
34
|
+
testno = index + 1
|
35
|
+
command = "#{ This.ruby } -w -I ./lib -I ./test/lib #{ test_rb }"
|
36
|
+
|
37
|
+
puts
|
38
|
+
say(div, :color => :cyan, :bold => true)
|
39
|
+
say("@#{ testno } => ", :bold => true, :method => :print)
|
40
|
+
say(command, :color => :cyan, :bold => true)
|
41
|
+
say(line, :color => :cyan, :bold => true)
|
42
|
+
|
43
|
+
system(command)
|
44
|
+
|
45
|
+
say(line, :color => :cyan, :bold => true)
|
46
|
+
|
47
|
+
status = $?.exitstatus
|
48
|
+
|
49
|
+
if status.zero?
|
50
|
+
say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
|
51
|
+
say("SUCCESS", :color => :green, :bold => true)
|
52
|
+
else
|
53
|
+
say("@#{ testno } <= ", :bold => true, :color => :white, :method => :print)
|
54
|
+
say("FAILURE", :color => :red, :bold => true)
|
55
|
+
end
|
56
|
+
say(line, :color => :cyan, :bold => true)
|
57
|
+
|
58
|
+
exit(status) unless status.zero?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
task :gemspec do
|
64
|
+
ignore_extensions = ['git', 'svn', 'tmp', /sw./, 'bak', 'gem']
|
65
|
+
ignore_directories = ['pkg']
|
66
|
+
ignore_files = ['test/log']
|
67
|
+
|
68
|
+
shiteless =
|
69
|
+
lambda do |list|
|
70
|
+
list.delete_if do |entry|
|
71
|
+
next unless test(?e, entry)
|
72
|
+
extension = File.basename(entry).split(%r/[.]/).last
|
73
|
+
ignore_extensions.any?{|ext| ext === extension}
|
74
|
+
end
|
75
|
+
list.delete_if do |entry|
|
76
|
+
next unless test(?d, entry)
|
77
|
+
dirname = File.expand_path(entry)
|
78
|
+
ignore_directories.any?{|dir| File.expand_path(dir) == dirname}
|
79
|
+
end
|
80
|
+
list.delete_if do |entry|
|
81
|
+
next unless test(?f, entry)
|
82
|
+
filename = File.expand_path(entry)
|
83
|
+
ignore_files.any?{|file| File.expand_path(file) == filename}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
lib = This.lib
|
88
|
+
object = This.object
|
89
|
+
version = This.version
|
90
|
+
files = shiteless[Dir::glob("**/**")]
|
91
|
+
executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)}
|
92
|
+
#has_rdoc = true #File.exist?('doc')
|
93
|
+
test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb")
|
94
|
+
summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass"
|
95
|
+
description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass"
|
96
|
+
license = object.respond_to?(:license) ? object.license : "same as ruby's"
|
97
|
+
|
98
|
+
if This.extensions.nil?
|
99
|
+
This.extensions = []
|
100
|
+
extensions = This.extensions
|
101
|
+
%w( Makefile configure extconf.rb ).each do |ext|
|
102
|
+
extensions << ext if File.exists?(ext)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
extensions = [extensions].flatten.compact
|
106
|
+
|
107
|
+
if This.dependencies.nil?
|
108
|
+
dependencies = []
|
109
|
+
else
|
110
|
+
case This.dependencies
|
111
|
+
when Hash
|
112
|
+
dependencies = This.dependencies.values
|
113
|
+
when Array
|
114
|
+
dependencies = This.dependencies
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
template =
|
119
|
+
if test(?e, 'gemspec.erb')
|
120
|
+
Template{ IO.read('gemspec.erb') }
|
121
|
+
else
|
122
|
+
Template {
|
123
|
+
<<-__
|
124
|
+
## <%= lib %>.gemspec
|
125
|
+
#
|
126
|
+
|
127
|
+
Gem::Specification::new do |spec|
|
128
|
+
spec.name = <%= lib.inspect %>
|
129
|
+
spec.version = <%= version.inspect %>
|
130
|
+
spec.platform = Gem::Platform::RUBY
|
131
|
+
spec.summary = <%= lib.inspect %>
|
132
|
+
spec.description = <%= description.inspect %>
|
133
|
+
spec.license = <%= license.inspect %>
|
134
|
+
|
135
|
+
spec.files =\n<%= files.sort.pretty_inspect %>
|
136
|
+
spec.executables = <%= executables.inspect %>
|
137
|
+
|
138
|
+
spec.require_path = "lib"
|
139
|
+
|
140
|
+
spec.test_files = <%= test_files.inspect %>
|
141
|
+
|
142
|
+
<% dependencies.each do |lib_version| %>
|
143
|
+
spec.add_dependency(*<%= Array(lib_version).flatten.inspect %>)
|
144
|
+
<% end %>
|
145
|
+
|
146
|
+
spec.extensions.push(*<%= extensions.inspect %>)
|
147
|
+
|
148
|
+
spec.rubyforge_project = <%= This.rubyforge_project.inspect %>
|
149
|
+
spec.author = <%= This.author.inspect %>
|
150
|
+
spec.email = <%= This.email.inspect %>
|
151
|
+
spec.homepage = <%= This.homepage.inspect %>
|
152
|
+
end
|
153
|
+
__
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
Fu.mkdir_p(This.pkgdir)
|
158
|
+
gemspec = "#{ lib }.gemspec"
|
159
|
+
open(gemspec, "w"){|fd| fd.puts(template)}
|
160
|
+
This.gemspec = gemspec
|
161
|
+
end
|
162
|
+
|
163
|
+
task :gem => [:clean, :gemspec] do
|
164
|
+
Fu.mkdir_p(This.pkgdir)
|
165
|
+
before = Dir['*.gem']
|
166
|
+
cmd = "gem build #{ This.gemspec }"
|
167
|
+
`#{ cmd }`
|
168
|
+
after = Dir['*.gem']
|
169
|
+
gem = ((after - before).first || after.first) or abort('no gem!')
|
170
|
+
Fu.mv(gem, This.pkgdir)
|
171
|
+
This.gem = File.join(This.pkgdir, File.basename(gem))
|
172
|
+
end
|
173
|
+
|
174
|
+
task :readme do
|
175
|
+
samples = ''
|
176
|
+
prompt = '~ > '
|
177
|
+
lib = This.lib
|
178
|
+
version = This.version
|
179
|
+
|
180
|
+
Dir['sample*/*'].sort.each do |sample|
|
181
|
+
samples << "\n" << " <========< #{ sample } >========>" << "\n\n"
|
182
|
+
|
183
|
+
cmd = "cat #{ sample }"
|
184
|
+
samples << Util.indent(prompt + cmd, 2) << "\n\n"
|
185
|
+
samples << Util.indent(`#{ cmd }`, 4) << "\n"
|
186
|
+
|
187
|
+
cmd = "ruby #{ sample }"
|
188
|
+
samples << Util.indent(prompt + cmd, 2) << "\n\n"
|
189
|
+
|
190
|
+
cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'"
|
191
|
+
samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n"
|
192
|
+
end
|
193
|
+
|
194
|
+
template =
|
195
|
+
if test(?e, 'README.erb')
|
196
|
+
Template{ IO.read('README.erb') }
|
197
|
+
else
|
198
|
+
Template {
|
199
|
+
<<-__
|
200
|
+
NAME
|
201
|
+
#{ lib }
|
202
|
+
|
203
|
+
DESCRIPTION
|
204
|
+
|
205
|
+
INSTALL
|
206
|
+
gem install #{ lib }
|
207
|
+
|
208
|
+
SAMPLES
|
209
|
+
#{ samples }
|
210
|
+
__
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
open("README", "w"){|fd| fd.puts template}
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
task :clean do
|
219
|
+
Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)}
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
task :release => [:clean, :gemspec, :gem] do
|
224
|
+
gems = Dir[File.join(This.pkgdir, '*.gem')].flatten
|
225
|
+
raise "which one? : #{ gems.inspect }" if gems.size > 1
|
226
|
+
raise "no gems?" if gems.size < 1
|
227
|
+
|
228
|
+
cmd = "gem push #{ This.gem }"
|
229
|
+
puts cmd
|
230
|
+
puts
|
231
|
+
system(cmd)
|
232
|
+
abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
|
233
|
+
|
234
|
+
cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.gem }"
|
235
|
+
puts cmd
|
236
|
+
puts
|
237
|
+
system(cmd)
|
238
|
+
abort("cmd(#{ cmd }) failed with (#{ $?.inspect })") unless $?.exitstatus.zero?
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
|
245
|
+
BEGIN {
|
246
|
+
# support for this rakefile
|
247
|
+
#
|
248
|
+
$VERBOSE = nil
|
249
|
+
|
250
|
+
require 'ostruct'
|
251
|
+
require 'erb'
|
252
|
+
require 'fileutils'
|
253
|
+
require 'rbconfig'
|
254
|
+
require 'pp'
|
255
|
+
|
256
|
+
# fu shortcut
|
257
|
+
#
|
258
|
+
Fu = FileUtils
|
259
|
+
|
260
|
+
# cache a bunch of stuff about this rakefile/environment
|
261
|
+
#
|
262
|
+
This = OpenStruct.new
|
263
|
+
|
264
|
+
This.file = File.expand_path(__FILE__)
|
265
|
+
This.dir = File.dirname(This.file)
|
266
|
+
This.pkgdir = File.join(This.dir, 'pkg')
|
267
|
+
|
268
|
+
# grok lib
|
269
|
+
#
|
270
|
+
lib = ENV['LIB']
|
271
|
+
unless lib
|
272
|
+
lib = File.basename(Dir.pwd).sub(/[-].*$/, '')
|
273
|
+
end
|
274
|
+
This.lib = lib
|
275
|
+
|
276
|
+
# grok version
|
277
|
+
#
|
278
|
+
version = ENV['VERSION']
|
279
|
+
unless version
|
280
|
+
require "./lib/#{ This.lib }"
|
281
|
+
This.name = lib.capitalize
|
282
|
+
This.object = eval(This.name)
|
283
|
+
version = This.object.send(:version)
|
284
|
+
end
|
285
|
+
This.version = version
|
286
|
+
|
287
|
+
# see if dependencies are export by the module
|
288
|
+
#
|
289
|
+
if This.object.respond_to?(:dependencies)
|
290
|
+
This.dependencies = This.object.dependencies
|
291
|
+
end
|
292
|
+
|
293
|
+
# we need to know the name of the lib an it's version
|
294
|
+
#
|
295
|
+
abort('no lib') unless This.lib
|
296
|
+
abort('no version') unless This.version
|
297
|
+
|
298
|
+
# discover full path to this ruby executable
|
299
|
+
#
|
300
|
+
c = Config::CONFIG
|
301
|
+
bindir = c["bindir"] || c['BINDIR']
|
302
|
+
ruby_install_name = c['ruby_install_name'] || c['RUBY_INSTALL_NAME'] || 'ruby'
|
303
|
+
ruby_ext = c['EXEEXT'] || ''
|
304
|
+
ruby = File.join(bindir, (ruby_install_name + ruby_ext))
|
305
|
+
This.ruby = ruby
|
306
|
+
|
307
|
+
# some utils
|
308
|
+
#
|
309
|
+
module Util
|
310
|
+
def indent(s, n = 2)
|
311
|
+
s = unindent(s)
|
312
|
+
ws = ' ' * n
|
313
|
+
s.gsub(%r/^/, ws)
|
314
|
+
end
|
315
|
+
|
316
|
+
def unindent(s)
|
317
|
+
indent = nil
|
318
|
+
s.each_line do |line|
|
319
|
+
next if line =~ %r/^\s*$/
|
320
|
+
indent = line[%r/^\s*/] and break
|
321
|
+
end
|
322
|
+
indent ? s.gsub(%r/^#{ indent }/, "") : s
|
323
|
+
end
|
324
|
+
extend self
|
325
|
+
end
|
326
|
+
|
327
|
+
# template support
|
328
|
+
#
|
329
|
+
class Template
|
330
|
+
def initialize(&block)
|
331
|
+
@block = block
|
332
|
+
@template = block.call.to_s
|
333
|
+
end
|
334
|
+
def expand(b=nil)
|
335
|
+
ERB.new(Util.unindent(@template)).result((b||@block).binding)
|
336
|
+
end
|
337
|
+
alias_method 'to_s', 'expand'
|
338
|
+
end
|
339
|
+
def Template(*args, &block) Template.new(*args, &block) end
|
340
|
+
|
341
|
+
# colored console output support
|
342
|
+
#
|
343
|
+
This.ansi = {
|
344
|
+
:clear => "\e[0m",
|
345
|
+
:reset => "\e[0m",
|
346
|
+
:erase_line => "\e[K",
|
347
|
+
:erase_char => "\e[P",
|
348
|
+
:bold => "\e[1m",
|
349
|
+
:dark => "\e[2m",
|
350
|
+
:underline => "\e[4m",
|
351
|
+
:underscore => "\e[4m",
|
352
|
+
:blink => "\e[5m",
|
353
|
+
:reverse => "\e[7m",
|
354
|
+
:concealed => "\e[8m",
|
355
|
+
:black => "\e[30m",
|
356
|
+
:red => "\e[31m",
|
357
|
+
:green => "\e[32m",
|
358
|
+
:yellow => "\e[33m",
|
359
|
+
:blue => "\e[34m",
|
360
|
+
:magenta => "\e[35m",
|
361
|
+
:cyan => "\e[36m",
|
362
|
+
:white => "\e[37m",
|
363
|
+
:on_black => "\e[40m",
|
364
|
+
:on_red => "\e[41m",
|
365
|
+
:on_green => "\e[42m",
|
366
|
+
:on_yellow => "\e[43m",
|
367
|
+
:on_blue => "\e[44m",
|
368
|
+
:on_magenta => "\e[45m",
|
369
|
+
:on_cyan => "\e[46m",
|
370
|
+
:on_white => "\e[47m"
|
371
|
+
}
|
372
|
+
def say(phrase, *args)
|
373
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
374
|
+
options[:color] = args.shift.to_s.to_sym unless args.empty?
|
375
|
+
keys = options.keys
|
376
|
+
keys.each{|key| options[key.to_s.to_sym] = options.delete(key)}
|
377
|
+
|
378
|
+
color = options[:color]
|
379
|
+
bold = options.has_key?(:bold)
|
380
|
+
|
381
|
+
parts = [phrase]
|
382
|
+
parts.unshift(This.ansi[color]) if color
|
383
|
+
parts.unshift(This.ansi[:bold]) if bold
|
384
|
+
parts.push(This.ansi[:clear]) if parts.size > 1
|
385
|
+
|
386
|
+
method = options[:method] || :puts
|
387
|
+
|
388
|
+
Kernel.send(method, parts.join)
|
389
|
+
end
|
390
|
+
|
391
|
+
# always run out of the project dir
|
392
|
+
#
|
393
|
+
Dir.chdir(This.dir)
|
394
|
+
}
|
data/assassin.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
## assassin.gemspec
|
2
|
+
#
|
3
|
+
|
4
|
+
Gem::Specification::new do |spec|
|
5
|
+
spec.name = "assassin"
|
6
|
+
spec.version = "0.4.2"
|
7
|
+
spec.platform = Gem::Platform::RUBY
|
8
|
+
spec.summary = "assassin"
|
9
|
+
spec.description = "no zombies ever, not even on `exit!` or `kill -9`"
|
10
|
+
spec.license = "same as ruby's"
|
11
|
+
|
12
|
+
spec.files =
|
13
|
+
["LICENSE",
|
14
|
+
"README.md",
|
15
|
+
"Rakefile",
|
16
|
+
"assassin.gemspec",
|
17
|
+
"lib",
|
18
|
+
"lib/assassin.rb",
|
19
|
+
"test",
|
20
|
+
"test/assassin_test.rb",
|
21
|
+
"test/child.rb",
|
22
|
+
"test/lib",
|
23
|
+
"test/lib/testing.rb",
|
24
|
+
"test/parent.rb"]
|
25
|
+
|
26
|
+
spec.executables = []
|
27
|
+
|
28
|
+
spec.require_path = "lib"
|
29
|
+
|
30
|
+
spec.test_files = nil
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
spec.extensions.push(*[])
|
35
|
+
|
36
|
+
spec.rubyforge_project = "codeforpeople"
|
37
|
+
spec.author = "Ara T. Howard"
|
38
|
+
spec.email = "ara.t.howard@gmail.com"
|
39
|
+
spec.homepage = "https://github.com/ahoward/assassin"
|
40
|
+
end
|
data/lib/assassin.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
class Assassin
|
5
|
+
Version = '0.4.2' unless defined?(Version)
|
6
|
+
|
7
|
+
def Assassin.version
|
8
|
+
Version
|
9
|
+
end
|
10
|
+
|
11
|
+
def Assassin.description
|
12
|
+
'no zombies ever, not even on `exit!` or `kill -9`'
|
13
|
+
end
|
14
|
+
|
15
|
+
def Assassin.at_exit_kill(*args, &block)
|
16
|
+
new(*args, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def Assassin.ate(*args, &block)
|
20
|
+
new(*args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_accessor :parent_pid
|
24
|
+
attr_accessor :child_pid
|
25
|
+
attr_accessor :pid
|
26
|
+
attr_accessor :path
|
27
|
+
|
28
|
+
def initialize(child_pid, options = {})
|
29
|
+
@child_pid = child_pid.to_s.to_i
|
30
|
+
@parent_pid = Process.pid
|
31
|
+
@options = Assassin.options_for(options)
|
32
|
+
@pid, @path = Assassin.generate(@child_pid, @options)
|
33
|
+
end
|
34
|
+
|
35
|
+
def Assassin.options_for(options)
|
36
|
+
options.inject({}){|h, kv| k,v = kv; h.update(k.to_s.to_sym => v)}
|
37
|
+
end
|
38
|
+
|
39
|
+
def Assassin.generate(child_pid, options = {})
|
40
|
+
path = File.join(Dir.tmpdir, "assassin-#{ child_pid }-#{ SecureRandom.uuid }.rb")
|
41
|
+
script = Assassin.script_for(child_pid, options)
|
42
|
+
IO.binwrite(path, script)
|
43
|
+
pid = Process.spawn "ruby #{ path }"
|
44
|
+
[pid, path]
|
45
|
+
end
|
46
|
+
|
47
|
+
def Assassin.script_for(child_pid, options = {})
|
48
|
+
parent_pid = Process.pid
|
49
|
+
|
50
|
+
script = <<-__
|
51
|
+
Process.daemon
|
52
|
+
|
53
|
+
require 'fileutils'
|
54
|
+
at_exit{ FileUtils.rm_f(__FILE__) }
|
55
|
+
|
56
|
+
parent_pid = #{ parent_pid }
|
57
|
+
child_pid = #{ child_pid }
|
58
|
+
|
59
|
+
m = 24*60*60
|
60
|
+
n = 42
|
61
|
+
|
62
|
+
m.times do
|
63
|
+
begin
|
64
|
+
Process.kill(0, parent_pid)
|
65
|
+
rescue Object => e
|
66
|
+
if e.is_a?(Errno::ESRCH)
|
67
|
+
n.times do
|
68
|
+
begin
|
69
|
+
Process.kill(15, child_pid) rescue nil
|
70
|
+
sleep(rand + rand)
|
71
|
+
Process.kill(9, child_pid) rescue nil
|
72
|
+
sleep(rand + rand)
|
73
|
+
Process.kill(0, child_pid)
|
74
|
+
rescue Errno::ESRCH
|
75
|
+
break
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
exit
|
81
|
+
end
|
82
|
+
|
83
|
+
sleep(1)
|
84
|
+
end
|
85
|
+
__
|
86
|
+
|
87
|
+
return script
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'testing'
|
3
|
+
require 'assassin'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
Testing Assassin do
|
7
|
+
testing 'that not using assassin.rb leaks zombie processes' do
|
8
|
+
assert do
|
9
|
+
parent_pid, child_pid = `#{ TESTDIR }/parent.rb`.scan(/\d+/).map(&:to_i)
|
10
|
+
|
11
|
+
parent_was_killed = false
|
12
|
+
child_was_killed = false
|
13
|
+
|
14
|
+
10.times do
|
15
|
+
begin
|
16
|
+
Process.kill(0, parent_pid)
|
17
|
+
sleep(rand)
|
18
|
+
rescue Errno::ESRCH
|
19
|
+
parent_was_killed = true
|
20
|
+
break
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
10.times do
|
25
|
+
begin
|
26
|
+
Process.kill(0, child_pid)
|
27
|
+
sleep(rand)
|
28
|
+
rescue Errno::ESRCH
|
29
|
+
child_was_killed = true
|
30
|
+
break
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
cleanup!(parent_pid, child_pid)
|
35
|
+
|
36
|
+
parent_was_killed && !child_was_killed
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
testing 'that __using__ assassin.rb does __not__ leak zombie processes' do
|
41
|
+
assert do
|
42
|
+
parent_pid, child_pid = `#{ TESTDIR }/parent.rb assassin`.scan(/\d+/).map(&:to_i)
|
43
|
+
|
44
|
+
parent_was_killed = false
|
45
|
+
child_was_killed = false
|
46
|
+
|
47
|
+
10.times do
|
48
|
+
begin
|
49
|
+
Process.kill(0, parent_pid)
|
50
|
+
sleep(rand)
|
51
|
+
rescue Errno::ESRCH
|
52
|
+
parent_was_killed = true
|
53
|
+
break
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
10.times do
|
58
|
+
begin
|
59
|
+
Process.kill(0, child_pid)
|
60
|
+
sleep(rand)
|
61
|
+
rescue Errno::ESRCH
|
62
|
+
child_was_killed = true
|
63
|
+
break
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
cleanup!(parent_pid, child_pid)
|
68
|
+
|
69
|
+
parent_was_killed && child_was_killed
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
def cleanup!(*pids)
|
76
|
+
pids.flatten.compact.each do |pid|
|
77
|
+
pid = pid.to_s.to_i
|
78
|
+
begin
|
79
|
+
Process.kill(9, pid) rescue nil
|
80
|
+
Process.kill(-9, pid) rescue nil
|
81
|
+
`kill -9 #{ pid } 2>&1`
|
82
|
+
rescue
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
BEGIN {
|
94
|
+
TESTDIR = File.dirname(File.expand_path(__FILE__))
|
95
|
+
TESTLIBDIR = File.join(TESTDIR, 'lib')
|
96
|
+
ROOTDIR = File.dirname(TESTDIR)
|
97
|
+
LIBDIR = File.join(ROOTDIR, 'lib')
|
98
|
+
$LOAD_PATH.push(LIBDIR)
|
99
|
+
$LOAD_PATH.push(TESTLIBDIR)
|
100
|
+
}
|
data/test/child.rb
ADDED
data/test/lib/testing.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# simple testing support
|
3
|
+
#
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
def Testing(*args, &block)
|
7
|
+
Class.new(Test::Unit::TestCase) do
|
8
|
+
eval("This=self")
|
9
|
+
|
10
|
+
def self.slug_for(*args)
|
11
|
+
string = args.flatten.compact.join('-')
|
12
|
+
words = string.to_s.scan(%r/\w+/)
|
13
|
+
words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''}
|
14
|
+
words.delete_if{|word| word.nil? or word.strip.empty?}
|
15
|
+
words.join('-').downcase
|
16
|
+
end
|
17
|
+
|
18
|
+
def This.testing_subclass_count
|
19
|
+
@testing_subclass_count ||= 1
|
20
|
+
ensure
|
21
|
+
@testing_subclass_count += 1
|
22
|
+
end
|
23
|
+
|
24
|
+
slug = slug_for(*args).gsub(%r/-/,'_')
|
25
|
+
name = ['TESTING', '%03d' % This.testing_subclass_count, slug].delete_if{|part| part.empty?}.join('_')
|
26
|
+
name = name.upcase!
|
27
|
+
const_set(:Name, name)
|
28
|
+
def self.name() const_get(:Name) end
|
29
|
+
|
30
|
+
def self.testno()
|
31
|
+
'%05d' % (@testno ||= 0)
|
32
|
+
ensure
|
33
|
+
@testno += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.testing(*args, &block)
|
37
|
+
define_method("test_#{ testno }_#{ slug_for(*args) }", &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method '__assert__', 'assert'
|
41
|
+
|
42
|
+
def assert(*args, &block)
|
43
|
+
if block
|
44
|
+
label = "assert(#{ args.join ' ' })"
|
45
|
+
result = nil
|
46
|
+
assert_nothing_raised{ result = block.call }
|
47
|
+
__assert__(result, label)
|
48
|
+
result
|
49
|
+
else
|
50
|
+
result = args.shift
|
51
|
+
label = "assert(#{ args.join ' ' })"
|
52
|
+
__assert__(result, label)
|
53
|
+
result
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def subclass_of exception
|
58
|
+
class << exception
|
59
|
+
def ==(other) super or self > other end
|
60
|
+
end
|
61
|
+
exception
|
62
|
+
end
|
63
|
+
|
64
|
+
alias_method '__assert_raises__', 'assert_raises'
|
65
|
+
|
66
|
+
def assert_raises(*args, &block)
|
67
|
+
args.push(subclass_of(Exception)) if args.empty?
|
68
|
+
__assert_raises__(*args, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
module_eval(&block)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
end
|
data/test/parent.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
STDOUT.sync = true
|
5
|
+
STDERR.sync = true
|
6
|
+
puts Process.pid
|
7
|
+
|
8
|
+
#
|
9
|
+
TESTDIR = File.dirname(__FILE__)
|
10
|
+
ROOTDIR = File.dirname(TESTDIR)
|
11
|
+
LIBDIR = File.join(ROOTDIR, 'lib')
|
12
|
+
|
13
|
+
#
|
14
|
+
child_program = "#{ TESTDIR }/child.rb"
|
15
|
+
child = IO.popen(child_program)
|
16
|
+
puts child.pid
|
17
|
+
|
18
|
+
#
|
19
|
+
if ARGV.first.to_s =~ /assassin/
|
20
|
+
require "#{ LIBDIR }/assassin.rb"
|
21
|
+
Assassin.at_exit_kill(child.pid)
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
exit!
|
metadata
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: assassin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ara T. Howard
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-10-17 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: no zombies ever, not even on `exit!` or `kill -9`
|
15
|
+
email: ara.t.howard@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- Rakefile
|
23
|
+
- assassin.gemspec
|
24
|
+
- lib/assassin.rb
|
25
|
+
- test/assassin_test.rb
|
26
|
+
- test/child.rb
|
27
|
+
- test/lib/testing.rb
|
28
|
+
- test/parent.rb
|
29
|
+
homepage: https://github.com/ahoward/assassin
|
30
|
+
licenses:
|
31
|
+
- same as ruby's
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project: codeforpeople
|
50
|
+
rubygems_version: 1.8.23.2
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: assassin
|
54
|
+
test_files: []
|
55
|
+
has_rdoc:
|