childprocess 0.8.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.document +6 -6
- data/.gitignore +28 -28
- data/.rspec +1 -1
- data/.travis.yml +40 -36
- data/CHANGELOG.md +73 -44
- data/Gemfile +21 -15
- data/LICENSE +20 -20
- data/README.md +218 -192
- data/Rakefile +61 -61
- data/appveyor.yml +42 -43
- data/childprocess.gemspec +26 -30
- data/lib/childprocess.rb +210 -205
- 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.rb +56 -56
- 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/tools/generator.rb +145 -145
- data/lib/childprocess/unix.rb +9 -9
- 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/version.rb +3 -3
- data/lib/childprocess/windows.rb +38 -33
- 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/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 +8 -39
@@ -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
|