From 2d00985607b5529b5f59ff298630333463395184 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Thu, 10 Dec 2020 20:49:51 +0100 Subject: [PATCH] Use Process.spawn, it is enough to implement ChildProcess * Process.spawn requires only very minimal variants between platforms. * Remove all other backends, Process.spawn is enough. --- .github/workflows/ci.yml | 6 +- lib/childprocess.rb | 63 +-- lib/childprocess/abstract_process.rb | 4 +- lib/childprocess/jruby.rb | 56 --- lib/childprocess/jruby/io.rb | 16 - lib/childprocess/jruby/process.rb | 184 -------- lib/childprocess/jruby/pump.rb | 53 --- lib/childprocess/process_spawn_process.rb | 118 +++++ lib/childprocess/tools/generator.rb | 146 ------ lib/childprocess/unix.rb | 6 +- lib/childprocess/unix/fork_exec_process.rb | 78 ---- lib/childprocess/unix/lib.rb | 186 -------- .../unix/platform/arm64-macosx.rb | 11 - lib/childprocess/unix/platform/i386-linux.rb | 12 - .../unix/platform/i386-solaris.rb | 11 - .../unix/platform/x86_64-linux.rb | 12 - .../unix/platform/x86_64-macosx.rb | 11 - lib/childprocess/unix/posix_spawn_process.rb | 134 ------ lib/childprocess/unix/process.rb | 67 +-- lib/childprocess/windows.rb | 21 - lib/childprocess/windows/handle.rb | 91 ---- lib/childprocess/windows/lib.rb | 416 ------------------ lib/childprocess/windows/process.rb | 129 +----- lib/childprocess/windows/process_builder.rb | 178 -------- lib/childprocess/windows/structs.rb | 149 ------- spec/childprocess_spec.rb | 55 ++- spec/io_spec.rb | 2 +- spec/jruby_spec.rb | 24 - spec/spec_helper.rb | 17 +- spec/unix_spec.rb | 2 +- 30 files changed, 204 insertions(+), 2054 deletions(-) delete mode 100644 lib/childprocess/jruby.rb delete mode 100644 lib/childprocess/jruby/io.rb delete mode 100755 lib/childprocess/jruby/process.rb delete mode 100644 lib/childprocess/jruby/pump.rb create mode 100644 lib/childprocess/process_spawn_process.rb delete mode 100644 lib/childprocess/tools/generator.rb delete mode 100644 lib/childprocess/unix/fork_exec_process.rb delete mode 100644 lib/childprocess/unix/lib.rb delete mode 100644 lib/childprocess/unix/platform/arm64-macosx.rb delete mode 100644 lib/childprocess/unix/platform/i386-linux.rb delete mode 100644 lib/childprocess/unix/platform/i386-solaris.rb delete mode 100644 lib/childprocess/unix/platform/x86_64-linux.rb delete mode 100644 lib/childprocess/unix/platform/x86_64-macosx.rb delete mode 100644 lib/childprocess/unix/posix_spawn_process.rb delete mode 100644 lib/childprocess/windows/handle.rb delete mode 100644 lib/childprocess/windows/lib.rb mode change 100755 => 100644 lib/childprocess/windows/process.rb delete mode 100644 lib/childprocess/windows/process_builder.rb delete mode 100644 lib/childprocess/windows/structs.rb delete mode 100644 spec/jruby_spec.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b065dd..db693b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,11 @@ jobs: fail-fast: false matrix: os: [ ubuntu, macos, windows ] - ruby: [ 2.6, jruby, truffleruby ] + ruby: [ '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', jruby, truffleruby ] exclude: - - { os: windows, ruby: truffleruby } + - { os: windows, ruby: truffleruby } + # fails to load rspec: RuntimeError: CRITICAL: RUBYGEMS_ACTIVATION_MONITOR.owned?: before false -> after true + - { os: windows, ruby: jruby } runs-on: ${{ matrix.os }}-latest env: CHILDPROCESS_UNSET: should-be-unset diff --git a/lib/childprocess.rb b/lib/childprocess.rb index 41612f7..cfc5440 100644 --- a/lib/childprocess.rb +++ b/lib/childprocess.rb @@ -2,6 +2,7 @@ require 'childprocess/errors' require 'childprocess/abstract_process' require 'childprocess/abstract_io' +require 'childprocess/process_spawn_process' require "fcntl" require 'logger' @@ -15,13 +16,7 @@ class << self def new(*args) case os when :macosx, :linux, :solaris, :bsd, :cygwin, :aix - if jruby? && !posix_spawn_chosen_explicitly? - JRuby::Process.new(args) - elsif posix_spawn? - Unix::PosixSpawnProcess.new(args) - else - Unix::ForkExecProcess.new(*args) - end + Unix::Process.new(*args) when :windows Windows::Process.new(*args) else @@ -40,13 +35,7 @@ def logger end def platform - if RUBY_PLATFORM == "java" - :jruby - elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == "ironruby" - :ironruby - else - os - end + os end def platform_name @@ -62,7 +51,7 @@ def linux? end def jruby? - platform == :jruby + RUBY_ENGINE == 'jruby' end def windows? @@ -74,27 +63,6 @@ def posix_spawn_chosen_explicitly? end def posix_spawn? - enabled = posix_spawn_chosen_explicitly? || !Process.respond_to?(:fork) - return false unless enabled - - begin - require 'ffi' - rescue LoadError - raise ChildProcess::MissingFFIError - end - - begin - require "childprocess/unix/platform/#{ChildProcess.platform_name}" - rescue LoadError - raise ChildProcess::MissingPlatformError - end - - require "childprocess/unix/lib" - require 'childprocess/unix/posix_spawn_process' - - true - rescue ChildProcess::MissingPlatformError => ex - warn_once ex.message false end @@ -107,6 +75,8 @@ def posix_spawn=(bool) end def os + return :windows if ENV['FAKE_WINDOWS'] == 'true' + @os ||= ( require "rbconfig" host_os = RbConfig::CONFIG['host_os'].downcase @@ -162,17 +132,6 @@ def arch def close_on_exec(file) if file.respond_to?(:close_on_exec=) file.close_on_exec = true - elsif file.respond_to?(:fcntl) && defined?(Fcntl::FD_CLOEXEC) - file.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC - - if jruby? && posix_spawn? - # on JRuby, the fcntl call above apparently isn't enough when - # we're launching the process through posix_spawn. - fileno = JRuby.posix_fileno_for(file) - Unix::Lib.fcntl fileno, Fcntl::F_SETFD, Fcntl::FD_CLOEXEC - end - elsif windows? - Windows::Lib.dont_inherit file else raise Error, "not sure how to set close-on-exec for #{file.inspect} on #{platform_name.inspect}" end @@ -207,8 +166,8 @@ def is_64_bit? end # class << self end # ChildProcess -require 'jruby' if ChildProcess.jruby? - -require 'childprocess/unix' if ChildProcess.unix? -require 'childprocess/windows' if ChildProcess.windows? -require 'childprocess/jruby' if ChildProcess.jruby? +if ChildProcess.windows? + require 'childprocess/windows' +else + require 'childprocess/unix' +end diff --git a/lib/childprocess/abstract_process.rb b/lib/childprocess/abstract_process.rb index 93fd064..5d70208 100644 --- a/lib/childprocess/abstract_process.rb +++ b/lib/childprocess/abstract_process.rb @@ -28,7 +28,9 @@ class AbstractProcess # Set this to true to make the child process the leader of a new process group # # This can be used to make sure that all grandchildren are killed - # when the child process dies. + # when the child process dies on non-Windows. + # + # Note that grandchildren are not killed on Windows even when the process is the leader of a new process group. # attr_accessor :leader diff --git a/lib/childprocess/jruby.rb b/lib/childprocess/jruby.rb deleted file mode 100644 index f8bbc02..0000000 --- a/lib/childprocess/jruby.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'java' -require 'jruby' - -class Java::SunNioCh::FileChannelImpl - field_reader :fd -end - -class Java::JavaIo::FileDescriptor - if ChildProcess.os == :windows - field_reader :handle - end - - field_reader :fd -end - -module ChildProcess - module JRuby - def self.posix_fileno_for(obj) - channel = ::JRuby.reference(obj).channel - begin - channel.getFDVal - rescue NoMethodError - fileno = channel.fd - if fileno.kind_of?(Java::JavaIo::FileDescriptor) - fileno = fileno.fd - end - - fileno == -1 ? obj.fileno : fileno - end - rescue - # fall back - obj.fileno - end - - def self.windows_handle_for(obj) - channel = ::JRuby.reference(obj).channel - fileno = obj.fileno - - begin - fileno = channel.getFDVal - rescue NoMethodError - fileno = channel.fd if channel.respond_to?(:fd) - end - - if fileno.kind_of? Java::JavaIo::FileDescriptor - fileno.handle - else - Windows::Lib.handle_for fileno - end - end - end -end - -require "childprocess/jruby/pump" -require "childprocess/jruby/io" -require "childprocess/jruby/process" diff --git a/lib/childprocess/jruby/io.rb b/lib/childprocess/jruby/io.rb deleted file mode 100644 index f4e8c2b..0000000 --- a/lib/childprocess/jruby/io.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ChildProcess - module JRuby - class IO < AbstractIO - private - - def check_type(output) - unless output.respond_to?(:to_outputstream) && output.respond_to?(:write) - raise ArgumentError, "expected #{output.inspect} to respond to :to_outputstream" - end - end - - end # IO - end # Unix -end # ChildProcess - - diff --git a/lib/childprocess/jruby/process.rb b/lib/childprocess/jruby/process.rb deleted file mode 100755 index cd80fad..0000000 --- a/lib/childprocess/jruby/process.rb +++ /dev/null @@ -1,184 +0,0 @@ -require "java" - -module ChildProcess - module JRuby - class Process < AbstractProcess - def initialize(args) - super(args) - - @pumps = [] - end - - def io - @io ||= JRuby::IO.new - end - - def exited? - return true if @exit_code - - assert_started - @exit_code = @process.exitValue - stop_pumps - - true - rescue java.lang.IllegalThreadStateException => ex - log(ex.class => ex.message) - false - ensure - log(:exit_code => @exit_code) - end - - def stop(timeout = nil) - assert_started - - @process.destroy - wait # no way to actually use the timeout here.. - end - - def wait - if exited? - exit_code - else - @process.waitFor - - stop_pumps - @exit_code = @process.exitValue - end - end - - # Implementation of ChildProcess::JRuby::Process#pid depends heavily on - # what Java SDK is being used; here, we look it up once at load, then - # define the method once to avoid runtime overhead. - normalised_java_version_major = java.lang.System.get_property("java.version") - .slice(/^(1\.)?([0-9]+)/, 2) - .to_i - if normalised_java_version_major >= 9 - - # On modern Javas, we can simply delegate through to `Process#pid`, - # which was introduced in Java 9. - # - # @return [Integer] the pid of the process after it has started - # @raise [NotImplementedError] when trying to access pid on platform for - # which it is unsupported in Java - def pid - @process.pid - rescue java.lang.UnsupportedOperationException => e - raise NotImplementedError, "pid is not supported on this platform: #{e.message}" - end - - else - - # On Legacy Javas, fall back to reflection. - # - # Only supported in JRuby on a Unix operating system, thanks to limitations - # in Java's classes - # - # @return [Integer] the pid of the process after it has started - # @raise [NotImplementedError] when trying to access pid on non-Unix platform - # - def pid - if @process.getClass.getName != "java.lang.UNIXProcess" - raise NotImplementedError, "pid is only supported by JRuby child processes on Unix" - end - - # About the best way we can do this is with a nasty reflection-based impl - # Thanks to Martijn Courteaux - # http://stackoverflow.com/questions/2950338/how-can-i-kill-a-linux-process-in-java-with-sigkill-process-destroy-does-sigter/2951193#2951193 - field = @process.getClass.getDeclaredField("pid") - field.accessible = true - field.get(@process) - end - - end - - private - - def launch_process(&blk) - pb = java.lang.ProcessBuilder.new(@args) - - pb.directory java.io.File.new(@cwd || Dir.pwd) - set_env pb.environment - - begin - @process = pb.start - rescue java.io.IOException => ex - raise LaunchError, ex.message - end - - setup_io - end - - def setup_io - if @io - redirect(@process.getErrorStream, @io.stderr) - redirect(@process.getInputStream, @io.stdout) - else - @process.getErrorStream.close - @process.getInputStream.close - end - - if duplex? - io._stdin = create_stdin - else - @process.getOutputStream.close - end - end - - def redirect(input, output) - if output.nil? - input.close - return - end - - @pumps << Pump.new(input, output.to_outputstream).run - end - - def stop_pumps - @pumps.each { |pump| pump.stop } - end - - def set_env(env) - merged = ENV.to_hash - - @environment.each { |k, v| merged[k.to_s] = v } - - merged.each do |k, v| - if v - env.put(k, v.to_s) - elsif env.has_key? k - env.remove(k) - end - end - - removed_keys = env.key_set.to_a - merged.keys - removed_keys.each { |k| env.remove(k) } - end - - def create_stdin - output_stream = @process.getOutputStream - - stdin = output_stream.to_io - stdin.sync = true - stdin.instance_variable_set(:@childprocess_java_stream, output_stream) - - class << stdin - # The stream provided is a BufferedeOutputStream, so we - # have to flush it to make the bytes flow to the process - def __childprocess_flush__ - @childprocess_java_stream.flush - end - - [:flush, :print, :printf, :putc, :puts, :write, :write_nonblock].each do |m| - define_method(m) do |*args| - super(*args) - self.__childprocess_flush__ - end - end - end - - stdin - end - - end # Process - end # JRuby -end # ChildProcess diff --git a/lib/childprocess/jruby/pump.rb b/lib/childprocess/jruby/pump.rb deleted file mode 100644 index 64ac32d..0000000 --- a/lib/childprocess/jruby/pump.rb +++ /dev/null @@ -1,53 +0,0 @@ -module ChildProcess - module JRuby - class Pump - BUFFER_SIZE = 2048 - - def initialize(input, output) - @input = input - @output = output - @stop = false - end - - def stop - @stop = true - @thread && @thread.join - end - - def run - @thread = Thread.new { pump } - - self - end - - private - - def pump - buffer = Java.byte[BUFFER_SIZE].new - - until @stop && (@input.available == 0) - read, avail = 0, 0 - - while read != -1 - avail = [@input.available, 1].max - avail = BUFFER_SIZE if avail > BUFFER_SIZE - read = @input.read(buffer, 0, avail) - - if read > 0 - @output.write(buffer, 0, read) - @output.flush - end - end - - sleep 0.1 - end - - @output.flush - rescue java.io.IOException => ex - ChildProcess.logger.debug ex.message - ChildProcess.logger.debug ex.backtrace - end - - end # Pump - end # JRuby -end # ChildProcess diff --git a/lib/childprocess/process_spawn_process.rb b/lib/childprocess/process_spawn_process.rb new file mode 100644 index 0000000..04b20d3 --- /dev/null +++ b/lib/childprocess/process_spawn_process.rb @@ -0,0 +1,118 @@ +require_relative 'abstract_process' + +module ChildProcess + class ProcessSpawnProcess < AbstractProcess + attr_reader :pid + + def exited? + return true if @exit_code + + assert_started + pid, status = ::Process.waitpid2(@pid, ::Process::WNOHANG | ::Process::WUNTRACED) + pid = nil if pid == 0 # may happen on jruby + + log(:pid => pid, :status => status) + + if pid + set_exit_code(status) + end + + !!pid + rescue Errno::ECHILD + # may be thrown for detached processes + true + end + + def wait + assert_started + + if exited? + exit_code + else + _, status = ::Process.waitpid2(@pid) + + set_exit_code(status) + end + end + + private + + def launch_process + environment = {} + @environment.each_pair { |key, value| environment[key.to_s] = (value.nil? ? nil : value.to_s) } + + options = {} + + options[:out] = io.stdout ? io.stdout.fileno : File::NULL + options[:err] = io.stderr ? io.stderr.fileno : File::NULL + + if duplex? + reader, writer = ::IO.pipe + options[:in] = reader.fileno + unless ChildProcess.windows? + options[writer.fileno] = :close + end + end + + if leader? + if ChildProcess.windows? + options[:new_pgroup] = true + else + options[:pgroup] = true + end + end + + options[:chdir] = @cwd if @cwd + + if @args.size == 1 + # When given a single String, Process.spawn would think it should use the shell + # if there is any special character in it. However, ChildProcess should never + # use the shell. So we use the [cmdname, argv0] form to force no shell. + arg = @args[0] + args = [[arg, arg]] + else + args = @args + end + + begin + @pid = ::Process.spawn(environment, *args, options) + rescue SystemCallError => e + raise LaunchError, e.message + end + + if duplex? + io._stdin = writer + reader.close + end + + ::Process.detach(@pid) if detach? + end + + def set_exit_code(status) + @exit_code = status.exitstatus || status.termsig + end + + def send_term + send_signal 'TERM' + end + + def send_kill + send_signal 'KILL' + end + + def send_signal(sig) + assert_started + + log "sending #{sig}" + ::Process.kill sig, _pid + end + + def _pid + if leader? and ChildProcess.unix? + -@pid # negative pid == process group + else + @pid + end + end + end +end diff --git a/lib/childprocess/tools/generator.rb b/lib/childprocess/tools/generator.rb deleted file mode 100644 index bb863df..0000000 --- a/lib/childprocess/tools/generator.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'fileutils' - -module ChildProcess - module Tools - class Generator - EXE_NAME = "childprocess-sizeof-generator" - TMP_PROGRAM = "childprocess-sizeof-generator.c" - DEFAULT_INCLUDES = %w[stdio.h stddef.h] - - def self.generate - new.generate - end - - def initialize - @cc = ENV['CC'] || 'gcc' - @out = File.expand_path("../../unix/platform/#{ChildProcess.platform_name}.rb", __FILE__) - @sizeof = {} - @constants = {} - end - - def generate - fetch_size 'posix_spawn_file_actions_t', :include => "spawn.h" - fetch_size 'posix_spawnattr_t', :include => "spawn.h" - fetch_size 'sigset_t', :include => "signal.h" - - fetch_constant 'POSIX_SPAWN_RESETIDS', :include => 'spawn.h' - fetch_constant 'POSIX_SPAWN_SETPGROUP', :include => 'spawn.h' - fetch_constant 'POSIX_SPAWN_SETSIGDEF', :include => 'spawn.h' - fetch_constant 'POSIX_SPAWN_SETSIGMASK', :include => 'spawn.h' - - if ChildProcess.linux? - fetch_constant 'POSIX_SPAWN_USEVFORK', :include => 'spawn.h', :define => {'_GNU_SOURCE' => nil} - end - - write - end - - def write - FileUtils.mkdir_p(File.dirname(@out)) - File.open(@out, 'w') do |io| - io.puts result - end - - puts "wrote #{@out}" - end - - def fetch_size(type_name, opts = {}) - print "sizeof(#{type_name}): " - src = <<-EOF -int main() { - printf("%d", (unsigned int)sizeof(#{type_name})); - return 0; -} - EOF - - output = execute(src, opts) - - if output.to_i < 1 - raise "sizeof(#{type_name}) == #{output.to_i} (output=#{output})" - end - - size = output.to_i - @sizeof[type_name] = size - - puts size - end - - def fetch_constant(name, opts) - print "#{name}: " - src = <<-EOF -int main() { - printf("%d", (unsigned int)#{name}); - return 0; -} - EOF - - output = execute(src, opts) - value = Integer(output) - @constants[name] = value - - puts value - end - - - def execute(src, opts) - program = Array(opts[:define]).map do |key, value| - <<-SRC -#ifndef #{key} -#define #{key} #{value} -#endif - SRC - end.join("\n") - program << "\n" - - includes = Array(opts[:include]) + DEFAULT_INCLUDES - program << includes.map { |include| "#include <#{include}>" }.join("\n") - program << "\n#{src}" - - File.open(TMP_PROGRAM, 'w') do |file| - file << program - end - - cmd = "#{@cc} #{TMP_PROGRAM} -o #{EXE_NAME}" - system cmd - unless $?.success? - raise "failed to compile program: #{cmd.inspect}\n#{program}" - end - - output = `./#{EXE_NAME} 2>&1` - - unless $?.success? - raise "failed to run program: #{cmd.inspect}\n#{output}" - end - - output.chomp - ensure - File.delete TMP_PROGRAM if File.exist?(TMP_PROGRAM) - File.delete EXE_NAME if File.exist?(EXE_NAME) - end - - def result - if @sizeof.empty? && @constants.empty? - raise "no data collected, nothing to do" - end - - out = ['module ChildProcess::Unix::Platform'] - out << ' SIZEOF = {' - - max = @sizeof.keys.map { |e| e.length }.max - @sizeof.each_with_index do |(type, size), idx| - out << " :#{type.ljust max} => #{size}#{',' unless idx == @sizeof.size - 1}" - end - out << ' }' - - max = @constants.keys.map { |e| e.length }.max - @constants.each do |name, val| - out << " #{name.ljust max} = #{val}" - end - out << 'end' - - out.join "\n" - end - - end - end -end \ No newline at end of file diff --git a/lib/childprocess/unix.rb b/lib/childprocess/unix.rb index 09a8054..700a715 100644 --- a/lib/childprocess/unix.rb +++ b/lib/childprocess/unix.rb @@ -3,7 +3,5 @@ module Unix end end -require "childprocess/unix/io" -require "childprocess/unix/process" -require "childprocess/unix/fork_exec_process" -# PosixSpawnProcess + ffi is required on demand. +require_relative "unix/io" +require_relative "unix/process" diff --git a/lib/childprocess/unix/fork_exec_process.rb b/lib/childprocess/unix/fork_exec_process.rb deleted file mode 100644 index cc7a850..0000000 --- a/lib/childprocess/unix/fork_exec_process.rb +++ /dev/null @@ -1,78 +0,0 @@ -module ChildProcess - module Unix - class ForkExecProcess < Process - private - - def launch_process - if @io - stdout = @io.stdout - stderr = @io.stderr - end - - # pipe used to detect exec() failure - exec_r, exec_w = ::IO.pipe - ChildProcess.close_on_exec exec_w - - if duplex? - reader, writer = ::IO.pipe - end - - @pid = Kernel.fork { - # Children of the forked process will inherit its process group - # This is to make sure that all grandchildren dies when this Process instance is killed - ::Process.setpgid 0, 0 if leader? - - if @cwd - Dir.chdir(@cwd) - end - - exec_r.close - set_env - - if stdout - STDOUT.reopen(stdout) - else - STDOUT.reopen("/dev/null", "a+") - end - if stderr - STDERR.reopen(stderr) - else - STDERR.reopen("/dev/null", "a+") - end - - if duplex? - STDIN.reopen(reader) - writer.close - end - - executable, *args = @args - - begin - Kernel.exec([executable, executable], *args) - rescue SystemCallError => ex - exec_w << ex.message - end - } - - exec_w.close - - if duplex? - io._stdin = writer - reader.close - end - - # if we don't eventually get EOF, exec() failed - unless exec_r.eof? - raise LaunchError, exec_r.read || "executing command with #{@args.inspect} failed" - end - - ::Process.detach(@pid) if detach? - end - - def set_env - @environment.each { |k, v| ENV[k.to_s] = v.nil? ? nil : v.to_s } - end - - end # Process - end # Unix -end # ChildProcess diff --git a/lib/childprocess/unix/lib.rb b/lib/childprocess/unix/lib.rb deleted file mode 100644 index 22f19f2..0000000 --- a/lib/childprocess/unix/lib.rb +++ /dev/null @@ -1,186 +0,0 @@ -module ChildProcess - module Unix - module Lib - extend FFI::Library - ffi_lib FFI::Library::LIBC - - if ChildProcess.os == :macosx - attach_function :_NSGetEnviron, [], :pointer - def self.environ - _NSGetEnviron().read_pointer - end - elsif respond_to? :attach_variable - attach_variable :environ, :pointer - end - - attach_function :strerror, [:int], :string - attach_function :chdir, [:string], :int - attach_function :fcntl, [:int, :int, :int], :int # fcntl actually takes varags, but we only need this version. - - # int posix_spawnp( - # pid_t *restrict pid, - # const char *restrict file, - # const posix_spawn_file_actions_t *file_actions, - # const posix_spawnattr_t *restrict attrp, - # char *const argv[restrict], - # char *const envp[restrict] - # ); - - attach_function :posix_spawnp, [ - :pointer, - :string, - :pointer, - :pointer, - :pointer, - :pointer - ], :int - - # int posix_spawn_file_actions_init(posix_spawn_file_actions_t *file_actions); - attach_function :posix_spawn_file_actions_init, [:pointer], :int - - # int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *file_actions); - attach_function :posix_spawn_file_actions_destroy, [:pointer], :int - - # int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *file_actions, int filedes); - attach_function :posix_spawn_file_actions_addclose, [:pointer, :int], :int - - # int posix_spawn_file_actions_addopen( - # posix_spawn_file_actions_t *restrict file_actions, - # int filedes, - # const char *restrict path, - # int oflag, - # mode_t mode - # ); - attach_function :posix_spawn_file_actions_addopen, [:pointer, :int, :string, :int, :mode_t], :int - - # int posix_spawn_file_actions_adddup2( - # posix_spawn_file_actions_t *file_actions, - # int filedes, - # int newfiledes - # ); - attach_function :posix_spawn_file_actions_adddup2, [:pointer, :int, :int], :int - - # int posix_spawnattr_init(posix_spawnattr_t *attr); - attach_function :posix_spawnattr_init, [:pointer], :int - - # int posix_spawnattr_destroy(posix_spawnattr_t *attr); - attach_function :posix_spawnattr_destroy, [:pointer], :int - - # int posix_spawnattr_setflags(posix_spawnattr_t *attr, short flags); - attach_function :posix_spawnattr_setflags, [:pointer, :short], :int - - # int posix_spawnattr_getflags(const posix_spawnattr_t *restrict attr, short *restrict flags); - attach_function :posix_spawnattr_getflags, [:pointer, :pointer], :int - - # int posix_spawnattr_setpgroup(posix_spawnattr_t *attr, pid_t pgroup); - attach_function :posix_spawnattr_setpgroup, [:pointer, :pid_t], :int - - # int posix_spawnattr_getpgroup(const posix_spawnattr_t *restrict attr, pid_t *restrict pgroup); - attach_function :posix_spawnattr_getpgroup, [:pointer, :pointer], :int - - # int posix_spawnattr_setsigdefault(posix_spawnattr_t *restrict attr, const sigset_t *restrict sigdefault); - attach_function :posix_spawnattr_setsigdefault, [:pointer, :pointer], :int - - # int posix_spawnattr_getsigdefault(const posix_spawnattr_t *restrict attr, sigset_t *restrict sigdefault); - attach_function :posix_spawnattr_getsigdefault, [:pointer, :pointer], :int - - # int posix_spawnattr_setsigmask(posix_spawnattr_t *restrict attr, const sigset_t *restrict sigmask); - attach_function :posix_spawnattr_setsigmask, [:pointer, :pointer], :int - - # int posix_spawnattr_getsigmask(const posix_spawnattr_t *restrict attr, sigset_t *restrict sigmask); - attach_function :posix_spawnattr_getsigmask, [:pointer, :pointer], :int - - def self.check(errno) - if errno != 0 - raise Error, Lib.strerror(FFI.errno) - end - end - - class FileActions - def initialize - @ptr = FFI::MemoryPointer.new(1, Platform::SIZEOF.fetch(:posix_spawn_file_actions_t), false) - Lib.check Lib.posix_spawn_file_actions_init(@ptr) - end - - def add_close(fileno) - Lib.check Lib.posix_spawn_file_actions_addclose( - @ptr, - fileno - ) - end - - def add_open(fileno, path, oflag, mode) - Lib.check Lib.posix_spawn_file_actions_addopen( - @ptr, - fileno, - path, - oflag, - mode - ) - end - - def add_dup(fileno, new_fileno) - Lib.check Lib.posix_spawn_file_actions_adddup2( - @ptr, - fileno, - new_fileno - ) - end - - def free - Lib.check Lib.posix_spawn_file_actions_destroy(@ptr) - @ptr = nil - end - - def to_ptr - @ptr - end - end # FileActions - - class Attrs - def initialize - @ptr = FFI::MemoryPointer.new(1, Platform::SIZEOF.fetch(:posix_spawnattr_t), false) - Lib.check Lib.posix_spawnattr_init(@ptr) - end - - def free - Lib.check Lib.posix_spawnattr_destroy(@ptr) - @ptr = nil - end - - def flags=(flags) - Lib.check Lib.posix_spawnattr_setflags(@ptr, flags) - end - - def flags - ptr = FFI::MemoryPointer.new(:short) - Lib.check Lib.posix_spawnattr_getflags(@ptr, ptr) - - ptr.read_short - end - - def pgroup=(pid) - self.flags |= Platform::POSIX_SPAWN_SETPGROUP - Lib.check Lib.posix_spawnattr_setpgroup(@ptr, pid) - end - - def to_ptr - @ptr - end - end # Attrs - - end - end -end - -# missing on rubinius -class FFI::MemoryPointer - unless method_defined?(:from_string) - def self.from_string(str) - ptr = new(1, str.bytesize + 1) - ptr.write_string("#{str}\0") - - ptr - end - end -end diff --git a/lib/childprocess/unix/platform/arm64-macosx.rb b/lib/childprocess/unix/platform/arm64-macosx.rb deleted file mode 100644 index fc0383d..0000000 --- a/lib/childprocess/unix/platform/arm64-macosx.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ChildProcess::Unix::Platform - SIZEOF = { - :posix_spawn_file_actions_t => 8, - :posix_spawnattr_t => 8, - :sigset_t => 4 - } - POSIX_SPAWN_RESETIDS = 1 - POSIX_SPAWN_SETPGROUP = 2 - POSIX_SPAWN_SETSIGDEF = 4 - POSIX_SPAWN_SETSIGMASK = 8 -end diff --git a/lib/childprocess/unix/platform/i386-linux.rb b/lib/childprocess/unix/platform/i386-linux.rb deleted file mode 100644 index ecf86ed..0000000 --- a/lib/childprocess/unix/platform/i386-linux.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ChildProcess::Unix::Platform - SIZEOF = { - :posix_spawn_file_actions_t => 76, - :posix_spawnattr_t => 336, - :sigset_t => 128 - } - POSIX_SPAWN_RESETIDS = 1 - POSIX_SPAWN_SETPGROUP = 2 - POSIX_SPAWN_SETSIGDEF = 4 - POSIX_SPAWN_SETSIGMASK = 8 - POSIX_SPAWN_USEVFORK = 64 -end diff --git a/lib/childprocess/unix/platform/i386-solaris.rb b/lib/childprocess/unix/platform/i386-solaris.rb deleted file mode 100644 index 5b55788..0000000 --- a/lib/childprocess/unix/platform/i386-solaris.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ChildProcess::Unix::Platform - SIZEOF = { - :posix_spawn_file_actions_t => 4, - :posix_spawnattr_t => 4, - :sigset_t => 16 - } - POSIX_SPAWN_RESETIDS = 1 - POSIX_SPAWN_SETPGROUP = 2 - POSIX_SPAWN_SETSIGDEF = 4 - POSIX_SPAWN_SETSIGMASK = 8 -end diff --git a/lib/childprocess/unix/platform/x86_64-linux.rb b/lib/childprocess/unix/platform/x86_64-linux.rb deleted file mode 100644 index b0c8777..0000000 --- a/lib/childprocess/unix/platform/x86_64-linux.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ChildProcess::Unix::Platform - SIZEOF = { - :posix_spawn_file_actions_t => 80, - :posix_spawnattr_t => 336, - :sigset_t => 128 - } - POSIX_SPAWN_RESETIDS = 1 - POSIX_SPAWN_SETPGROUP = 2 - POSIX_SPAWN_SETSIGDEF = 4 - POSIX_SPAWN_SETSIGMASK = 8 - POSIX_SPAWN_USEVFORK = 64 -end diff --git a/lib/childprocess/unix/platform/x86_64-macosx.rb b/lib/childprocess/unix/platform/x86_64-macosx.rb deleted file mode 100644 index fc0383d..0000000 --- a/lib/childprocess/unix/platform/x86_64-macosx.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ChildProcess::Unix::Platform - SIZEOF = { - :posix_spawn_file_actions_t => 8, - :posix_spawnattr_t => 8, - :sigset_t => 4 - } - POSIX_SPAWN_RESETIDS = 1 - POSIX_SPAWN_SETPGROUP = 2 - POSIX_SPAWN_SETSIGDEF = 4 - POSIX_SPAWN_SETSIGMASK = 8 -end diff --git a/lib/childprocess/unix/posix_spawn_process.rb b/lib/childprocess/unix/posix_spawn_process.rb deleted file mode 100644 index 9622eeb..0000000 --- a/lib/childprocess/unix/posix_spawn_process.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'ffi' -require 'thread' - -module ChildProcess - module Unix - class PosixSpawnProcess < Process - private - - @@cwd_lock = Mutex.new - - def launch_process - pid_ptr = FFI::MemoryPointer.new(:pid_t) - actions = Lib::FileActions.new - attrs = Lib::Attrs.new - - if io.stdout - actions.add_dup fileno_for(io.stdout), fileno_for(STDOUT) - else - actions.add_open fileno_for(STDOUT), "/dev/null", File::WRONLY, 0644 - end - - if io.stderr - actions.add_dup fileno_for(io.stderr), fileno_for(STDERR) - else - actions.add_open fileno_for(STDERR), "/dev/null", File::WRONLY, 0644 - end - - if duplex? - reader, writer = ::IO.pipe - actions.add_dup fileno_for(reader), fileno_for(STDIN) - actions.add_close fileno_for(writer) - end - - attrs.pgroup = 0 if leader? - attrs.flags |= Platform::POSIX_SPAWN_USEVFORK if defined? Platform::POSIX_SPAWN_USEVFORK - - # wrap in helper classes in order to avoid GC'ed pointers - argv = Argv.new(@args) - envp = Envp.new(ENV.to_hash.merge(@environment)) - - ret = 0 - @@cwd_lock.synchronize do - Dir.chdir(@cwd || Dir.pwd) do - if ChildProcess.jruby? - # on JRuby, the current working directory is for some reason not inherited. - # We'll work around it by making a chdir call through FFI. - # TODO: report this to JRuby - Lib.chdir Dir.pwd - end - - ret = Lib.posix_spawnp( - pid_ptr, - @args.first, # TODO: not sure this matches exec() behaviour - actions, - attrs, - argv, - envp - ) - end - end - - if duplex? - io._stdin = writer - reader.close - end - - actions.free - attrs.free - - if ret != 0 - raise LaunchError, "#{Lib.strerror(ret)} (#{ret})" - end - - @pid = pid_ptr.read_int - ::Process.detach(@pid) if detach? - end - - if ChildProcess.jruby? - def fileno_for(obj) - ChildProcess::JRuby.posix_fileno_for(obj) - end - else - def fileno_for(obj) - obj.fileno - end - end - - class Argv - def initialize(args) - @ptrs = args.map do |e| - if e.include?("\0") - raise ArgumentError, "argument cannot contain null bytes: #{e.inspect}" - end - - FFI::MemoryPointer.from_string(e.to_s) - end - - @ptrs << FFI::Pointer.new(0) - end - - def to_ptr - argv = FFI::MemoryPointer.new(:pointer, @ptrs.size) - argv.put_array_of_pointer(0, @ptrs) - - argv - end - end # Argv - - class Envp - def initialize(env) - @ptrs = env.map do |key, val| - next if val.nil? - - if key =~ /=|\0/ || val.to_s.include?("\0") - raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.to_s.inspect}" - end - - FFI::MemoryPointer.from_string("#{key}=#{val.to_s}") - end.compact - - @ptrs << FFI::Pointer.new(0) - end - - def to_ptr - env = FFI::MemoryPointer.new(:pointer, @ptrs.size) - env.put_array_of_pointer(0, @ptrs) - - env - end - end # Envp - - end - end -end diff --git a/lib/childprocess/unix/process.rb b/lib/childprocess/unix/process.rb index 5933060..d7afce4 100644 --- a/lib/childprocess/unix/process.rb +++ b/lib/childprocess/unix/process.rb @@ -1,8 +1,8 @@ +require_relative '../process_spawn_process' + module ChildProcess module Unix - class Process < AbstractProcess - attr_reader :pid - + class Process < ProcessSpawnProcess def io @io ||= Unix::IO.new end @@ -24,67 +24,6 @@ def stop(timeout = 3) # and send_kill true end - - def exited? - return true if @exit_code - - assert_started - pid, status = ::Process.waitpid2(@pid, ::Process::WNOHANG | ::Process::WUNTRACED) - pid = nil if pid == 0 # may happen on jruby - - log(:pid => pid, :status => status) - - if pid - set_exit_code(status) - end - - !!pid - rescue Errno::ECHILD - # may be thrown for detached processes - true - end - - def wait - assert_started - - if exited? - exit_code - else - _, status = ::Process.waitpid2(@pid) - - set_exit_code(status) - end - end - - private - - def send_term - send_signal 'TERM' - end - - def send_kill - send_signal 'KILL' - end - - def send_signal(sig) - assert_started - - log "sending #{sig}" - ::Process.kill sig, _pid - end - - def set_exit_code(status) - @exit_code = status.exitstatus || status.termsig - end - - def _pid - if leader? - -@pid # negative pid == process group - else - @pid - end - end - end # Process end # Unix end # ChildProcess diff --git a/lib/childprocess/windows.rb b/lib/childprocess/windows.rb index 957c402..c009c5c 100644 --- a/lib/childprocess/windows.rb +++ b/lib/childprocess/windows.rb @@ -1,30 +1,9 @@ require "rbconfig" -begin - require 'ffi' -rescue LoadError - raise ChildProcess::MissingFFIError -end - module ChildProcess module Windows - module Lib - extend FFI::Library - - def self.msvcrt_name - RbConfig::CONFIG['RUBY_SO_NAME'][/msvc\w+/] || 'ucrtbase' - end - - ffi_lib "kernel32", msvcrt_name - ffi_convention :stdcall - - end # Library end # Windows end # ChildProcess -require "childprocess/windows/lib" -require "childprocess/windows/structs" -require "childprocess/windows/handle" require "childprocess/windows/io" -require "childprocess/windows/process_builder" require "childprocess/windows/process" diff --git a/lib/childprocess/windows/handle.rb b/lib/childprocess/windows/handle.rb deleted file mode 100644 index 9435684..0000000 --- a/lib/childprocess/windows/handle.rb +++ /dev/null @@ -1,91 +0,0 @@ -module ChildProcess - module Windows - class Handle - - class << self - private :new - - def open(pid, access = PROCESS_ALL_ACCESS) - handle = Lib.open_process(access, false, pid) - - if handle.null? - raise Error, Lib.last_error_message - end - - h = new(handle, pid) - return h unless block_given? - - begin - yield h - ensure - h.close - end - end - end - - attr_reader :pointer - - def initialize(pointer, pid) - unless pointer.kind_of?(FFI::Pointer) - raise TypeError, "invalid handle: #{pointer.inspect}" - end - - if pointer.null? - raise ArgumentError, "handle is null: #{pointer.inspect}" - end - - @pid = pid - @pointer = pointer - @closed = false - end - - def exit_code - code_pointer = FFI::MemoryPointer.new :ulong - ok = Lib.get_exit_code(@pointer, code_pointer) - - if ok - code_pointer.get_ulong(0) - else - close - raise Error, Lib.last_error_message - end - end - - def send(signal) - case signal - when 0 - exit_code == PROCESS_STILL_ALIVE - when WIN_SIGINT - Lib.generate_console_ctrl_event(CTRL_C_EVENT, @pid) - when WIN_SIGBREAK - Lib.generate_console_ctrl_event(CTRL_BREAK_EVENT, @pid) - when WIN_SIGKILL - ok = Lib.terminate_process(@pointer, @pid) - Lib.check_error ok - else - thread_id = FFI::MemoryPointer.new(:ulong) - module_handle = Lib.get_module_handle("kernel32") - proc_address = Lib.get_proc_address(module_handle, "ExitProcess") - - thread = Lib.create_remote_thread(@pointer, 0, 0, proc_address, 0, 0, thread_id) - check_error thread - - Lib.wait_for_single_object(thread, 5) - true - end - end - - def close - return if @closed - - Lib.close_handle(@pointer) - @closed = true - end - - def wait(milliseconds = nil) - Lib.wait_for_single_object(@pointer, milliseconds || INFINITE) - end - - end # Handle - end # Windows -end # ChildProcess diff --git a/lib/childprocess/windows/lib.rb b/lib/childprocess/windows/lib.rb deleted file mode 100644 index fd7f478..0000000 --- a/lib/childprocess/windows/lib.rb +++ /dev/null @@ -1,416 +0,0 @@ -module ChildProcess - module Windows - FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000 - FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000 - - PROCESS_ALL_ACCESS = 0x1F0FFF - PROCESS_QUERY_INFORMATION = 0x0400 - PROCESS_VM_READ = 0x0010 - PROCESS_STILL_ACTIVE = 259 - - INFINITE = 0xFFFFFFFF - - WIN_SIGINT = 2 - WIN_SIGBREAK = 3 - WIN_SIGKILL = 9 - - CTRL_C_EVENT = 0 - CTRL_BREAK_EVENT = 1 - - CREATE_BREAKAWAY_FROM_JOB = 0x01000000 - DETACHED_PROCESS = 0x00000008 - - STARTF_USESTDHANDLES = 0x00000100 - INVALID_HANDLE_VALUE = -1 - HANDLE_FLAG_INHERIT = 0x00000001 - - DUPLICATE_SAME_ACCESS = 0x00000002 - CREATE_UNICODE_ENVIRONMENT = 0x00000400 - - JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 - JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800 - JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9 - JOB_OBJECT_BASIC_LIMIT_INFORMATION = 2 - - module Lib - enum :wait_status, [ - :wait_object_0, 0, - :wait_timeout, 0x102, - :wait_abandoned, 0x80, - :wait_failed, 0xFFFFFFFF - ] - - # - # BOOL WINAPI CreateProcess( - # __in_opt LPCTSTR lpApplicationName, - # __inout_opt LPTSTR lpCommandLine, - # __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, - # __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, - # __in BOOL bInheritHandles, - # __in DWORD dwCreationFlags, - # __in_opt LPVOID lpEnvironment, - # __in_opt LPCTSTR lpCurrentDirectory, - # __in LPSTARTUPINFO lpStartupInfo, - # __out LPPROCESS_INFORMATION lpProcessInformation - # ); - # - - attach_function :create_process, :CreateProcessW, [ - :pointer, - :buffer_inout, - :pointer, - :pointer, - :bool, - :ulong, - :pointer, - :pointer, - :pointer, - :pointer], :bool - - # - # DWORD WINAPI FormatMessage( - # __in DWORD dwFlags, - # __in_opt LPCVOID lpSource, - # __in DWORD dwMessageId, - # __in DWORD dwLanguageId, - # __out LPTSTR lpBuffer, - # __in DWORD nSize, - # __in_opt va_list *Arguments - # ); - # - - attach_function :format_message, :FormatMessageA, [ - :ulong, - :pointer, - :ulong, - :ulong, - :pointer, - :ulong, - :pointer], :ulong - - - attach_function :close_handle, :CloseHandle, [:pointer], :bool - - # - # HANDLE WINAPI OpenProcess( - # __in DWORD dwDesiredAccess, - # __in BOOL bInheritHandle, - # __in DWORD dwProcessId - # ); - # - - attach_function :open_process, :OpenProcess, [:ulong, :bool, :ulong], :pointer - - # - # HANDLE WINAPI CreateJobObject( - # _In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes, - # _In_opt_ LPCTSTR lpName - # ); - # - - attach_function :create_job_object, :CreateJobObjectA, [:pointer, :pointer], :pointer - - # - # BOOL WINAPI AssignProcessToJobObject( - # _In_ HANDLE hJob, - # _In_ HANDLE hProcess - # ); - - attach_function :assign_process_to_job_object, :AssignProcessToJobObject, [:pointer, :pointer], :bool - - # - # BOOL WINAPI SetInformationJobObject( - # _In_ HANDLE hJob, - # _In_ JOBOBJECTINFOCLASS JobObjectInfoClass, - # _In_ LPVOID lpJobObjectInfo, - # _In_ DWORD cbJobObjectInfoLength - # ); - # - - attach_function :set_information_job_object, :SetInformationJobObject, [:pointer, :int, :pointer, :ulong], :bool - - # - # - # DWORD WINAPI WaitForSingleObject( - # __in HANDLE hHandle, - # __in DWORD dwMilliseconds - # ); - # - - attach_function :wait_for_single_object, :WaitForSingleObject, [:pointer, :ulong], :wait_status, :blocking => true - - # - # BOOL WINAPI GetExitCodeProcess( - # __in HANDLE hProcess, - # __out LPDWORD lpExitCode - # ); - # - - attach_function :get_exit_code, :GetExitCodeProcess, [:pointer, :pointer], :bool - - # - # BOOL WINAPI GenerateConsoleCtrlEvent( - # __in DWORD dwCtrlEvent, - # __in DWORD dwProcessGroupId - # ); - # - - attach_function :generate_console_ctrl_event, :GenerateConsoleCtrlEvent, [:ulong, :ulong], :bool - - # - # BOOL WINAPI TerminateProcess( - # __in HANDLE hProcess, - # __in UINT uExitCode - # ); - # - - attach_function :terminate_process, :TerminateProcess, [:pointer, :uint], :bool - - # - # intptr_t _get_osfhandle( - # int fd - # ); - # - - attach_function :get_osfhandle, :_get_osfhandle, [:int], :intptr_t - - # - # int _open_osfhandle ( - # intptr_t osfhandle, - # int flags - # ); - # - - attach_function :open_osfhandle, :_open_osfhandle, [:pointer, :int], :int - - # BOOL WINAPI SetHandleInformation( - # __in HANDLE hObject, - # __in DWORD dwMask, - # __in DWORD dwFlags - # ); - - attach_function :set_handle_information, :SetHandleInformation, [:pointer, :ulong, :ulong], :bool - - # BOOL WINAPI GetHandleInformation( - # __in HANDLE hObject, - # __out LPDWORD lpdwFlags - # ); - - attach_function :get_handle_information, :GetHandleInformation, [:pointer, :pointer], :bool - - # BOOL WINAPI CreatePipe( - # __out PHANDLE hReadPipe, - # __out PHANDLE hWritePipe, - # __in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes, - # __in DWORD nSize - # ); - - attach_function :create_pipe, :CreatePipe, [:pointer, :pointer, :pointer, :ulong], :bool - - # - # HANDLE WINAPI GetCurrentProcess(void); - # - - attach_function :current_process, :GetCurrentProcess, [], :pointer - - # - # BOOL WINAPI DuplicateHandle( - # __in HANDLE hSourceProcessHandle, - # __in HANDLE hSourceHandle, - # __in HANDLE hTargetProcessHandle, - # __out LPHANDLE lpTargetHandle, - # __in DWORD dwDesiredAccess, - # __in BOOL bInheritHandle, - # __in DWORD dwOptions - # ); - # - - attach_function :_duplicate_handle, :DuplicateHandle, [ - :pointer, - :pointer, - :pointer, - :pointer, - :ulong, - :bool, - :ulong - ], :bool - - class << self - def kill(signal, *pids) - case signal - when 'SIGINT', 'INT', :SIGINT, :INT - signal = WIN_SIGINT - when 'SIGBRK', 'BRK', :SIGBREAK, :BRK - signal = WIN_SIGBREAK - when 'SIGKILL', 'KILL', :SIGKILL, :KILL - signal = WIN_SIGKILL - when 0..9 - # Do nothing - else - raise Error, "invalid signal #{signal.inspect}" - end - - pids.map { |pid| pid if Lib.send_signal(signal, pid) }.compact - end - - def waitpid(pid, flags = 0) - wait_for_pid(pid, no_hang?(flags)) - end - - def waitpid2(pid, flags = 0) - code = wait_for_pid(pid, no_hang?(flags)) - - [pid, code] if code - end - - def dont_inherit(file) - unless file.respond_to?(:fileno) - raise ArgumentError, "expected #{file.inspect} to respond to :fileno" - end - - set_handle_inheritance(handle_for(file.fileno), false) - end - - def last_error_message - errnum = FFI.errno - - buf = FFI::MemoryPointer.new :char, 512 - - size = format_message( - FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY, - nil, errnum, 0, buf, buf.size, nil - ) - - str = buf.read_string(size).strip - if errnum == 0 - "Unknown error (Windows says #{str.inspect}, but it did not.)" - else - "#{str} (#{errnum})" - end - end - - def each_child_of(pid, &blk) - raise NotImplementedError - - # http://stackoverflow.com/questions/1173342/terminate-a-process-tree-c-for-windows?rq=1 - - # for each process entry - # if pe.th32ParentProcessID == pid - # Handle.open(pe.pe.th32ProcessId, &blk) - # end - # - end - - def handle_for(fd_or_io) - if fd_or_io.kind_of?(IO) || fd_or_io.respond_to?(:fileno) - if ChildProcess.jruby? - handle = ChildProcess::JRuby.windows_handle_for(fd_or_io) - else - handle = get_osfhandle(fd_or_io.fileno) - end - elsif fd_or_io.kind_of?(Integer) - handle = get_osfhandle(fd_or_io) - elsif fd_or_io.respond_to?(:to_io) - io = fd_or_io.to_io - - unless io.kind_of?(IO) - raise TypeError, "expected #to_io to return an instance of IO" - end - - handle = get_osfhandle(io.fileno) - else - raise TypeError, "invalid type: #{fd_or_io.inspect}" - end - - if handle == INVALID_HANDLE_VALUE - raise Error, last_error_message - end - - FFI::Pointer.new handle - end - - def io_for(handle, flags = File::RDONLY) - fd = open_osfhandle(handle, flags) - if fd == -1 - raise Error, last_error_message - end - - FFI::IO.for_fd fd, flags - end - - def duplicate_handle(handle) - dup = FFI::MemoryPointer.new(:pointer) - proc = current_process - - ok = Lib._duplicate_handle( - proc, - handle, - proc, - dup, - 0, - false, - DUPLICATE_SAME_ACCESS - ) - - check_error ok - - dup.read_pointer - ensure - close_handle proc - end - - def set_handle_inheritance(handle, bool) - status = set_handle_information( - handle, - HANDLE_FLAG_INHERIT, - bool ? HANDLE_FLAG_INHERIT : 0 - ) - - check_error status - end - - def get_handle_inheritance(handle) - flags = FFI::MemoryPointer.new(:uint) - - status = get_handle_information( - handle, - flags - ) - - check_error status - - flags.read_uint - end - - def check_error(bool) - bool or raise Error, last_error_message - end - - def alive?(pid) - handle = Lib.open_process(PROCESS_ALL_ACCESS, false, pid) - if handle.null? - false - else - ptr = FFI::MemoryPointer.new :ulong - Lib.check_error Lib.get_exit_code(handle, ptr) - ptr.read_ulong == PROCESS_STILL_ACTIVE - end - end - - def no_hang?(flags) - (flags & Process::WNOHANG) == Process::WNOHANG - end - - def wait_for_pid(pid, no_hang) - code = Handle.open(pid) { |handle| - handle.wait unless no_hang - handle.exit_code - } - - code if code != PROCESS_STILL_ACTIVE - end - end - - end # Lib - end # Windows -end # ChildProcess diff --git a/lib/childprocess/windows/process.rb b/lib/childprocess/windows/process.rb old mode 100755 new mode 100644 index 11cfd20..0f6c623 --- a/lib/childprocess/windows/process.rb +++ b/lib/childprocess/windows/process.rb @@ -1,131 +1,28 @@ +require_relative '../process_spawn_process' + module ChildProcess module Windows - class Process < AbstractProcess - - attr_reader :pid - + class Process < ProcessSpawnProcess def io @io ||= Windows::IO.new end def stop(timeout = 3) assert_started + send_kill - log "sending KILL" - @handle.send(WIN_SIGKILL) - - poll_for_exit(timeout) - ensure - close_handle - close_job_if_necessary - end - - def wait - if exited? - exit_code - else - @handle.wait - @exit_code = @handle.exit_code - - close_handle - close_job_if_necessary - - @exit_code - end - end - - def exited? - return true if @exit_code - assert_started - - code = @handle.exit_code - exited = code != PROCESS_STILL_ACTIVE - - log(:exited? => exited, :code => code) - - if exited - @exit_code = code - close_handle - close_job_if_necessary - end - - exited - end - - private - - def launch_process - builder = ProcessBuilder.new(@args) - builder.leader = leader? - builder.detach = detach? - builder.duplex = duplex? - builder.environment = @environment unless @environment.empty? - builder.cwd = @cwd - - if @io - builder.stdout = @io.stdout - builder.stderr = @io.stderr - end - - @pid = builder.start - @handle = Handle.open @pid - - if leader? - @job = Job.new(detach?, true) - @job << @handle - end - - if duplex? - raise Error, "no stdin stream" unless builder.stdin - io._stdin = builder.stdin - end - - self - end - - def close_handle - @handle.close - end - - def close_job_if_necessary - @job.close if leader? - end - - - class Job - def initialize(detach, leader) - @pointer = Lib.create_job_object(nil, nil) - - if @pointer.nil? || @pointer.null? - raise Error, "unable to create job object" - end - - basic = JobObjectBasicLimitInformation.new - basic[:LimitFlags] |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE if !detach - basic[:LimitFlags] |= JOB_OBJECT_LIMIT_BREAKAWAY_OK if leader - - extended = JobObjectExtendedLimitInformation.new - extended[:BasicLimitInformation] = basic - - ret = Lib.set_information_job_object( - @pointer, - JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, - extended, - extended.size - ) - - Lib.check_error ret - end - - def <<(handle) - Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer) + begin + return poll_for_exit(timeout) + rescue TimeoutError + # try next end - def close - Lib.close_handle @pointer - end + wait + rescue Errno::ECHILD, Errno::ESRCH + # handle race condition where process dies between timeout + # and send_kill + true end - end # Process end # Windows end # ChildProcess diff --git a/lib/childprocess/windows/process_builder.rb b/lib/childprocess/windows/process_builder.rb deleted file mode 100644 index 1e849d7..0000000 --- a/lib/childprocess/windows/process_builder.rb +++ /dev/null @@ -1,178 +0,0 @@ -module ChildProcess - module Windows - class ProcessBuilder - attr_accessor :leader, :detach, :duplex, :environment, :stdout, :stderr, :cwd - attr_reader :stdin - - def initialize(args) - @args = args - - @detach = false - @duplex = false - @environment = nil - @cwd = nil - - @stdout = nil - @stderr = nil - @stdin = nil - - @flags = 0 - @job_ptr = nil - @cmd_ptr = nil - @env_ptr = nil - @cwd_ptr = nil - end - - def start - create_command_pointer - create_environment_pointer - create_cwd_pointer - - setup_flags - setup_io - - pid = create_process - close_handles - - pid - end - - private - - def to_wide_string(str) - newstr = str + "\0".encode(str.encoding) - newstr.encode!('UTF-16LE') - end - - def create_command_pointer - string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join(' ') - @cmd_ptr = to_wide_string(string) - end - - def create_environment_pointer - return unless @environment.kind_of?(Hash) && @environment.any? - - strings = [] - - ENV.to_hash.merge(@environment).each do |key, val| - next if val.nil? - - if key.to_s =~ /=|\0/ || val.to_s.include?("\0") - raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}" - end - - strings << "#{key}=#{val}\0" - end - - env_str = to_wide_string(strings.join) - @env_ptr = FFI::MemoryPointer.from_string(env_str) - end - - def create_cwd_pointer - @cwd_ptr = FFI::MemoryPointer.from_string(to_wide_string(@cwd || Dir.pwd)) - end - - def create_process - ok = Lib.create_process( - nil, # application name - @cmd_ptr, # command line - nil, # process attributes - nil, # thread attributes - true, # inherit handles - @flags, # creation flags - @env_ptr, # environment - @cwd_ptr, # current directory - startup_info, # startup info - process_info # process info - ) - - ok or raise LaunchError, Lib.last_error_message - - process_info[:dwProcessId] - end - - def startup_info - @startup_info ||= StartupInfo.new - end - - def process_info - @process_info ||= ProcessInfo.new - end - - def setup_flags - @flags |= CREATE_UNICODE_ENVIRONMENT - @flags |= DETACHED_PROCESS if @detach - @flags |= CREATE_BREAKAWAY_FROM_JOB if @leader - end - - def setup_io - startup_info[:dwFlags] ||= 0 - startup_info[:dwFlags] |= STARTF_USESTDHANDLES - - if @stdout - startup_info[:hStdOutput] = std_stream_handle_for(@stdout) - end - - if @stderr - startup_info[:hStdError] = std_stream_handle_for(@stderr) - end - - if @duplex - read_pipe_ptr = FFI::MemoryPointer.new(:pointer) - write_pipe_ptr = FFI::MemoryPointer.new(:pointer) - sa = SecurityAttributes.new(:inherit => true) - - ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0) - Lib.check_error ok - - @read_pipe = read_pipe_ptr.read_pointer - @write_pipe = write_pipe_ptr.read_pointer - - Lib.set_handle_inheritance @read_pipe, true - Lib.set_handle_inheritance @write_pipe, false - - startup_info[:hStdInput] = @read_pipe - else - startup_info[:hStdInput] = std_stream_handle_for(STDIN) - end - end - - def std_stream_handle_for(io) - handle = Lib.handle_for(io) - - begin - Lib.set_handle_inheritance handle, true - rescue ChildProcess::Error - # If the IO was set to close on exec previously, this call will fail. - # That's probably OK, since the user explicitly asked for it to be - # closed (at least I have yet to find other cases where this will - # happen...) - end - - handle - end - - def close_handles - Lib.close_handle process_info[:hProcess] - Lib.close_handle process_info[:hThread] - - if @duplex - @stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY) - Lib.close_handle @read_pipe - Lib.close_handle @write_pipe - end - end - - def quote_if_necessary(str) - quote = str.start_with?('"') ? "'" : '"' - - case str - when /[\s\\'"]/ - [quote, str, quote].join - else - str - end - end - end # ProcessBuilder - end # Windows -end # ChildProcess diff --git a/lib/childprocess/windows/structs.rb b/lib/childprocess/windows/structs.rb deleted file mode 100644 index 92253c3..0000000 --- a/lib/childprocess/windows/structs.rb +++ /dev/null @@ -1,149 +0,0 @@ -module ChildProcess - module Windows - # typedef struct _STARTUPINFO { - # DWORD cb; - # LPTSTR lpReserved; - # LPTSTR lpDesktop; - # LPTSTR lpTitle; - # DWORD dwX; - # DWORD dwY; - # DWORD dwXSize; - # DWORD dwYSize; - # DWORD dwXCountChars; - # DWORD dwYCountChars; - # DWORD dwFillAttribute; - # DWORD dwFlags; - # WORD wShowWindow; - # WORD cbReserved2; - # LPBYTE lpReserved2; - # HANDLE hStdInput; - # HANDLE hStdOutput; - # HANDLE hStdError; - # } STARTUPINFO, *LPSTARTUPINFO; - - class StartupInfo < FFI::Struct - layout :cb, :ulong, - :lpReserved, :pointer, - :lpDesktop, :pointer, - :lpTitle, :pointer, - :dwX, :ulong, - :dwY, :ulong, - :dwXSize, :ulong, - :dwYSize, :ulong, - :dwXCountChars, :ulong, - :dwYCountChars, :ulong, - :dwFillAttribute, :ulong, - :dwFlags, :ulong, - :wShowWindow, :ushort, - :cbReserved2, :ushort, - :lpReserved2, :pointer, - :hStdInput, :pointer, # void ptr - :hStdOutput, :pointer, # void ptr - :hStdError, :pointer # void ptr - end - - # - # typedef struct _PROCESS_INFORMATION { - # HANDLE hProcess; - # HANDLE hThread; - # DWORD dwProcessId; - # DWORD dwThreadId; - # } PROCESS_INFORMATION, *LPPROCESS_INFORMATION; - # - - class ProcessInfo < FFI::Struct - layout :hProcess, :pointer, # void ptr - :hThread, :pointer, # void ptr - :dwProcessId, :ulong, - :dwThreadId, :ulong - end - - # - # typedef struct _SECURITY_ATTRIBUTES { - # DWORD nLength; - # LPVOID lpSecurityDescriptor; - # BOOL bInheritHandle; - # } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; - # - - class SecurityAttributes < FFI::Struct - layout :nLength, :ulong, - :lpSecurityDescriptor, :pointer, # void ptr - :bInheritHandle, :int - - def initialize(opts = {}) - super() - - self[:nLength] = self.class.size - self[:lpSecurityDescriptor] = nil - self[:bInheritHandle] = opts[:inherit] ? 1 : 0 - end - end - - # - # typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION { - # LARGE_INTEGER PerProcessUserTimeLimit; - # LARGE_INTEGER PerJobUserTimeLimit; - # DWORD LimitFlags; - # SIZE_T MinimumWorkingSetSize; - # SIZE_T MaximumWorkingSetSize; - # DWORD ActiveProcessLimit; - # ULONG_PTR Affinity; - # DWORD PriorityClass; - # DWORD SchedulingClass; - # } JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION; - # - class JobObjectBasicLimitInformation < FFI::Struct - layout :PerProcessUserTimeLimit, :int64, - :PerJobUserTimeLimit, :int64, - :LimitFlags, :ulong, - :MinimumWorkingSetSize, :size_t, - :MaximumWorkingSetSize, :size_t, - :ActiveProcessLimit, :ulong, - :Affinity, :pointer, - :PriorityClass, :ulong, - :SchedulingClass, :ulong - end - - # - # typedef struct _IO_COUNTERS { - # ULONGLONG ReadOperationCount; - # ULONGLONG WriteOperationCount; - # ULONGLONG OtherOperationCount; - # ULONGLONG ReadTransferCount; - # ULONGLONG WriteTransferCount; - # ULONGLONG OtherTransferCount; - # } IO_COUNTERS, *PIO_COUNTERS; - # - - class IoCounters < FFI::Struct - layout :ReadOperationCount, :ulong_long, - :WriteOperationCount, :ulong_long, - :OtherOperationCount, :ulong_long, - :ReadTransferCount, :ulong_long, - :WriteTransferCount, :ulong_long, - :OtherTransferCount, :ulong_long - end - # - # typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION { - # JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; - # IO_COUNTERS IoInfo; - # SIZE_T ProcessMemoryLimit; - # SIZE_T JobMemoryLimit; - # SIZE_T PeakProcessMemoryUsed; - # SIZE_T PeakJobMemoryUsed; - # } JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION; - # - - class JobObjectExtendedLimitInformation < FFI::Struct - layout :BasicLimitInformation, JobObjectBasicLimitInformation, - :IoInfo, IoCounters, - :ProcessMemoryLimit, :size_t, - :JobMemoryLimit, :size_t, - :PeakProcessMemoryUsed, :size_t, - :PeakJobMemoryUsed, :size_t - end - - - end # Windows -end # ChildProcess \ No newline at end of file diff --git a/spec/childprocess_spec.rb b/spec/childprocess_spec.rb index 48200e0..ab5e938 100644 --- a/spec/childprocess_spec.rb +++ b/spec/childprocess_spec.rb @@ -99,7 +99,7 @@ end end - it "can override env vars only for the current process" do + it "can override env vars only for the child process" do Tempfile.open("env-spec") do |file| file.close process = write_env(file.path) @@ -131,6 +131,17 @@ end end + it "can set env vars using Symbol keys and values" do + Tempfile.open("env-spec") do |file| + process = ruby('puts ENV["SYMBOL_KEY"]') + process.environment[:SYMBOL_KEY] = :VALUE + process.io.stdout = file + process.start + process.wait + expect(rewind_and_read(file)).to eq "VALUE\n" + end + end + it "inherits the parent's env vars also when some are overridden" do Tempfile.open("env-spec") do |file| file.close @@ -281,18 +292,42 @@ } end - it 'kills the full process tree', :process_builder => false do - Tempfile.open('kill-process-tree') do |file| - process = write_pid_in_sleepy_grand_child(file.path) - process.leader = true - process.start + if ChildProcess.unix? + it 'kills the full process tree on unix' do + Tempfile.open('kill-process-tree') do |file| + process = write_pid_in_sleepy_grand_child(file.path) + process.leader = true + process.start - pid = wait_until(30) do - Integer(rewind_and_read(file)) rescue nil + pid = wait_until(30) do + Integer(rewind_and_read(file)) rescue nil + end + + process.stop + expect(alive?(process.pid)).to eql(false) + + wait_until(3) { expect(alive?(pid)).to eql(false) } end + end + elsif ChildProcess.windows? + it 'does not kill the full process tree on windows' do + Tempfile.open('no-kill-process-tree') do |file| + process = write_pid_in_sleepy_grand_child(file.path) + process.leader = true + process.start - process.stop - wait_until(3) { expect(alive?(pid)).to eql(false) } + pid = wait_until(30) do + Integer(rewind_and_read(file)) rescue nil + end + + process.stop + expect(alive?(process.pid)).to eql(false) + + # The grand child is not killed on Windows: + expect(alive?(pid)).to eql(true) + Process.kill(:SGIKILL, pid) + wait_until(3) { expect(alive?(pid)).to eql(false) } + end end end diff --git a/spec/io_spec.rb b/spec/io_spec.rb index cd3777d..0c16ee9 100644 --- a/spec/io_spec.rb +++ b/spec/io_spec.rb @@ -151,7 +151,7 @@ # http://travis-ci.org/#!/enkessler/childprocess/jobs/487331 # - it "works with pipes", :process_builder => false do + it "works with pipes" do process = ruby(<<-CODE) STDOUT.print "stdout" STDERR.print "stderr" diff --git a/spec/jruby_spec.rb b/spec/jruby_spec.rb deleted file mode 100644 index 02ea936..0000000 --- a/spec/jruby_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require File.expand_path('../spec_helper', __FILE__) -require "pid_behavior" - -if ChildProcess.jruby? && !ChildProcess.windows? - describe ChildProcess::JRuby::IO do - let(:io) { ChildProcess::JRuby::IO.new } - - it "raises an ArgumentError if given IO does not respond to :to_outputstream" do - expect { io.stdout = nil }.to raise_error(ArgumentError) - end - end - - describe ChildProcess::JRuby::Process do - if ChildProcess.unix? - it_behaves_like "a platform that provides the child's pid" - else - it "raises an error when trying to access the child's pid" do - process = exit_with(0) - process.start - expect { process.pid }.to raise_error(NotImplementedError) - end - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 127c456..103c318 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -210,16 +210,9 @@ def rewind_and_read(io) end def alive?(pid) - if ChildProcess.windows? - ChildProcess::Windows::Lib.alive?(pid) - else - begin - Process.getpgid pid - true - rescue Errno::ESRCH - false - end - end + !!Process.kill(0, pid) + rescue Errno::ESRCH + false end def capture_std @@ -260,10 +253,6 @@ def generate_log_messages defined?(@process) && @process.alive? && @process.stop } - if ChildProcess.jruby? && ChildProcess.new("true").instance_of?(ChildProcess::JRuby::Process) - c.filter_run_excluding :process_builder => false - end - if ChildProcess.linux? && ChildProcess.posix_spawn? c.filter_run_excluding :posix_spawn_on_linux => false end diff --git a/spec/unix_spec.rb b/spec/unix_spec.rb index b80e4c5..e94301a 100644 --- a/spec/unix_spec.rb +++ b/spec/unix_spec.rb @@ -1,7 +1,7 @@ require File.expand_path('../spec_helper', __FILE__) require "pid_behavior" -if ChildProcess.unix? && !ChildProcess.jruby? && !ChildProcess.posix_spawn? +if ChildProcess.unix? describe ChildProcess::Unix::Process do it_behaves_like "a platform that provides the child's pid"