diff options
author | ara.t.howard <ara.t.howard@gmail.com> | 2010-01-07 13:20:44 -0700 |
---|---|---|
committer | ara.t.howard <ara.t.howard@gmail.com> | 2010-01-07 13:20:44 -0700 |
commit | 0ff13d7f3547fcce760004fc3071612a4866114d (patch) | |
tree | d1bd21ac18909ebc32b73ef99ef4ff0a9ac709c0 | |
download | systemu-0ff13d7f3547fcce760004fc3071612a4866114d.tar.gz |
first commit
-rw-r--r-- | LICENSE | 3 | ||||
-rw-r--r-- | README | 163 | ||||
-rw-r--r-- | README.erb | 31 | ||||
-rw-r--r-- | Rakefile | 233 | ||||
-rw-r--r-- | lib/systemu.rb | 301 | ||||
-rw-r--r-- | samples/a.rb | 11 | ||||
-rw-r--r-- | samples/b.rb | 12 | ||||
-rw-r--r-- | samples/c.rb | 10 | ||||
-rw-r--r-- | samples/d.rb | 11 | ||||
-rw-r--r-- | samples/e.rb | 9 | ||||
-rw-r--r-- | samples/f.rb | 18 | ||||
-rw-r--r-- | systemu.gemspec | 27 |
12 files changed, 829 insertions, 0 deletions
@@ -0,0 +1,3 @@ +same as Ruby's + +http://www.ruby-lang.org/en/LICENSE.txt @@ -0,0 +1,163 @@ +NAME + + systemu + +SYNOPSIS + + univeral capture of stdout and stderr and handling of child process pid for windows, *nix, etc. + +URIS + + http://github.com/ahoward/systemu + http://rubyforge.org/projects/codeforpeople/ + +INSTALL + + gem install systemu + +HISTORY + 1.3.0 + - move to github + + 1.2.0 + + - fixed handling of background thread management - needed + Thread.current.abort_on_exception = true + + - fixed reporting of child pid, it was reported as the parent's pid before + +SAMPLES + + + <========< samples/a.rb >========> + + ~ > cat samples/a.rb + + # + # systemu can be used on any platform to return status, stdout, and stderr of + # any command. unlike other methods like open3/popen4 there is zero danger of + # full pipes or threading issues hanging your process or subprocess. + # + require 'systemu' + + date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) + + status, stdout, stderr = systemu date + p [ status, stdout, stderr ] + + ~ > ruby samples/a.rb + + [#<Process::Status: pid=5138,exited(0)>, "Thu Jan 07 13:19:37 -0700 2010\n", "Thu Jan 07 13:19:37 -0700 2010\n"] + + + <========< samples/b.rb >========> + + ~ > cat samples/b.rb + + # + # quite a few keys can be passed to the command to alter it's behaviour. if + # either stdout or stderr is supplied those objects should respond_to? '<<' + # and only status will be returned + # + require 'systemu' + + date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) + + stdout, stderr = '', '' + status = systemu date, 'stdout' => stdout, 'stderr' => stderr + p [ status, stdout, stderr ] + + ~ > ruby samples/b.rb + + [#<Process::Status: pid=5143,exited(0)>, "Thu Jan 07 13:19:38 -0700 2010\n", "Thu Jan 07 13:19:38 -0700 2010\n"] + + + <========< samples/c.rb >========> + + ~ > cat samples/c.rb + + # + # of course stdin can be supplied too. synonyms for 'stdin' include '0' and + # 0. the other stdio streams have similar shortcuts + # + require 'systemu' + + cat = %q( ruby -e" ARGF.each{|line| puts line} " ) + + status = systemu cat, 0=>'the stdin for cat', 1=>stdout='' + puts stdout + + ~ > ruby samples/c.rb + + the stdin for cat + + + <========< samples/d.rb >========> + + ~ > cat samples/d.rb + + # + # the cwd can be supplied + # + require 'systemu' + require 'tmpdir' + + pwd = %q( ruby -e" STDERR.puts Dir.pwd " ) + + status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir + puts stderr + + + ~ > ruby samples/d.rb + + /private/var/folders/nO/nOjBCb30ELegrm98Bhyvbk+++TM/-Tmp- + + + <========< samples/e.rb >========> + + ~ > cat samples/e.rb + + # + # any environment vars specified are merged into the child's environment + # + require 'systemu' + + env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " ) + + status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 } + puts stdout + + ~ > ruby samples/e.rb + + 42 + + + <========< samples/f.rb >========> + + ~ > cat samples/f.rb + + # + # if a block is specified then it is passed the child pid and run in a + # background thread. note that this thread will __not__ be blocked during the + # execution of the command so it may do useful work such as killing the child + # if execution time passes a certain threshold + # + require 'systemu' + + looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " ) + + status, stdout, stderr = + systemu looper do |cid| + sleep 3 + Process.kill 9, cid + end + + p status + p stderr + + ~ > ruby samples/f.rb + + #<Process::Status: pid=5163,signaled(SIGKILL=9)> + "1262895578\n1262895579\n1262895580\n" + + diff --git a/README.erb b/README.erb new file mode 100644 index 0000000..7b823c4 --- /dev/null +++ b/README.erb @@ -0,0 +1,31 @@ +NAME + + systemu + +SYNOPSIS + + univeral capture of stdout and stderr and handling of child process pid for windows, *nix, etc. + +URIS + + http://github.com/ahoward/systemu + http://rubyforge.org/projects/codeforpeople/ + +INSTALL + + gem install systemu + +HISTORY + 1.3.0 + - move to github + + 1.2.0 + + - fixed handling of background thread management - needed + Thread.current.abort_on_exception = true + + - fixed reporting of child pid, it was reported as the parent's pid before + +SAMPLES + +<%= samples %> diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..cd11779 --- /dev/null +++ b/Rakefile @@ -0,0 +1,233 @@ + +This.rubyforge_project = 'codeforpeople' +This.author = "Ara T. Howard" +This.email = "ara.t.howard@gmail.com" +This.homepage = "http://github.com/ahoward/#{ This.lib }/tree/master" + + +task :default do + puts(Rake::Task.tasks.map{|task| task.name} - ['default']) +end + + +task :gemspec do + ignore_extensions = 'git', 'svn', 'tmp', /sw./, 'bak', 'gem' + ignore_directories = 'pkg' + ignore_files = 'test/log' + + shiteless = + lambda do |list| + list.delete_if do |entry| + next unless test(?e, entry) + extension = File.basename(entry).split(%r/[.]/).last + ignore_extensions.any?{|ext| ext === extension} + end + list.delete_if do |entry| + next unless test(?d, entry) + dirname = File.expand_path(entry) + ignore_directories.any?{|dir| File.expand_path(dir) == dirname} + end + list.delete_if do |entry| + next unless test(?f, entry) + filename = File.expand_path(entry) + ignore_files.any?{|file| File.expand_path(file) == filename} + end + end + + lib = This.lib + object = This.object + version = This.version + files = shiteless[Dir::glob("**/**")] + executables = shiteless[Dir::glob("bin/*")].map{|exe| File.basename(exe)} + has_rdoc = true #File.exist?('doc') + test_files = "test/#{ lib }.rb" if File.file?("test/#{ lib }.rb") + summary = object.respond_to?(:summary) ? object.summary : "summary: #{ lib } kicks the ass" + description = object.respond_to?(:description) ? object.description : "description: #{ lib } kicks the ass" + + extensions = This.extensions + if extensions.nil? + %w( Makefile configure extconf.rb ).each do |ext| + extensions << ext if File.exists?(ext) + end + end + extensions = [extensions].flatten.compact + + template = + if test(?e, 'gemspec.erb') + Template{ IO.read('gemspec.erb') } + else + Template { + <<-__ + ## #{ lib }.gemspec + # + + Gem::Specification::new do |spec| + spec.name = #{ lib.inspect } + spec.version = #{ version.inspect } + spec.platform = Gem::Platform::RUBY + spec.summary = #{ lib.inspect } + spec.description = #{ description.inspect } + + spec.files = #{ files.inspect } + spec.executables = #{ executables.inspect } + + spec.require_path = "lib" + + spec.has_rdoc = #{ has_rdoc.inspect } + spec.test_files = #{ test_files.inspect } + #spec.add_dependency 'lib', '>= version' + spec.add_dependency 'fattr' + + spec.extensions.push(*#{ extensions.inspect }) + + spec.rubyforge_project = #{ This.rubyforge_project.inspect } + spec.author = #{ This.author.inspect } + spec.email = #{ This.email.inspect } + spec.homepage = #{ This.homepage.inspect } + end + __ + } + end + + open("#{ lib }.gemspec", "w"){|fd| fd.puts template} + This.gemspec = "#{ lib }.gemspec" +end + +task :gem => [:clean, :gemspec] do + Fu.mkdir_p This.pkgdir + before = Dir['*.gem'] + cmd = "gem build #{ This.gemspec }" + `#{ cmd }` + after = Dir['*.gem'] + gem = ((after - before).first || after.first) or abort('no gem!') + Fu.mv gem, This.pkgdir + This.gem = File.basename(gem) +end + +task :readme do + samples = '' + prompt = '~ > ' + lib = This.lib + version = This.version + + Dir['sample*/*'].sort.each do |sample| + samples << "\n" << " <========< #{ sample } >========>" << "\n\n" + + cmd = "cat #{ sample }" + samples << Util.indent(prompt + cmd, 2) << "\n\n" + samples << Util.indent(`#{ cmd }`, 4) << "\n" + + cmd = "ruby #{ sample }" + samples << Util.indent(prompt + cmd, 2) << "\n\n" + + cmd = "ruby -e'STDOUT.sync=true; exec %(ruby -I ./lib #{ sample })'" + samples << Util.indent(`#{ cmd } 2>&1`, 4) << "\n" + end + + template = + if test(?e, 'readme.erb') + Template{ IO.read('readme.erb') } + else + Template { + <<-__ + NAME + #{ lib } + + DESCRIPTION + + INSTALL + gem install #{ lib } + + SAMPLES + #{ samples } + __ + } + end + + open("README", "w"){|fd| fd.puts template} +end + + +task :clean do + Dir[File.join(This.pkgdir, '**/**')].each{|entry| Fu.rm_rf(entry)} +end + + +task :release => [:clean, :gemspec, :gem] do + gems = Dir[File.join(This.pkgdir, '*.gem')].flatten + raise "which one? : #{ gems.inspect }" if gems.size > 1 + raise "no gems?" if gems.size < 1 + cmd = "rubyforge login && rubyforge add_release #{ This.rubyforge_project } #{ This.lib } #{ This.version } #{ This.pkgdir }/#{ This.gem }" + puts cmd + system cmd +end + + + + + +BEGIN { + $VERBOSE = nil + + require 'ostruct' + require 'erb' + require 'fileutils' + + Fu = FileUtils + + This = OpenStruct.new + + This.file = File.expand_path(__FILE__) + This.dir = File.dirname(This.file) + This.pkgdir = File.join(This.dir, 'pkg') + + lib = ENV['LIB'] + unless lib + lib = File.basename(Dir.pwd) + end + This.lib = lib + + version = ENV['VERSION'] + unless version + require "./lib/#{ This.lib }" + This.name = lib.capitalize + This.object = eval(This.name) + version = This.object.send(:version) + end + This.version = version + + abort('no lib') unless This.lib + abort('no version') unless This.version + + module Util + def indent(s, n = 2) + s = unindent(s) + ws = ' ' * n + s.gsub(%r/^/, ws) + end + + def unindent(s) + indent = nil + s.each do |line| + next if line =~ %r/^\s*$/ + indent = line[%r/^\s*/] and break + end + indent ? s.gsub(%r/^#{ indent }/, "") : s + end + extend self + end + + class Template + def initialize(&block) + @block = block + @template = block.call.to_s + end + def expand(b=nil) + ERB.new(Util.unindent(@template)).result(b||@block) + end + alias_method 'to_s', 'expand' + end + def Template(*args, &block) Template.new(*args, &block) end + + Dir.chdir(This.dir) +} diff --git a/lib/systemu.rb b/lib/systemu.rb new file mode 100644 index 0000000..a29087c --- /dev/null +++ b/lib/systemu.rb @@ -0,0 +1,301 @@ +# vim: ts=2:sw=2:sts=2:et:fdm=marker +require 'tmpdir' +require 'socket' +require 'fileutils' +require 'rbconfig' +require 'thread' +require 'yaml' + +class Object + def systemu(*a, &b) SystemUniversal.new(*a, &b).systemu end +end + +class SystemUniversal +# +# constants +# + SystemUniversal::VERSION = '1.3.0' unless defined? SystemUniversal::VERSION + def SystemUniversal.version() SystemUniversal::VERSION end + def version() SystemUniversal::VERSION end +# +# class methods +# + + @host = Socket.gethostname + @ppid = Process.ppid + @pid = Process.pid + @turd = ENV['SYSTEMU_TURD'] + + c = ::Config::CONFIG + ruby = File.join(c['bindir'], c['ruby_install_name']) << c['EXEEXT'] + @ruby = if system('%s -e 42' % ruby) + ruby + else + system('%s -e 42' % 'ruby') ? 'ruby' : warn('no ruby in PATH/CONFIG') + end + + class << self + %w( host ppid pid ruby turd ).each{|a| attr_accessor a} + end + +# +# instance methods +# + + def initialize argv, opts = {}, &block + getopt = getopts opts + + @argv = argv + @block = block + + @stdin = getopt[ ['stdin', 'in', '0', 0] ] + @stdout = getopt[ ['stdout', 'out', '1', 1] ] + @stderr = getopt[ ['stderr', 'err', '2', 2] ] + @env = getopt[ 'env' ] + @cwd = getopt[ 'cwd' ] + + @host = getopt[ 'host', self.class.host ] + @ppid = getopt[ 'ppid', self.class.ppid ] + @pid = getopt[ 'pid', self.class.pid ] + @ruby = getopt[ 'ruby', self.class.ruby ] + end + + def systemu + tmpdir do |tmp| + c = child_setup tmp + status = nil + + begin + thread = nil + + quietly{ + IO.popen "#{ @ruby } #{ c['program'] }", 'r+' do |pipe| + line = pipe.gets + case line + when %r/^pid: \d+$/ + cid = Integer line[%r/\d+/] + else + begin + buf = pipe.read + buf = "#{ line }#{ buf }" + e = Marshal.load buf + raise unless Exception === e + raise e + rescue + raise "wtf?\n#{ buf }\n" + end + end + thread = new_thread cid, @block if @block + pipe.read rescue nil + end + } + status = $? + ensure + if thread + begin + class << status + attr 'thread' + end + status.instance_eval{ @thread = thread } + rescue + 42 + end + end + end + + if @stdout or @stderr + open(c['stdout']){|f| relay f => @stdout} if @stdout + open(c['stderr']){|f| relay f => @stderr} if @stderr + status + else + [status, IO.read(c['stdout']), IO.read(c['stderr'])] + end + end + end + + def new_thread cid, block + q = Queue.new + Thread.new(cid) do |cid| + current = Thread.current + current.abort_on_exception = true + q.push current + block.call cid + end + q.pop + end + + def child_setup tmp + stdin = File.expand_path(File.join(tmp, 'stdin')) + stdout = File.expand_path(File.join(tmp, 'stdout')) + stderr = File.expand_path(File.join(tmp, 'stderr')) + program = File.expand_path(File.join(tmp, 'program')) + config = File.expand_path(File.join(tmp, 'config')) + + if @stdin + open(stdin, 'w'){|f| relay @stdin => f} + else + FileUtils.touch stdin + end + FileUtils.touch stdout + FileUtils.touch stderr + + c = {} + c['argv'] = @argv + c['env'] = @env + c['cwd'] = @cwd + c['stdin'] = stdin + c['stdout'] = stdout + c['stderr'] = stderr + c['program'] = program + open(config, 'w'){|f| YAML.dump c, f} + + open(program, 'w'){|f| f.write child_program(config)} + + c + end + + def quietly + v = $VERBOSE + $VERBOSE = nil + yield + ensure + $VERBOSE = v + end + + def child_program config + <<-program + PIPE = STDOUT.dup + begin + require 'yaml' + + config = YAML.load(IO.read('#{ config }')) + + argv = config['argv'] + env = config['env'] + cwd = config['cwd'] + stdin = config['stdin'] + stdout = config['stdout'] + stderr = config['stderr'] + + Dir.chdir cwd if cwd + env.each{|k,v| ENV[k.to_s] = v.to_s} if env + + STDIN.reopen stdin + STDOUT.reopen stdout + STDERR.reopen stderr + + PIPE.puts "pid: \#{ Process.pid }" + PIPE.flush ### the process is ready yo! + PIPE.close + + exec *argv + rescue Exception => e + PIPE.write Marshal.dump(e) rescue nil + exit 42 + end + program + end + + def relay srcdst + src, dst, ignored = srcdst.to_a.first + if src.respond_to? 'read' + while((buf = src.read(8192))); dst << buf; end + else + src.each{|buf| dst << buf} + end + end + + def tmpdir d = Dir.tmpdir, max = 42, &b + i = -1 and loop{ + i += 1 + + tmp = File.join d, "systemu_#{ @host }_#{ @ppid }_#{ @pid }_#{ rand }_#{ i += 1 }" + + begin + Dir.mkdir tmp + rescue Errno::EEXIST + raise if i >= max + next + end + + break( + if b + begin + b.call tmp + ensure + FileUtils.rm_rf tmp unless SystemU.turd + end + else + tmp + end + ) + } + end + + def getopts opts = {} + lambda do |*args| + keys, default, ignored = args + catch('opt') do + [keys].flatten.each do |key| + [key, key.to_s, key.to_s.intern].each do |key| + throw 'opt', opts[key] if opts.has_key?(key) + end + end + default + end + end + end +end + +SystemU = SystemUniversal unless defined? SystemU +Systemu = SystemUniversal unless defined? Systemu + + + + + + + + + + + + + +if $0 == __FILE__ +# +# date +# + date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) + + status, stdout, stderr = systemu date + p [status, stdout, stderr] + + status = systemu date, 1=>(stdout = '') + p [status, stdout] + + status = systemu date, 2=>(stderr = '') + p [status, stderr] +# +# sleep +# + sleep = %q( ruby -e" p(sleep(1)) " ) + status, stdout, stderr = systemu sleep + p [status, stdout, stderr] + + sleep = %q( ruby -e" p(sleep(42)) " ) + status, stdout, stderr = systemu(sleep){|cid| Process.kill 9, cid} + p [status, stdout, stderr] +# +# env +# + env = %q( ruby -e" p ENV['A'] " ) + status, stdout, stderr = systemu env, :env => {'A' => 42} + p [status, stdout, stderr] +# +# cwd +# + env = %q( ruby -e" p Dir.pwd " ) + status, stdout, stderr = systemu env, :cwd => Dir.tmpdir + p [status, stdout, stderr] +end diff --git a/samples/a.rb b/samples/a.rb new file mode 100644 index 0000000..37af06a --- /dev/null +++ b/samples/a.rb @@ -0,0 +1,11 @@ +# +# systemu can be used on any platform to return status, stdout, and stderr of +# any command. unlike other methods like open3/popen4 there is zero danger of +# full pipes or threading issues hanging your process or subprocess. +# + require 'systemu' + + date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) + + status, stdout, stderr = systemu date + p [ status, stdout, stderr ] diff --git a/samples/b.rb b/samples/b.rb new file mode 100644 index 0000000..951dce1 --- /dev/null +++ b/samples/b.rb @@ -0,0 +1,12 @@ +# +# quite a few keys can be passed to the command to alter it's behaviour. if +# either stdout or stderr is supplied those objects should respond_to? '<<' +# and only status will be returned +# + require 'systemu' + + date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) + + stdout, stderr = '', '' + status = systemu date, 'stdout' => stdout, 'stderr' => stderr + p [ status, stdout, stderr ] diff --git a/samples/c.rb b/samples/c.rb new file mode 100644 index 0000000..c3ffc54 --- /dev/null +++ b/samples/c.rb @@ -0,0 +1,10 @@ +# +# of course stdin can be supplied too. synonyms for 'stdin' include '0' and +# 0. the other stdio streams have similar shortcuts +# + require 'systemu' + + cat = %q( ruby -e" ARGF.each{|line| puts line} " ) + + status = systemu cat, 0=>'the stdin for cat', 1=>stdout='' + puts stdout diff --git a/samples/d.rb b/samples/d.rb new file mode 100644 index 0000000..84d4ae9 --- /dev/null +++ b/samples/d.rb @@ -0,0 +1,11 @@ +# +# the cwd can be supplied +# + require 'systemu' + require 'tmpdir' + + pwd = %q( ruby -e" STDERR.puts Dir.pwd " ) + + status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir + puts stderr + diff --git a/samples/e.rb b/samples/e.rb new file mode 100644 index 0000000..2c26e62 --- /dev/null +++ b/samples/e.rb @@ -0,0 +1,9 @@ +# +# any environment vars specified are merged into the child's environment +# + require 'systemu' + + env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " ) + + status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 } + puts stdout diff --git a/samples/f.rb b/samples/f.rb new file mode 100644 index 0000000..158301d --- /dev/null +++ b/samples/f.rb @@ -0,0 +1,18 @@ +# +# if a block is specified then it is passed the child pid and run in a +# background thread. note that this thread will __not__ be blocked during the +# execution of the command so it may do useful work such as killing the child +# if execution time passes a certain threshold +# + require 'systemu' + + looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " ) + + status, stdout, stderr = + systemu looper do |cid| + sleep 3 + Process.kill 9, cid + end + + p status + p stderr diff --git a/systemu.gemspec b/systemu.gemspec new file mode 100644 index 0000000..1f38a94 --- /dev/null +++ b/systemu.gemspec @@ -0,0 +1,27 @@ +## systemu.gemspec +# + +Gem::Specification::new do |spec| + spec.name = "systemu" + spec.version = "1.8.6" + spec.platform = Gem::Platform::RUBY + spec.summary = "systemu" + spec.description = "description: systemu kicks the ass" + + spec.files = ["lib", "lib/systemu.rb", "LICENSE", "Rakefile", "README.erb", "samples", "samples/a.rb", "samples/b.rb", "samples/c.rb", "samples/d.rb", "samples/e.rb", "samples/f.rb", "systemu.gemspec"] + spec.executables = [] + + spec.require_path = "lib" + + spec.has_rdoc = true + spec.test_files = nil + #spec.add_dependency 'lib', '>= version' + spec.add_dependency 'fattr' + + spec.extensions.push(*[]) + + spec.rubyforge_project = "codeforpeople" + spec.author = "Ara T. Howard" + spec.email = "ara.t.howard@gmail.com" + spec.homepage = "http://github.com/ahoward/systemu/tree/master" +end |