childprocess 0.8.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.
- checksums.yaml +7 -0
- data/.document +6 -0
- data/.gitignore +28 -0
- data/.rspec +1 -0
- data/.travis.yml +36 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +15 -0
- data/LICENSE +20 -0
- data/README.md +192 -0
- data/Rakefile +61 -0
- data/appveyor.yml +43 -0
- data/childprocess.gemspec +30 -0
- data/lib/childprocess.rb +205 -0
- data/lib/childprocess/abstract_io.rb +36 -0
- data/lib/childprocess/abstract_process.rb +192 -0
- data/lib/childprocess/errors.rb +26 -0
- data/lib/childprocess/jruby.rb +56 -0
- data/lib/childprocess/jruby/io.rb +16 -0
- data/lib/childprocess/jruby/process.rb +159 -0
- data/lib/childprocess/jruby/pump.rb +53 -0
- data/lib/childprocess/tools/generator.rb +146 -0
- data/lib/childprocess/unix.rb +9 -0
- data/lib/childprocess/unix/fork_exec_process.rb +70 -0
- data/lib/childprocess/unix/io.rb +21 -0
- data/lib/childprocess/unix/lib.rb +186 -0
- data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
- data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
- data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
- data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
- data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
- data/lib/childprocess/unix/process.rb +89 -0
- data/lib/childprocess/version.rb +3 -0
- data/lib/childprocess/windows.rb +33 -0
- data/lib/childprocess/windows/handle.rb +91 -0
- data/lib/childprocess/windows/io.rb +25 -0
- data/lib/childprocess/windows/lib.rb +416 -0
- data/lib/childprocess/windows/process.rb +130 -0
- data/lib/childprocess/windows/process_builder.rb +175 -0
- data/lib/childprocess/windows/structs.rb +149 -0
- data/spec/abstract_io_spec.rb +12 -0
- data/spec/childprocess_spec.rb +391 -0
- data/spec/io_spec.rb +228 -0
- data/spec/jruby_spec.rb +24 -0
- data/spec/pid_behavior.rb +12 -0
- data/spec/platform_detection_spec.rb +86 -0
- data/spec/spec_helper.rb +261 -0
- data/spec/unix_spec.rb +57 -0
- data/spec/windows_spec.rb +23 -0
- metadata +179 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class TimeoutError < Error
|
6
|
+
end
|
7
|
+
|
8
|
+
class SubclassResponsibility < Error
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidEnvironmentVariable < Error
|
12
|
+
end
|
13
|
+
|
14
|
+
class LaunchError < Error
|
15
|
+
end
|
16
|
+
|
17
|
+
class MissingPlatformError < Error
|
18
|
+
def initialize
|
19
|
+
message = "posix_spawn is not yet supported on #{ChildProcess.platform_name} (#{RUBY_PLATFORM}), falling back to default implementation. " +
|
20
|
+
"If you believe this is an error, please file a bug at http://github.com/enkessler/childprocess/issues"
|
21
|
+
|
22
|
+
super(message)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'java'
|
2
|
+
require 'jruby'
|
3
|
+
|
4
|
+
class Java::SunNioCh::FileChannelImpl
|
5
|
+
field_reader :fd
|
6
|
+
end
|
7
|
+
|
8
|
+
class Java::JavaIo::FileDescriptor
|
9
|
+
if ChildProcess.os == :windows
|
10
|
+
field_reader :handle
|
11
|
+
end
|
12
|
+
|
13
|
+
field_reader :fd
|
14
|
+
end
|
15
|
+
|
16
|
+
module ChildProcess
|
17
|
+
module JRuby
|
18
|
+
def self.posix_fileno_for(obj)
|
19
|
+
channel = ::JRuby.reference(obj).channel
|
20
|
+
begin
|
21
|
+
channel.getFDVal
|
22
|
+
rescue NoMethodError
|
23
|
+
fileno = channel.fd
|
24
|
+
if fileno.kind_of?(Java::JavaIo::FileDescriptor)
|
25
|
+
fileno = fileno.fd
|
26
|
+
end
|
27
|
+
|
28
|
+
fileno == -1 ? obj.fileno : fileno
|
29
|
+
end
|
30
|
+
rescue
|
31
|
+
# fall back
|
32
|
+
obj.fileno
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.windows_handle_for(obj)
|
36
|
+
channel = ::JRuby.reference(obj).channel
|
37
|
+
fileno = obj.fileno
|
38
|
+
|
39
|
+
begin
|
40
|
+
fileno = channel.getFDVal
|
41
|
+
rescue NoMethodError
|
42
|
+
fileno = channel.fd if channel.respond_to?(:fd)
|
43
|
+
end
|
44
|
+
|
45
|
+
if fileno.kind_of? Java::JavaIo::FileDescriptor
|
46
|
+
fileno.handle
|
47
|
+
else
|
48
|
+
Windows::Lib.handle_for fileno
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
require "childprocess/jruby/pump"
|
55
|
+
require "childprocess/jruby/io"
|
56
|
+
require "childprocess/jruby/process"
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module JRuby
|
3
|
+
class IO < AbstractIO
|
4
|
+
private
|
5
|
+
|
6
|
+
def check_type(output)
|
7
|
+
unless output.respond_to?(:to_outputstream) && output.respond_to?(:write)
|
8
|
+
raise ArgumentError, "expected #{output.inspect} to respond to :to_outputstream"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end # IO
|
13
|
+
end # Unix
|
14
|
+
end # ChildProcess
|
15
|
+
|
16
|
+
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "java"
|
2
|
+
|
3
|
+
module ChildProcess
|
4
|
+
module JRuby
|
5
|
+
class Process < AbstractProcess
|
6
|
+
def initialize(args)
|
7
|
+
super(args)
|
8
|
+
|
9
|
+
@pumps = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def io
|
13
|
+
@io ||= JRuby::IO.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def exited?
|
17
|
+
return true if @exit_code
|
18
|
+
|
19
|
+
assert_started
|
20
|
+
@exit_code = @process.exitValue
|
21
|
+
stop_pumps
|
22
|
+
|
23
|
+
true
|
24
|
+
rescue java.lang.IllegalThreadStateException => ex
|
25
|
+
log(ex.class => ex.message)
|
26
|
+
false
|
27
|
+
ensure
|
28
|
+
log(:exit_code => @exit_code)
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop(timeout = nil)
|
32
|
+
assert_started
|
33
|
+
|
34
|
+
@process.destroy
|
35
|
+
wait # no way to actually use the timeout here..
|
36
|
+
end
|
37
|
+
|
38
|
+
def wait
|
39
|
+
if exited?
|
40
|
+
exit_code
|
41
|
+
else
|
42
|
+
@process.waitFor
|
43
|
+
|
44
|
+
stop_pumps
|
45
|
+
@exit_code = @process.exitValue
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Only supported in JRuby on a Unix operating system, thanks to limitations
|
51
|
+
# in Java's classes
|
52
|
+
#
|
53
|
+
# @return [Integer] the pid of the process after it has started
|
54
|
+
# @raise [NotImplementedError] when trying to access pid on non-Unix platform
|
55
|
+
#
|
56
|
+
def pid
|
57
|
+
if @process.getClass.getName != "java.lang.UNIXProcess"
|
58
|
+
raise NotImplementedError, "pid is only supported by JRuby child processes on Unix"
|
59
|
+
end
|
60
|
+
|
61
|
+
# About the best way we can do this is with a nasty reflection-based impl
|
62
|
+
# Thanks to Martijn Courteaux
|
63
|
+
# http://stackoverflow.com/questions/2950338/how-can-i-kill-a-linux-process-in-java-with-sigkill-process-destroy-does-sigter/2951193#2951193
|
64
|
+
field = @process.getClass.getDeclaredField("pid")
|
65
|
+
field.accessible = true
|
66
|
+
field.get(@process)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def launch_process(&blk)
|
72
|
+
pb = java.lang.ProcessBuilder.new(@args)
|
73
|
+
|
74
|
+
pb.directory java.io.File.new(@cwd || Dir.pwd)
|
75
|
+
set_env pb.environment
|
76
|
+
|
77
|
+
begin
|
78
|
+
@process = pb.start
|
79
|
+
rescue java.io.IOException => ex
|
80
|
+
raise LaunchError, ex.message
|
81
|
+
end
|
82
|
+
|
83
|
+
setup_io
|
84
|
+
end
|
85
|
+
|
86
|
+
def setup_io
|
87
|
+
if @io
|
88
|
+
redirect(@process.getErrorStream, @io.stderr)
|
89
|
+
redirect(@process.getInputStream, @io.stdout)
|
90
|
+
else
|
91
|
+
@process.getErrorStream.close
|
92
|
+
@process.getInputStream.close
|
93
|
+
end
|
94
|
+
|
95
|
+
if duplex?
|
96
|
+
io._stdin = create_stdin
|
97
|
+
else
|
98
|
+
@process.getOutputStream.close
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def redirect(input, output)
|
103
|
+
if output.nil?
|
104
|
+
input.close
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
@pumps << Pump.new(input, output.to_outputstream).run
|
109
|
+
end
|
110
|
+
|
111
|
+
def stop_pumps
|
112
|
+
@pumps.each { |pump| pump.stop }
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_env(env)
|
116
|
+
merged = ENV.to_hash
|
117
|
+
|
118
|
+
@environment.each { |k, v| merged[k.to_s] = v }
|
119
|
+
|
120
|
+
merged.each do |k, v|
|
121
|
+
if v
|
122
|
+
env.put(k, v.to_s)
|
123
|
+
elsif env.has_key? k
|
124
|
+
env.remove(k)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
removed_keys = env.key_set.to_a - merged.keys
|
129
|
+
removed_keys.each { |k| env.remove(k) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_stdin
|
133
|
+
output_stream = @process.getOutputStream
|
134
|
+
|
135
|
+
stdin = output_stream.to_io
|
136
|
+
stdin.sync = true
|
137
|
+
stdin.instance_variable_set(:@childprocess_java_stream, output_stream)
|
138
|
+
|
139
|
+
class << stdin
|
140
|
+
# The stream provided is a BufferedeOutputStream, so we
|
141
|
+
# have to flush it to make the bytes flow to the process
|
142
|
+
def __childprocess_flush__
|
143
|
+
@childprocess_java_stream.flush
|
144
|
+
end
|
145
|
+
|
146
|
+
[:flush, :print, :printf, :putc, :puts, :write, :write_nonblock].each do |m|
|
147
|
+
define_method(m) do |*args|
|
148
|
+
super(*args)
|
149
|
+
self.__childprocess_flush__
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
stdin
|
155
|
+
end
|
156
|
+
|
157
|
+
end # Process
|
158
|
+
end # JRuby
|
159
|
+
end # ChildProcess
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module JRuby
|
3
|
+
class Pump
|
4
|
+
BUFFER_SIZE = 2048
|
5
|
+
|
6
|
+
def initialize(input, output)
|
7
|
+
@input = input
|
8
|
+
@output = output
|
9
|
+
@stop = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def stop
|
13
|
+
@stop = true
|
14
|
+
@thread && @thread.join
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
@thread = Thread.new { pump }
|
19
|
+
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def pump
|
26
|
+
buffer = Java.byte[BUFFER_SIZE].new
|
27
|
+
|
28
|
+
until @stop && (@input.available == 0)
|
29
|
+
read, avail = 0, 0
|
30
|
+
|
31
|
+
while read != -1
|
32
|
+
avail = [@input.available, 1].max
|
33
|
+
avail = BUFFER_SIZE if avail > BUFFER_SIZE
|
34
|
+
read = @input.read(buffer, 0, avail)
|
35
|
+
|
36
|
+
if read > 0
|
37
|
+
@output.write(buffer, 0, read)
|
38
|
+
@output.flush
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
sleep 0.1
|
43
|
+
end
|
44
|
+
|
45
|
+
@output.flush
|
46
|
+
rescue java.io.IOException => ex
|
47
|
+
ChildProcess.logger.debug ex.message
|
48
|
+
ChildProcess.logger.debug ex.backtrace
|
49
|
+
end
|
50
|
+
|
51
|
+
end # Pump
|
52
|
+
end # JRuby
|
53
|
+
end # ChildProcess
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ChildProcess
|
4
|
+
module Tools
|
5
|
+
class Generator
|
6
|
+
EXE_NAME = "childprocess-sizeof-generator"
|
7
|
+
TMP_PROGRAM = "childprocess-sizeof-generator.c"
|
8
|
+
DEFAULT_INCLUDES = %w[stdio.h stddef.h]
|
9
|
+
|
10
|
+
def self.generate
|
11
|
+
new.generate
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@cc = ENV['CC'] || 'gcc'
|
16
|
+
@out = File.expand_path("../../unix/platform/#{ChildProcess.platform_name}.rb", __FILE__)
|
17
|
+
@sizeof = {}
|
18
|
+
@constants = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate
|
22
|
+
fetch_size 'posix_spawn_file_actions_t', :include => "spawn.h"
|
23
|
+
fetch_size 'posix_spawnattr_t', :include => "spawn.h"
|
24
|
+
fetch_size 'sigset_t', :include => "signal.h"
|
25
|
+
|
26
|
+
fetch_constant 'POSIX_SPAWN_RESETIDS', :include => 'spawn.h'
|
27
|
+
fetch_constant 'POSIX_SPAWN_SETPGROUP', :include => 'spawn.h'
|
28
|
+
fetch_constant 'POSIX_SPAWN_SETSIGDEF', :include => 'spawn.h'
|
29
|
+
fetch_constant 'POSIX_SPAWN_SETSIGMASK', :include => 'spawn.h'
|
30
|
+
|
31
|
+
if ChildProcess.linux?
|
32
|
+
fetch_constant 'POSIX_SPAWN_USEVFORK', :include => 'spawn.h', :define => {'_GNU_SOURCE' => nil}
|
33
|
+
end
|
34
|
+
|
35
|
+
write
|
36
|
+
end
|
37
|
+
|
38
|
+
def write
|
39
|
+
FileUtils.mkdir_p(File.dirname(@out))
|
40
|
+
File.open(@out, 'w') do |io|
|
41
|
+
io.puts result
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "wrote #{@out}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch_size(type_name, opts = {})
|
48
|
+
print "sizeof(#{type_name}): "
|
49
|
+
src = <<-EOF
|
50
|
+
int main() {
|
51
|
+
printf("%d", (unsigned int)sizeof(#{type_name}));
|
52
|
+
return 0;
|
53
|
+
}
|
54
|
+
EOF
|
55
|
+
|
56
|
+
output = execute(src, opts)
|
57
|
+
|
58
|
+
if output.to_i < 1
|
59
|
+
raise "sizeof(#{type_name}) == #{output.to_i} (output=#{output})"
|
60
|
+
end
|
61
|
+
|
62
|
+
size = output.to_i
|
63
|
+
@sizeof[type_name] = size
|
64
|
+
|
65
|
+
puts size
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch_constant(name, opts)
|
69
|
+
print "#{name}: "
|
70
|
+
src = <<-EOF
|
71
|
+
int main() {
|
72
|
+
printf("%d", (unsigned int)#{name});
|
73
|
+
return 0;
|
74
|
+
}
|
75
|
+
EOF
|
76
|
+
|
77
|
+
output = execute(src, opts)
|
78
|
+
value = Integer(output)
|
79
|
+
@constants[name] = value
|
80
|
+
|
81
|
+
puts value
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def execute(src, opts)
|
86
|
+
program = Array(opts[:define]).map do |key, value|
|
87
|
+
<<-SRC
|
88
|
+
#ifndef #{key}
|
89
|
+
#define #{key} #{value}
|
90
|
+
#endif
|
91
|
+
SRC
|
92
|
+
end.join("\n")
|
93
|
+
program << "\n"
|
94
|
+
|
95
|
+
includes = Array(opts[:include]) + DEFAULT_INCLUDES
|
96
|
+
program << includes.map { |include| "#include <#{include}>" }.join("\n")
|
97
|
+
program << "\n#{src}"
|
98
|
+
|
99
|
+
File.open(TMP_PROGRAM, 'w') do |file|
|
100
|
+
file << program
|
101
|
+
end
|
102
|
+
|
103
|
+
cmd = "#{@cc} #{TMP_PROGRAM} -o #{EXE_NAME}"
|
104
|
+
system cmd
|
105
|
+
unless $?.success?
|
106
|
+
raise "failed to compile program: #{cmd.inspect}\n#{program}"
|
107
|
+
end
|
108
|
+
|
109
|
+
output = `./#{EXE_NAME} 2>&1`
|
110
|
+
|
111
|
+
unless $?.success?
|
112
|
+
raise "failed to run program: #{cmd.inspect}\n#{output}"
|
113
|
+
end
|
114
|
+
|
115
|
+
output.chomp
|
116
|
+
ensure
|
117
|
+
File.delete TMP_PROGRAM if File.exist?(TMP_PROGRAM)
|
118
|
+
File.delete EXE_NAME if File.exist?(EXE_NAME)
|
119
|
+
end
|
120
|
+
|
121
|
+
def result
|
122
|
+
if @sizeof.empty? && @constants.empty?
|
123
|
+
raise "no data collected, nothing to do"
|
124
|
+
end
|
125
|
+
|
126
|
+
out = ['module ChildProcess::Unix::Platform']
|
127
|
+
out << ' SIZEOF = {'
|
128
|
+
|
129
|
+
max = @sizeof.keys.map { |e| e.length }.max
|
130
|
+
@sizeof.each_with_index do |(type, size), idx|
|
131
|
+
out << " :#{type.ljust max} => #{size}#{',' unless idx == @sizeof.size - 1}"
|
132
|
+
end
|
133
|
+
out << ' }'
|
134
|
+
|
135
|
+
max = @constants.keys.map { |e| e.length }.max
|
136
|
+
@constants.each do |name, val|
|
137
|
+
out << " #{name.ljust max} = #{val}"
|
138
|
+
end
|
139
|
+
out << 'end'
|
140
|
+
|
141
|
+
out.join "\n"
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|