├── BSDL ├── LICENSE ├── README ├── README.erb ├── Rakefile ├── lib └── systemu.rb ├── pkg └── systemu-2.6.4.gem ├── samples ├── a.rb ├── b.rb ├── c.rb ├── d.rb ├── e.rb └── f.rb ├── systemu.gemspec └── test ├── systemu_test.rb └── testing.rb /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Ara T. Howard 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 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | systemu is copyrighted free software by Ara T. Howard . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a) place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b) use the modified software only within your corporation or 18 | organization. 19 | 20 | c) give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d) make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a) distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b) accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c) give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d) make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | 58 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | NAME 2 | 3 | systemu 4 | 5 | SYNOPSIS 6 | 7 | universal capture of stdout and stderr and handling of child process pid for 8 | windows, *nix, etc. 9 | 10 | URIS 11 | 12 | http://github.com/ahoward/systemu 13 | http://rubyforge.org/projects/codeforpeople/ 14 | 15 | INSTALL 16 | 17 | gem install systemu 18 | 19 | HISTORY 20 | 2.0.0 21 | - versioning issue. new gem release. 22 | 23 | 1.3.1 24 | - updates for ruby 1.9.1 25 | 26 | 1.3.0 27 | - move to github 28 | 29 | 1.2.0 30 | 31 | - fixed handling of background thread management - needed 32 | Thread.current.abort_on_exception = true 33 | 34 | - fixed reporting of child pid, it was reported as the parent's pid before 35 | 36 | SAMPLES 37 | 38 | 39 | <========< samples/a.rb >========> 40 | 41 | ~ > cat samples/a.rb 42 | 43 | # 44 | # systemu can be used on any platform to return status, stdout, and stderr of 45 | # any command. unlike other methods like open3/popen4 there is zero danger of 46 | # full pipes or threading issues hanging your process or subprocess. 47 | # 48 | require 'systemu' 49 | 50 | date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) 51 | 52 | status, stdout, stderr = systemu date 53 | p [ status, stdout, stderr ] 54 | 55 | ~ > ruby samples/a.rb 56 | 57 | [#, "2011-12-11 22:07:30 -0700\n", "2011-12-11 22:07:30 -0700\n"] 58 | 59 | 60 | <========< samples/b.rb >========> 61 | 62 | ~ > cat samples/b.rb 63 | 64 | # 65 | # quite a few keys can be passed to the command to alter it's behaviour. if 66 | # either stdout or stderr is supplied those objects should respond_to? '<<' 67 | # and only status will be returned 68 | # 69 | require 'systemu' 70 | 71 | date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) 72 | 73 | stdout, stderr = '', '' 74 | status = systemu date, 'stdout' => stdout, 'stderr' => stderr 75 | p [ status, stdout, stderr ] 76 | 77 | ~ > ruby samples/b.rb 78 | 79 | [#, "2011-12-11 22:07:30 -0700\n", "2011-12-11 22:07:30 -0700\n"] 80 | 81 | 82 | <========< samples/c.rb >========> 83 | 84 | ~ > cat samples/c.rb 85 | 86 | # 87 | # of course stdin can be supplied too. synonyms for 'stdin' include '0' and 88 | # 0. the other stdio streams have similar shortcuts 89 | # 90 | require 'systemu' 91 | 92 | cat = %q( ruby -e" ARGF.each{|line| puts line} " ) 93 | 94 | status = systemu cat, 0=>'the stdin for cat', 1=>stdout='' 95 | puts stdout 96 | 97 | ~ > ruby samples/c.rb 98 | 99 | the stdin for cat 100 | 101 | 102 | <========< samples/d.rb >========> 103 | 104 | ~ > cat samples/d.rb 105 | 106 | # 107 | # the cwd can be supplied 108 | # 109 | require 'systemu' 110 | require 'tmpdir' 111 | 112 | pwd = %q( ruby -e" STDERR.puts Dir.pwd " ) 113 | 114 | status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir 115 | puts stderr 116 | 117 | 118 | ~ > ruby samples/d.rb 119 | 120 | /private/var/folders/sp/nwtflj890qnb6z4b53dqxvlw0000gp/T 121 | 122 | 123 | <========< samples/e.rb >========> 124 | 125 | ~ > cat samples/e.rb 126 | 127 | # 128 | # any environment vars specified are merged into the child's environment 129 | # 130 | require 'systemu' 131 | 132 | env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " ) 133 | 134 | status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 } 135 | puts stdout 136 | 137 | ~ > ruby samples/e.rb 138 | 139 | 42 140 | 141 | 142 | <========< samples/f.rb >========> 143 | 144 | ~ > cat samples/f.rb 145 | 146 | # 147 | # if a block is specified then it is passed the child pid and run in a 148 | # background thread. note that this thread will __not__ be blocked during the 149 | # execution of the command so it may do useful work such as killing the child 150 | # if execution time passes a certain threshold 151 | # 152 | require 'systemu' 153 | 154 | looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " ) 155 | 156 | status, stdout, stderr = 157 | systemu looper do |cid| 158 | sleep 3 159 | Process.kill 9, cid 160 | end 161 | 162 | p status 163 | p stderr 164 | 165 | ~ > ruby samples/f.rb 166 | 167 | # 168 | "1323666451\n1323666452\n1323666453\n" 169 | 170 | 171 | -------------------------------------------------------------------------------- /README.erb: -------------------------------------------------------------------------------- 1 | NAME 2 | 3 | systemu 4 | 5 | SYNOPSIS 6 | 7 | universal capture of stdout and stderr and handling of child process pid for windows, *nix, etc. 8 | 9 | URIS 10 | 11 | http://github.com/ahoward/systemu 12 | http://rubyforge.org/projects/codeforpeople/ 13 | 14 | INSTALL 15 | 16 | gem install systemu 17 | 18 | HISTORY 19 | 2.0.0 20 | - versioning issue. new gem release. 21 | 22 | 1.3.1 23 | - updates for ruby 1.9.1 24 | 25 | 1.3.0 26 | - move to github 27 | 28 | 1.2.0 29 | 30 | - fixed handling of background thread management - needed 31 | Thread.current.abort_on_exception = true 32 | 33 | - fixed reporting of child pid, it was reported as the parent's pid before 34 | 35 | SAMPLES 36 | 37 | <%= samples %> 38 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 "Ruby"} 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 : "Ruby" 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 | } 395 | -------------------------------------------------------------------------------- /lib/systemu.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'tmpdir' 4 | require 'socket' 5 | require 'fileutils' 6 | require 'rbconfig' 7 | require 'thread' 8 | 9 | class Object 10 | def systemu(*a, &b) SystemUniversal.new(*a, &b).systemu end 11 | end 12 | 13 | class SystemUniversal 14 | # 15 | # constants 16 | # 17 | SystemUniversal::VERSION = '2.6.5' unless SystemUniversal.send(:const_defined?, :VERSION) 18 | def SystemUniversal.version() SystemUniversal::VERSION end 19 | def version() SystemUniversal::VERSION end 20 | def SystemUniversal.description 21 | "universal capture of stdout and stderr and handling of child process pid for windows, *nix, etc." 22 | end 23 | # 24 | # class methods 25 | # 26 | 27 | @host = Socket.gethostname 28 | @ppid = Process.ppid 29 | @pid = Process.pid 30 | @turd = ENV['SYSTEMU_TURD'] 31 | @ruby = nil 32 | 33 | def self.ruby 34 | return @ruby if @ruby 35 | 36 | c = begin; ::RbConfig::CONFIG; rescue NameError; ::Config::CONFIG; end 37 | ruby = File.join(c['bindir'], c['ruby_install_name']) << c['EXEEXT'] 38 | @ruby = if system(ruby, '-e', '42') 39 | ruby 40 | else 41 | system('ruby', '-e', '42') ? 'ruby' : warn('no ruby in PATH/CONFIG') 42 | end 43 | end 44 | 45 | class << SystemUniversal 46 | %w( host ppid pid turd ).each{|a| attr_accessor a} 47 | 48 | def quote(*words) 49 | words.map{|word| word.inspect}.join(' ') 50 | end 51 | end 52 | 53 | # 54 | # instance methods 55 | # 56 | 57 | def initialize argv, opts = {}, &block 58 | getopt = getopts opts 59 | 60 | @argv = argv 61 | @block = block 62 | 63 | @stdin = getopt[ ['stdin', 'in', '0', 0] ] 64 | @stdout = getopt[ ['stdout', 'out', '1', 1] ] 65 | @stderr = getopt[ ['stderr', 'err', '2', 2] ] 66 | @env = getopt[ 'env' ] 67 | @cwd = getopt[ 'cwd' ] 68 | 69 | @host = getopt[ 'host', self.class.host ] 70 | @ppid = getopt[ 'ppid', self.class.ppid ] 71 | @pid = getopt[ 'pid', self.class.pid ] 72 | @ruby = getopt[ 'ruby', self.class.ruby ] 73 | end 74 | 75 | def systemu 76 | tmpdir do |tmp| 77 | c = child_setup tmp 78 | status = nil 79 | 80 | begin 81 | thread = nil 82 | 83 | quietly{ 84 | IO.popen "#{ quote(@ruby) } #{ quote(c['program']) }", 'rb+' do |pipe| 85 | line = pipe.gets 86 | case line 87 | when %r/^pid: \d+$/ 88 | cid = Integer line[%r/\d+/] 89 | else 90 | begin 91 | buf = pipe.read 92 | buf = "#{ line }#{ buf }" 93 | e = Marshal.load buf 94 | raise unless Exception === e 95 | raise e 96 | rescue 97 | raise "systemu: Error - process interrupted!\n#{ buf }\n" 98 | end 99 | end 100 | thread = new_thread cid, @block if @block 101 | pipe.read rescue nil 102 | end 103 | } 104 | status = $? 105 | ensure 106 | if thread 107 | begin 108 | class << status 109 | attr 'thread' 110 | end 111 | status.instance_eval{ @thread = thread } 112 | rescue 113 | 42 114 | end 115 | end 116 | end 117 | 118 | if @stdout or @stderr 119 | open(c['stdout'], 'rb'){|f| relay f => @stdout} if @stdout 120 | open(c['stderr'], 'rb'){|f| relay f => @stderr} if @stderr 121 | status 122 | else 123 | [status, open(c['stdout'], 'rb'){|f| f.read}, open(c['stderr'], 'rb'){|f| f.read}] 124 | end 125 | end 126 | end 127 | 128 | def quote *args, &block 129 | SystemUniversal.quote(*args, &block) 130 | end 131 | 132 | def new_thread child_pid, block 133 | q = Queue.new 134 | Thread.new(child_pid) do |cid| 135 | current = Thread.current 136 | current.abort_on_exception = true 137 | q.push current 138 | block.call cid 139 | end 140 | q.pop 141 | end 142 | 143 | def child_setup tmp 144 | stdin = File.expand_path(File.join(tmp, 'stdin')) 145 | stdout = File.expand_path(File.join(tmp, 'stdout')) 146 | stderr = File.expand_path(File.join(tmp, 'stderr')) 147 | program = File.expand_path(File.join(tmp, 'program')) 148 | config = File.expand_path(File.join(tmp, 'config')) 149 | 150 | if @stdin 151 | open(stdin, 'wb'){|f| relay @stdin => f} 152 | else 153 | FileUtils.touch stdin 154 | end 155 | FileUtils.touch stdout 156 | FileUtils.touch stderr 157 | 158 | c = {} 159 | c['argv'] = @argv 160 | c['env'] = @env 161 | c['cwd'] = @cwd 162 | c['stdin'] = stdin 163 | c['stdout'] = stdout 164 | c['stderr'] = stderr 165 | c['program'] = program 166 | open(config, 'wb'){|f| Marshal.dump(c, f)} 167 | 168 | open(program, 'wb'){|f| f.write child_program(config)} 169 | 170 | c 171 | end 172 | 173 | def quietly 174 | v = $VERBOSE 175 | $VERBOSE = nil 176 | yield 177 | ensure 178 | $VERBOSE = v 179 | end 180 | 181 | def child_program config 182 | <<-program 183 | # encoding: utf-8 184 | 185 | PIPE = STDOUT.dup 186 | begin 187 | config = Marshal.load(IO.read('#{ config }',:mode=>"rb")) 188 | 189 | argv = config['argv'] 190 | env = config['env'] 191 | cwd = config['cwd'] 192 | stdin = config['stdin'] 193 | stdout = config['stdout'] 194 | stderr = config['stderr'] 195 | 196 | Dir.chdir cwd if cwd 197 | env.each{|k,v| ENV[k.to_s] = v.to_s} if env 198 | 199 | STDIN.reopen stdin 200 | STDOUT.reopen stdout 201 | STDERR.reopen stderr 202 | 203 | PIPE.puts "pid: \#{ Process.pid }" 204 | PIPE.flush ### the process is ready yo! 205 | PIPE.close 206 | 207 | exec *argv 208 | rescue Exception => e 209 | PIPE.write Marshal.dump(e) rescue nil 210 | exit 42 211 | end 212 | program 213 | end 214 | 215 | def relay srcdst 216 | src, dst, _ = srcdst.to_a.first 217 | if src.respond_to? 'read' 218 | while((buffer = src.read(8192))); dst << buffer; end 219 | else 220 | if src.respond_to?(:each_line) 221 | src.each_line{|buf| dst << buf} 222 | else 223 | src.each{|buf| dst << buf} 224 | end 225 | end 226 | end 227 | 228 | def slug_for(*args) 229 | options = args.last.is_a?(Hash) ? args.pop : {} 230 | join = (options[:join] || options['join'] || '_').to_s 231 | string = args.flatten.compact.join(join) 232 | words = string.to_s.scan(%r|[/\w]+|) 233 | words.map!{|word| word.gsub %r|[^/0-9a-zA-Z_-]|, ''} 234 | words.delete_if{|word| word.nil? or word.strip.empty?} 235 | words.join(join).downcase.gsub('/', (join * 2)) 236 | end 237 | 238 | def tmpdir d = Dir.tmpdir, max = 42, &b 239 | i = -1 and loop{ 240 | i += 1 241 | 242 | tmp = File.join(d, slug_for("systemu_#{ @host }_#{ @ppid }_#{ @pid }_#{ rand }_#{ i += 1 }")) 243 | 244 | begin 245 | Dir.mkdir tmp 246 | rescue Errno::EEXIST 247 | raise if i >= max 248 | next 249 | end 250 | 251 | break( 252 | if b 253 | begin 254 | b.call tmp 255 | ensure 256 | FileUtils.rm_rf tmp unless SystemU.turd 257 | end 258 | else 259 | tmp 260 | end 261 | ) 262 | } 263 | end 264 | 265 | def getopts opts = {} 266 | lambda do |*args| 267 | keys, default, _ = args 268 | catch(:opt) do 269 | [keys].flatten.each do |key| 270 | [key, key.to_s, key.to_s.intern].each do |k| 271 | throw :opt, opts[k] if opts.has_key?(k) 272 | end 273 | end 274 | default 275 | end 276 | end 277 | end 278 | end 279 | 280 | # some monkeypatching for JRuby 281 | if defined? JRUBY_VERSION 282 | require 'jruby' 283 | java_import org.jruby.RubyProcess 284 | 285 | class SystemUniversal 286 | def systemu 287 | split_argv = JRuby::PathHelper.smart_split_command @argv 288 | process = java.lang.Runtime.runtime.exec split_argv.to_java(:string) 289 | 290 | stdout, stderr = [process.input_stream, process.error_stream].map do |stream| 291 | StreamReader.new(stream) 292 | end 293 | 294 | field = process.get_class.get_declared_field("pid") 295 | field.set_accessible(true) 296 | pid = field.get(process) 297 | _thread = new_thread pid, @block if @block 298 | exit_code = process.wait_for 299 | [ 300 | RubyProcess::RubyStatus.new_process_status(JRuby.runtime, exit_code, pid), 301 | stdout.join, 302 | stderr.join 303 | ] 304 | end 305 | 306 | class StreamReader 307 | def initialize(stream) 308 | @data = "" 309 | @thread = Thread.new do 310 | reader = java.io.BufferedReader.new java.io.InputStreamReader.new(stream) 311 | 312 | while line = reader.read_line 313 | @data << line << "\n" 314 | end 315 | end 316 | end 317 | 318 | def join 319 | @thread.join 320 | @data 321 | end 322 | end 323 | end 324 | end 325 | 326 | 327 | 328 | SystemU = SystemUniversal unless defined? SystemU 329 | Systemu = SystemUniversal unless defined? Systemu 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | if $0 == __FILE__ 344 | # 345 | # date 346 | # 347 | date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) 348 | 349 | status, stdout, stderr = systemu date 350 | p [status, stdout, stderr] 351 | 352 | status = systemu date, 1=>(stdout = '') 353 | p [status, stdout] 354 | 355 | status = systemu date, 2=>(stderr = '') 356 | p [status, stderr] 357 | # 358 | # sleep 359 | # 360 | sleep = %q( ruby -e" p(sleep(1)) " ) 361 | status, stdout, stderr = systemu sleep 362 | p [status, stdout, stderr] 363 | 364 | sleep = %q( ruby -e" p(sleep(42)) " ) 365 | status, stdout, stderr = systemu(sleep){|cid| Process.kill 9, cid} 366 | p [status, stdout, stderr] 367 | # 368 | # env 369 | # 370 | env = %q( ruby -e" p ENV['A'] " ) 371 | status, stdout, stderr = systemu env, :env => {'A' => 42} 372 | p [status, stdout, stderr] 373 | # 374 | # cwd 375 | # 376 | env = %q( ruby -e" p Dir.pwd " ) 377 | status, stdout, stderr = systemu env, :cwd => Dir.tmpdir 378 | p [status, stdout, stderr] 379 | end 380 | -------------------------------------------------------------------------------- /pkg/systemu-2.6.4.gem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahoward/systemu/c910f79a46f739aee0b11e10a6158d8d3545501f/pkg/systemu-2.6.4.gem -------------------------------------------------------------------------------- /samples/a.rb: -------------------------------------------------------------------------------- 1 | # 2 | # systemu can be used on any platform to return status, stdout, and stderr of 3 | # any command. unlike other methods like open3/popen4 there is zero danger of 4 | # full pipes or threading issues hanging your process or subprocess. 5 | # 6 | require 'systemu' 7 | 8 | date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) 9 | 10 | status, stdout, stderr = systemu date 11 | p [ status, stdout, stderr ] 12 | -------------------------------------------------------------------------------- /samples/b.rb: -------------------------------------------------------------------------------- 1 | # 2 | # quite a few keys can be passed to the command to alter it's behaviour. if 3 | # either stdout or stderr is supplied those objects should respond_to? '<<' 4 | # and only status will be returned 5 | # 6 | require 'systemu' 7 | 8 | date = %q( ruby -e" t = Time.now; STDOUT.puts t; STDERR.puts t " ) 9 | 10 | stdout, stderr = '', '' 11 | status = systemu date, 'stdout' => stdout, 'stderr' => stderr 12 | p [ status, stdout, stderr ] 13 | -------------------------------------------------------------------------------- /samples/c.rb: -------------------------------------------------------------------------------- 1 | # 2 | # of course stdin can be supplied too. synonyms for 'stdin' include '0' and 3 | # 0. the other stdio streams have similar shortcuts 4 | # 5 | require 'systemu' 6 | 7 | cat = %q( ruby -e" ARGF.each{|line| puts line} " ) 8 | 9 | status = systemu cat, 0=>'the stdin for cat', 1=>stdout='' 10 | puts stdout 11 | -------------------------------------------------------------------------------- /samples/d.rb: -------------------------------------------------------------------------------- 1 | # 2 | # the cwd can be supplied 3 | # 4 | require 'systemu' 5 | require 'tmpdir' 6 | 7 | pwd = %q( ruby -e" STDERR.puts Dir.pwd " ) 8 | 9 | status = systemu pwd, 2=>(stderr=''), :cwd=>Dir.tmpdir 10 | puts stderr 11 | 12 | -------------------------------------------------------------------------------- /samples/e.rb: -------------------------------------------------------------------------------- 1 | # 2 | # any environment vars specified are merged into the child's environment 3 | # 4 | require 'systemu' 5 | 6 | env = %q( ruby -r yaml -e" puts ENV[ 'answer' ] " ) 7 | 8 | status = systemu env, 1=>stdout='', 'env'=>{ 'answer' => 0b101010 } 9 | puts stdout 10 | -------------------------------------------------------------------------------- /samples/f.rb: -------------------------------------------------------------------------------- 1 | # 2 | # if a block is specified then it is passed the child pid and run in a 3 | # background thread. note that this thread will __not__ be blocked during the 4 | # execution of the command so it may do useful work such as killing the child 5 | # if execution time passes a certain threshold 6 | # 7 | require 'systemu' 8 | 9 | looper = %q( ruby -e" loop{ STDERR.puts Time.now.to_i; sleep 1 } " ) 10 | 11 | status, stdout, stderr = 12 | systemu looper do |cid| 13 | sleep 3 14 | Process.kill 9, cid 15 | end 16 | 17 | p status 18 | p stderr 19 | -------------------------------------------------------------------------------- /systemu.gemspec: -------------------------------------------------------------------------------- 1 | ## systemu.gemspec 2 | # 3 | 4 | Gem::Specification::new do |spec| 5 | spec.name = "systemu" 6 | spec.version = "2.6.4" 7 | spec.platform = Gem::Platform::RUBY 8 | spec.summary = "systemu" 9 | spec.description = "universal capture of stdout and stderr and handling of child process pid for windows, *nix, etc." 10 | spec.license = "Ruby" 11 | 12 | spec.files = 13 | ["LICENSE", 14 | "README", 15 | "README.erb", 16 | "Rakefile", 17 | "lib", 18 | "lib/systemu.rb", 19 | "samples", 20 | "samples/a.rb", 21 | "samples/b.rb", 22 | "samples/c.rb", 23 | "samples/d.rb", 24 | "samples/e.rb", 25 | "samples/f.rb", 26 | "systemu.gemspec", 27 | "test", 28 | "test/systemu_test.rb", 29 | "test/testing.rb"] 30 | 31 | spec.executables = [] 32 | 33 | spec.require_path = "lib" 34 | 35 | spec.test_files = nil 36 | 37 | 38 | 39 | spec.extensions.push(*[]) 40 | 41 | spec.rubyforge_project = "codeforpeople" 42 | spec.author = "Ara T. Howard" 43 | spec.email = "ara.t.howard@gmail.com" 44 | spec.homepage = "https://github.com/ahoward/systemu" 45 | end 46 | -------------------------------------------------------------------------------- /test/systemu_test.rb: -------------------------------------------------------------------------------- 1 | 2 | Testing SystemU do 3 | 4 | ## 5 | # 6 | testing 'that simple usage works' do 7 | status, stdout, stderr = assert{ systemu :bin/:ls } 8 | assert{ status == 0 } 9 | assert{ stdout['lib'] } 10 | assert{ stderr.strip.empty? } 11 | end 12 | 13 | testing 'program with stdin' do 14 | stdin = '42' 15 | status, stdout, stderr = assert{ systemu :bin/:cat, :stdin => stdin } 16 | assert{ status == 0 } 17 | assert{ stdout == stdin } 18 | end 19 | 20 | testing 'silly hostnames' do 21 | host = SystemU.instance_variable_get('@host') 22 | silly_hostname = "silly's hostname with spaces" 23 | begin 24 | SystemU.instance_variable_set('@host', silly_hostname) 25 | assert{ SystemU.instance_variable_get('@host') == silly_hostname } 26 | stdin = '42' 27 | status, stdout, stderr = assert{ systemu :bin/:cat, :stdin => stdin } 28 | assert{ status == 0 } 29 | ensure 30 | assert{ SystemU.instance_variable_set('@host', host) } 31 | end 32 | end 33 | 34 | end 35 | 36 | 37 | 38 | 39 | 40 | BEGIN { 41 | 42 | # silly hax to build commands we can shell out to on any platform. since 43 | # tests might run on windoze we assume only that 'ruby' is available and build 44 | # other command-line programs from it. 45 | # 46 | module Kernel 47 | private 48 | def bin(which, options = {}, &block) 49 | case which.to_s 50 | when 'ls' 51 | %| ruby -e'puts Dir.glob("*").sort' | 52 | 53 | when 'cat' 54 | %| ruby -e'STDOUT.write(ARGF.read)' | 55 | 56 | when 'find' 57 | %| ruby -e'puts Dir.glob("**/**").sort' | 58 | end 59 | end 60 | end 61 | 62 | # just let's us write: :bin/:ls 63 | # 64 | class Symbol 65 | def / other, options = {}, &block 66 | eval "#{ self }(:#{ other }, options, &block)" 67 | end 68 | end 69 | 70 | testdir = File.dirname(File.expand_path(__FILE__)) 71 | rootdir = File.dirname(testdir) 72 | libdir = File.join(rootdir, 'lib') 73 | require File.join(libdir, 'systemu') 74 | require File.join(testdir, 'testing') 75 | 76 | 77 | Dir.chdir(rootdir) 78 | } 79 | -------------------------------------------------------------------------------- /test/testing.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | 3 | testdir = File.expand_path(File.dirname(__FILE__)) 4 | rootdir = File.dirname(testdir) 5 | libdir = File.join(rootdir, 'lib') 6 | 7 | STDOUT.sync = true 8 | 9 | $:.unshift(testdir) unless $:.include?(testdir) 10 | $:.unshift(libdir) unless $:.include?(libdir) 11 | $:.unshift(rootdir) unless $:.include?(rootdir) 12 | 13 | class Testing 14 | class Slug < ::String 15 | def Slug.for(*args) 16 | string = args.flatten.compact.join('-') 17 | words = string.to_s.scan(%r/\w+/) 18 | words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''} 19 | words.delete_if{|word| word.nil? or word.strip.empty?} 20 | new(words.join('-').downcase) 21 | end 22 | end 23 | 24 | class Context 25 | attr_accessor :name 26 | 27 | def initialize(name, *args) 28 | @name = name 29 | end 30 | 31 | def to_s 32 | Slug.for(name) 33 | end 34 | end 35 | end 36 | 37 | def Testing(*args, &block) 38 | Class.new(::Test::Unit::TestCase) do 39 | 40 | ## class methods 41 | # 42 | class << self 43 | def contexts 44 | @contexts ||= [] 45 | end 46 | 47 | def context(*args, &block) 48 | return contexts.last if(args.empty? and block.nil?) 49 | 50 | context = Testing::Context.new(*args) 51 | contexts.push(context) 52 | 53 | begin 54 | block.call(context) 55 | ensure 56 | contexts.pop 57 | end 58 | end 59 | 60 | def slug_for(*args) 61 | string = [context, args].flatten.compact.join('-') 62 | words = string.to_s.scan(%r/\w+/) 63 | words.map!{|word| word.gsub %r/[^0-9a-zA-Z_-]/, ''} 64 | words.delete_if{|word| word.nil? or word.strip.empty?} 65 | words.join('-').downcase.sub(/_$/, '') 66 | end 67 | 68 | def name() const_get(:Name) end 69 | 70 | def testno() 71 | '%05d' % (@testno ||= 0) 72 | ensure 73 | @testno += 1 74 | end 75 | 76 | def testing(*args, &block) 77 | method = ["test", testno, slug_for(*args)].delete_if{|part| part.empty?}.join('_') 78 | define_method(method, &block) 79 | end 80 | 81 | def test(*args, &block) 82 | testing(*args, &block) 83 | end 84 | 85 | def setup(&block) 86 | define_method(:setup, &block) if block 87 | end 88 | 89 | def teardown(&block) 90 | define_method(:teardown, &block) if block 91 | end 92 | 93 | def prepare(&block) 94 | @prepare ||= [] 95 | @prepare.push(block) if block 96 | @prepare 97 | end 98 | 99 | def cleanup(&block) 100 | @cleanup ||= [] 101 | @cleanup.push(block) if block 102 | @cleanup 103 | end 104 | end 105 | 106 | ## configure the subclass! 107 | # 108 | const_set(:Testno, '0') 109 | slug = slug_for(*args).gsub(%r/-/,'_') 110 | name = ['TESTING', '%03d' % const_get(:Testno), slug].delete_if{|part| part.empty?}.join('_') 111 | name = name.upcase! 112 | const_set(:Name, name) 113 | const_set(:Missing, Object.new.freeze) 114 | 115 | ## instance methods 116 | # 117 | alias_method('__assert__', 'assert') 118 | 119 | def assert(*args, &block) 120 | if args.size == 1 and args.first.is_a?(Hash) 121 | options = args.first 122 | expected = getopt(:expected, options){ missing } 123 | actual = getopt(:actual, options){ missing } 124 | if expected == missing and actual == missing 125 | actual, expected, *ignored = options.to_a.flatten 126 | end 127 | expected = expected.call() if expected.respond_to?(:call) 128 | actual = actual.call() if actual.respond_to?(:call) 129 | assert_equal(expected, actual) 130 | end 131 | 132 | if block 133 | label = "assert(#{ args.join(' ') })" 134 | result = nil 135 | assert_nothing_raised{ result = block.call } 136 | __assert__(result, label) 137 | result 138 | else 139 | result = args.shift 140 | label = "assert(#{ args.join(' ') })" 141 | __assert__(result, label) 142 | result 143 | end 144 | end 145 | 146 | def missing 147 | self.class.const_get(:Missing) 148 | end 149 | 150 | def getopt(opt, hash, options = nil, &block) 151 | [opt.to_s, opt.to_s.to_sym].each do |key| 152 | return hash[key] if hash.has_key?(key) 153 | end 154 | default = 155 | if block 156 | block.call 157 | else 158 | options.is_a?(Hash) ? options[:default] : nil 159 | end 160 | return default 161 | end 162 | 163 | def subclass_of exception 164 | class << exception 165 | def ==(other) super or self > other end 166 | end 167 | exception 168 | end 169 | 170 | ## 171 | # 172 | module_eval(&block) 173 | 174 | self.setup() 175 | self.prepare.each{|b| b.call()} 176 | 177 | at_exit{ 178 | self.teardown() 179 | self.cleanup.each{|b| b.call()} 180 | } 181 | 182 | self 183 | end 184 | end 185 | 186 | 187 | if $0 == __FILE__ 188 | 189 | Testing 'Testing' do 190 | testing('foo'){ assert true } 191 | test{ assert true } 192 | p instance_methods.grep(/test/) 193 | end 194 | 195 | end 196 | --------------------------------------------------------------------------------