cosine-brush 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/LICENSE +21 -0
  2. data/lib/brush.rb +4 -0
  3. data/lib/brush/pipeline.rb +506 -0
  4. metadata +4 -3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2009, Michael H. Buselli
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+ * Redistributions of source code must retain the above copyright
7
+ notice, this list of conditions and the following disclaimer.
8
+ * Redistributions in binary form must reproduce the above copyright
9
+ notice, this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ''AS IS'' AND ANY
13
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
16
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
19
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -1,3 +1,7 @@
1
+ #
2
+ # Copyright (c) 2009, Michael H. Buselli
3
+ # See LICENSE for details on permitted use.
4
+ #
1
5
 
2
6
  module Brush
3
7
  class Shell
@@ -0,0 +1,506 @@
1
+ #
2
+ # Copyright (c) 2009, Michael H. Buselli
3
+ # See LICENSE for details on permitted use.
4
+ #
5
+
6
+ module Brush; end
7
+
8
+
9
+ module Brush::Pipeline
10
+
11
+ #
12
+ # Create and execute a pipeline consisting of commands. Each
13
+ # element of the pipeline is an array of command arguments and
14
+ # options for that element of the pipeline.
15
+ #
16
+ # Options to each pipeline element include:
17
+ # :executable — specifies an alternative binary to run, instead of
18
+ # using the value for argv[0].
19
+ # :cd — change into this directory for this program.
20
+ # ---- probable future options
21
+ # :env — pass an alternative set of environment variables to the
22
+ # process.
23
+ # :stderr — file, pipe, or buffer to collect error information.
24
+ # :as_user — Array specifying user and credentials to run as.
25
+ #
26
+ # Options to the entire pipeline include:
27
+ # :stdin — file, pipe, or buffer to feed into the first element of
28
+ # the pipeline.
29
+ # :stdout — file, pipe, or buffer to collect the output of the
30
+ # last element of the pipeline.
31
+ #
32
+ # The return value is an Array reporting the success or failure of
33
+ # each element of the pipeline. Each element of the array is an
34
+ # Object that emulates a Process::Status object.
35
+ #
36
+ # Example:
37
+ # extracted_files = String.new
38
+ # Brush::Pipeline.pipeline(
39
+ # ['gzip', '-cd', 'filename.tar.gz', :cd => 'Downloads'],
40
+ # ['tar', 'xvf', '-', :cd => 'Extractions'],
41
+ # :stdout => extracted_files)
42
+ #
43
+ def pipeline (*elements)
44
+ options = {
45
+ :stdin => $stdin,
46
+ :stdout => $stdout
47
+ }
48
+
49
+ if elements[-1].respond_to?(:has_key?)
50
+ options.merge!(elements.pop)
51
+ end
52
+
53
+ if elements.size == 0
54
+ raise "invalid use of pipeline: no commands given"
55
+ end
56
+
57
+ # Don't modify the originals, and make sure we have an options hash
58
+ # for each element.
59
+ elements = elements.collect do |argv|
60
+ argv = argv.dup
61
+ argv.push(Hash.new) if not argv[-1].respond_to?(:has_key?)
62
+ argv
63
+ end
64
+
65
+ # Feed the input and the output
66
+ elements[0][-1][:stdin] = options[:stdin]
67
+ elements[-1][-1][:stdout] = options[:stdout]
68
+
69
+ # Build up the structure for the call to #sys.
70
+ elements.each_with_index do |argv, index|
71
+ argv[-1][:stdout] = elements[index + 1] if index < elements.size - 1
72
+ argv[-1][:stderr] = options[:stderr] if options.has_key?(:stderr)
73
+ end
74
+
75
+ sys(*elements[0])
76
+ end
77
+
78
+
79
+ PARENT_PIPES = {}
80
+
81
+ SysInfo = Struct.new(:process_infos, :threads)
82
+
83
+ #
84
+ # Options to each pipeline element include:
85
+ # :stdin — file, pipe, buffer, or :console to feed into the first
86
+ # element of the pipeline.
87
+ # :stdout — file, pipe, or buffer to collect the output of the
88
+ # last element of the pipeline.
89
+ # :stderr — file, pipe, or buffer to collect error information.
90
+ # :executable — specifies an alternative binary to run, instead of
91
+ # using the value for argv[0].
92
+ # :cd — change into this directory for this program.
93
+ # :close — File or IO.fileno values to close after fork() or set
94
+ # un-inheritable prior to calling ProcessCreate().
95
+ # ---- probable future options
96
+ # :keep — File or IO.fileno values to keep open in child.
97
+ # :timeout — terminate after a given time.
98
+ # :env — pass an alternative set of environment variables.
99
+ # :as_user — Array specifying user and credentials to run as.
100
+ # process.
101
+ #
102
+ # Returns an array of arrays:
103
+ # [process objects, threads, pipes]
104
+ #
105
+ # Process objects contain the pid and any relevent process and thread
106
+ # handles. Threads returned need to be joined to guarentee their
107
+ # input or output is completely processed after the program
108
+ # terminates. Pipes returned need to be closed after the program
109
+ # terminates.
110
+ #
111
+ def sys_start (*argv)
112
+ options = {
113
+ :stdin => $stdin,
114
+ :stdout => $stdout,
115
+ :stderr => $stderr,
116
+ :executable => argv[0],
117
+ :cd => '.',
118
+ :close => []
119
+ }
120
+
121
+ if argv[-1].respond_to?(:has_key?)
122
+ options.merge!(argv.pop)
123
+ end
124
+
125
+ options[:executable] = find_in_path(options[:executable])
126
+
127
+ original_stdfiles = [:stdout, :stderr].collect do |io_sym|
128
+ options[io_sym]
129
+ end
130
+
131
+ upper_child_pipes = [] # pipes for children up the pipeline
132
+ lower_child_pipes = [] # pipes for children down the pipeline
133
+ threads = [] # threads handling special needs I/O
134
+ process_infos = [] # info for children down the pipeline
135
+
136
+ [:stdin, :stdout, :stderr].each do |io_sym|
137
+ pior = process_io(io_sym, options[io_sym], original_stdfiles,
138
+ upper_child_pipes + lower_child_pipes + options[:close])
139
+ options[io_sym] = pior.io
140
+
141
+ upper_child_pipes << pior.io if pior.threads
142
+ lower_child_pipes << pior.pipe if pior.pipe
143
+ threads.push *pior.threads if pior.threads
144
+ process_infos.push *pior.process_infos if pior.process_infos
145
+ end
146
+
147
+ process_infos.unshift(
148
+ create_process(argv, options, lower_child_pipes + options[:close]))
149
+ upper_child_pipes.each { |io| io.close }
150
+ lower_child_pipes.each { |io| io.close }
151
+ SysInfo.new(process_infos, threads)
152
+ end
153
+
154
+
155
+ def sys (*argv)
156
+ sysinfo = sys_start(*argv)
157
+ overall_result = nil
158
+
159
+ results = sysinfo.process_infos.collect do |pi|
160
+ status = sys_wait(pi)
161
+ overall_result = status if overall_result.nil? and not status.success?
162
+ status
163
+ end
164
+
165
+ sysinfo.threads.each { |t| t.join }
166
+ overall_result = results[-1] if overall_result.nil?
167
+ duck_type_status_object(results, overall_result)
168
+ end
169
+
170
+
171
+ ProcessIOResult = Struct.new(:io, :pipe, :threads, :process_infos)
172
+
173
+ # File or IO, String (empty), String (filename), String (data),
174
+ # StringIO, Integer, :stdout, :stderr, :null or nil, :zero, Symbol (other),
175
+ # Array (new command)
176
+ # ---- supported:
177
+ # File or IO,
178
+ # StringIO, Integer, :stdout, :stderr, :null or nil, :zero,
179
+ # Array (new command)
180
+ # ---- future:
181
+ # String (empty), String (filename), String (data),
182
+ # Symbol (other),
183
+ #
184
+ # Returns an array of stuff:
185
+ # [IO object, thread or threads, pipe or pipes, process info objects]
186
+ #
187
+ # The IO object is the processed IO object based on the input IO
188
+ # object (+taret+), which may not have actually been an IO object.
189
+ # Threads returned, if any, need to be joined after the process
190
+ # terminates. Pipes returned, if any, need to be closed after the
191
+ # process terminates. Process info objects, if any, refer to other
192
+ # processes running in the pipeline that this call to process_io
193
+ # created.
194
+ #
195
+ def process_io (io_sym, target, original_stdfiles, close_pipes)
196
+
197
+ # Firstly, any File or IO value can be returned as it is.
198
+ # We will duck-type recognize File and IO objects if they respond
199
+ # to :fileno and the result of calling #fileno is not nil.
200
+ if target.respond_to?(:fileno) and not target.fileno.nil?
201
+ return ProcessIOResult.new(target)
202
+ end
203
+
204
+ # Integer (Fixnum in particular) arguments represent file
205
+ # descriptors to attach this IO to.
206
+ return ProcessIOResult.new(IO.new(target)) if target.is_a? Fixnum
207
+
208
+ # Handle special symbol values for :stdin. Valid symbols are
209
+ # :null and :zero. Using :null is the same as +nil+ (no input),
210
+ # and using :zero is like sending an endless stream of null
211
+ # characters to the process.
212
+ if io_sym == :stdin
213
+ if target.nil? or target == :null
214
+ return input_pipe {}
215
+ elsif target == :zero
216
+ return input_pipe { |w| w.syswrite("\x00" * 1024) while true }
217
+ elsif target.respond_to?(:sysread) # "fake" IO and StringIO
218
+ return input_pipe do |w|
219
+ data = nil; w.syswrite(data) while data = target.sysread(1024)
220
+ end
221
+ else
222
+ raise "Invalid input object in Brush#sys"
223
+ end
224
+
225
+ # Handle special symbol values for :stdout and :stderr. Valid
226
+ # symbols are :null, :zero, :stdout, and :stderr. The symbols
227
+ # :null and :zero mean the output is thrown away. :stdout means
228
+ # this output goes where standard output should go and :stderr
229
+ # means this output goes where standard error should go.
230
+ else # io_sym is :stdout or :stderr
231
+ if target.nil? or target == :null or target == :zero
232
+ return output_pipe { |r| r.sysread(1024) while true }
233
+ elsif target == :stdout
234
+ return process_io(io_sym, original_stdfiles[0], nil, close_pipes)
235
+ elsif target == :stderr
236
+ return process_io(io_sym, original_stdfiles[1], nil, close_pipes)
237
+ elsif target.respond_to?(:syswrite) # "fake" IO and StringIO
238
+ return output_pipe do |r|
239
+ data = nil; target.syswrite(data) while data = r.sysread(1024)
240
+ end
241
+ elsif target.is_a?(Array) # pipeline
242
+ return child_pipe do |r, w|
243
+ argv = target.dup
244
+ argv.push(Hash.new) if not argv[-1].respond_to?(:has_key?)
245
+ argv[-1].merge!(:stdin => r, :close => [w] + close_pipes)
246
+ sys_start(*argv)
247
+ end
248
+ else
249
+ raise "Invalid output object in Brush#sys"
250
+ end
251
+ end
252
+ end
253
+
254
+
255
+ def generic_pipe (p_pipe, ch_pipe)
256
+ mark_parent_pipe(p_pipe)
257
+ t = Thread.new do
258
+ begin
259
+ yield p_pipe
260
+ rescue Exception
261
+ ensure
262
+ p_pipe.close
263
+ end
264
+ end
265
+ ProcessIOResult.new(ch_pipe, nil, [t])
266
+ end
267
+
268
+ def mark_parent_pipe (pipe)
269
+ class << pipe
270
+ def close
271
+ super
272
+ ensure
273
+ Brush::Pipeline::PARENT_PIPES.delete(self)
274
+ end
275
+ end
276
+
277
+ Brush::Pipeline::PARENT_PIPES[pipe] = true
278
+ end
279
+
280
+ def input_pipe (&block)
281
+ generic_pipe *IO.pipe.reverse, &block
282
+ end
283
+
284
+ def output_pipe (&block)
285
+ generic_pipe *IO.pipe, &block
286
+ end
287
+
288
+ def child_pipe
289
+ r, w = *IO.pipe
290
+ sysinfo = yield r, w
291
+ ProcessIOResult.new(w, r, sysinfo.threads, sysinfo.process_infos)
292
+ end
293
+
294
+
295
+ def each_path_element
296
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir|
297
+ yield File.expand_path(dir)
298
+ end
299
+ end
300
+
301
+
302
+ def duck_type_status_object (object, status_or_pid, status_integer = nil)
303
+ if status_integer.nil? and status_or_pid.respond_to?(:success?)
304
+ class << object
305
+ # Act like the Process::Status @status.
306
+ (Process::Status.instance_methods - self.instance_methods).each do |m|
307
+ eval("def #{m} (*args); @status.#{m}(*args); end")
308
+ end
309
+ end
310
+ object.instance_variable_set(:@status, status_or_pid)
311
+
312
+ else
313
+ class << object
314
+ end
315
+
316
+ object.instance_variable_set(:@status, status_or_pid)
317
+ class << object
318
+ attr_reader :to_i, :pid
319
+
320
+ # We have no idea if we exited normally, coredumped, etc. Just
321
+ # pretend it's normal.
322
+ def coredump?; false; end
323
+ def exited?; true; end
324
+ def signaled?; false; end
325
+ def stopped?; false; end
326
+ def stopsig; nil; end
327
+ def success?; @to_i.zero?; end
328
+ def termsig; nil; end
329
+ alias exitstatus to_i
330
+
331
+ # Act like the Fixnum in @to_i.
332
+ (Fixnum.instance_methods - self.instance_methods).each do |m|
333
+ eval("def #{m} (*args); @to_i.#{m}(*args); end")
334
+ end
335
+ end
336
+ object.instance_variable_set(:@to_i, status_integer)
337
+ object.instance_variable_set(:@pid, status_or_pid)
338
+ end
339
+
340
+ object
341
+ end
342
+ end
343
+
344
+
345
+ module Brush::Pipeline::POSIX
346
+ ProcessInfo = Struct.new(:process_id)
347
+
348
+ def sys_wait (process_info)
349
+ #system("ls -l /proc/#{$$}/fd /proc/#{process_info.process_id}/fd")
350
+ Process.waitpid2(process_info.process_id)[1]
351
+ end
352
+
353
+ def create_process (argv, options, close_pipes)
354
+
355
+ # The following is used for manual specification testing to verify
356
+ # that pipes are correctly closed after fork. This is extremely
357
+ # difficult to write an RSpec test for, and it is only possible on
358
+ # platforms that have a /proc filesystem anyway. Regardless, this
359
+ # will be moved into an RSpec test at some point.
360
+ #
361
+ #$stderr.puts "===== P #{$$}: #{ %x"ls -l /proc/#{$$}/fd" }"
362
+
363
+ pid = fork do # child process
364
+ [:stdin, :stdout, :stderr].each do |io_sym|
365
+ io = options[io_sym]
366
+ if io != Kernel.const_get(io_sym.to_s.upcase)
367
+ Kernel.const_get(io_sym.to_s.upcase).reopen(io)
368
+ io.close
369
+ end
370
+ end
371
+
372
+ close_pipes.each { |io| io.close }
373
+ Brush::Pipeline::PARENT_PIPES.each_key { |io| io.close }
374
+
375
+ # This is the second half of the manual specification testing
376
+ # started above. See comment above for more information.
377
+ #
378
+ #$stderr.puts "===== C #{$$}: #{ %x"ls -l /proc/#{$$}/fd" }"
379
+
380
+ Dir.chdir(options[:cd]) do
381
+ exec [options[:executable], argv[0]], *argv[1..-1]
382
+ end
383
+
384
+ raise Error, "failed to exec"
385
+ end
386
+
387
+ ProcessInfo.new(pid)
388
+ end
389
+
390
+ def find_in_path (name)
391
+ if name.index(File::SEPARATOR) # Path is absolute or relative.
392
+ return File.expand_path(name)
393
+ else
394
+ each_path_element do |dir|
395
+ chkname = nil
396
+ return chkname if File.exists?(chkname = File.join(dir, name))
397
+ end
398
+ end
399
+
400
+ nil # Didn't find a match. :(
401
+ end
402
+ end
403
+
404
+
405
+ module Brush::Pipeline::Win32
406
+
407
+ def sys_wait (process_info)
408
+ numeric_status = Process.waitpid2(process_info.process_id)[1]
409
+ Process.CloseHandle(process_info.process_handle)
410
+ Process.CloseHandle(process_info.thread_handle)
411
+ duck_type_status_object(Object.new, process_info.process_id, numeric_status)
412
+ end
413
+
414
+
415
+ def make_handle_inheritable (io, inheritable = true)
416
+ if not SetHandleInformation(
417
+ get_osfhandle(io.fileno), Windows::Handle::HANDLE_FLAG_INHERIT,
418
+ inheritable ? Windows::Handle::HANDLE_FLAG_INHERIT : 0) \
419
+ then
420
+ raise Error, get_last_error
421
+ end
422
+ end
423
+
424
+
425
+ def create_process (argv, options, close_pipes)
426
+ close_pipes.each do |io|
427
+ make_handle_inheritable(io, false)
428
+ end
429
+
430
+ Brush::Pipeline::PARENT_PIPES.each_key do |io|
431
+ make_handle_inheritable(io, false)
432
+ end
433
+
434
+ [:stdin, :stdout, :stderr].each do |io_sym|
435
+ make_handle_inheritable(options[io_sym]) if options[io_sym]
436
+ end
437
+
438
+ Dir.chdir(options[:cd]) do
439
+ return Process.create(
440
+ :app_name => options[:executable],
441
+ :command_line => Escape.shell_command(argv),
442
+ :inherit => true,
443
+ :close_handles => false,
444
+ :creation_flags => Process::CREATE_NO_WINDOW,
445
+ :startup_info => {
446
+ :startf_flags => Process::STARTF_USESTDHANDLES,
447
+ :stdin => options[:stdin],
448
+ :stdout => options[:stdout],
449
+ :stderr => options[:stderr]
450
+ })
451
+ end
452
+ end
453
+
454
+
455
+ def find_in_path (name)
456
+ chkname = nil
457
+
458
+ if name =~ %r"[:/\\]" # Path is absolute or relative.
459
+ basename = File.basename(name)
460
+ fullname = File.expand_path(name)
461
+
462
+ if basename =~ /\./ # Path comes with extension.
463
+ return fullname
464
+ else # Need to find extension.
465
+ each_pathext_element do |ext|
466
+ return chkname if File.exists?(chkname = [fullname, ext].join)
467
+ end
468
+ end
469
+
470
+ elsif name =~ /\./ # Given extension.
471
+ each_path_element do |dir|
472
+ return chkname if File.exists?(chkname = File.join(dir, name))
473
+ end
474
+
475
+ else # Just a name—no path or extension.
476
+ each_path_element do |dir|
477
+ each_pathext_element do |ext|
478
+ if File.exists?(chkname = File.join(dir, [name, ext].join))
479
+ return chkname
480
+ end
481
+ end
482
+ end
483
+ end
484
+
485
+ nil # Didn't find a match. :(
486
+ end
487
+
488
+
489
+ def each_pathext_element
490
+ ENV['PATHEXT'].split(File::PATH_SEPARATOR).each { |ext| yield ext }
491
+ end
492
+
493
+ end
494
+
495
+
496
+ module Brush::Pipeline
497
+ if RUBY_PLATFORM =~ /-(mswin|mingw)/
498
+ gem 'win32-process', '>= 0.6.1'
499
+ require 'win32/process'
500
+ require 'escape'
501
+ include Windows::Handle
502
+ include Brush::Pipeline::Win32
503
+ else
504
+ include Brush::Pipeline::POSIX
505
+ end
506
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cosine-brush
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael H Buselli
@@ -24,11 +24,12 @@ extensions: []
24
24
  extra_rdoc_files: []
25
25
 
26
26
  files:
27
+ - LICENSE
27
28
  - bin/brush
28
29
  - lib/brush.rb
30
+ - lib/brush/pipeline.rb
29
31
  has_rdoc: true
30
32
  homepage: http://cosine.org/ruby/brush/
31
- licenses:
32
33
  post_install_message:
33
34
  rdoc_options: []
34
35
 
@@ -49,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
49
50
  requirements: []
50
51
 
51
52
  rubyforge_project: brush
52
- rubygems_version: 1.3.5
53
+ rubygems_version: 1.2.0
53
54
  signing_key:
54
55
  specification_version: 2
55
56
  summary: "Brush \xE2\x80\x94 the Bourne RUby SHell"