childprocess 0.8.0 → 2.0.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 +5 -5
- data/.document +6 -6
- data/.gitignore +28 -28
- data/.rspec +1 -1
- data/.travis.yml +42 -36
- data/CHANGELOG.md +67 -44
- data/Gemfile +18 -15
- data/LICENSE +20 -20
- data/README.md +216 -192
- data/Rakefile +61 -61
- data/appveyor.yml +42 -43
- data/childprocess.gemspec +32 -30
- data/ext/mkrf_conf.rb +24 -0
- data/lib/childprocess/abstract_io.rb +36 -36
- data/lib/childprocess/abstract_process.rb +192 -192
- data/lib/childprocess/errors.rb +37 -26
- data/lib/childprocess/jruby/io.rb +16 -16
- data/lib/childprocess/jruby/process.rb +184 -159
- data/lib/childprocess/jruby/pump.rb +53 -53
- data/lib/childprocess/jruby.rb +56 -56
- data/lib/childprocess/tools/generator.rb +145 -145
- data/lib/childprocess/unix/fork_exec_process.rb +78 -70
- data/lib/childprocess/unix/io.rb +21 -21
- data/lib/childprocess/unix/lib.rb +186 -186
- data/lib/childprocess/unix/platform/i386-linux.rb +12 -12
- data/lib/childprocess/unix/platform/i386-solaris.rb +11 -11
- data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -12
- data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -11
- data/lib/childprocess/unix/posix_spawn_process.rb +134 -134
- data/lib/childprocess/unix/process.rb +90 -89
- data/lib/childprocess/unix.rb +9 -9
- data/lib/childprocess/version.rb +3 -3
- data/lib/childprocess/windows/handle.rb +91 -91
- data/lib/childprocess/windows/io.rb +25 -25
- data/lib/childprocess/windows/lib.rb +416 -416
- data/lib/childprocess/windows/process.rb +130 -130
- data/lib/childprocess/windows/process_builder.rb +178 -175
- data/lib/childprocess/windows/structs.rb +148 -148
- data/lib/childprocess/windows.rb +33 -33
- data/lib/childprocess.rb +210 -205
- data/spec/abstract_io_spec.rb +12 -12
- data/spec/childprocess_spec.rb +447 -391
- data/spec/get_env.ps1 +13 -0
- data/spec/io_spec.rb +228 -228
- data/spec/jruby_spec.rb +24 -24
- data/spec/pid_behavior.rb +12 -12
- data/spec/platform_detection_spec.rb +86 -86
- data/spec/spec_helper.rb +270 -261
- data/spec/unix_spec.rb +57 -57
- data/spec/windows_spec.rb +23 -23
- metadata +18 -33
@@ -1,130 +1,130 @@
|
|
1
|
-
module ChildProcess
|
2
|
-
module Windows
|
3
|
-
class Process < AbstractProcess
|
4
|
-
|
5
|
-
attr_reader :pid
|
6
|
-
|
7
|
-
def io
|
8
|
-
@io ||= Windows::IO.new
|
9
|
-
end
|
10
|
-
|
11
|
-
def stop(timeout = 3)
|
12
|
-
assert_started
|
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
|
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
|
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 | JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
105
|
-
|
106
|
-
extended = JobObjectExtendedLimitInformation.new
|
107
|
-
extended[:BasicLimitInformation] = basic
|
108
|
-
|
109
|
-
ret = Lib.set_information_job_object(
|
110
|
-
@pointer,
|
111
|
-
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
|
112
|
-
extended,
|
113
|
-
extended.size
|
114
|
-
)
|
115
|
-
|
116
|
-
Lib.check_error ret
|
117
|
-
end
|
118
|
-
|
119
|
-
def <<(handle)
|
120
|
-
Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
|
121
|
-
end
|
122
|
-
|
123
|
-
def close
|
124
|
-
Lib.close_handle @pointer
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
end # Process
|
129
|
-
end # Windows
|
130
|
-
end # ChildProcess
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
class Process < AbstractProcess
|
4
|
+
|
5
|
+
attr_reader :pid
|
6
|
+
|
7
|
+
def io
|
8
|
+
@io ||= Windows::IO.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def stop(timeout = 3)
|
12
|
+
assert_started
|
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
|
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
|
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 | JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
105
|
+
|
106
|
+
extended = JobObjectExtendedLimitInformation.new
|
107
|
+
extended[:BasicLimitInformation] = basic
|
108
|
+
|
109
|
+
ret = Lib.set_information_job_object(
|
110
|
+
@pointer,
|
111
|
+
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
|
112
|
+
extended,
|
113
|
+
extended.size
|
114
|
+
)
|
115
|
+
|
116
|
+
Lib.check_error ret
|
117
|
+
end
|
118
|
+
|
119
|
+
def <<(handle)
|
120
|
+
Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
|
121
|
+
end
|
122
|
+
|
123
|
+
def close
|
124
|
+
Lib.close_handle @pointer
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end # Process
|
129
|
+
end # Windows
|
130
|
+
end # ChildProcess
|
@@ -1,175 +1,178 @@
|
|
1
|
-
module ChildProcess
|
2
|
-
module Windows
|
3
|
-
class ProcessBuilder
|
4
|
-
attr_accessor :leader, :detach, :duplex, :environment, :stdout, :stderr, :cwd
|
5
|
-
attr_reader :stdin
|
6
|
-
|
7
|
-
def initialize(args)
|
8
|
-
@args = args
|
9
|
-
|
10
|
-
@detach = false
|
11
|
-
@duplex = false
|
12
|
-
@environment = nil
|
13
|
-
@cwd = nil
|
14
|
-
|
15
|
-
@stdout = nil
|
16
|
-
@stderr = nil
|
17
|
-
@stdin = nil
|
18
|
-
|
19
|
-
@flags = 0
|
20
|
-
@job_ptr = nil
|
21
|
-
@cmd_ptr = nil
|
22
|
-
@env_ptr = nil
|
23
|
-
@cwd_ptr = nil
|
24
|
-
end
|
25
|
-
|
26
|
-
def start
|
27
|
-
create_command_pointer
|
28
|
-
create_environment_pointer
|
29
|
-
create_cwd_pointer
|
30
|
-
|
31
|
-
setup_flags
|
32
|
-
setup_io
|
33
|
-
|
34
|
-
pid = create_process
|
35
|
-
close_handles
|
36
|
-
|
37
|
-
pid
|
38
|
-
end
|
39
|
-
|
40
|
-
private
|
41
|
-
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
nil, #
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
end
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
class ProcessBuilder
|
4
|
+
attr_accessor :leader, :detach, :duplex, :environment, :stdout, :stderr, :cwd
|
5
|
+
attr_reader :stdin
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@args = args
|
9
|
+
|
10
|
+
@detach = false
|
11
|
+
@duplex = false
|
12
|
+
@environment = nil
|
13
|
+
@cwd = nil
|
14
|
+
|
15
|
+
@stdout = nil
|
16
|
+
@stderr = nil
|
17
|
+
@stdin = nil
|
18
|
+
|
19
|
+
@flags = 0
|
20
|
+
@job_ptr = nil
|
21
|
+
@cmd_ptr = nil
|
22
|
+
@env_ptr = nil
|
23
|
+
@cwd_ptr = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
create_command_pointer
|
28
|
+
create_environment_pointer
|
29
|
+
create_cwd_pointer
|
30
|
+
|
31
|
+
setup_flags
|
32
|
+
setup_io
|
33
|
+
|
34
|
+
pid = create_process
|
35
|
+
close_handles
|
36
|
+
|
37
|
+
pid
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def to_wide_string(str)
|
43
|
+
newstr = str + "\0".encode(str.encoding)
|
44
|
+
newstr.encode!('UTF-16LE')
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_command_pointer
|
48
|
+
string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join(' ')
|
49
|
+
@cmd_ptr = to_wide_string(string)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_environment_pointer
|
53
|
+
return unless @environment.kind_of?(Hash) && @environment.any?
|
54
|
+
|
55
|
+
strings = []
|
56
|
+
|
57
|
+
ENV.to_hash.merge(@environment).each do |key, val|
|
58
|
+
next if val.nil?
|
59
|
+
|
60
|
+
if key.to_s =~ /=|\0/ || val.to_s.include?("\0")
|
61
|
+
raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}"
|
62
|
+
end
|
63
|
+
|
64
|
+
strings << "#{key}=#{val}\0"
|
65
|
+
end
|
66
|
+
|
67
|
+
env_str = to_wide_string(strings.join)
|
68
|
+
@env_ptr = FFI::MemoryPointer.from_string(env_str)
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_cwd_pointer
|
72
|
+
@cwd_ptr = FFI::MemoryPointer.from_string(to_wide_string(@cwd || Dir.pwd))
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_process
|
76
|
+
ok = Lib.create_process(
|
77
|
+
nil, # application name
|
78
|
+
@cmd_ptr, # command line
|
79
|
+
nil, # process attributes
|
80
|
+
nil, # thread attributes
|
81
|
+
true, # inherit handles
|
82
|
+
@flags, # creation flags
|
83
|
+
@env_ptr, # environment
|
84
|
+
@cwd_ptr, # current directory
|
85
|
+
startup_info, # startup info
|
86
|
+
process_info # process info
|
87
|
+
)
|
88
|
+
|
89
|
+
ok or raise LaunchError, Lib.last_error_message
|
90
|
+
|
91
|
+
process_info[:dwProcessId]
|
92
|
+
end
|
93
|
+
|
94
|
+
def startup_info
|
95
|
+
@startup_info ||= StartupInfo.new
|
96
|
+
end
|
97
|
+
|
98
|
+
def process_info
|
99
|
+
@process_info ||= ProcessInfo.new
|
100
|
+
end
|
101
|
+
|
102
|
+
def setup_flags
|
103
|
+
@flags |= CREATE_UNICODE_ENVIRONMENT
|
104
|
+
@flags |= DETACHED_PROCESS if @detach
|
105
|
+
@flags |= CREATE_BREAKAWAY_FROM_JOB if @leader
|
106
|
+
end
|
107
|
+
|
108
|
+
def setup_io
|
109
|
+
startup_info[:dwFlags] ||= 0
|
110
|
+
startup_info[:dwFlags] |= STARTF_USESTDHANDLES
|
111
|
+
|
112
|
+
if @stdout
|
113
|
+
startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
|
114
|
+
end
|
115
|
+
|
116
|
+
if @stderr
|
117
|
+
startup_info[:hStdError] = std_stream_handle_for(@stderr)
|
118
|
+
end
|
119
|
+
|
120
|
+
if @duplex
|
121
|
+
read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
122
|
+
write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
123
|
+
sa = SecurityAttributes.new(:inherit => true)
|
124
|
+
|
125
|
+
ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
|
126
|
+
Lib.check_error ok
|
127
|
+
|
128
|
+
@read_pipe = read_pipe_ptr.read_pointer
|
129
|
+
@write_pipe = write_pipe_ptr.read_pointer
|
130
|
+
|
131
|
+
Lib.set_handle_inheritance @read_pipe, true
|
132
|
+
Lib.set_handle_inheritance @write_pipe, false
|
133
|
+
|
134
|
+
startup_info[:hStdInput] = @read_pipe
|
135
|
+
else
|
136
|
+
startup_info[:hStdInput] = std_stream_handle_for(STDIN)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def std_stream_handle_for(io)
|
141
|
+
handle = Lib.handle_for(io)
|
142
|
+
|
143
|
+
begin
|
144
|
+
Lib.set_handle_inheritance handle, true
|
145
|
+
rescue ChildProcess::Error
|
146
|
+
# If the IO was set to close on exec previously, this call will fail.
|
147
|
+
# That's probably OK, since the user explicitly asked for it to be
|
148
|
+
# closed (at least I have yet to find other cases where this will
|
149
|
+
# happen...)
|
150
|
+
end
|
151
|
+
|
152
|
+
handle
|
153
|
+
end
|
154
|
+
|
155
|
+
def close_handles
|
156
|
+
Lib.close_handle process_info[:hProcess]
|
157
|
+
Lib.close_handle process_info[:hThread]
|
158
|
+
|
159
|
+
if @duplex
|
160
|
+
@stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
|
161
|
+
Lib.close_handle @read_pipe
|
162
|
+
Lib.close_handle @write_pipe
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def quote_if_necessary(str)
|
167
|
+
quote = str.start_with?('"') ? "'" : '"'
|
168
|
+
|
169
|
+
case str
|
170
|
+
when /[\s\\'"]/
|
171
|
+
[quote, str, quote].join
|
172
|
+
else
|
173
|
+
str
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end # ProcessBuilder
|
177
|
+
end # Windows
|
178
|
+
end # ChildProcess
|