posix-spawn-0.3.8/0000755000004100000410000000000012253342007014040 5ustar www-datawww-dataposix-spawn-0.3.8/Rakefile0000644000004100000410000000164212253342007015510 0ustar www-datawww-datatask :default => :test # ========================================================== # Packaging # ========================================================== GEMSPEC = eval(File.read('posix-spawn.gemspec')) require 'rubygems/package_task' Gem::PackageTask.new(GEMSPEC) do |pkg| end # ========================================================== # Ruby Extension # ========================================================== require 'rake/extensiontask' Rake::ExtensionTask.new('posix_spawn_ext', GEMSPEC) do |ext| ext.ext_dir = 'ext' end task :build => :compile # ========================================================== # Testing # ========================================================== require 'rake/testtask' Rake::TestTask.new 'test' do |t| t.test_files = FileList['test/test_*.rb'] end task :test => :build desc 'Run some benchmarks' task :benchmark => :build do ruby '-Ilib', 'bin/posix-spawn-benchmark' end posix-spawn-0.3.8/bin/0000755000004100000410000000000012253342007014610 5ustar www-datawww-dataposix-spawn-0.3.8/bin/posix-spawn-benchmark0000755000004100000410000000640612253342007020764 0ustar www-datawww-data#!/usr/bin/env ruby #/ Usage: posix-spawn-benchmark [-n ] [-m ] #/ Run posix-spawn (Ruby extension) benchmarks and report to standard output. #/ #/ Options: #/ -n, --count=NUM total number of processes to spawn. #/ -m, --mem-size=MB RES size to bloat to before performing benchmarks. #/ -g, --graph benchmark at 10MB itervals up to RES and graph results. #/ #/ Benchmarks run with -n 1000 -m 100 by default. require 'optparse' require 'posix-spawn' require 'benchmark' include Benchmark allocate = 100 * (1024 ** 2) iterations = 1_000 graphmode = false ARGV.options do |o| o.set_summary_indent(' ') o.on("-n", "--count=num") { |val| iterations = val.to_i } o.on("-m", "--mem-size=MB") { |val| allocate = val.to_i * (1024 ** 2) } o.on("-g", "--graph") { graphmode = true } o.on_tail("-h", "--help") { exec "grep ^#/ <'#{__FILE__}' |cut -c4-" } o.parse! end if graphmode bloat = [] data = {} chunk = allocate / 10 max = 0 10.times do puts "allocating #{chunk / (1024 ** 2)}MB (#{(bloat.size+1) * chunk / (1024 ** 2)}MB total)" bloat << ('x' * chunk) # size = bloat.size / (1024 ** 2) %w[ fspawn pspawn ].each do |type| print " - benchmarking #{type}... " time = Benchmark.realtime do iterations.times do pid = POSIX::Spawn.send(type, 'true') Process.wait(pid) end end puts "done (#{time})" data[type] ||= [] data[type] << time max = time if time > max end end max = max < 0.5 ? (max * 10).round / 10.0 : max.ceil minmb, maxmb = chunk/(1024**2), allocate/(1024**2) series = %w[ fspawn pspawn ].map{|name| data[name].map{|d| "%.2f" % d }.join(',') } chart = { :chs => '900x200', :cht => 'bvg', # grouped vertical bar chart :chtt => "posix-spawn-benchmark --graph --count #{iterations} --mem-size #{maxmb} (#{RUBY_PLATFORM})", :chf => 'bg,s,f8f8f8', # background :chbh => 'a,5,25', # 25px between bar groups :chd => "t:#{series.join('|')}", # data :chds => "0,#{max}", # scale :chdl => 'fspawn (fork+exec)|pspawn (posix_spawn)', # legend :chco => '1f77b4,ff7f0e', # colors :chxt => 'x,y', :chxr => "1,0,#{max},#{max/5}", # y labels up to max time :chxs => '1N** secs', # y labels are +=' secs' :chxl => "0:|#{minmb.step(maxmb, maxmb/10).map{ |mb| "#{mb} MB"}.join('|')}", # x bucket labels } url = "https://chart.googleapis.com/chart?" url += chart.map do |key, val| "#{key}=#{val.gsub(' ','%20').gsub('(','%28').gsub(')','%29').gsub('+','%2B')}" end.join('&') url += '#.png' puts url exit! end puts "benchmarking fork/exec vs. posix_spawn over #{iterations} runs" + " at #{allocate / (1024 ** 2)}M res" # bloat the process bloat = 'x' * allocate # run the benchmarks bm 40 do |x| x.report("fspawn (fork/exec):") do iterations.times do pid = POSIX::Spawn.fspawn('true') Process.wait(pid) end end x.report("pspawn (posix_spawn):") do iterations.times do pid = POSIX::Spawn.pspawn('true') Process.wait(pid) end end if Process.respond_to?(:spawn) x.report("spawn (native):") do iterations.times do pid = Process.spawn('true') Process.wait(pid) end end end end posix-spawn-0.3.8/Gemfile0000644000004100000410000000003112253342007015325 0ustar www-datawww-datasource :rubygems gemspec posix-spawn-0.3.8/posix-spawn.gemspec0000644000004100000410000000125412253342007017677 0ustar www-datawww-datarequire File.expand_path('../lib/posix/spawn/version', __FILE__) Gem::Specification.new do |s| s.name = 'posix-spawn' s.version = POSIX::Spawn::VERSION s.summary = 'posix_spawnp(2) for ruby' s.description = 'posix-spawn uses posix_spawnp(2) for faster process spawning' s.homepage = 'http://github.com/rtomayko/posix-spawn' s.authors = ['Ryan Tomayko', 'Aman Gupta'] s.email = ['r@tomayko.com', 'aman@tmm1.net'] s.add_development_dependency 'rake-compiler', '0.7.6' s.extensions = ['ext/extconf.rb'] s.executables << 'posix-spawn-benchmark' s.require_paths = ['lib'] s.files = `git ls-files`.split("\n") s.extra_rdoc_files = %w[ COPYING HACKING ] end posix-spawn-0.3.8/lib/0000755000004100000410000000000012253342007014606 5ustar www-datawww-dataposix-spawn-0.3.8/lib/posix/0000755000004100000410000000000012253342007015750 5ustar www-datawww-dataposix-spawn-0.3.8/lib/posix/spawn.rb0000644000004100000410000004337712253342007017443 0ustar www-datawww-dataunless RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ require 'posix_spawn_ext' end require 'posix/spawn/version' require 'posix/spawn/child' class IO if defined? JRUBY_VERSION require 'jruby' def posix_fileno case self when STDIN, $stdin 0 when STDOUT, $stdout 1 when STDERR, $stderr 2 else JRuby.reference(self).getOpenFile.getMainStream.getDescriptor.getChannel.getFDVal end end else alias :posix_fileno :fileno end end module POSIX # The POSIX::Spawn module implements a compatible subset of Ruby 1.9's # Process::spawn and related methods using the IEEE Std 1003.1 posix_spawn(2) # system interfaces where available, or a pure Ruby fork/exec based # implementation when not. # # In Ruby 1.9, a versatile new process spawning interface was added # (Process::spawn) as the foundation for enhanced versions of existing # process-related methods like Kernel#system, Kernel#`, and IO#popen. These # methods are backward compatible with their Ruby 1.8 counterparts but # support a large number of new options. The POSIX::Spawn module implements # many of these methods with support for most of Ruby 1.9's features. # # The argument signatures for all of these methods follow a new convention, # making it possible to take advantage of Process::spawn features: # # spawn([env], command, [argv1, ...], [options]) # system([env], command, [argv1, ...], [options]) # popen([[env], command, [argv1, ...]], mode="r", [options]) # # The env, command, and options arguments are described below. # # == Environment # # If a hash is given in the first argument (env), the child process's # environment becomes a merge of the parent's and any modifications # specified in the hash. When a value in env is nil, the variable is # unset in the child: # # # set FOO as BAR and unset BAZ. # spawn({"FOO" => "BAR", "BAZ" => nil}, 'echo', 'hello world') # # == Command # # The command and optional argvN string arguments specify the command to # execute and any program arguments. When only command is given and # includes a space character, the command text is executed by the system # shell interpreter, as if by: # # /bin/sh -c 'command' # # When command does not include a space character, or one or more argvN # arguments are given, the command is executed as if by execve(2) with # each argument forming the new program's argv. # # NOTE: Use of the shell variation is generally discouraged unless you # indeed want to execute a shell program. Specifying an explicitly argv is # typically more secure and less error prone in most cases. # # == Options # # When a hash is given in the last argument (options), it specifies a # current directory and zero or more fd redirects for the child process. # # The :chdir option specifies the current directory: # # spawn(command, :chdir => "/var/tmp") # # The :in, :out, :err, a Fixnum, an IO object or an Array option specify # fd redirection. For example, stderr can be merged into stdout as follows: # # spawn(command, :err => :out) # spawn(command, 2 => 1) # spawn(command, STDERR => :out) # spawn(command, STDERR => STDOUT) # # The key is a fd in the newly spawned child process (stderr in this case). # The value is a fd in the parent process (stdout in this case). # # You can also specify a filename for redirection instead of an fd: # # spawn(command, :in => "/dev/null") # read mode # spawn(command, :out => "/dev/null") # write mode # spawn(command, :err => "log") # write mode # spawn(command, 3 => "/dev/null") # read mode # # When redirecting to stdout or stderr, the files are opened in write mode; # otherwise, read mode is used. # # It's also possible to control the open flags and file permissions # directly by passing an array value: # # spawn(command, :in=>["file"]) # read mode assumed # spawn(command, :in=>["file", "r"]) # explicit read mode # spawn(command, :out=>["log", "w"]) # explicit write mode, 0644 assumed # spawn(command, :out=>["log", "w", 0600]) # spawn(command, :out=>["log", File::APPEND | File::CREAT, 0600]) # # The array is a [filename, open_mode, perms] tuple. open_mode can be a # string or an integer. When open_mode is omitted or nil, File::RDONLY is # assumed. The perms element should be an integer. When perms is omitted or # nil, 0644 is assumed. # # The :close It's possible to direct an fd be closed in the child process. This is # important for implementing `popen`-style logic and other forms of IPC between # processes using `IO.pipe`: # # rd, wr = IO.pipe # pid = spawn('echo', 'hello world', rd => :close, :stdout => wr) # wr.close # output = rd.read # Process.wait(pid) # # == Spawn Implementation # # The POSIX::Spawn#spawn method uses the best available implementation given # the current platform and Ruby version. In order of preference, they are: # # 1. The posix_spawn based C extension method (pspawn). # 2. Process::spawn when available (Ruby 1.9 only). # 3. A simple pure-Ruby fork/exec based spawn implementation compatible # with Ruby >= 1.8.7. # module Spawn extend self # Spawn a child process with a variety of options using the best # available implementation for the current platform and Ruby version. # # spawn([env], command, [argv1, ...], [options]) # # env - Optional hash specifying the new process's environment. # command - A string command name, or shell program, used to determine the # program to execute. # argvN - Zero or more string program arguments (argv). # options - Optional hash of operations to perform before executing the # new child process. # # Returns the integer pid of the newly spawned process. # Raises any number of Errno:: exceptions on failure. def spawn(*args) if respond_to?(:_pspawn) pspawn(*args) elsif ::Process.respond_to?(:spawn) ::Process::spawn(*args) else fspawn(*args) end end # Spawn a child process with a variety of options using the posix_spawn(2) # systems interfaces. Supports the standard spawn interface as described in # the POSIX::Spawn module documentation. # # Raises NotImplementedError when the posix_spawn_ext module could not be # loaded due to lack of platform support. def pspawn(*args) env, argv, options = extract_process_spawn_arguments(*args) raise NotImplementedError unless respond_to?(:_pspawn) if defined? JRUBY_VERSION # On the JVM, changes made to the environment are not propagated down # to C via get/setenv, so we have to fake it here. unless options[:unsetenv_others] == true env = ENV.merge(env) options[:unsetenv_others] = true end end _pspawn(env, argv, options) end # Spawn a child process with a variety of options using a pure # Ruby fork + exec. Supports the standard spawn interface as described in # the POSIX::Spawn module documentation. def fspawn(*args) env, argv, options = extract_process_spawn_arguments(*args) valid_options = [:chdir, :unsetenv_others, :pgroup] if badopt = options.find{ |key,val| !fd?(key) && !valid_options.include?(key) } raise ArgumentError, "Invalid option: #{badopt[0].inspect}" elsif !argv.is_a?(Array) || !argv[0].is_a?(Array) || argv[0].size != 2 raise ArgumentError, "Invalid command name" end fork do begin # handle FD => {FD, :close, [file,mode,perms]} options options.each do |key, val| if fd?(key) key = fd_to_io(key) if fd?(val) val = fd_to_io(val) key.reopen(val) elsif val == :close if key.respond_to?(:close_on_exec=) key.close_on_exec = true else key.close end elsif val.is_a?(Array) file, mode_string, perms = *val key.reopen(File.open(file, mode_string, perms)) end end end # setup child environment ENV.replace({}) if options[:unsetenv_others] == true env.each { |k, v| ENV[k] = v } # { :chdir => '/' } in options means change into that dir ::Dir.chdir(options[:chdir]) if options[:chdir] # { :pgroup => pgid } options pgroup = options[:pgroup] pgroup = 0 if pgroup == true Process::setpgid(0, pgroup) if pgroup # do the deed ::Kernel::exec(*argv) ensure exit!(127) end end end # Executes a command and waits for it to complete. The command's exit # status is available as $?. Supports the standard spawn interface as # described in the POSIX::Spawn module documentation. # # This method is compatible with Kernel#system. # # Returns true if the command returns a zero exit status, or false for # non-zero exit. def system(*args) pid = spawn(*args) return false if pid <= 0 ::Process.waitpid(pid) $?.exitstatus == 0 rescue Errno::ENOENT false end # Executes a command in a subshell using the system's shell interpreter # and returns anything written to the new process's stdout. This method # is compatible with Kernel#`. # # Returns the String output of the command. def `(cmd) r, w = IO.pipe if RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ sh = ENV['COMSPEC'] || 'cmd.exe' pid = spawn([sh, sh], '/c', cmd, :out => w, r => :close) else pid = spawn(['/bin/sh', '/bin/sh'], '-c', cmd, :out => w, r => :close) end if pid > 0 w.close out = r.read ::Process.waitpid(pid) out else '' end ensure [r, w].each{ |io| io.close rescue nil } end # Spawn a child process with all standard IO streams piped in and out of # the spawning process. Supports the standard spawn interface as described # in the POSIX::Spawn module documentation. # # Returns a [pid, stdin, stdout, stderr] tuple, where pid is the new # process's pid, stdin is a writeable IO object, and stdout / stderr are # readable IO objects. The caller should take care to close all IO objects # when finished and the child process's status must be collected by a call # to Process::waitpid or equivalent. def popen4(*argv) # create some pipes (see pipe(2) manual -- the ruby docs suck) ird, iwr = IO.pipe ord, owr = IO.pipe erd, ewr = IO.pipe # spawn the child process with either end of pipes hooked together opts = ((argv.pop if argv[-1].is_a?(Hash)) || {}).merge( # redirect fds # close other sides :in => ird, iwr => :close, :out => owr, ord => :close, :err => ewr, erd => :close ) pid = spawn(*(argv + [opts])) [pid, iwr, ord, erd] ensure # we're in the parent, close child-side fds [ird, owr, ewr].each { |fd| fd.close if fd } end ## # Process::Spawn::Child Exceptions # Exception raised when the total number of bytes output on the command's # stderr and stdout streams exceeds the maximum output size (:max option). # Currently class MaximumOutputExceeded < StandardError end # Exception raised when timeout is exceeded. class TimeoutExceeded < StandardError end private # Turns the various varargs incantations supported by Process::spawn into a # simple [env, argv, options] tuple. This just makes life easier for the # extension functions. # # The following method signature is supported: # Process::spawn([env], command, ..., [options]) # # The env and options hashes are optional. The command may be a variable # number of strings or an Array full of strings that make up the new process's # argv. # # Returns an [env, argv, options] tuple. All elements are guaranteed to be # non-nil. When no env or options are given, empty hashes are returned. def extract_process_spawn_arguments(*args) # pop the options hash off the end if it's there options = if args[-1].respond_to?(:to_hash) args.pop.to_hash else {} end flatten_process_spawn_options!(options) normalize_process_spawn_redirect_file_options!(options) # shift the environ hash off the front if it's there and account for # possible :env key in options hash. env = if args[0].respond_to?(:to_hash) args.shift.to_hash else {} end env.merge!(options.delete(:env)) if options.key?(:env) # remaining arguments are the argv supporting a number of variations. argv = adjust_process_spawn_argv(args) [env, argv, options] end # Convert { [fd1, fd2, ...] => (:close|fd) } options to individual keys, # like: { fd1 => :close, fd2 => :close }. This just makes life easier for the # spawn implementations. # # options - The options hash. This is modified in place. # # Returns the modified options hash. def flatten_process_spawn_options!(options) options.to_a.each do |key, value| if key.respond_to?(:to_ary) key.to_ary.each { |fd| options[fd] = value } options.delete(key) end end end # Mapping of string open modes to integer oflag versions. OFLAGS = { "r" => File::RDONLY, "r+" => File::RDWR | File::CREAT, "w" => File::WRONLY | File::CREAT | File::TRUNC, "w+" => File::RDWR | File::CREAT | File::TRUNC, "a" => File::WRONLY | File::APPEND | File::CREAT, "a+" => File::RDWR | File::APPEND | File::CREAT } # Convert variations of redirecting to a file to a standard tuple. # # :in => '/some/file' => ['/some/file', 'r', 0644] # :out => '/some/file' => ['/some/file', 'w', 0644] # :err => '/some/file' => ['/some/file', 'w', 0644] # STDIN => '/some/file' => ['/some/file', 'r', 0644] # # Returns the modified options hash. def normalize_process_spawn_redirect_file_options!(options) options.to_a.each do |key, value| next if !fd?(key) # convert string and short array values to if value.respond_to?(:to_str) value = default_file_reopen_info(key, value) elsif value.respond_to?(:to_ary) && value.size < 3 defaults = default_file_reopen_info(key, value[0]) value += defaults[value.size..-1] else value = nil end # replace string open mode flag maybe and replace original value if value value[1] = OFLAGS[value[1]] if value[1].respond_to?(:to_str) options[key] = value end end end # The default [file, flags, mode] tuple for a given fd and filename. The # default flags vary based on the what fd is being redirected. stdout and # stderr default to write, while stdin and all other fds default to read. # # fd - The file descriptor that is being redirected. This may be an IO # object, integer fd number, or :in, :out, :err for one of the standard # streams. # file - The string path to the file that fd should be redirected to. # # Returns a [file, flags, mode] tuple. def default_file_reopen_info(fd, file) case fd when :in, STDIN, $stdin, 0 [file, "r", 0644] when :out, STDOUT, $stdout, 1 [file, "w", 0644] when :err, STDERR, $stderr, 2 [file, "w", 0644] else [file, "r", 0644] end end # Determine whether object is fd-like. # # Returns true if object is an instance of IO, Fixnum >= 0, or one of the # the symbolic names :in, :out, or :err. def fd?(object) case object when Fixnum object >= 0 when :in, :out, :err, STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, IO true else object.respond_to?(:to_io) && !object.to_io.nil? end end # Convert a fd identifier to an IO object. # # Returns nil or an instance of IO. def fd_to_io(object) case object when STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr object when :in, 0 STDIN when :out, 1 STDOUT when :err, 2 STDERR when Fixnum object >= 0 ? IO.for_fd(object) : nil when IO object else object.respond_to?(:to_io) ? object.to_io : nil end end # Converts the various supported command argument variations into a # standard argv suitable for use with exec. This includes detecting commands # to be run through the shell (single argument strings with spaces). # # The args array may follow any of these variations: # # 'true' => [['true', 'true']] # 'echo', 'hello', 'world' => [['echo', 'echo'], 'hello', 'world'] # 'echo hello world' => [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'] # ['echo', 'fuuu'], 'hello' => [['echo', 'fuuu'], 'hello'] # # Returns a [[cmdname, argv0], argv1, ...] array. def adjust_process_spawn_argv(args) if args.size == 1 && args[0] =~ /[ |>]/ # single string with these characters means run it through the shell [['/bin/sh', '/bin/sh'], '-c', args[0]] elsif !args[0].respond_to?(:to_ary) # [argv0, argv1, ...] [[args[0], args[0]], *args[1..-1]] else # [[cmdname, argv0], argv1, ...] args end end end end posix-spawn-0.3.8/lib/posix/spawn/0000755000004100000410000000000012253342007017100 5ustar www-datawww-dataposix-spawn-0.3.8/lib/posix/spawn/child.rb0000644000004100000410000002017512253342007020515 0ustar www-datawww-datarequire 'posix/spawn' module POSIX module Spawn # POSIX::Spawn::Child includes logic for executing child processes and # reading/writing from their standard input, output, and error streams. It's # designed to take all input in a single string and provides all output # (stderr and stdout) as single strings and is therefore not well-suited # to streaming large quantities of data in and out of commands. # # Create and run a process to completion: # # >> child = POSIX::Spawn::Child.new('git', '--help') # # Retrieve stdout or stderr output: # # >> child.out # => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..." # >> child.err # => "" # # Check process exit status information: # # >> child.status # => # # # To write data on the new process's stdin immediately after spawning: # # >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2') # >> child.out # "42\n" # # Q: Why use POSIX::Spawn::Child instead of popen3, hand rolled fork/exec # code, or Process::spawn? # # - It's more efficient than popen3 and provides meaningful process # hierarchies because it performs a single fork/exec. (popen3 double forks # to avoid needing to collect the exit status and also calls # Process::detach which creates a Ruby Thread!!!!). # # - It handles all max pipe buffer (PIPE_BUF) hang cases when reading and # writing semi-large amounts of data. This is non-trivial to implement # correctly and must be accounted for with popen3, spawn, or hand rolled # fork/exec code. # # - It's more portable than hand rolled pipe, fork, exec code because # fork(2) and exec aren't available on all platforms. In those cases, # POSIX::Spawn::Child falls back to using whatever janky substitutes # the platform provides. class Child include POSIX::Spawn # Spawn a new process, write all input and read all output, and wait for # the program to exit. Supports the standard spawn interface as described # in the POSIX::Spawn module documentation: # # new([env], command, [argv1, ...], [options]) # # The following options are supported in addition to the standard # POSIX::Spawn options: # # :input => str Write str to the new process's standard input. # :timeout => int Maximum number of seconds to allow the process # to execute before aborting with a TimeoutExceeded # exception. # :max => total Maximum number of bytes of output to allow the # process to generate before aborting with a # MaximumOutputExceeded exception. # # Returns a new Child instance whose underlying process has already # executed to completion. The out, err, and status attributes are # immediately available. def initialize(*args) @env, @argv, options = extract_process_spawn_arguments(*args) @options = options.dup @input = @options.delete(:input) @timeout = @options.delete(:timeout) @max = @options.delete(:max) @options.delete(:chdir) if @options[:chdir].nil? exec! end # All data written to the child process's stdout stream as a String. attr_reader :out # All data written to the child process's stderr stream as a String. attr_reader :err # A Process::Status object with information on how the child exited. attr_reader :status # Total command execution time (wall-clock time) attr_reader :runtime # Determine if the process did exit with a zero exit status. def success? @status && @status.success? end private # Execute command, write input, and read output. This is called # immediately when a new instance of this object is initialized. def exec! # spawn the process and hook up the pipes pid, stdin, stdout, stderr = popen4(@env, *(@argv + [@options])) # async read from all streams into buffers @out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max) # grab exit status @status = waitpid(pid) rescue Object => boom [stdin, stdout, stderr].each { |fd| fd.close rescue nil } if @status.nil? ::Process.kill('TERM', pid) rescue nil @status = waitpid(pid) rescue nil end raise ensure # let's be absolutely certain these are closed [stdin, stdout, stderr].each { |fd| fd.close rescue nil } end # Maximum buffer size for reading BUFSIZE = (32 * 1024) # Start a select loop writing any input on the child's stdin and reading # any output from the child's stdout or stderr. # # input - String input to write on stdin. May be nil. # stdin - The write side IO object for the child's stdin stream. # stdout - The read side IO object for the child's stdout stream. # stderr - The read side IO object for the child's stderr stream. # timeout - An optional Numeric specifying the total number of seconds # the read/write operations should occur for. # # Returns an [out, err] tuple where both elements are strings with all # data written to the stdout and stderr streams, respectively. # Raises TimeoutExceeded when all data has not been read / written within # the duration specified in the timeout argument. # Raises MaximumOutputExceeded when the total number of bytes output # exceeds the amount specified by the max argument. def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil) max = nil if max && max <= 0 out, err = '', '' offset = 0 # force all string and IO encodings to BINARY under 1.9 for now if out.respond_to?(:force_encoding) and stdin.respond_to?(:set_encoding) [stdin, stdout, stderr].each do |fd| fd.set_encoding('BINARY', 'BINARY') end out.force_encoding('BINARY') err.force_encoding('BINARY') input = input.dup.force_encoding('BINARY') if input end timeout = nil if timeout && timeout <= 0.0 @runtime = 0.0 start = Time.now readers = [stdout, stderr] writers = if input [stdin] else stdin.close [] end t = timeout while readers.any? || writers.any? ready = IO.select(readers, writers, readers + writers, t) raise TimeoutExceeded if ready.nil? # write to stdin stream ready[1].each do |fd| begin boom = nil size = fd.write_nonblock(input) input = input[size, input.size] rescue Errno::EPIPE => boom rescue Errno::EAGAIN, Errno::EINTR end if boom || input.size == 0 stdin.close writers.delete(stdin) end end # read from stdout and stderr streams ready[0].each do |fd| buf = (fd == stdout) ? out : err begin buf << fd.readpartial(BUFSIZE) rescue Errno::EAGAIN, Errno::EINTR rescue EOFError readers.delete(fd) fd.close end end # keep tabs on the total amount of time we've spent here @runtime = Time.now - start if timeout t = timeout - @runtime raise TimeoutExceeded if t < 0.0 end # maybe we've hit our max output if max && ready[0].any? && (out.size + err.size) > max raise MaximumOutputExceeded end end [out, err] end # Wait for the child process to exit # # Returns the Process::Status object obtained by reaping the process. def waitpid(pid) ::Process::waitpid(pid) $? end end end end posix-spawn-0.3.8/lib/posix/spawn/version.rb0000644000004100000410000000007412253342007021113 0ustar www-datawww-datamodule POSIX module Spawn VERSION = '0.3.8' end end posix-spawn-0.3.8/lib/posix-spawn.rb0000644000004100000410000000002612253342007017421 0ustar www-datawww-datarequire "posix/spawn" posix-spawn-0.3.8/metadata.yml0000644000004100000410000000334012253342007016343 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: posix-spawn version: !ruby/object:Gem::Version version: 0.3.8 platform: ruby authors: - Ryan Tomayko - Aman Gupta autorequire: bindir: bin cert_chain: [] date: 2013-12-06 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: rake-compiler requirement: !ruby/object:Gem::Requirement requirements: - - '=' - !ruby/object:Gem::Version version: 0.7.6 type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '=' - !ruby/object:Gem::Version version: 0.7.6 description: posix-spawn uses posix_spawnp(2) for faster process spawning email: - r@tomayko.com - aman@tmm1.net executables: - posix-spawn-benchmark extensions: - ext/extconf.rb extra_rdoc_files: - COPYING - HACKING files: - .gitignore - COPYING - Gemfile - HACKING - README.md - Rakefile - TODO - bin/posix-spawn-benchmark - ext/extconf.rb - ext/posix-spawn.c - lib/posix-spawn.rb - lib/posix/spawn.rb - lib/posix/spawn/child.rb - lib/posix/spawn/version.rb - posix-spawn.gemspec - test/test_backtick.rb - test/test_child.rb - test/test_popen.rb - test/test_spawn.rb - test/test_system.rb homepage: http://github.com/rtomayko/posix-spawn licenses: [] metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.0.3 signing_key: specification_version: 4 summary: posix_spawnp(2) for ruby test_files: [] posix-spawn-0.3.8/test/0000755000004100000410000000000012253342007015017 5ustar www-datawww-dataposix-spawn-0.3.8/test/test_backtick.rb0000644000004100000410000000140512253342007020156 0ustar www-datawww-datarequire 'test/unit' require 'posix-spawn' class BacktickTest < Test::Unit::TestCase include POSIX::Spawn def test_backtick_simple out = `exit` assert_equal '', out assert_equal 0, $?.exitstatus end def test_backtick_output out = `echo 123` assert_equal "123\n", out assert_equal 0, $?.exitstatus, 0 end def test_backtick_failure out = `nosuchcmd 2> /dev/null` assert_equal '', out assert_equal 127, $?.exitstatus end def test_backtick_redirect out = `nosuchcmd 2>&1` assert_equal "/bin/sh: nosuchcmd: command not found\n", out assert_equal 127, $?.exitstatus, 127 end def test_backtick_huge out = `yes | head -50000` assert_equal 100000, out.size assert_equal 0, $?.exitstatus end end posix-spawn-0.3.8/test/test_spawn.rb0000644000004100000410000002542712253342007017545 0ustar www-datawww-datarequire 'test/unit' require 'posix-spawn' module SpawnImplementationTests def test_spawn_simple pid = _spawn('true') assert_process_exit_ok pid end def test_spawn_with_args pid = _spawn('true', 'with', 'some stuff') assert_process_exit_ok pid end def test_spawn_with_shell pid = _spawn('true && exit 13') assert_process_exit_status pid, 13 end def test_spawn_with_cmdname_and_argv0_tuple pid = _spawn(['true', 'not-true'], 'some', 'args', 'toooo') assert_process_exit_ok pid end def test_spawn_with_invalid_argv assert_raise ArgumentError do _spawn(['echo','b','c','d']) end end ## # Environ def test_spawn_inherit_env ENV['PSPAWN'] = 'parent' pid = _spawn('test "$PSPAWN" = "parent"') assert_process_exit_ok pid ensure ENV.delete('PSPAWN') end def test_spawn_clean_env ENV['PSPAWN'] = 'parent' pid = _spawn({'TEMP'=>'child'}, 'test -z "$PSPAWN" && test "$TEMP" = "child"', :unsetenv_others => true) assert_process_exit_ok pid ensure ENV.delete('PSPAWN') end def test_spawn_set_env ENV['PSPAWN'] = 'parent' pid = _spawn({'PSPAWN'=>'child'}, 'test "$PSPAWN" = "child"') assert_process_exit_ok pid ensure ENV.delete('PSPAWN') end def test_spawn_unset_env ENV['PSPAWN'] = 'parent' pid = _spawn({'PSPAWN'=>nil}, 'test -z "$PSPAWN"') assert_process_exit_ok pid ensure ENV.delete('PSPAWN') end ## # FD => :close options def test_sanity_of_checking_clone_with_sh rd, wr = IO.pipe pid = _spawn("exec 2>/dev/null 100<&#{rd.posix_fileno} || exit 1", rd => rd) assert_process_exit_status pid, 0 ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_close_option_with_symbolic_standard_stream_names pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', :in => :close) assert_process_exit_status pid, 1 pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1', :out => :close, :err => :close) assert_process_exit_status pid, 1 end def test_spawn_close_on_standard_stream_io_object pid = _spawn('exec 2>/dev/null 100<&0 || exit 1', STDIN => :close) assert_process_exit_status pid, 1 pid = _spawn('exec 2>/dev/null 101>&1 102>&2 || exit 1', STDOUT => :close, STDOUT => :close) assert_process_exit_status pid, 1 end def test_spawn_close_option_with_fd_number rd, wr = IO.pipe pid = _spawn("exec 2>/dev/null 100<&#{rd.posix_fileno} || exit 1", rd.posix_fileno => :close) assert_process_exit_status pid, 1 assert !rd.closed? assert !wr.closed? ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_close_option_with_io_object rd, wr = IO.pipe pid = _spawn("exec 2>/dev/null 100<&#{rd.posix_fileno} || exit 1", rd => :close) assert_process_exit_status pid, 1 assert !rd.closed? assert !wr.closed? ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_close_invalid_fd_raises_exception pid = _spawn("echo", "hiya", 250 => :close) assert_process_exit_status pid, 127 rescue Errno::EBADF # this happens on darwin only. GNU does spawn and exits 127. end def test_spawn_closing_multiple_fds_with_array_keys rd, wr = IO.pipe pid = _spawn("exec 2>/dev/null 101>&#{wr.posix_fileno} || exit 1", [rd, wr, :out] => :close) assert_process_exit_status pid, 1 ensure [rd, wr].each { |fd| fd.close rescue nil } end ## # FD => FD options def test_spawn_redirect_fds_with_symbolic_names_and_io_objects rd, wr = IO.pipe pid = _spawn("echo", "hello world", :out => wr, rd => :close) wr.close output = rd.read assert_process_exit_ok pid assert_equal "hello world\n", output ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_redirect_fds_with_fd_numbers rd, wr = IO.pipe pid = _spawn("echo", "hello world", 1 => wr.posix_fileno, rd.posix_fileno => :close) wr.close output = rd.read assert_process_exit_ok pid assert_equal "hello world\n", output ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_redirect_invalid_fds_raises_exception pid = _spawn("echo", "hiya", 1 => 250) assert_process_exit_status pid, 127 rescue Errno::EBADF # this happens on darwin only. GNU does spawn and exits 127. end def test_spawn_redirect_stderr_and_stdout_to_same_fd rd, wr = IO.pipe pid = _spawn("echo hello world 1>&2", :err => wr, :out => wr, rd => :close) wr.close output = rd.read assert_process_exit_ok pid assert_equal "hello world\n", output ensure [rd, wr].each { |fd| fd.close rescue nil } end def test_spawn_does_not_close_fd_when_redirecting pid = _spawn("exec 2>&1", :err => :out) assert_process_exit_ok pid end # Ruby 1.9 Process::spawn closes all fds by default. To keep an fd open, you # have to pass it explicitly as fd => fd. def test_explicitly_passing_an_fd_as_open rd, wr = IO.pipe pid = _spawn("exec 101>&#{wr.posix_fileno} || exit 1", wr => wr) assert_process_exit_ok pid ensure [rd, wr].each { |fd| fd.close rescue nil } end ## # FD => file options def test_spawn_redirect_fd_to_file_with_symbolic_name file = File.expand_path('../test-output', __FILE__) text = 'redirect_fd_to_file_with_symbolic_name' pid = _spawn('echo', text, :out => file) assert_process_exit_ok pid assert File.exist?(file) assert_equal "#{text}\n", File.read(file) ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_to_file_with_fd_number file = File.expand_path('../test-output', __FILE__) text = 'redirect_fd_to_file_with_fd_number' pid = _spawn('echo', text, 1 => file) assert_process_exit_ok pid assert File.exist?(file) assert_equal "#{text}\n", File.read(file) ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_to_file_with_io_object file = File.expand_path('../test-output', __FILE__) text = 'redirect_fd_to_file_with_io_object' pid = _spawn('echo', text, STDOUT => file) assert_process_exit_ok pid assert File.exist?(file) assert_equal "#{text}\n", File.read(file) ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_from_file_with_symbolic_name file = File.expand_path('../test-input', __FILE__) text = 'redirect_fd_from_file_with_symbolic_name' File.open(file, 'w') { |fd| fd.write(text) } pid = _spawn(%Q{test "$(cat)" = "#{text}"}, :in => file) assert_process_exit_ok pid ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_from_file_with_fd_number file = File.expand_path('../test-input', __FILE__) text = 'redirect_fd_from_file_with_fd_number' File.open(file, 'w') { |fd| fd.write(text) } pid = _spawn(%Q{test "$(cat)" = "#{text}"}, 0 => file) assert_process_exit_ok pid ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_from_file_with_io_object file = File.expand_path('../test-input', __FILE__) text = 'redirect_fd_from_file_with_io_object' File.open(file, 'w') { |fd| fd.write(text) } pid = _spawn(%Q{test "$(cat)" = "#{text}"}, STDIN => file) assert_process_exit_ok pid ensure File.unlink(file) rescue nil end def test_spawn_redirect_fd_to_file_with_symbolic_name_and_flags file = File.expand_path('../test-output', __FILE__) text = 'redirect_fd_to_file_with_symbolic_name' 5.times do pid = _spawn('echo', text, :out => [file, 'a']) assert_process_exit_ok pid end assert File.exist?(file) assert_equal "#{text}\n" * 5, File.read(file) ensure File.unlink(file) rescue nil end ## # :pgroup => def test_spawn_inherit_pgroup_from_parent_by_default pgrp = Process.getpgrp pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)") assert_process_exit_ok pid end def test_spawn_inherit_pgroup_from_parent_when_nil pgrp = Process.getpgrp pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)", :pgroup => nil) assert_process_exit_ok pid end def test_spawn_new_pgroup_with_true pid = _spawn("ruby", "-e", "exit(Process.getpgrp == $$ ? 0 : 1)", :pgroup => true) assert_process_exit_ok pid end def test_spawn_new_pgroup_with_zero pid = _spawn("ruby", "-e", "exit(Process.getpgrp == $$ ? 0 : 1)", :pgroup => 0) assert_process_exit_ok pid end def test_spawn_explicit_pgroup pgrp = Process.getpgrp pid = _spawn("ruby", "-e", "exit(Process.getpgrp == #{pgrp} ? 0 : 1)", :pgroup => pgrp) assert_process_exit_ok pid end ## # Exceptions def test_spawn_raises_exception_on_unsupported_options exception = nil assert_raise ArgumentError do begin _spawn('echo howdy', :out => '/dev/null', :oops => 'blaahh') rescue Exception => e exception = e raise e end end assert_match /oops/, exception.message end ## # Assertion Helpers def assert_process_exit_ok(pid) assert_process_exit_status pid, 0 end def assert_process_exit_status(pid, status) assert pid.to_i > 0, "pid [#{pid}] should be > 0" chpid = ::Process.wait(pid) assert_equal chpid, pid assert_equal status, $?.exitstatus end end class SpawnTest < Test::Unit::TestCase include POSIX::Spawn def test_spawn_methods_exposed_at_module_level assert POSIX::Spawn.respond_to?(:pspawn) assert POSIX::Spawn.respond_to?(:_pspawn) end ## # Options Preprocessing def test_extract_process_spawn_arguments_with_options assert_equal [{}, [['echo', 'echo'], 'hello', 'world'], {:err => :close}], extract_process_spawn_arguments('echo', 'hello', 'world', :err => :close) end def test_extract_process_spawn_arguments_with_options_and_env options = {:err => :close} env = {'X' => 'Y'} assert_equal [env, [['echo', 'echo'], 'hello world'], options], extract_process_spawn_arguments(env, 'echo', 'hello world', options) end def test_extract_process_spawn_arguments_with_shell_command assert_equal [{}, [['/bin/sh', '/bin/sh'], '-c', 'echo hello world'], {}], extract_process_spawn_arguments('echo hello world') end def test_extract_process_spawn_arguments_with_special_cmdname_argv_tuple assert_equal [{}, [['echo', 'fuuu'], 'hello world'], {}], extract_process_spawn_arguments(['echo', 'fuuu'], 'hello world') end end class PosixSpawnTest < Test::Unit::TestCase include SpawnImplementationTests def _spawn(*argv) POSIX::Spawn.pspawn(*argv) end end class ForkSpawnTest < Test::Unit::TestCase include SpawnImplementationTests def _spawn(*argv) POSIX::Spawn.fspawn(*argv) end end if ::Process::respond_to?(:spawn) class NativeSpawnTest < Test::Unit::TestCase include SpawnImplementationTests def _spawn(*argv) ::Process.spawn(*argv) end end end posix-spawn-0.3.8/test/test_child.rb0000644000004100000410000000510612253342007017470 0ustar www-datawww-data# coding: UTF-8 require 'test/unit' require 'posix-spawn' class ChildTest < Test::Unit::TestCase include POSIX::Spawn def test_sanity assert_same POSIX::Spawn::Child, Child end def test_argv_array_execs p = Child.new('printf', '%s %s %s', '1', '2', '3 4') assert p.success? assert_equal "1 2 3 4", p.out end def test_argv_string_uses_sh p = Child.new("echo via /bin/sh") assert p.success? assert_equal "via /bin/sh\n", p.out end def test_stdout p = Child.new('echo', 'boom') assert_equal "boom\n", p.out assert_equal "", p.err end def test_stderr p = Child.new('echo boom 1>&2') assert_equal "", p.out assert_equal "boom\n", p.err end def test_status p = Child.new('exit 3') assert !p.status.success? assert_equal 3, p.status.exitstatus end def test_env p = Child.new({ 'FOO' => 'BOOYAH' }, 'echo $FOO') assert_equal "BOOYAH\n", p.out end def test_chdir p = Child.new("pwd", :chdir => File.dirname(Dir.pwd)) assert_equal File.dirname(Dir.pwd) + "\n", p.out end def test_input input = "HEY NOW\n" * 100_000 # 800K p = Child.new('wc', '-l', :input => input) assert_equal 100_000, p.out.strip.to_i end def test_max assert_raise MaximumOutputExceeded do Child.new('yes', :max => 100_000) end end def test_max_with_child_hierarchy assert_raise MaximumOutputExceeded do Child.new('/bin/sh', '-c', 'yes', :max => 100_000) end end def test_max_with_stubborn_child assert_raise MaximumOutputExceeded do Child.new("trap '' TERM; yes", :max => 100_000) end end def test_timeout start = Time.now assert_raise TimeoutExceeded do Child.new('sleep', '1', :timeout => 0.05) end assert (Time.now-start) <= 0.2 end def test_timeout_with_child_hierarchy assert_raise TimeoutExceeded do Child.new('/bin/sh', '-c', 'sleep 1', :timeout => 0.05) end end def test_lots_of_input_and_lots_of_output_at_the_same_time input = "stuff on stdin \n" * 1_000 command = " while read line do echo stuff on stdout; echo stuff on stderr 1>&2; done " p = Child.new(command, :input => input) assert_equal input.size, p.out.size assert_equal input.size, p.err.size assert p.success? end def test_input_cannot_be_written_due_to_broken_pipe input = "1" * 100_000 p = Child.new('false', :input => input) assert !p.success? end def test_utf8_input input = "hålø" p = Child.new('cat', :input => input) assert p.success? end end posix-spawn-0.3.8/test/test_system.rb0000644000004100000410000000105712253342007017732 0ustar www-datawww-datarequire 'test/unit' require 'posix-spawn' class SystemTest < Test::Unit::TestCase include POSIX::Spawn def test_system ret = system("true") assert_equal true, ret assert_equal 0, $?.exitstatus end def test_system_nonzero ret = system("false") assert_equal false, ret assert_equal 1, $?.exitstatus end def test_system_nonzero_via_sh ret = system("exit 1") assert_equal false, ret assert_equal 1, $?.exitstatus end def test_system_failure ret = system("nosuch") assert_equal false, ret end end posix-spawn-0.3.8/test/test_popen.rb0000644000004100000410000000054712253342007017532 0ustar www-datawww-datarequire 'test/unit' require 'posix-spawn' class PopenTest < Test::Unit::TestCase include POSIX::Spawn def test_popen4 pid, i, o, e = popen4("cat") i.write "hello world" i.close ::Process.wait(pid) assert_equal "hello world", o.read assert_equal 0, $?.exitstatus ensure [i, o, e].each{ |io| io.close rescue nil } end end posix-spawn-0.3.8/HACKING0000644000004100000410000000127712253342007015036 0ustar www-datawww-dataClone the project: git clone http://github.com/rtomayko/posix-spawn.git cd posix-spawn bundle install Rake tasks can be run without further setup: rake build rake test rake benchmark Just `rake' builds the extension and runs the tests. If you want to run the benchmark scripts or tests directly out of a working copy, first setup your PATH and RUBYLIB environment: PATH="$(pwd)/bin:$PATH" RUBYLIB="$(pwd)/lib:$(pwd)/ext:$RUBYLIB" export RUBYLIB Or, use the following rbdev script to quickly setup your PATH and RUBYLIB environment for this and other projects adhering to the Ruby Packaging Standard: https://github.com/rtomayko/dotfiles/blob/rtomayko/bin/rbdev posix-spawn-0.3.8/.gitignore0000644000004100000410000000007012253342007016025 0ustar www-datawww-dataGemfile.lock ext/Makefile lib/posix_spawn_ext.* tmp pkg posix-spawn-0.3.8/TODO0000644000004100000410000000163612253342007014536 0ustar www-datawww-data[x] license (LGPL) [x] fucking name this thing [x] fastspawn-bm should take iterations and memsize arguments [x] high level Grit::Process like class (string based) (tmm1) [x] add FD => '/path/to/file' (all variations) (rtomayko) [x] raise exception on unhandled options [x] benchmarks in README (tmm1) [x] POSIX::Spawn::spawn usage examples in README [x] POSIX::Spawn#pspawn should be just #spawn [x] :err => :out case -- currently closing out after dup2'ing [x] POSIX::Spawn::Process.new should have same method signature as Process::spawn [x] POSIX::Spawn::Process renamed to POSIX::Spawn::Child [x] Better POSIX::Spawn#spawn comment docs [x] POSIX::Spawn::Child usage examples in README [ ] popen* interfaces [x] system interface [x] ` interface [ ] jruby Grit::Process stuff [ ] make :vfork an option to Spawn#spawn [ ] check all posix_spawn_* function call return values [ ] POSIX::Spawn as ::Spawn? (maybe, we'll see) posix-spawn-0.3.8/checksums.yaml.gz0000444000004100000410000000041312253342007017324 0ustar www-datawww-dataRe;V0 "O_KdSRqzwvGn|\?랝|{6[Q@{ - :ܓ3ba$Pj{f2=Ve7&CAX#-{2aH' $b4kYJ^PǪH?/l2 `;^Cf:/dW;:Y]j>> )'pAghY!qiN^&L{>B[@AٱWǺqposix-spawn-0.3.8/ext/0000755000004100000410000000000012253342007014640 5ustar www-datawww-dataposix-spawn-0.3.8/ext/posix-spawn.c0000644000004100000410000002675112253342007017307 0ustar www-datawww-data/* we want GNU extensions like POSIX_SPAWN_USEVFORK */ #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #ifdef RUBY_VM #include #else #include #endif #ifndef RARRAY_LEN #define RARRAY_LEN(ary) RARRAY(ary)->len #endif #ifndef RARRAY_PTR #define RARRAY_PTR(ary) RARRAY(ary)->ptr #endif #ifndef RHASH_SIZE #define RHASH_SIZE(hash) RHASH(hash)->tbl->num_entries #endif #ifdef __APPLE__ #include #define environ (*_NSGetEnviron()) #else extern char **environ; #endif static VALUE rb_mPOSIX; static VALUE rb_mPOSIXSpawn; /* Determine the fd number for a Ruby object VALUE. * * obj - This can be any valid Ruby object, but only the following return * an actual fd number: * - The symbols :in, :out, or :err for fds 0, 1, or 2. * - An IO object. (IO#fileno is returned) * - A Fixnum. * * Returns the fd number >= 0 if one could be established, or -1 if the object * does not map to an fd. */ static int posixspawn_obj_to_fd(VALUE obj) { int fd = -1; switch (TYPE(obj)) { case T_FIXNUM: /* Fixnum fd number */ fd = FIX2INT(obj); break; case T_SYMBOL: /* (:in|:out|:err) */ if (SYM2ID(obj) == rb_intern("in")) fd = 0; else if (SYM2ID(obj) == rb_intern("out")) fd = 1; else if (SYM2ID(obj) == rb_intern("err")) fd = 2; break; case T_FILE: /* IO object */ if (rb_respond_to(obj, rb_intern("posix_fileno"))) { fd = FIX2INT(rb_funcall(obj, rb_intern("posix_fileno"), 0)); } else { fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0)); } break; case T_OBJECT: /* some other object */ if (rb_respond_to(obj, rb_intern("to_io"))) { obj = rb_funcall(obj, rb_intern("to_io"), 0); if (rb_respond_to(obj, rb_intern("posix_fileno"))) { fd = FIX2INT(rb_funcall(obj, rb_intern("posix_fileno"), 0)); } else { fd = FIX2INT(rb_funcall(obj, rb_intern("fileno"), 0)); } } break; } return fd; } /* * Hash iterator that sets up the posix_spawn_file_actions_t with addclose * operations. Only hash pairs whose value is :close are processed. Keys may * be the :in, :out, :err, an IO object, or a Fixnum fd number. * * Returns ST_DELETE when an addclose operation was added; ST_CONTINUE when * no operation was performed. */ static int posixspawn_file_actions_addclose(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) { int fd; /* we only care about { (IO|FD|:in|:out|:err) => :close } */ if (TYPE(val) != T_SYMBOL || SYM2ID(val) != rb_intern("close")) return ST_CONTINUE; fd = posixspawn_obj_to_fd(key); if (fd >= 0) { posix_spawn_file_actions_addclose(fops, fd); return ST_DELETE; } else { return ST_CONTINUE; } } /* * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 + * close operations for all redirects. Only hash pairs whose key and value * represent fd numbers are processed. * * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when * no operation was performed. */ static int posixspawn_file_actions_adddup2(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) { int fd, newfd; newfd = posixspawn_obj_to_fd(key); if (newfd < 0) return ST_CONTINUE; fd = posixspawn_obj_to_fd(val); if (fd < 0) return ST_CONTINUE; posix_spawn_file_actions_adddup2(fops, fd, newfd); return ST_DELETE; } /* * Hash iterator that sets up the posix_spawn_file_actions_t with adddup2 + * clone operations for all file redirects. Only hash pairs whose key is an * fd number and value is a valid three-tuple [file, flags, mode] are * processed. * * Returns ST_DELETE when an adddup2 operation was added; ST_CONTINUE when * no operation was performed. */ static int posixspawn_file_actions_addopen(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) { int fd; char *path; int oflag; mode_t mode; fd = posixspawn_obj_to_fd(key); if (fd < 0) return ST_CONTINUE; if (TYPE(val) != T_ARRAY || RARRAY_LEN(val) != 3) return ST_CONTINUE; path = StringValuePtr(RARRAY_PTR(val)[0]); oflag = FIX2INT(RARRAY_PTR(val)[1]); mode = FIX2INT(RARRAY_PTR(val)[2]); posix_spawn_file_actions_addopen(fops, fd, path, oflag, mode); return ST_DELETE; } /* * Main entry point for iterating over the options hash to perform file actions. * This function dispatches to the addclose and adddup2 functions, stopping once * an operation was added. * * Returns ST_DELETE if one of the handlers performed an operation; ST_CONTINUE * if not. */ static int posixspawn_file_actions_operations_iter(VALUE key, VALUE val, posix_spawn_file_actions_t *fops) { int act; act = posixspawn_file_actions_addclose(key, val, fops); if (act != ST_CONTINUE) return act; act = posixspawn_file_actions_adddup2(key, val, fops); if (act != ST_CONTINUE) return act; act = posixspawn_file_actions_addopen(key, val, fops); if (act != ST_CONTINUE) return act; return ST_CONTINUE; } /* * Initialize the posix_spawn_file_actions_t structure and add operations from * the options hash. Keys in the options Hash that are processed by handlers are * removed. * * Returns nothing. */ static void posixspawn_file_actions_init(posix_spawn_file_actions_t *fops, VALUE options) { posix_spawn_file_actions_init(fops); rb_hash_foreach(options, posixspawn_file_actions_operations_iter, (VALUE)fops); } /* * Initialize pgroup related flags in the posix_spawnattr struct based on the * options Hash. * * :pgroup => 0 | true - spawned process is in a new process group with the * same id as the new process's pid. * :pgroup => pgid - spawned process is in a new process group with id * pgid. * :pgroup => nil - spawned process has the same pgid as the parent * process (this is the default). * * The options Hash is modified in place with the :pgroup key being removed. */ static void posixspawn_set_pgroup(VALUE options, posix_spawnattr_t *pattr, short *pflags) { VALUE pgroup_val; pgroup_val = rb_hash_delete(options, ID2SYM(rb_intern("pgroup"))); switch (TYPE(pgroup_val)) { case T_TRUE: (*pflags) |= POSIX_SPAWN_SETPGROUP; posix_spawnattr_setpgroup(pattr, 0); break; case T_FIXNUM: (*pflags) |= POSIX_SPAWN_SETPGROUP; posix_spawnattr_setpgroup(pattr, FIX2INT(pgroup_val)); break; case T_NIL: break; default: rb_raise(rb_eTypeError, ":pgroup option is invalid"); break; } } static int each_env_check_i(VALUE key, VALUE val, VALUE arg) { StringValuePtr(key); if (!NIL_P(val)) StringValuePtr(val); return ST_CONTINUE; } static int each_env_i(VALUE key, VALUE val, VALUE arg) { char *name = StringValuePtr(key); size_t len = strlen(name); /* * Delete any existing values for this variable before inserting the new value. * This implementation was copied from glibc's unsetenv(). */ char **ep = (char **)arg; while (*ep != NULL) if (!strncmp (*ep, name, len) && (*ep)[len] == '=') { /* Found it. Remove this pointer by moving later ones back. */ char **dp = ep; do dp[0] = dp[1]; while (*dp++); /* Continue the loop in case NAME appears again. */ } else ++ep; /* * Insert the new value if we have one. We can assume there is space * at the end of the list, since ep was preallocated to be big enough * for the new entries. */ if (RTEST(val)) { char **ep = (char **)arg; char *cval = StringValuePtr(val); size_t cval_len = strlen(cval); size_t ep_len = len + 1 + cval_len + 1; /* +2 for null terminator and '=' separator */ /* find the last entry */ while (*ep != NULL) ++ep; *ep = malloc(ep_len); strncpy(*ep, name, len); (*ep)[len] = '='; strncpy(*ep + len + 1, cval, cval_len); (*ep)[ep_len-1] = 0; } return ST_CONTINUE; } /* * POSIX::Spawn#_pspawn(env, argv, options) * * env - Hash of the new environment. * argv - The [[cmdname, argv0], argv1, ...] exec array. * options - The options hash with fd redirect and close operations. * * Returns the pid of the newly spawned process. */ static VALUE rb_posixspawn_pspawn(VALUE self, VALUE env, VALUE argv, VALUE options) { int i, ret; char **envp = NULL; VALUE dirname; VALUE cmdname; VALUE unsetenv_others_p = Qfalse; char *file; char *cwd = NULL; pid_t pid; posix_spawn_file_actions_t fops; posix_spawnattr_t attr; sigset_t mask; short flags = 0; /* argv is a [[cmdname, argv0], argv1, argvN, ...] array. */ if (TYPE(argv) != T_ARRAY || TYPE(RARRAY_PTR(argv)[0]) != T_ARRAY || RARRAY_LEN(RARRAY_PTR(argv)[0]) != 2) rb_raise(rb_eArgError, "Invalid command name"); long argc = RARRAY_LEN(argv); char *cargv[argc + 1]; cmdname = RARRAY_PTR(argv)[0]; file = StringValuePtr(RARRAY_PTR(cmdname)[0]); cargv[0] = StringValuePtr(RARRAY_PTR(cmdname)[1]); for (i = 1; i < argc; i++) cargv[i] = StringValuePtr(RARRAY_PTR(argv)[i]); cargv[argc] = NULL; if (TYPE(options) == T_HASH) { unsetenv_others_p = rb_hash_delete(options, ID2SYM(rb_intern("unsetenv_others"))); } if (RTEST(env)) { /* * Make sure env is a hash, and all keys and values are strings. * We do this before allocating space for the new environment to * prevent a leak when raising an exception after the calloc() below. */ Check_Type(env, T_HASH); rb_hash_foreach(env, each_env_check_i, 0); if (RHASH_SIZE(env) > 0) { int size = 0; char **curr = environ; if (curr) { while (*curr != NULL) ++curr, ++size; } if (unsetenv_others_p == Qtrue) { /* * ignore the parent's environment by pretending it had * no entries. the loop below will do nothing. */ size = 0; } char **new_env = calloc(size+RHASH_SIZE(env)+1, sizeof(char*)); for (i = 0; i < size; i++) { new_env[i] = strdup(environ[i]); } envp = new_env; rb_hash_foreach(env, each_env_i, (VALUE)envp); } } posixspawn_file_actions_init(&fops, options); posix_spawnattr_init(&attr); /* child does not block any signals */ flags |= POSIX_SPAWN_SETSIGMASK; sigemptyset(&mask); posix_spawnattr_setsigmask(&attr, &mask); #if defined(POSIX_SPAWN_USEVFORK) || defined(__linux__) /* Force USEVFORK on linux. If this is undefined, it's probably because * you forgot to define _GNU_SOURCE at the top of this file. */ flags |= POSIX_SPAWN_USEVFORK; #endif /* setup pgroup options */ posixspawn_set_pgroup(options, &attr, &flags); posix_spawnattr_setflags(&attr, flags); if (RTEST(dirname = rb_hash_delete(options, ID2SYM(rb_intern("chdir"))))) { char *new_cwd = StringValuePtr(dirname); cwd = getcwd(NULL, 0); chdir(new_cwd); } if (RHASH_SIZE(options) == 0) { ret = posix_spawnp(&pid, file, &fops, &attr, cargv, envp ? envp : environ); if (cwd) { chdir(cwd); free(cwd); } } else { ret = -1; } posix_spawn_file_actions_destroy(&fops); posix_spawnattr_destroy(&attr); if (envp) { char **ep = envp; while (*ep != NULL) free(*ep), ++ep; free(envp); } if (RHASH_SIZE(options) > 0) { rb_raise(rb_eArgError, "Invalid option: %s", RSTRING_PTR(rb_inspect(rb_funcall(options, rb_intern("first"), 0)))); return -1; } if (ret != 0) { char error_context[PATH_MAX+32]; snprintf(error_context, sizeof(error_context), "when spawning '%s'", file); errno = ret; rb_sys_fail(error_context); } return INT2FIX(pid); } void Init_posix_spawn_ext() { rb_mPOSIX = rb_define_module("POSIX"); rb_mPOSIXSpawn = rb_define_module_under(rb_mPOSIX, "Spawn"); rb_define_method(rb_mPOSIXSpawn, "_pspawn", rb_posixspawn_pspawn, 3); } /* vim: set noexpandtab sts=0 ts=4 sw=4: */ posix-spawn-0.3.8/ext/extconf.rb0000644000004100000410000000033512253342007016634 0ustar www-datawww-datarequire 'mkmf' # warnings save lives $CFLAGS << " -Wall " if RUBY_PLATFORM =~ /(mswin|mingw|cygwin|bccwin)/ File.open('Makefile','w'){|f| f.puts "default: \ninstall: " } else create_makefile('posix_spawn_ext') end posix-spawn-0.3.8/README.md0000644000004100000410000002412112253342007015317 0ustar www-datawww-data# posix-spawn `fork(2)` calls slow down as the parent process uses more memory due to the need to copy page tables. In many common uses of fork(), where it is followed by one of the exec family of functions to spawn child processes (`Kernel#system`, `IO::popen`, `Process::spawn`, etc.), it's possible to remove this overhead by using special process spawning interfaces (`posix_spawn()`, `vfork()`, etc.) The posix-spawn library aims to implement a subset of the Ruby 1.9 `Process::spawn` interface in a way that takes advantage of fast process spawning interfaces when available and provides sane fallbacks on systems that do not. ### FEATURES - Fast, constant-time spawn times across a variety of platforms. - A largish compatible subset of Ruby 1.9's `Process::spawn` interface and enhanced versions of `Kernel#system`, Kernel#`, etc. under Ruby >= 1.8.7 (currently MRI only). - High level `POSIX::Spawn::Child` class for quick (but correct!) non-streaming IPC scenarios. ## BENCHMARKS The following benchmarks illustrate time needed to fork/exec a child process at increasing resident memory sizes on Linux 2.6 and MacOS X. Tests were run using the [`posix-spawn-benchmark`][pb] program included with the package. [pb]: https://github.com/rtomayko/posix-spawn/tree/master/bin ### Linux ![](https://chart.googleapis.com/chart?chbh=a,5,25&chxr=1,0,36,7&chd=t:5.77,10.37,15.72,18.31,19.73,25.13,26.70,29.31,31.44,35.49|0.86,0.82,1.06,0.99,0.79,1.06,0.84,0.79,0.93,0.94&chxs=1N**%20secs&chs=900x200&chds=0,36&chxl=0:|50%20MB|100%20MB|150%20MB|200%20MB|250%20MB|300%20MB|350%20MB|400%20MB|450%20MB|500%20MB&cht=bvg&chdl=fspawn%20%28fork%2Bexec%29|pspawn%20%28posix_spawn%29&chtt=posix-spawn-benchmark%20--graph%20--count%20500%20--mem-size%20500%20%28x86_64-linux%29&chco=1f77b4,ff7f0e&chf=bg,s,f8f8f8&chxt=x,y#.png) `posix_spawn` is faster than `fork+exec`, and executes in constant time when used with `POSIX_SPAWN_USEVFORK`. `fork+exec` is extremely slow for large parent processes. ### OSX ![](https://chart.googleapis.com/chart?chxl=0:|50%20MB|100%20MB|150%20MB|200%20MB|250%20MB|300%20MB|350%20MB|400%20MB|450%20MB|500%20MB&cht=bvg&chdl=fspawn%20%28fork%2Bexec%29|pspawn%20%28posix_spawn%29&chtt=posix-spawn-benchmark%20--graph%20--count%20500%20--mem-size%20500%20%28i686-darwin10.5.0%29&chco=1f77b4,ff7f0e&chf=bg,s,f8f8f8&chxt=x,y&chbh=a,5,25&chxr=1,0,3,0&chd=t:1.95,2.07,2.56,2.29,2.21,2.32,2.15,2.25,1.96,2.02|0.84,0.97,0.89,0.82,1.13,0.89,0.93,0.81,0.83,0.81&chxs=1N**%20secs&chs=900x200&chds=0,3#.png) `posix_spawn` is faster than `fork+exec`, but neither is affected by the size of the parent process. ## USAGE This library includes two distinct interfaces: `POSIX::Spawn::spawn`, a lower level process spawning interface based on the new Ruby 1.9 `Process::spawn` method, and `POSIX::Spawn::Child`, a higher level class geared toward easy spawning of processes with simple string based standard input/output/error stream handling. The former is much more versatile, the latter requires much less code for certain common scenarios. ### POSIX::Spawn::spawn The `POSIX::Spawn` module (with help from the accompanying C extension) implements a subset of the [Ruby 1.9 Process::spawn][ps] interface, largely through the use of the [IEEE Std 1003.1 `posix_spawn(2)` systems interfaces][po]. These are widely supported by various UNIX operating systems. [ps]: http://www.ruby-doc.org/core-1.9/classes/Process.html#M002230 [po]: http://pubs.opengroup.org/onlinepubs/009695399/functions/posix_spawn.html In its simplest form, the `POSIX::Spawn::spawn` method can be used to execute a child process similar to `Kernel#system`: require 'posix/spawn' pid = POSIX::Spawn::spawn('echo', 'hello world') stat = Process::waitpid(pid) The first line executes `echo` with a single argument and immediately returns the new process's `pid`. The second line waits for the process to complete and returns a `Process::Status` object. Note that `spawn` *does not* wait for the process to finish execution like `system` and does not reap the child's exit status -- you must call `Process::waitpid` (or equivalent) or the process will become a zombie. The `spawn` method is capable of performing a large number of additional operations, from setting up the new process's environment, to changing the child's working directory, to redirecting arbitrary file descriptors. See the Ruby 1.9 [`Process::spawn` documentation][ps] for details and the `STATUS` section below for a full account of the various `Process::spawn` features supported by `POSIX::Spawn::spawn`. ### `system`, `popen4`, and ` In addition to the `spawn` method, Ruby 1.9 compatible implementations of `Kernel#system` and Kernel#\` are provided in the `POSIX::Spawn` module. The `popen4` method can be used to spawn a process with redirected stdin, stdout, and stderr objects. ### POSIX::Spawn as a Mixin The `POSIX::Spawn` module can also be mixed in to classes and modules to include `spawn` and all utility methods in that namespace: require 'posix/spawn' class YourGreatClass include POSIX::Spawn def speak(message) pid = spawn('echo', message) Process::waitpid(pid) end def calculate(expression) pid, in, out, err = popen4('bc') in.write(expression) in.close out.read ensure [in, out, err].each { |io| io.close if !io.closed? } Process::waitpid(pid) end end ### POSIX::Spawn::Child The `POSIX::Spawn::Child` class includes logic for executing child processes and reading/writing from their standard input, output, and error streams. It's designed to take all input in a single string and provides all output as single strings and is therefore not well-suited to streaming large quantities of data in and out of commands. That said, it has some benefits: - **Simple** - requires little code for simple stream input and capture. - **Internally non-blocking** (using `select(2)`) - handles all pipe hang cases due to exceeding `PIPE_BUF` limits on one or more streams. - **Potentially portable** - abstracts lower-level process and stream management APIs so the class can be made to work on platforms like Java and Windows where UNIX process spawning and stream APIs are not supported. `POSIX::Spawn::Child` takes the standard `spawn` arguments when instantiated, and runs the process to completion after writing all input and reading all output: >> require 'posix/spawn' >> child = POSIX::Spawn::Child.new('git', '--help') Retrieve process output written to stdout / stderr, or inspect the process's exit status: >> child.out => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..." >> child.err => "" >> child.status => # Use the `:input` option to write data on the new process's stdin immediately after spawning: >> child = POSIX::Spawn::Child.new('bc', :input => '40 + 2') >> child.out "42\n" Additional options can be used to specify the maximum output size and time of execution before the child process is aborted. See the `POSIX::Spawn::Child` docs for more info. ## STATUS The `POSIX::Spawn::spawn` method is designed to be as compatible with Ruby 1.9's `Process::spawn` as possible. Right now, it is a compatible subset. These `Process::spawn` arguments are currently supported to any of `Spawn::spawn`, `Spawn::system`, `Spawn::popen4`, and `Spawn::Child.new`: env: hash name => val : set the environment variable name => nil : unset the environment variable command...: commandline : command line string which is passed to a shell cmdname, arg1, ... : command name and one or more arguments (no shell) [cmdname, argv0], arg1, ... : command name, argv[0] and zero or more arguments (no shell) options: hash clearing environment variables: :unsetenv_others => true : clear environment variables except specified by env :unsetenv_others => false : don't clear (default) current directory: :chdir => str process group: :pgroup => true or 0 : make a new process group :pgroup => pgid : join to specified process group :pgroup => nil : don't change the process group (default) redirection: key: FD : single file descriptor in child process [FD, FD, ...] : multiple file descriptor in child process value: FD : redirect to the file descriptor in parent process :close : close the file descriptor in child process string : redirect to file with open(string, "r" or "w") [string] : redirect to file with open(string, File::RDONLY) [string, open_mode] : redirect to file with open(string, open_mode, 0644) [string, open_mode, perm] : redirect to file with open(string, open_mode, perm) FD is one of follows :in : the file descriptor 0 which is the standard input :out : the file descriptor 1 which is the standard output :err : the file descriptor 2 which is the standard error integer : the file descriptor of specified the integer io : the file descriptor specified as io.fileno These options are currently NOT supported: options: hash resource limit: resourcename is core, cpu, data, etc. See Process.setrlimit. :rlimit_resourcename => limit :rlimit_resourcename => [cur_limit, max_limit] umask: :umask => int redirection: value: [:child, FD] : redirect to the redirected file descriptor file descriptor inheritance: close non-redirected non-standard fds (3, 4, 5, ...) or not :close_others => false : inherit fds (default for system and exec) :close_others => true : don't inherit (default for spawn and IO.popen) ## ACKNOWLEDGEMENTS Copyright (c) by [Ryan Tomayko](http://tomayko.com/about) and [Aman Gupta](https://github.com/tmm1). See the `COPYING` file for more information on license and redistribution. posix-spawn-0.3.8/COPYING0000644000004100000410000000265312253342007015101 0ustar www-datawww-dataCopyright (c) 2011 by Ryan Tomayko and Aman Gupta Permission is hereby granted, free of charge, to any person ob- taining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restric- tion, including without limitation the rights to use, copy, modi- fy, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is fur- nished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN- FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. A small portion of the environ dup'ing code in ext/posix-spawn.c was taken from glibc and is maybe Copyright (c) 2011 by The Free Software Foundation or maybe by others mentioned in the glibc LICENSES file. glibc is distributed under the terms of the LGPL license.