pax_global_header00006660000000000000000000000064135423231210014506gustar00rootroot0000000000000052 comment=fb2a85275db906bffb6da2d6f55b3b6f4365d78b ruby-childprocess-3.0.0/000077500000000000000000000000001354232312100151475ustar00rootroot00000000000000ruby-childprocess-3.0.0/.document000066400000000000000000000000761354232312100167710ustar00rootroot00000000000000README.rdoc lib/**/*.rb bin/* features/**/*.feature - LICENSE ruby-childprocess-3.0.0/.gitignore000066400000000000000000000003171354232312100171400ustar00rootroot00000000000000## MAC OS .DS_Store ## TEXTMATE *.tmproj tmtags ## EMACS *~ \#* .\#* ## VIM *.swp ## RubyMine .idea/* ## PROJECT::GENERAL coverage rdoc pkg .rbx Gemfile.lock .ruby-version .bundle ## PROJECT::SPECIFIC ruby-childprocess-3.0.0/.rspec000066400000000000000000000000101354232312100162530ustar00rootroot00000000000000--color ruby-childprocess-3.0.0/.travis.yml000066400000000000000000000016021354232312100172570ustar00rootroot00000000000000os: - linux - osx rvm: - rbx-3 - 2.3 - 2.4 - 2.5 - 2.6 - ruby-head sudo: false cache: bundler before_install: - "echo 'gem: --no-document' > ~/.gemrc" # RubyGems update is supported for Ruby 2.3 and later - ruby -e "system('gem update --system') if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.3')" - gem install bundler --version '~> 1.17' before_script: - 'export JAVA_OPTS="${JAVA_OPTS_FOR_SPECS}"' env: global: matrix: - CHILDPROCESS_POSIX_SPAWN=true CHILDPROCESS_UNSET=should-be-unset - CHILDPROCESS_POSIX_SPAWN=false CHILDPROCESS_UNSET=should-be-unset matrix: allow_failures: - rvm: rbx-3 - rvm: ruby-head - env: "CHILDPROCESS_POSIX_SPAWN=true" include: - rvm: jruby-9.2.5.0 jdk: openjdk11 env: "JAVA_OPTS_FOR_SPECS='--add-opens java.base/java.io=org.jruby.dist --add-opens java.base/sun.nio.ch=org.jruby.dist'" ruby-childprocess-3.0.0/CHANGELOG.md000066400000000000000000000047711354232312100167710ustar00rootroot00000000000000### Version 3.0.0 / 2019-09-20 * [#156](https://github.com/enkessler/childprocess/pull/156)Remove unused `rubyforge_project` from gemspec * [#160](https://github.com/enkessler/childprocess/pull/160): Remove extension to conditionally install `ffi` gem on Windows platforms * [#160](https://github.com/enkessler/childprocess/pull/160): Remove runtime dependency on `rake` gem ### Version 2.0.0 / 2019-07-11 * [#148](https://github.com/enkessler/childprocess/pull/148): Drop support for Ruby 2.0, 2.1, and 2.2 * [#149](https://github.com/enkessler/childprocess/pull/149): Fix Unix fork reopen to be compatible with Ruby 2.6 * [#152](https://github.com/enkessler/childprocess/pull/152)/[#154](https://github.com/enkessler/childprocess/pull/154): Fix hangs and permission errors introduced in Ruby 2.6 for leader processes of process groups ### Version 1.0.1 / 2019-02-03 * [#143](https://github.com/enkessler/childprocess/pull/144): Fix installs by adding `rake` gem as runtime dependency * [#147](https://github.com/enkessler/childprocess/pull/147): Relax `rake` gem constraint from `< 12` to `< 13` ### Version 1.0.0 / 2019-01-28 * [#134](https://github.com/enkessler/childprocess/pull/134): Add support for non-ASCII characters on Windows * [#132](https://github.com/enkessler/childprocess/pull/132): Install `ffi` gem requirement on Windows only * [#128](https://github.com/enkessler/childprocess/issues/128): Convert environment variable values to strings when `posix_spawn` enabled * [#141](https://github.com/enkessler/childprocess/pull/141): Support JRuby on Java >= 9 ### Version 0.9.0 / 2018-03-10 * Added support for DragonFly BSD. ### Version 0.8.0 / 2017-09-23 * Added a method for determining whether or not a process had been started. ### Version 0.7.1 / 2017-06-26 * Fixed a noisy uninitialized variable warning ### Version 0.7.0 / 2017-05-07 * Debugging information now uses a Logger, which can be configured. ### Version 0.6.3 / 2017-03-24 See beta release notes. ### Version 0.6.3.beta.1 / 2017-03-10 * Bug fix: Fixed child process creation problems on Windows 7 when a child was declared as a leader. ### Version 0.6.2 / 2017-02-25 * Bug fix: Fixed a potentially broken edge case that could occur on older 32-bit OSX systems. ### Version 0.6.1 / 2017-01-22 * Bug fix: Fixed a dependency that was accidentally declared as a runtime dependency instead of a development dependency. ### Version 0.6.0 / 2017-01-22 * Support for Ruby 2.4 added ### Version 0.5.9 / 2016-01-06 * The Great Before Times... ruby-childprocess-3.0.0/Gemfile000066400000000000000000000015651354232312100164510ustar00rootroot00000000000000source 'http://rubygems.org' # Specify your gem's dependencies in child_process.gemspec gemspec # Used for local development/testing only gem 'rake' if RUBY_VERSION =~ /^1\./ gem 'tins', '< 1.7' # The 'tins' gem requires Ruby 2.x on/after this version gem 'json', '< 2.0' # The 'json' gem drops pre-Ruby 2.x support on/after this version gem 'term-ansicolor', '< 1.4' # The 'term-ansicolor' gem requires Ruby 2.x on/after this version # ffi gem for Windows requires Ruby 2.x on/after this version gem 'ffi', '< 1.9.15' if ENV['CHILDPROCESS_POSIX_SPAWN'] == 'true' || Gem.win_platform? elsif Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2') # Ruby 2.0/2.1 support only ffi before 1.10 gem 'ffi', '~> 1.9.0' if ENV['CHILDPROCESS_POSIX_SPAWN'] == 'true' || Gem.win_platform? else gem 'ffi' if ENV['CHILDPROCESS_POSIX_SPAWN'] == 'true' || Gem.win_platform? end ruby-childprocess-3.0.0/LICENSE000066400000000000000000000020441354232312100161540ustar00rootroot00000000000000Copyright (c) 2010-2015 Jari Bakken Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished 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 NONINFRINGEMENT. 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. ruby-childprocess-3.0.0/README.md000066400000000000000000000152501354232312100164310ustar00rootroot00000000000000# childprocess This gem aims at being a simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination. The code originated in the [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver) gem, but should prove useful as a standalone library. [![Build Status](https://secure.travis-ci.org/enkessler/childprocess.svg)](http://travis-ci.org/enkessler/childprocess) [![Build status](https://ci.appveyor.com/api/projects/status/fn2snbcd7kku5myk/branch/dev?svg=true)](https://ci.appveyor.com/project/enkessler/childprocess/branch/dev) [![Gem Version](https://badge.fury.io/rb/childprocess.svg)](http://badge.fury.io/rb/childprocess) [![Code Climate](https://codeclimate.com/github/enkessler/childprocess.svg)](https://codeclimate.com/github/enkessler/childprocess) [![Coverage Status](https://coveralls.io/repos/enkessler/childprocess/badge.svg?branch=master)](https://coveralls.io/r/enkessler/childprocess?branch=master) # Requirements * Ruby 2.3+, JRuby 9+ Windows users **must** ensure the `ffi` gem (`>= 1.0.11`) is installed in order to use ChildProcess. # Usage The object returned from `ChildProcess.build` will implement `ChildProcess::AbstractProcess`. ### Basic examples ```ruby process = ChildProcess.build("ruby", "-e", "sleep") # inherit stdout/stderr from parent... process.io.inherit! # ...or pass an IO process.io.stdout = Tempfile.new("child-output") # modify the environment for the child process.environment["a"] = "b" process.environment["c"] = nil # set the child's working directory process.cwd = '/some/path' # start the process process.start # check process status process.alive? #=> true process.exited? #=> false # wait indefinitely for process to exit... process.wait process.exited? #=> true # get the exit code process.exit_code #=> 0 # ...or poll for exit + force quit begin process.poll_for_exit(10) rescue ChildProcess::TimeoutError process.stop # tries increasingly harsher methods to kill the process. end ``` ### Advanced examples #### Output to pipe ```ruby r, w = IO.pipe proc = ChildProcess.build("echo", "foo") proc.io.stdout = proc.io.stderr = w proc.start Thread.new { begin loop do print r.readpartial(8192) end rescue EOFError end } proc.wait w.close ``` Note that if you just want to get the output of a command, the backtick method on Kernel may be a better fit. #### Write to stdin ```ruby process = ChildProcess.build("cat") out = Tempfile.new("duplex") out.sync = true process.io.stdout = process.io.stderr = out process.duplex = true # sets up pipe so process.io.stdin will be available after .start process.start process.io.stdin.puts "hello world" process.io.stdin.close process.poll_for_exit(exit_timeout_in_seconds) out.rewind out.read #=> "hello world\n" ``` #### Pipe output to another ChildProcess ```ruby search = ChildProcess.build("grep", '-E', %w(redis memcached).join('|')) search.duplex = true # sets up pipe so search.io.stdin will be available after .start search.io.stdout = $stdout search.start listing = ChildProcess.build("ps", "aux") listing.io.stdout = search.io.stdin listing.start listing.wait search.io.stdin.close search.wait ``` #### Prefer posix_spawn on *nix If the parent process is using a lot of memory, `fork+exec` can be very expensive. The `posix_spawn()` API removes this overhead. ```ruby ChildProcess.posix_spawn = true process = ChildProcess.build(*args) ``` To be able to use this, please make sure that you have the `ffi` gem installed. ### Ensure entire process tree dies By default, the child process does not create a new process group. This means there's no guarantee that the entire process tree will die when the child process is killed. To solve this: ```ruby process = ChildProcess.build(*args) process.leader = true process.start ``` #### Detach from parent ```ruby process = ChildProcess.build("sleep", "10") process.detach = true process.start ``` #### Invoking a shell As opposed to `Kernel#system`, `Kernel#exec` et al., ChildProcess will not automatically execute your command in a shell (like `/bin/sh` or `cmd.exe`) depending on the arguments. This means that if you try to execute e.g. gem executables (like `bundle` or `gem`) or Windows executables (with `.com` or `.bat` extensions) you may see a `ChildProcess::LaunchError`. You can work around this by being explicit about what interpreter to invoke: ```ruby ChildProcess.build("cmd.exe", "/c", "bundle") ChildProcess.build("ruby", "-S", "bundle") ``` #### Log to file Errors and debugging information are logged to `$stderr` by default but a custom logger can be used instead. ```ruby logger = Logger.new('logfile.log') logger.level = Logger::DEBUG ChildProcess.logger = logger ``` ## Caveats * With JRuby on Unix, modifying `ENV["PATH"]` before using childprocess could lead to 'Command not found' errors, since JRuby is unable to modify the environment used for PATH searches in `java.lang.ProcessBuilder`. This can be avoided by setting `ChildProcess.posix_spawn = true`. * With JRuby on Java >= 9, the JVM may need to be configured to allow JRuby to access neccessary implementations; this can be done by adding `--add-opens java.base/java.io=org.jruby.dist` and `--add-opens java.base/sun.nio.ch=org.jruby.dist` to the `JAVA_OPTS` environment variable that is used by JRuby when launching the JVM. # Implementation How the process is launched and killed depends on the platform: * Unix : `fork + exec` (or `posix_spawn` if enabled) * Windows : `CreateProcess()` and friends * JRuby : `java.lang.{Process,ProcessBuilder}` # Note on Patches/Pull Requests 1. Fork it 2. Create your feature branch (off of the development branch) `git checkout -b my-new-feature dev` 3. Commit your changes `git commit -am 'Add some feature'` 4. Push to the branch `git push origin my-new-feature` 5. Create new Pull Request # Publishing a New Release When publishing a new gem release: 1. Ensure [latest build is green on the `dev` branch](https://travis-ci.org/enkessler/childprocess/branches) 2. Ensure [CHANGELOG](CHANGELOG.md) is updated 3. Ensure [version is bumped](lib/childprocess/version.rb) following [Semantic Versioning](https://semver.org/) 4. Merge the `dev` branch into `master`: `git checkout master && git merge dev` 5. Ensure [latest build is green on the `master` branch](https://travis-ci.org/enkessler/childprocess/branches) 6. Build gem from the green `master` branch: `git checkout master && gem build childprocess.gemspec` 7. Push gem to RubyGems: `gem push childprocess-.gem` 8. Tag commit with version, annotated with release notes: `git tag -a ` # Copyright Copyright (c) 2010-2015 Jari Bakken. See [LICENSE](LICENSE) for details. ruby-childprocess-3.0.0/Rakefile000066400000000000000000000030721354232312100166160ustar00rootroot00000000000000require 'rubygems' require 'rake' require 'tmpdir' require 'bundler' Bundler::GemHelper.install_tasks include Rake::DSL if defined?(::Rake::DSL) require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |spec| spec.ruby_opts = "-I lib:spec -w" spec.pattern = 'spec/**/*_spec.rb' end desc 'Run specs for rcov' RSpec::Core::RakeTask.new(:rcov) do |spec| spec.ruby_opts = "-I lib:spec" spec.pattern = 'spec/**/*_spec.rb' spec.rcov = true spec.rcov_opts = %w[--exclude spec,ruby-debug,/Library/Ruby,.gem --include lib/childprocess] end task :default => :spec begin require 'yard' YARD::Rake::YardocTask.new rescue LoadError task :yardoc do abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard" end end task :clean do rm_rf "pkg" rm_rf "childprocess.jar" end desc 'Create jar to bundle in selenium-webdriver' task :jar => [:clean, :build] do tmpdir = Dir.mktmpdir("childprocess-jar") gem_to_package = Dir['pkg/*.gem'].first gem_name = File.basename(gem_to_package, ".gem") p :gem_to_package => gem_to_package, :gem_name => gem_name sh "gem install -i #{tmpdir} #{gem_to_package} --ignore-dependencies --no-rdoc --no-ri" sh "jar cf childprocess.jar -C #{tmpdir}/gems/#{gem_name}/lib ." sh "jar tf childprocess.jar" end task :env do $:.unshift File.expand_path("../lib", __FILE__) require 'childprocess' end desc 'Calculate size of posix_spawn structs for the current platform' task :generate => :env do require 'childprocess/tools/generator' ChildProcess::Tools::Generator.generate end ruby-childprocess-3.0.0/appveyor.yml000066400000000000000000000020601354232312100175350ustar00rootroot00000000000000version: '1.0.{build}' environment: matrix: - CHILDPROCESS_POSIX_SPAWN: true CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 23-x64 - CHILDPROCESS_POSIX_SPAWN: false CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 23-x64 - CHILDPROCESS_POSIX_SPAWN: true CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 24-x64 - CHILDPROCESS_POSIX_SPAWN: false CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 24-x64 - CHILDPROCESS_POSIX_SPAWN: true CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 25-x64 - CHILDPROCESS_POSIX_SPAWN: false CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 25-x64 - CHILDPROCESS_POSIX_SPAWN: true CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 26-x64 - CHILDPROCESS_POSIX_SPAWN: false CHILDPROCESS_UNSET: should-be-unset RUBY_VERSION: 26-x64 install: - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH% - bundle install build: off before_test: - ruby -v - gem -v - bundle -v test_script: - bundle exec rake ruby-childprocess-3.0.0/childprocess.gemspec000066400000000000000000000021161354232312100211760ustar00rootroot00000000000000# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "childprocess/version" Gem::Specification.new do |s| s.name = "childprocess" s.version = ChildProcess::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Jari Bakken", "Eric Kessler", "Shane da Silva"] s.email = ["morrow748@gmail.com", "shane@dasilva.io"] s.homepage = "http://github.com/enkessler/childprocess" s.summary = %q{A simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination.} s.description = %q{This gem aims at being a simple and reliable solution for controlling external programs running in the background on any Ruby / OS combination.} s.license = 'MIT' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- spec/*`.split("\n") s.require_paths = ["lib"] s.required_ruby_version = '>= 2.3.0' s.add_development_dependency "rspec", "~> 3.0" s.add_development_dependency "yard", "~> 0.0" s.add_development_dependency 'coveralls', '< 1.0' end ruby-childprocess-3.0.0/lib/000077500000000000000000000000001354232312100157155ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess.rb000066400000000000000000000115411354232312100207260ustar00rootroot00000000000000require 'childprocess/version' require 'childprocess/errors' require 'childprocess/abstract_process' require 'childprocess/abstract_io' require "fcntl" require 'logger' module ChildProcess @posix_spawn = false class << self attr_writer :logger def new(*args) case os when :macosx, :linux, :solaris, :bsd, :cygwin, :aix if posix_spawn? Unix::PosixSpawnProcess.new(args) elsif jruby? JRuby::Process.new(args) else Unix::ForkExecProcess.new(args) end when :windows Windows::Process.new(args) else raise Error, "unsupported platform #{platform_name.inspect}" end end alias_method :build, :new def logger return @logger if defined?(@logger) and @logger @logger = Logger.new($stderr) @logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO @logger end def platform if RUBY_PLATFORM == "java" :jruby elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == "ironruby" :ironruby else os end end def platform_name @platform_name ||= "#{arch}-#{os}" end def unix? !windows? end def linux? os == :linux end def jruby? platform == :jruby end def windows? os == :windows end def posix_spawn? enabled = @posix_spawn || %w[1 true].include?(ENV['CHILDPROCESS_POSIX_SPAWN']) 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 # # Set this to true to enable experimental use of posix_spawn. # def posix_spawn=(bool) @posix_spawn = bool end def os @os ||= ( require "rbconfig" host_os = RbConfig::CONFIG['host_os'].downcase case host_os when /linux/ :linux when /darwin|mac os/ :macosx when /mswin|msys|mingw32/ :windows when /cygwin/ :cygwin when /solaris|sunos/ :solaris when /bsd|dragonfly/ :bsd when /aix/ :aix else raise Error, "unknown os: #{host_os.inspect}" end ) end def arch @arch ||= ( host_cpu = RbConfig::CONFIG['host_cpu'].downcase case host_cpu when /i[3456]86/ if workaround_older_macosx_misreported_cpu? # Workaround case: older 64-bit Darwin Rubies misreported as i686 "x86_64" else "i386" end when /amd64|x86_64/ "x86_64" when /ppc|powerpc/ "powerpc" else host_cpu end ) end # # By default, a child process will inherit open file descriptors from the # parent process. This helper provides a cross-platform way of making sure # that doesn't happen for the given file/io. # 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 end private def warn_once(msg) @warnings ||= {} unless @warnings[msg] @warnings[msg] = true logger.warn msg end end # Workaround: detect the situation that an older Darwin Ruby is actually # 64-bit, but is misreporting cpu as i686, which would imply 32-bit. # # @return [Boolean] `true` if: # (a) on Mac OS X # (b) actually running in 64-bit mode def workaround_older_macosx_misreported_cpu? os == :macosx && is_64_bit? end # @return [Boolean] `true` if this Ruby represents `1` in 64 bits (8 bytes). def is_64_bit? 1.size == 8 end 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? ruby-childprocess-3.0.0/lib/childprocess/000077500000000000000000000000001354232312100203775ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/abstract_io.rb000066400000000000000000000007541354232312100232240ustar00rootroot00000000000000module ChildProcess class AbstractIO attr_reader :stderr, :stdout, :stdin def inherit! @stdout = STDOUT @stderr = STDERR end def stderr=(io) check_type io @stderr = io end def stdout=(io) check_type io @stdout = io end # # @api private # def _stdin=(io) check_type io @stdin = io end private def check_type(io) raise SubclassResponsibility, "check_type" end end end ruby-childprocess-3.0.0/lib/childprocess/abstract_process.rb000066400000000000000000000070471354232312100242750ustar00rootroot00000000000000module ChildProcess class AbstractProcess POLL_INTERVAL = 0.1 attr_reader :exit_code # # Set this to true if you do not care about when or if the process quits. # attr_accessor :detach # # Set this to true if you want to write to the process' stdin (process.io.stdin) # attr_accessor :duplex # # Modify the child's environment variables # attr_reader :environment # # Set the child's current working directory. # attr_accessor :cwd # # 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. # attr_accessor :leader # # Create a new process with the given args. # # @api private # @see ChildProcess.build # def initialize(args) unless args.all? { |e| e.kind_of?(String) } raise ArgumentError, "all arguments must be String: #{args.inspect}" end @args = args @started = false @exit_code = nil @io = nil @cwd = nil @detach = false @duplex = false @leader = false @environment = {} end # # Returns a ChildProcess::AbstractIO subclass to configure the child's IO streams. # def io raise SubclassResponsibility, "io" end # # @return [Integer] the pid of the process after it has started # def pid raise SubclassResponsibility, "pid" end # # Launch the child process # # @return [AbstractProcess] self # def start launch_process @started = true self end # # Forcibly terminate the process, using increasingly harsher methods if possible. # # @param [Integer] timeout (3) Seconds to wait before trying the next method. # def stop(timeout = 3) raise SubclassResponsibility, "stop" end # # Block until the process has been terminated. # # @return [Integer] The exit status of the process # def wait raise SubclassResponsibility, "wait" end # # Did the process exit? # # @return [Boolean] # def exited? raise SubclassResponsibility, "exited?" end # # Has the process started? # # @return [Boolean] # def started? @started end # # Is this process running? # # @return [Boolean] # def alive? started? && !exited? end # # Returns true if the process has exited and the exit code was not 0. # # @return [Boolean] # def crashed? exited? && @exit_code != 0 end # # Wait for the process to exit, raising a ChildProcess::TimeoutError if # the timeout expires. # def poll_for_exit(timeout) log "polling #{timeout} seconds for exit" end_time = Time.now + timeout until (ok = exited?) || Time.now > end_time sleep POLL_INTERVAL end unless ok raise TimeoutError, "process still alive after #{timeout} seconds" end end private def launch_process raise SubclassResponsibility, "launch_process" end def detach? @detach end def duplex? @duplex end def leader? @leader end def log(*args) ChildProcess.logger.debug "#{self.inspect} : #{args.inspect}" end def assert_started raise Error, "process not started" unless started? end end # AbstractProcess end # ChildProcess ruby-childprocess-3.0.0/lib/childprocess/errors.rb000066400000000000000000000017061354232312100222440ustar00rootroot00000000000000module ChildProcess class Error < StandardError end class TimeoutError < Error end class SubclassResponsibility < Error end class InvalidEnvironmentVariable < Error end class LaunchError < Error end class MissingFFIError < Error def initialize message = "FFI is a required pre-requisite for Windows or posix_spawn support in the ChildProcess gem. " + "Ensure the `ffi` gem is installed. " + "If you believe this is an error, please file a bug at http://github.com/enkessler/childprocess/issues" super(message) end end class MissingPlatformError < Error def initialize message = "posix_spawn is not yet supported on #{ChildProcess.platform_name} (#{RUBY_PLATFORM}), falling back to default implementation. " + "If you believe this is an error, please file a bug at http://github.com/enkessler/childprocess/issues" super(message) end end end ruby-childprocess-3.0.0/lib/childprocess/jruby.rb000066400000000000000000000021671354232312100220650ustar00rootroot00000000000000require '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" ruby-childprocess-3.0.0/lib/childprocess/jruby/000077500000000000000000000000001354232312100215325ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/jruby/io.rb000066400000000000000000000005341354232312100224700ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/jruby/process.rb000077500000000000000000000116551354232312100235500ustar00rootroot00000000000000require "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 ruby-childprocess-3.0.0/lib/childprocess/jruby/pump.rb000066400000000000000000000020371354232312100230420ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/tools/000077500000000000000000000000001354232312100215375ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/tools/generator.rb000066400000000000000000000072261354232312100240610ustar00rootroot00000000000000require '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 endruby-childprocess-3.0.0/lib/childprocess/unix.rb000066400000000000000000000003171354232312100217100ustar00rootroot00000000000000module ChildProcess module Unix end end require "childprocess/unix/io" require "childprocess/unix/process" require "childprocess/unix/fork_exec_process" # PosixSpawnProcess + ffi is required on demand. ruby-childprocess-3.0.0/lib/childprocess/unix/000077500000000000000000000000001354232312100213625ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/unix/fork_exec_process.rb000066400000000000000000000034051354232312100254140ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/io.rb000066400000000000000000000007021354232312100223150ustar00rootroot00000000000000module ChildProcess module Unix class IO < AbstractIO private def check_type(io) unless io.respond_to? :to_io raise ArgumentError, "expected #{io.inspect} to respond to :to_io" end result = io.to_io unless result && result.kind_of?(::IO) raise TypeError, "expected IO, got #{result.inspect}:#{result.class}" end end end # IO end # Unix end # ChildProcess ruby-childprocess-3.0.0/lib/childprocess/unix/lib.rb000066400000000000000000000134111354232312100224550ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/platform/000077500000000000000000000000001354232312100232065ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/unix/platform/i386-linux.rb000066400000000000000000000005041354232312100253600ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/platform/i386-solaris.rb000066400000000000000000000004421354232312100256760ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/platform/x86_64-linux.rb000066400000000000000000000005041354232312100256250ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/platform/x86_64-macosx.rb000066400000000000000000000004411354232312100257600ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/unix/posix_spawn_process.rb000066400000000000000000000065201354232312100260220ustar00rootroot00000000000000require '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 ruby-childprocess-3.0.0/lib/childprocess/unix/process.rb000066400000000000000000000032301354232312100233630ustar00rootroot00000000000000module ChildProcess module Unix class Process < AbstractProcess attr_reader :pid def io @io ||= Unix::IO.new end def stop(timeout = 3) assert_started send_term begin return poll_for_exit(timeout) rescue TimeoutError # try next end send_kill wait rescue Errno::ECHILD, Errno::ESRCH # handle race condition where process dies between timeout # 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 ruby-childprocess-3.0.0/lib/childprocess/version.rb000066400000000000000000000000541354232312100224100ustar00rootroot00000000000000module ChildProcess VERSION = '3.0.0' end ruby-childprocess-3.0.0/lib/childprocess/windows.rb000066400000000000000000000015351354232312100224220ustar00rootroot00000000000000require "rbconfig" begin require 'ffi' rescue LoadError raise ChildProcess::MissingFFIError end module ChildProcess module Windows module Lib extend FFI::Library def self.msvcrt_name host_part = RbConfig::CONFIG['host_os'].split("_")[1] manifest = File.join(RbConfig::CONFIG['bindir'], 'ruby.exe.manifest') if host_part && host_part.to_i > 80 && File.exists?(manifest) "msvcr#{host_part}" else "msvcrt" end 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" ruby-childprocess-3.0.0/lib/childprocess/windows/000077500000000000000000000000001354232312100220715ustar00rootroot00000000000000ruby-childprocess-3.0.0/lib/childprocess/windows/handle.rb000066400000000000000000000042441354232312100236550ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/windows/io.rb000066400000000000000000000007551354232312100230340ustar00rootroot00000000000000module ChildProcess module Windows class IO < AbstractIO private def check_type(io) return if has_fileno?(io) return if has_to_io?(io) raise ArgumentError, "#{io.inspect}:#{io.class} must have :fileno or :to_io" end def has_fileno?(io) io.respond_to?(:fileno) && io.fileno end def has_to_io?(io) io.respond_to?(:to_io) && io.to_io.kind_of?(::IO) end end # IO end # Windows end # ChildProcess ruby-childprocess-3.0.0/lib/childprocess/windows/lib.rb000066400000000000000000000262101354232312100231650ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/windows/process.rb000077500000000000000000000053011354232312100240760ustar00rootroot00000000000000module ChildProcess module Windows class Process < AbstractProcess attr_reader :pid def io @io ||= Windows::IO.new end def stop(timeout = 3) assert_started 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 @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 @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 | JOB_OBJECT_LIMIT_BREAKAWAY_OK 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) end def close Lib.close_handle @pointer end end end # Process end # Windows end # ChildProcess ruby-childprocess-3.0.0/lib/childprocess/windows/process_builder.rb000066400000000000000000000111611354232312100256020ustar00rootroot00000000000000module 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 ruby-childprocess-3.0.0/lib/childprocess/windows/structs.rb000066400000000000000000000116331354232312100241310ustar00rootroot00000000000000module 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 # ChildProcessruby-childprocess-3.0.0/spec/000077500000000000000000000000001354232312100161015ustar00rootroot00000000000000ruby-childprocess-3.0.0/spec/abstract_io_spec.rb000066400000000000000000000004241354232312100217320ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) describe ChildProcess::AbstractIO do let(:io) { ChildProcess::AbstractIO.new } it "inherits the parent's IO streams" do io.inherit! expect(io.stdout).to eq STDOUT expect(io.stderr).to eq STDERR end end ruby-childprocess-3.0.0/spec/childprocess_spec.rb000066400000000000000000000253331354232312100221300ustar00rootroot00000000000000# encoding: utf-8 require File.expand_path('../spec_helper', __FILE__) require 'rubygems/mock_gem_ui' describe ChildProcess do here = File.dirname(__FILE__) let(:gemspec) { eval(File.read "#{here}/../childprocess.gemspec") } it 'validates cleanly' do mock_ui = Gem::MockGemUi.new Gem::DefaultUserInteraction.use_ui(mock_ui) { gemspec.validate } expect(mock_ui.error).to_not match(/warn/i) end it "returns self when started" do process = sleeping_ruby expect(process.start).to eq process expect(process).to be_alive end # We can't detect failure to execve() when using posix_spawn() on Linux # without waiting for the child to exit with code 127. # # See e.g. http://repo.or.cz/w/glibc.git/blob/669704fd:/sysdeps/posix/spawni.c#l34 # # We could work around this by doing the PATH search ourselves, but not sure # it's worth it. it "raises ChildProcess::LaunchError if the process can't be started", :posix_spawn_on_linux => false do expect { invalid_process.start }.to raise_error(ChildProcess::LaunchError) end it 'raises ArgumentError if given a non-string argument' do expect { ChildProcess.build(nil, "unlikelytoexist") }.to raise_error(ArgumentError) expect { ChildProcess.build("foo", 1) }.to raise_error(ArgumentError) end it "knows if the process crashed" do process = exit_with(1).start process.wait expect(process).to be_crashed end it "knows if the process didn't crash" do process = exit_with(0).start process.wait expect(process).to_not be_crashed end it "can wait for a process to finish" do process = exit_with(0).start return_value = process.wait expect(process).to_not be_alive expect(return_value).to eq 0 end it 'ignores #wait if process already finished' do process = exit_with(0).start sleep 0.01 until process.exited? expect(process.wait).to eql 0 end it "escalates if TERM is ignored" do process = ignored('TERM').start process.stop expect(process).to be_exited end it "accepts a timeout argument to #stop" do process = sleeping_ruby.start process.stop(exit_timeout) end it "lets child process inherit the environment of the current process" do Tempfile.open("env-spec") do |file| file.close with_env('INHERITED' => 'yes') do process = write_env(file.path).start process.wait end file.open child_env = eval rewind_and_read(file) expect(child_env['INHERITED']).to eql 'yes' end end it "can override env vars only for the current process" do Tempfile.open("env-spec") do |file| file.close process = write_env(file.path) process.environment['CHILD_ONLY'] = '1' process.start expect(ENV['CHILD_ONLY']).to be_nil process.wait file.open child_env = eval rewind_and_read(file) expect(child_env['CHILD_ONLY']).to eql '1' end end it 'allows unicode characters in the environment' do Tempfile.open("env-spec") do |file| file.close process = write_env(file.path) process.environment['FOö'] = 'baör' process.start process.wait file.open child_env = eval rewind_and_read(file) expect(child_env['FOö']).to eql 'baör' end end it "inherits the parent's env vars also when some are overridden" do Tempfile.open("env-spec") do |file| file.close with_env('INHERITED' => 'yes', 'CHILD_ONLY' => 'no') do process = write_env(file.path) process.environment['CHILD_ONLY'] = 'yes' process.start process.wait file.open child_env = eval rewind_and_read(file) expect(child_env['INHERITED']).to eq 'yes' expect(child_env['CHILD_ONLY']).to eq 'yes' end end end it "can unset env vars" do Tempfile.open("env-spec") do |file| file.close ENV['CHILDPROCESS_UNSET'] = '1' process = write_env(file.path) process.environment['CHILDPROCESS_UNSET'] = nil process.start process.wait file.open child_env = eval rewind_and_read(file) expect(child_env).to_not have_key('CHILDPROCESS_UNSET') end end it 'does not see env vars unset in parent' do Tempfile.open('env-spec') do |file| file.close ENV['CHILDPROCESS_UNSET'] = nil process = write_env(file.path) process.start process.wait file.open child_env = eval rewind_and_read(file) expect(child_env).to_not have_key('CHILDPROCESS_UNSET') end end it "passes arguments to the child" do args = ["foo", "bar"] Tempfile.open("argv-spec") do |file| process = write_argv(file.path, *args).start process.wait expect(rewind_and_read(file)).to eql args.inspect end end it "lets a detached child live on" do p_pid = nil c_pid = nil Tempfile.open('grandparent_out') do |gp_file| # Create a parent and detached child process that will spit out their PID. Make sure that the child process lasts longer than the parent. p_process = ruby("require 'childprocess' ; c_process = ChildProcess.build('ruby', '-e', 'puts \\\"Child PID: \#{Process.pid}\\\" ; sleep 5') ; c_process.io.inherit! ; c_process.detach = true ; c_process.start ; puts \"Child PID: \#{c_process.pid}\" ; puts \"Parent PID: \#{Process.pid}\"") p_process.io.stdout = p_process.io.stderr = gp_file # Let the parent process die p_process.start p_process.wait # Gather parent and child PIDs pids = rewind_and_read(gp_file).split("\n") pids.collect! { |pid| pid[/\d+/].to_i } c_pid, p_pid = pids end # Check that the parent process has dies but the child process is still alive expect(alive?(p_pid)).to_not be true expect(alive?(c_pid)).to be true end it "preserves Dir.pwd in the child" do Tempfile.open("dir-spec-out") do |file| process = ruby("print Dir.pwd") process.io.stdout = process.io.stderr = file expected_dir = nil Dir.chdir(Dir.tmpdir) do expected_dir = Dir.pwd process.start end process.wait expect(rewind_and_read(file)).to eq expected_dir end end it "can handle whitespace, special characters and quotes in arguments" do args = ["foo bar", 'foo\bar', "'i-am-quoted'", '"i am double quoted"'] Tempfile.open("argv-spec") do |file| process = write_argv(file.path, *args).start process.wait expect(rewind_and_read(file)).to eq args.inspect end end it 'handles whitespace in the executable name' do path = File.expand_path('foo bar') with_executable_at(path) do |proc| expect(proc.start).to eq proc expect(proc).to be_alive end end it "times out when polling for exit" do process = sleeping_ruby.start expect { process.poll_for_exit(0.1) }.to raise_error(ChildProcess::TimeoutError) end it "can change working directory" do process = ruby "print Dir.pwd" with_tmpdir { |dir| process.cwd = dir orig_pwd = Dir.pwd Tempfile.open('cwd') do |file| process.io.stdout = file process.start process.wait expect(rewind_and_read(file)).to eq dir end expect(Dir.pwd).to eq orig_pwd } 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 pid = wait_until(30) do Integer(rewind_and_read(file)) rescue nil end process.stop wait_until(3) { expect(alive?(pid)).to eql(false) } end end it 'releases the GIL while waiting for the process' do time = Time.now threads = [] threads << Thread.new { sleeping_ruby(1).start.wait } threads << Thread.new(time) { expect(Time.now - time).to be < 0.5 } threads.each { |t| t.join } end it 'can check if a detached child is alive' do proc = ruby_process("-e", "sleep") proc.detach = true proc.start expect(proc).to be_alive proc.stop(0) expect(proc).to be_exited end describe 'OS detection' do before(:all) do # Save off original OS so that it can be restored later @original_host_os = RbConfig::CONFIG['host_os'] end after(:each) do # Restore things to the real OS instead of the fake test OS RbConfig::CONFIG['host_os'] = @original_host_os ChildProcess.instance_variable_set(:@os, nil) end # TODO: add tests for other OSs context 'on a BSD system' do let(:bsd_patterns) { ['bsd', 'dragonfly'] } it 'correctly identifies BSD systems' do bsd_patterns.each do |pattern| RbConfig::CONFIG['host_os'] = pattern ChildProcess.instance_variable_set(:@os, nil) expect(ChildProcess.os).to eq(:bsd) end end end end it 'has a logger' do expect(ChildProcess).to respond_to(:logger) end it 'can change its logger' do expect(ChildProcess).to respond_to(:logger=) original_logger = ChildProcess.logger begin ChildProcess.logger = :some_other_logger expect(ChildProcess.logger).to eq(:some_other_logger) ensure ChildProcess.logger = original_logger end end describe 'logger' do before(:each) do ChildProcess.logger = logger end after(:all) do ChildProcess.logger = nil end context 'with the default logger' do let(:logger) { nil } it 'logs at INFO level by default' do expect(ChildProcess.logger.level).to eq(Logger::INFO) end it 'logs at DEBUG level by default if $DEBUG is on' do original_debug = $DEBUG begin $DEBUG = true expect(ChildProcess.logger.level).to eq(Logger::DEBUG) ensure $DEBUG = original_debug end end it "logs to stderr by default" do cap = capture_std { generate_log_messages } expect(cap.stdout).to be_empty expect(cap.stderr).to_not be_empty end end context 'with a custom logger' do let(:logger) { Logger.new($stdout) } it "logs to configured logger" do cap = capture_std { generate_log_messages } expect(cap.stdout).to_not be_empty expect(cap.stderr).to be_empty end end end describe '#started?' do subject { process.started? } context 'when not started' do let(:process) { sleeping_ruby(1) } it { is_expected.to be false } end context 'when started' do let(:process) { sleeping_ruby(1).start } it { is_expected.to be true } end context 'when finished' do before(:each) { process.wait } let(:process) { sleeping_ruby(0).start } it { is_expected.to be true } end end end ruby-childprocess-3.0.0/spec/get_env.ps1000066400000000000000000000005021354232312100201520ustar00rootroot00000000000000param($p1) $env_list = Get-ChildItem Env: # Builds a ruby hash compatible string $hash_string = "{" foreach ($item in $env_list) { $hash_string += "`"" + $item.Name + "`" => `"" + $item.value.replace('\','\\').replace('"','\"') + "`"," } $hash_string += "}" $hash_string | out-File -Encoding "UTF8" $p1 ruby-childprocess-3.0.0/spec/io_spec.rb000066400000000000000000000112201354232312100200430ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) describe ChildProcess do it "can run even when $stdout is a StringIO" do begin stdout = $stdout $stdout = StringIO.new expect { sleeping_ruby.start }.to_not raise_error ensure $stdout = stdout end end it "can redirect stdout, stderr" do process = ruby(<<-CODE) [STDOUT, STDERR].each_with_index do |io, idx| io.sync = true io.puts idx end CODE out = Tempfile.new("stdout-spec") err = Tempfile.new("stderr-spec") begin process.io.stdout = out process.io.stderr = err process.start expect(process.io.stdin).to be_nil process.wait expect(rewind_and_read(out)).to eq "0\n" expect(rewind_and_read(err)).to eq "1\n" ensure out.close err.close end end it "can redirect stdout only" do process = ruby(<<-CODE) [STDOUT, STDERR].each_with_index do |io, idx| io.sync = true io.puts idx end CODE out = Tempfile.new("stdout-spec") begin process.io.stdout = out process.start process.wait expect(rewind_and_read(out)).to eq "0\n" ensure out.close end end it "pumps all output" do process = echo out = Tempfile.new("pump") begin process.io.stdout = out process.start process.wait expect(rewind_and_read(out)).to eq "hello\n" ensure out.close end end it "can write to stdin if duplex = true" do process = cat out = Tempfile.new("duplex") out.sync = true begin process.io.stdout = out process.io.stderr = out process.duplex = true process.start process.io.stdin.puts "hello world" process.io.stdin.close process.poll_for_exit(exit_timeout) expect(rewind_and_read(out)).to eq "hello world\n" ensure out.close end end it "can write to stdin interactively if duplex = true" do process = cat out = Tempfile.new("duplex") out.sync = true out_receiver = File.open(out.path, "rb") begin process.io.stdout = out process.io.stderr = out process.duplex = true process.start stdin = process.io.stdin stdin.puts "hello" stdin.flush wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\n\z/m) } stdin.putc "n" stdin.flush wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nn\z/m) } stdin.print "e" stdin.flush wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nne\z/m) } stdin.printf "w" stdin.flush wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nnew\z/m) } stdin.write "\nworld\n" stdin.flush wait_until { expect(rewind_and_read(out_receiver)).to match(/\Ahello\r?\nnew\r?\nworld\r?\n\z/m) } stdin.close process.poll_for_exit(exit_timeout) ensure out_receiver.close out.close end end # # this works on JRuby 1.6.5 on my Mac, but for some reason # hangs on Travis (running 1.6.5.1 + OpenJDK). # # http://travis-ci.org/#!/enkessler/childprocess/jobs/487331 # it "works with pipes", :process_builder => false do process = ruby(<<-CODE) STDOUT.print "stdout" STDERR.print "stderr" CODE stdout, stdout_w = IO.pipe stderr, stderr_w = IO.pipe process.io.stdout = stdout_w process.io.stderr = stderr_w process.duplex = true process.start process.wait # write streams are closed *after* the process # has exited - otherwise it won't work on JRuby # with the current Process implementation stdout_w.close stderr_w.close out = stdout.read err = stderr.read expect([out, err]).to eq %w[stdout stderr] end it "can set close-on-exec when IO is inherited" do port = random_free_port server = TCPServer.new("127.0.0.1", port) ChildProcess.close_on_exec server process = sleeping_ruby process.io.inherit! process.start server.close wait_until { can_bind? "127.0.0.1", port } end it "handles long output" do process = ruby <<-CODE print 'a'*3000 CODE out = Tempfile.new("long-output") out.sync = true begin process.io.stdout = out process.start process.wait expect(rewind_and_read(out).size).to eq 3000 ensure out.close end end it 'should not inherit stdout and stderr by default' do cap = capture_std do process = echo process.start process.wait end expect(cap.stdout).to eq '' expect(cap.stderr).to eq '' end end ruby-childprocess-3.0.0/spec/jruby_spec.rb000066400000000000000000000013341354232312100205740ustar00rootroot00000000000000require 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 ruby-childprocess-3.0.0/spec/pid_behavior.rb000066400000000000000000000005221354232312100210600ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) shared_examples_for "a platform that provides the child's pid" do it "knows the child's pid" do Tempfile.open("pid-spec") do |file| process = write_pid(file.path).start process.wait expect(process.pid).to eq rewind_and_read(file).chomp.to_i end end end ruby-childprocess-3.0.0/spec/platform_detection_spec.rb000066400000000000000000000055031354232312100233250ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) # Q: Should platform detection concern be extracted from ChildProcess? describe ChildProcess do describe ".arch" do subject { described_class.arch } before(:each) { described_class.instance_variable_set(:@arch, nil) } after(:each) { described_class.instance_variable_set(:@arch, nil) } shared_examples 'expected_arch_for_host_cpu' do |host_cpu, expected_arch| context "when host_cpu is '#{host_cpu}'" do before :each do allow(RbConfig::CONFIG). to receive(:[]). with('host_cpu'). and_return(expected_arch) end it { is_expected.to eq expected_arch } end end # Normal cases: not macosx - depends only on host_cpu context "when os is *not* 'macosx'" do before :each do allow(described_class).to receive(:os).and_return(:not_macosx) end [ { host_cpu: 'i386', expected_arch: 'i386' }, { host_cpu: 'i486', expected_arch: 'i386' }, { host_cpu: 'i586', expected_arch: 'i386' }, { host_cpu: 'i686', expected_arch: 'i386' }, { host_cpu: 'amd64', expected_arch: 'x86_64' }, { host_cpu: 'x86_64', expected_arch: 'x86_64' }, { host_cpu: 'ppc', expected_arch: 'powerpc' }, { host_cpu: 'powerpc', expected_arch: 'powerpc' }, { host_cpu: 'unknown', expected_arch: 'unknown' }, ].each do |args| include_context 'expected_arch_for_host_cpu', args.values end end # Special cases: macosx - when host_cpu is i686, have to re-check context "when os is 'macosx'" do before :each do allow(described_class).to receive(:os).and_return(:macosx) end context "when host_cpu is 'i686' " do shared_examples 'expected_arch_on_macosx_i686' do |is_64, expected_arch| context "when Ruby is #{is_64 ? 64 : 32}-bit" do before :each do allow(described_class). to receive(:is_64_bit?). and_return(is_64) end include_context 'expected_arch_for_host_cpu', 'i686', expected_arch end end [ { is_64: true, expected_arch: 'x86_64' }, { is_64: false, expected_arch: 'i386' } ].each do |args| include_context 'expected_arch_on_macosx_i686', args.values end end [ { host_cpu: 'amd64', expected_arch: 'x86_64' }, { host_cpu: 'x86_64', expected_arch: 'x86_64' }, { host_cpu: 'ppc', expected_arch: 'powerpc' }, { host_cpu: 'powerpc', expected_arch: 'powerpc' }, { host_cpu: 'unknown', expected_arch: 'unknown' }, ].each do |args| include_context 'expected_arch_for_host_cpu', args.values end end end end ruby-childprocess-3.0.0/spec/spec_helper.rb000066400000000000000000000124761354232312100207310ustar00rootroot00000000000000$LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) unless defined?(JRUBY_VERSION) require 'coveralls' Coveralls.wear! end require 'childprocess' require 'rspec' require 'tempfile' require 'socket' require 'stringio' require 'ostruct' module ChildProcessSpecHelper RUBY = defined?(Gem) ? Gem.ruby : 'ruby' def ruby_process(*args) @process = ChildProcess.build(RUBY , *args) end def windows_process(*args) @process = ChildProcess.build("powershell", *args) end def sleeping_ruby(seconds = nil) if seconds ruby_process("-e", "sleep #{seconds}") else ruby_process("-e", "sleep") end end def invalid_process @process = ChildProcess.build("unlikelytoexist") end def ignored(signal) code = <<-RUBY trap(#{signal.inspect}, "IGNORE") sleep RUBY ruby_process tmp_script(code) end def write_env(path) if ChildProcess.os == :windows ps_env_file_path = File.expand_path(File.dirname(__FILE__)) args = ['-File', "#{ps_env_file_path}/get_env.ps1", path] windows_process(*args) else code = <<-RUBY File.open(#{path.inspect}, "w") { |f| f << ENV.inspect } RUBY ruby_process tmp_script(code) end end def write_argv(path, *args) code = <<-RUBY File.open(#{path.inspect}, "w") { |f| f << ARGV.inspect } RUBY ruby_process(tmp_script(code), *args) end def write_pid(path) code = <<-RUBY File.open(#{path.inspect}, "w") { |f| f << Process.pid } RUBY ruby_process tmp_script(code) end def write_pid_in_sleepy_grand_child(path) code = <<-RUBY system "ruby", "-e", 'File.open(#{path.inspect}, "w") { |f| f << Process.pid; f.flush }; sleep' RUBY ruby_process tmp_script(code) end def exit_with(exit_code) ruby_process(tmp_script("exit(#{exit_code})")) end def with_env(hash) hash.each { |k,v| ENV[k] = v } begin yield ensure hash.each_key { |k| ENV[k] = nil } end end def tmp_script(code) # use an ivar to avoid GC @tf = Tempfile.new("childprocess-temp") @tf << code @tf.close puts code if $DEBUG @tf.path end def cat if ChildProcess.os == :windows ruby(<<-CODE) STDIN.sync = STDOUT.sync = true IO.copy_stream(STDIN, STDOUT) CODE else ChildProcess.build("cat") end end def echo if ChildProcess.os == :windows ruby(<<-CODE) STDIN.sync = true STDOUT.sync = true puts "hello" CODE else ChildProcess.build("echo", "hello") end end def ruby(code) ruby_process(tmp_script(code)) end def with_executable_at(path, &blk) if ChildProcess.os == :windows path << ".cmd" content = "#{RUBY} -e 'sleep 10' \n @echo foo" else content = "#!/bin/sh\nsleep 10\necho foo" end File.open(path, 'w', 0744) { |io| io << content } proc = ChildProcess.build(path) begin yield proc ensure proc.stop if proc.alive? File.delete path end end def exit_timeout 10 end def random_free_port server = TCPServer.new('127.0.0.1', 0) port = server.addr[1] server.close port end def with_tmpdir(&blk) name = "#{Time.now.strftime("%Y%m%d")}-#{$$}-#{rand(0x100000000).to_s(36)}" FileUtils.mkdir_p(name) begin yield File.expand_path(name) ensure FileUtils.rm_rf name end end def wait_until(timeout = 10, &blk) end_time = Time.now + timeout last_exception = nil until Time.now >= end_time begin result = yield return result if result rescue RSpec::Expectations::ExpectationNotMetError => ex last_exception = ex end sleep 0.01 end msg = "timed out after #{timeout} seconds" msg << ":\n#{last_exception.message}" if last_exception raise msg end def can_bind?(host, port) TCPServer.new(host, port).close true rescue false end def rewind_and_read(io) io.rewind io.read end def alive?(pid) if ChildProcess.windows? ChildProcess::Windows::Lib.alive?(pid) else begin Process.getpgid pid true rescue Errno::ESRCH false end end end def capture_std orig_out = STDOUT.clone orig_err = STDERR.clone out = Tempfile.new 'captured-stdout' err = Tempfile.new 'captured-stderr' out.sync = true err.sync = true STDOUT.reopen out STDERR.reopen err yield OpenStruct.new stdout: rewind_and_read(out), stderr: rewind_and_read(err) ensure STDOUT.reopen orig_out STDERR.reopen orig_err end def generate_log_messages ChildProcess.logger.level = Logger::DEBUG process = exit_with(0).start process.wait process.poll_for_exit(0.1) end end # ChildProcessSpecHelper Thread.abort_on_exception = true RSpec.configure do |c| c.include(ChildProcessSpecHelper) c.after(:each) { 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 end ruby-childprocess-3.0.0/spec/unix_spec.rb000066400000000000000000000035251354232312100204300ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) require "pid_behavior" if ChildProcess.unix? && !ChildProcess.jruby? && !ChildProcess.posix_spawn? describe ChildProcess::Unix::Process do it_behaves_like "a platform that provides the child's pid" it "handles ECHILD race condition where process dies between timeout and KILL" do process = sleeping_ruby allow(process).to receive(:fork).and_return('fakepid') allow(process).to receive(:send_term) allow(process).to receive(:poll_for_exit).and_raise(ChildProcess::TimeoutError) allow(process).to receive(:send_kill).and_raise(Errno::ECHILD.new) process.start expect { process.stop }.not_to raise_error allow(process).to receive(:alive?).and_return(false) process.send(:send_signal, 'TERM') end it "handles ESRCH race condition where process dies between timeout and KILL" do process = sleeping_ruby allow(process).to receive(:fork).and_return('fakepid') allow(process).to receive(:send_term) allow(process).to receive(:poll_for_exit).and_raise(ChildProcess::TimeoutError) allow(process).to receive(:send_kill).and_raise(Errno::ESRCH.new) process.start expect { process.stop }.not_to raise_error allow(process).to receive(:alive?).and_return(false) process.send(:send_signal, 'TERM') end end describe ChildProcess::Unix::IO do let(:io) { ChildProcess::Unix::IO.new } it "raises an ArgumentError if given IO does not respond to :to_io" do expect { io.stdout = nil }.to raise_error(ArgumentError, /to respond to :to_io/) end it "raises a TypeError if #to_io does not return an IO" do fake_io = Object.new def fake_io.to_io() StringIO.new end expect { io.stdout = fake_io }.to raise_error(TypeError, /expected IO, got/) end end end ruby-childprocess-3.0.0/spec/windows_spec.rb000066400000000000000000000013501354232312100211310ustar00rootroot00000000000000require File.expand_path('../spec_helper', __FILE__) require "pid_behavior" if ChildProcess.windows? describe ChildProcess::Windows::Process do it_behaves_like "a platform that provides the child's pid" end describe ChildProcess::Windows::IO do let(:io) { ChildProcess::Windows::IO.new } it "raises an ArgumentError if given IO does not respond to :fileno" do expect { io.stdout = nil }.to raise_error(ArgumentError, /must have :fileno or :to_io/) end it "raises an ArgumentError if the #to_io does not return an IO " do fake_io = Object.new def fake_io.to_io() StringIO.new end expect { io.stdout = fake_io }.to raise_error(ArgumentError, /must have :fileno or :to_io/) end end end