crazy_ivan 0.3.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,401 @@
1
+ # vim: ts=2:sw=2:sts=2:et:fdm=marker
2
+ require 'fcntl'
3
+ require 'timeout'
4
+ require 'thread'
5
+
6
+ module Open4
7
+ #--{{{
8
+ VERSION = '1.0.1'
9
+ def self.version() VERSION end
10
+
11
+ class Error < ::StandardError; end
12
+
13
+ def popen4(*cmd, &b)
14
+ #--{{{
15
+ pw, pr, pe, ps = IO.pipe, IO.pipe, IO.pipe, IO.pipe
16
+
17
+ verbose = $VERBOSE
18
+ begin
19
+ $VERBOSE = nil
20
+ ps.first.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
21
+ ps.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
22
+
23
+ cid = fork {
24
+ pw.last.close
25
+ STDIN.reopen pw.first
26
+ pw.first.close
27
+
28
+ pr.first.close
29
+ STDOUT.reopen pr.last
30
+ pr.last.close
31
+
32
+ pe.first.close
33
+ STDERR.reopen pe.last
34
+ pe.last.close
35
+
36
+ STDOUT.sync = STDERR.sync = true
37
+
38
+ begin
39
+ exec(*cmd)
40
+ raise 'forty-two'
41
+ rescue Exception => e
42
+ Marshal.dump(e, ps.last)
43
+ ps.last.flush
44
+ end
45
+ ps.last.close unless (ps.last.closed?)
46
+ exit!
47
+ }
48
+ ensure
49
+ $VERBOSE = verbose
50
+ end
51
+
52
+ [pw.first, pr.last, pe.last, ps.last].each{|fd| fd.close}
53
+
54
+ begin
55
+ e = Marshal.load ps.first
56
+ raise(Exception === e ? e : "unknown failure!")
57
+ rescue EOFError # If we get an EOF error, then the exec was successful
58
+ 42
59
+ ensure
60
+ ps.first.close
61
+ end
62
+
63
+ pw.last.sync = true
64
+
65
+ pi = [pw.last, pr.first, pe.first]
66
+
67
+ if b
68
+ begin
69
+ b[cid, *pi]
70
+ Process.waitpid2(cid).last
71
+ ensure
72
+ pi.each{|fd| fd.close unless fd.closed?}
73
+ end
74
+ else
75
+ [cid, pw.last, pr.first, pe.first]
76
+ end
77
+ #--}}}
78
+ end
79
+ alias open4 popen4
80
+ module_function :popen4
81
+ module_function :open4
82
+
83
+ class SpawnError < Error
84
+ #--{{{
85
+ attr 'cmd'
86
+ attr 'status'
87
+ attr 'signals'
88
+ def exitstatus
89
+ @status.exitstatus
90
+ end
91
+ def initialize cmd, status
92
+ @cmd, @status = cmd, status
93
+ @signals = {}
94
+ if status.signaled?
95
+ @signals['termsig'] = status.termsig
96
+ @signals['stopsig'] = status.stopsig
97
+ end
98
+ sigs = @signals.map{|k,v| "#{ k }:#{ v.inspect }"}.join(' ')
99
+ super "cmd <#{ cmd }> failed with status <#{ exitstatus.inspect }> signals <#{ sigs }>"
100
+ end
101
+ #--}}}
102
+ end
103
+
104
+ class ThreadEnsemble
105
+ #--{{{
106
+ attr 'threads'
107
+
108
+ def initialize cid
109
+ @cid, @threads, @argv, @done, @running = cid, [], [], Queue.new, false
110
+ @killed = false
111
+ end
112
+
113
+ def add_thread *a, &b
114
+ @running ? raise : (@argv << [a, b])
115
+ end
116
+
117
+ #
118
+ # take down process more nicely
119
+ #
120
+ def killall
121
+ c = Thread.critical
122
+ return nil if @killed
123
+ Thread.critical = true
124
+ (@threads - [Thread.current]).each{|t| t.kill rescue nil}
125
+ @killed = true
126
+ ensure
127
+ Thread.critical = c
128
+ end
129
+
130
+ def run
131
+ @running = true
132
+
133
+ begin
134
+ @argv.each do |a, b|
135
+ @threads << Thread.new(*a) do |*a|
136
+ begin
137
+ b[*a]
138
+ ensure
139
+ killall rescue nil if $!
140
+ @done.push Thread.current
141
+ end
142
+ end
143
+ end
144
+ rescue
145
+ killall
146
+ raise
147
+ ensure
148
+ all_done
149
+ end
150
+
151
+ @threads.map{|t| t.value}
152
+ end
153
+
154
+ def all_done
155
+ @threads.size.times{ @done.pop }
156
+ end
157
+ #--}}}
158
+ end
159
+
160
+ def to timeout = nil
161
+ #--{{{
162
+ Timeout.timeout(timeout){ yield }
163
+ #--}}}
164
+ end
165
+ module_function :to
166
+
167
+ def new_thread *a, &b
168
+ #--{{{
169
+ cur = Thread.current
170
+ Thread.new(*a) do |*a|
171
+ begin
172
+ b[*a]
173
+ rescue Exception => e
174
+ cur.raise e
175
+ end
176
+ end
177
+ #--}}}
178
+ end
179
+ module_function :new_thread
180
+
181
+ def getopts opts = {}
182
+ #--{{{
183
+ lambda do |*args|
184
+ keys, default, ignored = args
185
+ catch('opt') do
186
+ [keys].flatten.each do |key|
187
+ [key, key.to_s, key.to_s.intern].each do |key|
188
+ throw 'opt', opts[key] if opts.has_key?(key)
189
+ end
190
+ end
191
+ default
192
+ end
193
+ end
194
+ #--}}}
195
+ end
196
+ module_function :getopts
197
+
198
+ def relay src, dst = nil, t = nil
199
+ #--{{{
200
+ send_dst =
201
+ if dst.respond_to?(:call)
202
+ lambda{|buf| dst.call(buf)}
203
+ else
204
+ lambda{|buf| dst << buf}
205
+ end
206
+
207
+ unless src.nil?
208
+ if src.respond_to? :gets
209
+ while buf = to(t){ src.gets }
210
+ send_dst[buf]
211
+ end
212
+
213
+ elsif src.respond_to? :each
214
+ q = Queue.new
215
+ th = nil
216
+
217
+ timer_set = lambda do |t|
218
+ th = new_thread{ to(t){ q.pop } }
219
+ end
220
+
221
+ timer_cancel = lambda do |t|
222
+ th.kill if th rescue nil
223
+ end
224
+
225
+ timer_set[t]
226
+ begin
227
+ src.each do |buf|
228
+ timer_cancel[t]
229
+ send_dst[buf]
230
+ timer_set[t]
231
+ end
232
+ ensure
233
+ timer_cancel[t]
234
+ end
235
+
236
+ elsif src.respond_to? :read
237
+ buf = to(t){ src.read }
238
+ send_dst[buf]
239
+
240
+ else
241
+ buf = to(t){ src.to_s }
242
+ send_dst[buf]
243
+ end
244
+ end
245
+ #--}}}
246
+ end
247
+ module_function :relay
248
+
249
+ def spawn arg, *argv
250
+ #--{{{
251
+ argv.unshift(arg)
252
+ opts = ((argv.size > 1 and Hash === argv.last) ? argv.pop : {})
253
+ argv.flatten!
254
+ cmd = argv.join(' ')
255
+
256
+
257
+ getopt = getopts opts
258
+
259
+ ignore_exit_failure = getopt[ 'ignore_exit_failure', getopt['quiet', false] ]
260
+ ignore_exec_failure = getopt[ 'ignore_exec_failure', !getopt['raise', true] ]
261
+ exitstatus = getopt[ %w( exitstatus exit_status status ) ]
262
+ stdin = getopt[ %w( stdin in i 0 ) << 0 ]
263
+ stdout = getopt[ %w( stdout out o 1 ) << 1 ]
264
+ stderr = getopt[ %w( stderr err e 2 ) << 2 ]
265
+ pid = getopt[ 'pid' ]
266
+ timeout = getopt[ %w( timeout spawn_timeout ) ]
267
+ stdin_timeout = getopt[ %w( stdin_timeout ) ]
268
+ stdout_timeout = getopt[ %w( stdout_timeout io_timeout ) ]
269
+ stderr_timeout = getopt[ %w( stderr_timeout ) ]
270
+ status = getopt[ %w( status ) ]
271
+ cwd = getopt[ %w( cwd dir ) ]
272
+
273
+ exitstatus =
274
+ case exitstatus
275
+ when TrueClass, FalseClass
276
+ ignore_exit_failure = true if exitstatus
277
+ [0]
278
+ else
279
+ [*(exitstatus || 0)].map{|i| Integer i}
280
+ end
281
+
282
+ stdin ||= '' if stdin_timeout
283
+ stdout ||= '' if stdout_timeout
284
+ stderr ||= '' if stderr_timeout
285
+
286
+ started = false
287
+
288
+ status =
289
+ begin
290
+ chdir(cwd) do
291
+ Timeout::timeout(timeout) do
292
+ popen4(*argv) do |c, i, o, e|
293
+ started = true
294
+
295
+ %w( replace pid= << push update ).each do |msg|
296
+ break(pid.send(msg, c)) if pid.respond_to? msg
297
+ end
298
+
299
+ te = ThreadEnsemble.new c
300
+
301
+ te.add_thread(i, stdin) do |i, stdin|
302
+ relay stdin, i, stdin_timeout
303
+ i.close rescue nil
304
+ end
305
+
306
+ te.add_thread(o, stdout) do |o, stdout|
307
+ relay o, stdout, stdout_timeout
308
+ end
309
+
310
+ te.add_thread(e, stderr) do |o, stderr|
311
+ relay e, stderr, stderr_timeout
312
+ end
313
+
314
+ te.run
315
+ end
316
+ end
317
+ end
318
+ rescue
319
+ raise unless(not started and ignore_exec_failure)
320
+ end
321
+
322
+ raise SpawnError.new(cmd, status) unless
323
+ (ignore_exit_failure or (status.nil? and ignore_exec_failure) or exitstatus.include?(status.exitstatus))
324
+
325
+ status
326
+ #--}}}
327
+ end
328
+ module_function :spawn
329
+
330
+ def chdir cwd, &block
331
+ return(block.call Dir.pwd) unless cwd
332
+ Dir.chdir cwd, &block
333
+ end
334
+ module_function :chdir
335
+
336
+ def background arg, *argv
337
+ #--{{{
338
+ require 'thread'
339
+ q = Queue.new
340
+ opts = { 'pid' => q, :pid => q }
341
+ case argv.last
342
+ when Hash
343
+ argv.last.update opts
344
+ else
345
+ argv.push opts
346
+ end
347
+ thread = Thread.new(arg, argv){|arg, argv| spawn arg, *argv}
348
+ sc = class << thread; self; end
349
+ sc.module_eval {
350
+ define_method(:pid){ @pid ||= q.pop }
351
+ define_method(:spawn_status){ @spawn_status ||= value }
352
+ define_method(:exitstatus){ @exitstatus ||= spawn_status.exitstatus }
353
+ }
354
+ thread
355
+ #--}}}
356
+ end
357
+ alias bg background
358
+ module_function :background
359
+ module_function :bg
360
+
361
+ def maim pid, opts = {}
362
+ #--{{{
363
+ getopt = getopts opts
364
+ sigs = getopt[ 'signals', %w(SIGTERM SIGQUIT SIGKILL) ]
365
+ suspend = getopt[ 'suspend', 4 ]
366
+ pid = Integer pid
367
+ existed = false
368
+ sigs.each do |sig|
369
+ begin
370
+ Process.kill sig, pid
371
+ existed = true
372
+ rescue Errno::ESRCH
373
+ return(existed ? nil : true)
374
+ end
375
+ return true unless alive? pid
376
+ sleep suspend
377
+ return true unless alive? pid
378
+ end
379
+ return(not alive?(pid))
380
+ #--}}}
381
+ end
382
+ module_function :maim
383
+
384
+ def alive pid
385
+ #--{{{
386
+ pid = Integer pid
387
+ begin
388
+ Process.kill 0, pid
389
+ true
390
+ rescue Errno::ESRCH
391
+ false
392
+ end
393
+ #--}}}
394
+ end
395
+ alias alive? alive
396
+ module_function :alive
397
+ module_function :'alive?'
398
+ #--}}}
399
+ end
400
+
401
+ def open4(*cmd, &b) cmd.size == 0 ? Open4 : Open4::popen4(*cmd, &b) end
@@ -0,0 +1,28 @@
1
+ ## open4.gemspec
2
+ #
3
+
4
+ Gem::Specification::new do |spec|
5
+ spec.name = "open4"
6
+ spec.version = "1.0.1"
7
+ spec.platform = Gem::Platform::RUBY
8
+ spec.summary = "open4"
9
+
10
+ spec.description = "manage child processes and their io handles easily"
11
+
12
+ spec.files = ["lib", "lib/open4.rb", "open4.gemspec", "Rakefile", "README", "README.erb", "samples", "samples/bg.rb", "samples/block.rb", "samples/exception.rb", "samples/simple.rb", "samples/spawn.rb", "samples/stdin_timeout.rb", "samples/timeout.rb", "white_box", "white_box/leak.rb"]
13
+ spec.executables = []
14
+
15
+ spec.require_path = "lib"
16
+
17
+ spec.has_rdoc = true
18
+ spec.test_files = nil
19
+ #spec.add_dependency 'lib', '>= version'
20
+ #spec.add_dependency 'fattr'
21
+
22
+ spec.extensions.push(*[])
23
+
24
+ spec.rubyforge_project = "codeforpeople"
25
+ spec.author = "Ara T. Howard"
26
+ spec.email = "ara.t.howard@gmail.com"
27
+ spec.homepage = "http://github.com/ahoward/open4/tree/master"
28
+ end
@@ -0,0 +1,21 @@
1
+ require 'yaml'
2
+ require 'open4'
3
+ include Open4
4
+
5
+ stdin = '42'
6
+ stdout = ''
7
+ stderr = ''
8
+
9
+ t = bg 'ruby -e"sleep 4; puts ARGF.read"', 0=>stdin, 1=>stdout, 2=>stderr
10
+
11
+ waiter = Thread.new{ y t.pid => t.exitstatus } # t.exitstatus is a blocking call!
12
+
13
+ while((status = t.status))
14
+ y "status" => status
15
+ sleep 1
16
+ end
17
+
18
+ waiter.join
19
+
20
+ y "stdout" => stdout
21
+
@@ -0,0 +1,19 @@
1
+ require 'open4'
2
+ #
3
+ # when using block form the child process is automatically waited using
4
+ # waitpid2
5
+ #
6
+
7
+ status =
8
+ Open4::popen4("sh") do |pid, stdin, stdout, stderr|
9
+ stdin.puts "echo 42.out"
10
+ stdin.puts "echo 42.err 1>&2"
11
+ stdin.close
12
+
13
+ puts "pid : #{ pid }"
14
+ puts "stdout : #{ stdout.read.strip }"
15
+ puts "stderr : #{ stderr.read.strip }"
16
+ end
17
+
18
+ puts "status : #{ status.inspect }"
19
+ puts "exitstatus : #{ status.exitstatus }"
@@ -0,0 +1,3 @@
1
+ require "open4"
2
+
3
+ Open4::popen4 "noexist"
@@ -0,0 +1,15 @@
1
+ require "open4"
2
+
3
+ pid, stdin, stdout, stderr = Open4::popen4 "sh"
4
+
5
+ stdin.puts "echo 42.out"
6
+ stdin.puts "echo 42.err 1>&2"
7
+ stdin.close
8
+
9
+ ignored, status = Process::waitpid2 pid
10
+
11
+ puts "pid : #{ pid }"
12
+ puts "stdout : #{ stdout.read.strip }"
13
+ puts "stderr : #{ stderr.read.strip }"
14
+ puts "status : #{ status.inspect }"
15
+ puts "exitstatus : #{ status.exitstatus }"
@@ -0,0 +1,16 @@
1
+ require 'open4'
2
+ include Open4
3
+
4
+ cat = 'ruby -e" ARGF.each{|line| STDOUT << line} "'
5
+
6
+ stdout, stderr = '', ''
7
+ status = spawn cat, 'stdin' => '42', 'stdout' => stdout, 'stderr' => stderr
8
+ p status
9
+ p stdout
10
+ p stderr
11
+
12
+ stdout, stderr = '', ''
13
+ status = spawn cat, 0=>'42', 1=>stdout, 2=>stderr
14
+ p status
15
+ p stdout
16
+ p stderr
@@ -0,0 +1,9 @@
1
+ require 'open4'
2
+
3
+ producer = 'ruby -e" STDOUT.sync = true; loop{sleep(rand+rand) and puts 42} " 2>/dev/null'
4
+
5
+ consumer = 'ruby -e" STDOUT.sync = true; STDIN.each{|line| puts line} "'
6
+
7
+ open4(producer) do |pid, i, o, e|
8
+ open4.spawn consumer, 0=>o, 1=>STDOUT, :stdin_timeout => 1.4
9
+ end
@@ -0,0 +1,37 @@
1
+
2
+ require 'open4'
3
+
4
+ def show_failure
5
+ fork{ yield }
6
+ Process.wait
7
+ puts
8
+ end
9
+
10
+ #
11
+ # command timeout
12
+ #
13
+ show_failure{
14
+ open4.spawn 'sleep 42', 'timeout' => 1
15
+ }
16
+
17
+ #
18
+ # stdin timeout
19
+ #
20
+ show_failure{
21
+
22
+ producer = 'ruby -e" STDOUT.sync = true; loop{sleep(rand+rand) and puts 42} " 2>/dev/null'
23
+
24
+ consumer = 'ruby -e" STDOUT.sync = true; STDIN.each{|line| puts line} "'
25
+
26
+ open4(producer) do |pid, i, o, e|
27
+ open4.spawn consumer, 0=>o, 1=>STDOUT, :stdin_timeout => 1.4
28
+ end
29
+ }
30
+
31
+ #
32
+ # stdout timeout (stderr is similar)
33
+ #
34
+
35
+ show_failure{
36
+ open4.spawn 'ruby -e" sleep 2 and puts 42 "', 'stdout_timeout' => 1
37
+ }
@@ -0,0 +1,17 @@
1
+ require 'open4'
2
+
3
+ pid = Process.pid
4
+ fds = lambda{|pid| Dir["/proc/#{ pid }/fd/*"]}
5
+
6
+ loop do
7
+ before = fds[pid]
8
+ Open4.popen4 'ruby -e"buf = STDIN.read; STDOUT.puts buf; STDERR.puts buf "' do |p,i,o,e|
9
+ i.puts 42
10
+ i.close_write
11
+ o.read
12
+ e.read
13
+ end
14
+ after = fds[pid]
15
+ p(after - before)
16
+ puts
17
+ end
@@ -0,0 +1,10 @@
1
+ # Prefer gems to the bundled libs.
2
+ require 'rubygems'
3
+
4
+ begin
5
+ gem 'open4', '>= 1.0.1'
6
+ rescue LoadError
7
+ $:.unshift "#{File.dirname(__FILE__)}/open4-1.0.1"
8
+ end
9
+
10
+ require 'open4'
@@ -1,8 +1,8 @@
1
1
  module CrazyIvan
2
2
  module VERSION #:nodoc:
3
- MAJOR = 3
4
- MINOR = 2
5
- TINY = 3
3
+ MAJOR = 0
4
+ MINOR = 4
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end