childprocess 4.1.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -0
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile +3 -2
  5. data/README.md +5 -23
  6. data/childprocess.gemspec +2 -0
  7. data/lib/childprocess/abstract_process.rb +1 -1
  8. data/lib/childprocess/errors.rb +0 -21
  9. data/lib/childprocess/process_spawn_process.rb +127 -0
  10. data/lib/childprocess/unix/process.rb +3 -64
  11. data/lib/childprocess/unix.rb +2 -4
  12. data/lib/childprocess/version.rb +1 -1
  13. data/lib/childprocess/windows/process.rb +13 -116
  14. data/lib/childprocess/windows.rb +0 -29
  15. data/lib/childprocess.rb +16 -53
  16. data/spec/childprocess_spec.rb +39 -15
  17. data/spec/io_spec.rb +1 -1
  18. data/spec/spec_helper.rb +5 -20
  19. data/spec/unix_spec.rb +3 -7
  20. metadata +19 -24
  21. data/.travis.yml +0 -37
  22. data/appveyor.yml +0 -36
  23. data/lib/childprocess/jruby/io.rb +0 -16
  24. data/lib/childprocess/jruby/process.rb +0 -184
  25. data/lib/childprocess/jruby/pump.rb +0 -53
  26. data/lib/childprocess/jruby.rb +0 -56
  27. data/lib/childprocess/tools/generator.rb +0 -146
  28. data/lib/childprocess/unix/fork_exec_process.rb +0 -78
  29. data/lib/childprocess/unix/lib.rb +0 -186
  30. data/lib/childprocess/unix/platform/arm64-macosx.rb +0 -11
  31. data/lib/childprocess/unix/platform/i386-linux.rb +0 -12
  32. data/lib/childprocess/unix/platform/i386-solaris.rb +0 -11
  33. data/lib/childprocess/unix/platform/x86_64-linux.rb +0 -12
  34. data/lib/childprocess/unix/platform/x86_64-macosx.rb +0 -11
  35. data/lib/childprocess/unix/posix_spawn_process.rb +0 -134
  36. data/lib/childprocess/windows/handle.rb +0 -91
  37. data/lib/childprocess/windows/lib.rb +0 -416
  38. data/lib/childprocess/windows/process_builder.rb +0 -178
  39. data/lib/childprocess/windows/structs.rb +0 -149
  40. data/spec/jruby_spec.rb +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a5e6dc8970dbfa81b42c587480b4cd7a564995c2a6a0be84f2c8192edcf6d0c
4
- data.tar.gz: 766be22f3575a6ef9ba837a37722f0a852f509fc9efb2a77b01ca121b865a28e
3
+ metadata.gz: 9af8db7e1ea6ba4571c4b26bf7631147d9dc3e9d1b96bf602534a054621d44dd
4
+ data.tar.gz: fcb40387f9f29e29019663fdea7c1fa94a531c80ee125f6b207194f76617b7cf
5
5
  SHA512:
6
- metadata.gz: 8e4ac84967566de68d200d983bfebca6fce9575d7e8fb0aa0324b64dc4b6e0d29d29abe173ce99a54f8b37501eb515573b457f6b612a395f3f5d2bc672da6802
7
- data.tar.gz: 16bc7aaffd67ab47f1f8a8ded31681fb88f1767421838c3ddb1f1033fb7dc0f5c5d664b7dce3fb4107edb9818a73ce2769f23a3e1f30bf51ffc3dd088d8a7690
6
+ metadata.gz: 8eec57e14538d054468f8582802596cc6ecafcc88660bc2242b90c9da2ba6ec9d5e053d39c8d6338aa9486ff8eb942942e6eae2216c52bde3cac53128192b796
7
+ data.tar.gz: 9e5335a82ab47954fff0d7781cd666f20742ab5c48b670bb8adb567466f22d20a96500b581ac750e9e91ea36d63f7a9a01d05a89a21a6eef20134c66891510f3
@@ -0,0 +1,32 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ os: [ ubuntu-latest, macos-latest, windows-latest ]
9
+ ruby: [ '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', head, jruby, truffleruby ]
10
+ # CRuby < 2.6 does not support macos-arm64, so test those on amd64 instead
11
+ # JRuby 9.4.7.0 does not have native console support on macos-arm64: https://github.com/jruby/jruby/issues/8271
12
+ include:
13
+ - { os: macos-13, ruby: '2.4' }
14
+ - { os: macos-13, ruby: '2.5' }
15
+ - { os: macos-13, ruby: jruby }
16
+ exclude:
17
+ - { os: macos-latest, ruby: '2.4' }
18
+ - { os: macos-latest, ruby: '2.5' }
19
+ - { os: macos-latest, ruby: jruby }
20
+ - { os: windows-latest, ruby: truffleruby }
21
+ # fails to load rspec: RuntimeError: CRITICAL: RUBYGEMS_ACTIVATION_MONITOR.owned?: before false -> after true
22
+ - { os: windows-latest, ruby: jruby }
23
+ runs-on: ${{ matrix.os }}
24
+ env:
25
+ CHILDPROCESS_UNSET: should-be-unset
26
+ steps:
27
+ - uses: actions/checkout@v2
28
+ - uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ bundler-cache: true
32
+ - run: bundle exec rake spec
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ### Version 5.1.0 / 2024-01-06
2
+
3
+ * [#196](https://github.com/enkessler/childprocess/pull/196): Remove `ostruct` dependency to fix deprecation warning on Ruby 3.4
4
+ * [#199](https://github.com/enkessler/childprocess/pull/199): Add `logger` dependency to fix deprecation warning on Ruby 3.4
5
+
6
+ ### Version 5.0.0 / 2024-01-06
7
+
8
+ * [#175](https://github.com/enkessler/childprocess/pull/175): Replace all backends by `Process.spawn` for portability, reliability and simplicity.
9
+ * [#185](https://github.com/enkessler/childprocess/pull/185): Add support for Ruby 3.x
10
+
1
11
  ### Version 4.1.0 / 2021-06-08
2
12
 
3
13
  * [#170](https://github.com/enkessler/childprocess/pull/170): Update gem homepage to use `https://`
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in child_process.gemspec
4
4
  gemspec
@@ -6,4 +6,5 @@ gemspec
6
6
  # Used for local development/testing only
7
7
  gem 'rake'
8
8
 
9
- gem 'ffi' if ENV['CHILDPROCESS_POSIX_SPAWN'] == 'true' || Gem.win_platform?
9
+ # Newer versions of term-ansicolor (used by coveralls) do not work on Ruby 2.4
10
+ gem 'term-ansicolor', '< 1.8.0' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5')
data/README.md CHANGED
@@ -6,9 +6,8 @@ external programs running in the background on any Ruby / OS combination.
6
6
  The code originated in the [selenium-webdriver](https://rubygems.org/gems/selenium-webdriver) gem, but should prove useful as
7
7
  a standalone library.
8
8
 
9
- [![Build Status](https://secure.travis-ci.org/enkessler/childprocess.svg)](http://travis-ci.org/enkessler/childprocess)
10
- [![Build status](https://ci.appveyor.com/api/projects/status/fn2snbcd7kku5myk/branch/dev?svg=true)](https://ci.appveyor.com/project/enkessler/childprocess/branch/dev)
11
- [![Gem Version](https://badge.fury.io/rb/childprocess.svg)](http://badge.fury.io/rb/childprocess)
9
+ [![CI](https://github.com/enkessler/childprocess/actions/workflows/ci.yml/badge.svg)](https://github.com/enkessler/childprocess/actions/workflows/ci.yml)
10
+ ![Gem Version](https://img.shields.io/gem/v/childprocess)
12
11
  [![Code Climate](https://codeclimate.com/github/enkessler/childprocess.svg)](https://codeclimate.com/github/enkessler/childprocess)
13
12
  [![Coverage Status](https://coveralls.io/repos/enkessler/childprocess/badge.svg?branch=master)](https://coveralls.io/r/enkessler/childprocess?branch=master)
14
13
 
@@ -16,8 +15,6 @@ a standalone library.
16
15
 
17
16
  * Ruby 2.4+, JRuby 9+
18
17
 
19
- Windows users **must** ensure the `ffi` gem (`>= 1.0.11`) is installed in order to use ChildProcess.
20
-
21
18
  # Usage
22
19
 
23
20
  The object returned from `ChildProcess.build` will implement `ChildProcess::AbstractProcess`.
@@ -73,9 +70,9 @@ begin
73
70
  process = ChildProcess.build("sh" , "-c",
74
71
  "for i in {1..3}; do echo $i; sleep 1; done")
75
72
  process.io.stdout = w
76
- process.start # This results in a fork, inheriting the write end of the pipe.
73
+ process.start # This results in a subprocess inheriting the write end of the pipe.
77
74
 
78
- # Close parent's copy of the write end of the pipe so when the (forked) child
75
+ # Close parent's copy of the write end of the pipe so when the child
79
76
  # process closes its write end of the pipe the parent receives EOF when
80
77
  # attempting to read from it. If the parent leaves its write end open, it
81
78
  # will not detect EOF.
@@ -138,17 +135,6 @@ search.io.stdin.close
138
135
  search.wait
139
136
  ```
140
137
 
141
- #### Prefer posix_spawn on *nix
142
-
143
- If the parent process is using a lot of memory, `fork+exec` can be very expensive. The `posix_spawn()` API removes this overhead.
144
-
145
- ```ruby
146
- ChildProcess.posix_spawn = true
147
- process = ChildProcess.build(*args)
148
- ```
149
-
150
- To be able to use this, please make sure that you have the `ffi` gem installed.
151
-
152
138
  ### Ensure entire process tree dies
153
139
 
154
140
  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:
@@ -195,11 +181,7 @@ ChildProcess.logger = logger
195
181
 
196
182
  # Implementation
197
183
 
198
- How the process is launched and killed depends on the platform:
199
-
200
- * Unix : `fork + exec` (or `posix_spawn` if enabled)
201
- * Windows : `CreateProcess()` and friends
202
- * JRuby : `java.lang.{Process,ProcessBuilder}`
184
+ ChildProcess 5+ uses `Process.spawn` from the Ruby core library for maximum portability.
203
185
 
204
186
  # Note on Patches/Pull Requests
205
187
 
data/childprocess.gemspec CHANGED
@@ -20,6 +20,8 @@ Gem::Specification.new do |s|
20
20
 
21
21
  s.required_ruby_version = '>= 2.4.0'
22
22
 
23
+ s.add_dependency "logger", "~> 1.5"
24
+
23
25
  s.add_development_dependency "rspec", "~> 3.0"
24
26
  s.add_development_dependency "yard", "~> 0.0"
25
27
  s.add_development_dependency 'coveralls', '< 1.0'
@@ -39,7 +39,7 @@ module ChildProcess
39
39
  # @see ChildProcess.build
40
40
  #
41
41
 
42
- def initialize(args)
42
+ def initialize(*args)
43
43
  unless args.all? { |e| e.kind_of?(String) }
44
44
  raise ArgumentError, "all arguments must be String: #{args.inspect}"
45
45
  end
@@ -13,25 +13,4 @@ module ChildProcess
13
13
 
14
14
  class LaunchError < Error
15
15
  end
16
-
17
- class MissingFFIError < Error
18
- def initialize
19
- message = "FFI is a required pre-requisite for Windows or posix_spawn support in the ChildProcess gem. " +
20
- "Ensure the `ffi` gem is installed. " +
21
- "If you believe this is an error, please file a bug at http://github.com/enkessler/childprocess/issues"
22
-
23
- super(message)
24
- end
25
-
26
- end
27
-
28
- class MissingPlatformError < Error
29
- def initialize
30
- message = "posix_spawn is not yet supported on #{ChildProcess.platform_name} (#{RUBY_PLATFORM}), falling back to default implementation. " +
31
- "If you believe this is an error, please file a bug at http://github.com/enkessler/childprocess/issues"
32
-
33
- super(message)
34
- end
35
-
36
- end
37
16
  end
@@ -0,0 +1,127 @@
1
+ require_relative 'abstract_process'
2
+
3
+ module ChildProcess
4
+ class ProcessSpawnProcess < AbstractProcess
5
+ attr_reader :pid
6
+
7
+ def exited?
8
+ return true if @exit_code
9
+
10
+ assert_started
11
+ pid, status = ::Process.waitpid2(@pid, ::Process::WNOHANG | ::Process::WUNTRACED)
12
+ pid = nil if pid == 0 # may happen on jruby
13
+
14
+ log(:pid => pid, :status => status)
15
+
16
+ if pid
17
+ set_exit_code(status)
18
+ end
19
+
20
+ !!pid
21
+ rescue Errno::ECHILD
22
+ # may be thrown for detached processes
23
+ true
24
+ end
25
+
26
+ def wait
27
+ assert_started
28
+
29
+ if exited?
30
+ exit_code
31
+ else
32
+ _, status = ::Process.waitpid2(@pid)
33
+
34
+ set_exit_code(status)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def launch_process
41
+ environment = {}
42
+ @environment.each_pair do |key, value|
43
+ key = key.to_s
44
+ value = value.nil? ? nil : value.to_s
45
+
46
+ if key.include?("\0") || key.include?("=") || value.to_s.include?("\0")
47
+ raise InvalidEnvironmentVariable, "#{key.inspect} => #{value.to_s.inspect}"
48
+ end
49
+ environment[key] = value
50
+ end
51
+
52
+ options = {}
53
+
54
+ options[:out] = io.stdout ? io.stdout.fileno : File::NULL
55
+ options[:err] = io.stderr ? io.stderr.fileno : File::NULL
56
+
57
+ if duplex?
58
+ reader, writer = ::IO.pipe
59
+ options[:in] = reader.fileno
60
+ unless ChildProcess.windows?
61
+ options[writer.fileno] = :close
62
+ end
63
+ end
64
+
65
+ if leader?
66
+ if ChildProcess.windows?
67
+ options[:new_pgroup] = true
68
+ else
69
+ options[:pgroup] = true
70
+ end
71
+ end
72
+
73
+ options[:chdir] = @cwd if @cwd
74
+
75
+ if @args.size == 1
76
+ # When given a single String, Process.spawn would think it should use the shell
77
+ # if there is any special character in it. However, ChildProcess should never
78
+ # use the shell. So we use the [cmdname, argv0] form to force no shell.
79
+ arg = @args[0]
80
+ args = [[arg, arg]]
81
+ else
82
+ args = @args
83
+ end
84
+
85
+ begin
86
+ @pid = ::Process.spawn(environment, *args, options)
87
+ rescue SystemCallError => e
88
+ raise LaunchError, e.message
89
+ end
90
+
91
+ if duplex?
92
+ io._stdin = writer
93
+ reader.close
94
+ end
95
+
96
+ ::Process.detach(@pid) if detach?
97
+ end
98
+
99
+ def set_exit_code(status)
100
+ @exit_code = status.exitstatus || status.termsig
101
+ end
102
+
103
+ def send_term
104
+ send_signal 'TERM'
105
+ end
106
+
107
+ def send_kill
108
+ send_signal 'KILL'
109
+ end
110
+
111
+ def send_signal(sig)
112
+ assert_started
113
+
114
+ log "sending #{sig}"
115
+ if leader?
116
+ if ChildProcess.unix?
117
+ ::Process.kill sig, -@pid # negative pid == process group
118
+ else
119
+ output = `taskkill /F /T /PID #{@pid}`
120
+ log output
121
+ end
122
+ else
123
+ ::Process.kill sig, @pid
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,8 +1,8 @@
1
+ require_relative '../process_spawn_process'
2
+
1
3
  module ChildProcess
2
4
  module Unix
3
- class Process < AbstractProcess
4
- attr_reader :pid
5
-
5
+ class Process < ProcessSpawnProcess
6
6
  def io
7
7
  @io ||= Unix::IO.new
8
8
  end
@@ -24,67 +24,6 @@ module ChildProcess
24
24
  # and send_kill
25
25
  true
26
26
  end
27
-
28
- def exited?
29
- return true if @exit_code
30
-
31
- assert_started
32
- pid, status = ::Process.waitpid2(@pid, ::Process::WNOHANG | ::Process::WUNTRACED)
33
- pid = nil if pid == 0 # may happen on jruby
34
-
35
- log(:pid => pid, :status => status)
36
-
37
- if pid
38
- set_exit_code(status)
39
- end
40
-
41
- !!pid
42
- rescue Errno::ECHILD
43
- # may be thrown for detached processes
44
- true
45
- end
46
-
47
- def wait
48
- assert_started
49
-
50
- if exited?
51
- exit_code
52
- else
53
- _, status = ::Process.waitpid2(@pid)
54
-
55
- set_exit_code(status)
56
- end
57
- end
58
-
59
- private
60
-
61
- def send_term
62
- send_signal 'TERM'
63
- end
64
-
65
- def send_kill
66
- send_signal 'KILL'
67
- end
68
-
69
- def send_signal(sig)
70
- assert_started
71
-
72
- log "sending #{sig}"
73
- ::Process.kill sig, _pid
74
- end
75
-
76
- def set_exit_code(status)
77
- @exit_code = status.exitstatus || status.termsig
78
- end
79
-
80
- def _pid
81
- if leader?
82
- -@pid # negative pid == process group
83
- else
84
- @pid
85
- end
86
- end
87
-
88
27
  end # Process
89
28
  end # Unix
90
29
  end # ChildProcess
@@ -3,7 +3,5 @@ module ChildProcess
3
3
  end
4
4
  end
5
5
 
6
- require "childprocess/unix/io"
7
- require "childprocess/unix/process"
8
- require "childprocess/unix/fork_exec_process"
9
- # PosixSpawnProcess + ffi is required on demand.
6
+ require_relative "unix/io"
7
+ require_relative "unix/process"
@@ -1,3 +1,3 @@
1
1
  module ChildProcess
2
- VERSION = '4.1.0'
2
+ VERSION = '5.1.0'
3
3
  end
@@ -1,131 +1,28 @@
1
+ require_relative '../process_spawn_process'
2
+
1
3
  module ChildProcess
2
4
  module Windows
3
- class Process < AbstractProcess
4
-
5
- attr_reader :pid
6
-
5
+ class Process < ProcessSpawnProcess
7
6
  def io
8
7
  @io ||= Windows::IO.new
9
8
  end
10
9
 
11
10
  def stop(timeout = 3)
12
11
  assert_started
12
+ send_kill
13
13
 
14
- log "sending KILL"
15
- @handle.send(WIN_SIGKILL)
16
-
17
- poll_for_exit(timeout)
18
- ensure
19
- close_handle
20
- close_job_if_necessary
21
- end
22
-
23
- def wait
24
- if exited?
25
- exit_code
26
- else
27
- @handle.wait
28
- @exit_code = @handle.exit_code
29
-
30
- close_handle
31
- close_job_if_necessary
32
-
33
- @exit_code
34
- end
35
- end
36
-
37
- def exited?
38
- return true if @exit_code
39
- assert_started
40
-
41
- code = @handle.exit_code
42
- exited = code != PROCESS_STILL_ACTIVE
43
-
44
- log(:exited? => exited, :code => code)
45
-
46
- if exited
47
- @exit_code = code
48
- close_handle
49
- close_job_if_necessary
50
- end
51
-
52
- exited
53
- end
54
-
55
- private
56
-
57
- def launch_process
58
- builder = ProcessBuilder.new(@args)
59
- builder.leader = leader?
60
- builder.detach = detach?
61
- builder.duplex = duplex?
62
- builder.environment = @environment unless @environment.empty?
63
- builder.cwd = @cwd
64
-
65
- if @io
66
- builder.stdout = @io.stdout
67
- builder.stderr = @io.stderr
68
- end
69
-
70
- @pid = builder.start
71
- @handle = Handle.open @pid
72
-
73
- if leader?
74
- @job = Job.new(detach?, true)
75
- @job << @handle
76
- end
77
-
78
- if duplex?
79
- raise Error, "no stdin stream" unless builder.stdin
80
- io._stdin = builder.stdin
81
- end
82
-
83
- self
84
- end
85
-
86
- def close_handle
87
- @handle.close
88
- end
89
-
90
- def close_job_if_necessary
91
- @job.close if leader?
92
- end
93
-
94
-
95
- class Job
96
- def initialize(detach, leader)
97
- @pointer = Lib.create_job_object(nil, nil)
98
-
99
- if @pointer.nil? || @pointer.null?
100
- raise Error, "unable to create job object"
101
- end
102
-
103
- basic = JobObjectBasicLimitInformation.new
104
- basic[:LimitFlags] |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE if !detach
105
- basic[:LimitFlags] |= JOB_OBJECT_LIMIT_BREAKAWAY_OK if leader
106
-
107
- extended = JobObjectExtendedLimitInformation.new
108
- extended[:BasicLimitInformation] = basic
109
-
110
- ret = Lib.set_information_job_object(
111
- @pointer,
112
- JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
113
- extended,
114
- extended.size
115
- )
116
-
117
- Lib.check_error ret
118
- end
119
-
120
- def <<(handle)
121
- Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
14
+ begin
15
+ return poll_for_exit(timeout)
16
+ rescue TimeoutError
17
+ # try next
122
18
  end
123
19
 
124
- def close
125
- Lib.close_handle @pointer
126
- end
20
+ wait
21
+ rescue Errno::ECHILD, Errno::ESRCH
22
+ # handle race condition where process dies between timeout
23
+ # and send_kill
24
+ true
127
25
  end
128
-
129
26
  end # Process
130
27
  end # Windows
131
28
  end # ChildProcess
@@ -1,38 +1,9 @@
1
1
  require "rbconfig"
2
2
 
3
- begin
4
- require 'ffi'
5
- rescue LoadError
6
- raise ChildProcess::MissingFFIError
7
- end
8
-
9
3
  module ChildProcess
10
4
  module Windows
11
- module Lib
12
- extend FFI::Library
13
-
14
- def self.msvcrt_name
15
- host_part = RbConfig::CONFIG['host_os'].split("_")[1]
16
- manifest = File.join(RbConfig::CONFIG['bindir'], 'ruby.exe.manifest')
17
-
18
- if host_part && host_part.to_i > 80 && File.exists?(manifest)
19
- "msvcr#{host_part}"
20
- else
21
- "msvcrt"
22
- end
23
- end
24
-
25
- ffi_lib "kernel32", msvcrt_name
26
- ffi_convention :stdcall
27
-
28
-
29
- end # Library
30
5
  end # Windows
31
6
  end # ChildProcess
32
7
 
33
- require "childprocess/windows/lib"
34
- require "childprocess/windows/structs"
35
- require "childprocess/windows/handle"
36
8
  require "childprocess/windows/io"
37
- require "childprocess/windows/process_builder"
38
9
  require "childprocess/windows/process"