From 01febcab3e6258051e2fc083b906d9ac2bdc3927 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Wed, 29 Sep 2021 13:21:31 -0500 Subject: [PATCH] [ruby/open3] Add JRuby's Windows (JDK non-native) Open3 support This adds JRuby's logic used on platforms where we do not have native access to posix_spawn and related posix functions needed to do fully-native subprocess launching and management. The code here instead uses the JDK ProcessBuilder logic to simulate most of the Open3 functionality. This code does not pass all tests, currently, but provides most of the key functionality on pure-Java (i.e. no native FFI) platforms. https://github.com/ruby/open3/commit/689da19c42 --- lib/open3.gemspec | 6 +- lib/open3.rb | 9 ++- lib/open3/jruby_windows.rb | 118 +++++++++++++++++++++++++++++++++++++ lib/open3/version.rb | 3 + 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 lib/open3/jruby_windows.rb create mode 100644 lib/open3/version.rb diff --git a/lib/open3.gemspec b/lib/open3.gemspec index ad9485adc7..89c2cbe564 100644 --- a/lib/open3.gemspec +++ b/lib/open3.gemspec @@ -1,10 +1,8 @@ # frozen_string_literal: true name = File.basename(__FILE__, ".gemspec") -version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir| - break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| - /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 - end rescue nil +version = File.foreach(File.join(__dir__, "lib/open3/version.rb")) do |line| + /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 end Gem::Specification.new do |spec| diff --git a/lib/open3.rb b/lib/open3.rb index c574696bb1..2f035e3bcd 100644 --- a/lib/open3.rb +++ b/lib/open3.rb @@ -29,9 +29,14 @@ # - Open3.pipeline : run a pipeline and wait for its completion # -module Open3 - VERSION = "0.1.1" +require 'open3/version' +if RUBY_ENGINE == 'jruby' && JRuby::Util::ON_WINDOWS + require_relative 'open3/jruby_windows' + return +end + +module Open3 # Open stdin, stdout, and stderr streams and start external executable. # In addition, a thread to wait for the started process is created. # The thread has a pid method and a thread variable :pid which is the pid of diff --git a/lib/open3/jruby_windows.rb b/lib/open3/jruby_windows.rb new file mode 100644 index 0000000000..24b9a1ba7e --- /dev/null +++ b/lib/open3/jruby_windows.rb @@ -0,0 +1,118 @@ +# +# Custom implementation of Open3.popen{3,2,2e} that uses java.lang.ProcessBuilder rather than pipes and spawns. +# + +require 'jruby' # need access to runtime for RubyStatus construction + +module Open3 + + java_import java.lang.ProcessBuilder + java_import org.jruby.RubyProcess + java_import org.jruby.util.ShellLauncher + + def popen3(*cmd, &block) + if cmd.size > 0 && Hash === cmd[-1] + opts = cmd.pop + else + opts = {} + end + processbuilder_run(cmd, opts, io: IO_3, &block) + end + module_function :popen3 + + IO_3 = proc do |process| + [process.getOutputStream.to_io, process.getInputStream.to_io, process.getErrorStream.to_io] + end + + BUILD_2 = proc do |builder| + builder.redirectError(ProcessBuilder::Redirect::INHERIT) + end + + IO_2 = proc do |process| + [process.getOutputStream.to_io, process.getInputStream.to_io] + end + + def popen2(*cmd, &block) + if cmd.size > 0 && Hash === cmd[-1] + opts = cmd.pop + else + opts = {} + end + processbuilder_run(cmd, opts, build: BUILD_2, io: IO_2, &block) + end + module_function :popen2 + + BUILD_2E = proc do |builder| + builder.redirectErrorStream(true) + end + + def popen2e(*cmd, &block) + if cmd.size > 0 && Hash === cmd[-1] + opts = cmd.pop + else + opts = {} + end + processbuilder_run(cmd, opts, build: BUILD_2E, io: IO_2, &block) + end + module_function :popen2e + + def processbuilder_run(cmd, opts, build: nil, io:) + if Hash === cmd[0] + env = cmd.shift; + else + env = {} + end + + if cmd.size == 1 && (cmd[0] =~ / / || ShellLauncher.shouldUseShell(cmd[0])) + cmd = [RbConfig::CONFIG['SHELL'], JRuby::Util::ON_WINDOWS ? '/c' : '-c', cmd[0]] + end + + builder = ProcessBuilder.new(cmd.to_java(:string)) + + builder.directory(java.io.File.new(opts[:chdir] || Dir.pwd)) + + environment = builder.environment + env.each { |k, v| v.nil? ? environment.remove(k) : environment.put(k, v) } + + build.call(builder) if build + + process = builder.start + + pid = org.jruby.util.ShellLauncher.getPidFromProcess(process) + + parent_io = io.call(process) + + parent_io.each {|i| i.sync = true} + + wait_thr = DetachThread.new(pid) { RubyProcess::RubyStatus.newProcessStatus(JRuby.runtime, process.waitFor << 8, pid) } + + result = [*parent_io, wait_thr] + + if defined? yield + begin + return yield(*result) + ensure + parent_io.each(&:close) + wait_thr.join + end + end + + result + end + module_function :processbuilder_run + class << self + private :processbuilder_run + end + + class DetachThread < Thread + attr_reader :pid + + def initialize(pid) + super + + @pid = pid + self[:pid] = pid + end + end + +end diff --git a/lib/open3/version.rb b/lib/open3/version.rb new file mode 100644 index 0000000000..5a8e84b4ae --- /dev/null +++ b/lib/open3/version.rb @@ -0,0 +1,3 @@ +module Open3 + VERSION = "0.1.1" +end