assassin 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1 @@
1
+ Ruby
@@ -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
@@ -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
+ }
@@ -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
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ #
4
+ STDOUT.sync = true
5
+ STDERR.sync = true
6
+ puts Process.pid rescue nil
7
+
8
+ #
9
+ loop{ sleep }
@@ -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
@@ -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: