childprocess 0.2.5 → 0.2.6
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.
- data/lib/childprocess.rb +5 -8
- data/lib/childprocess/version.rb +1 -1
- data/lib/childprocess/windows.rb +2 -3
- data/lib/childprocess/windows/handle.rb +2 -2
- data/lib/childprocess/windows/lib.rb +359 -0
- data/lib/childprocess/windows/process.rb +15 -28
- data/lib/childprocess/windows/process_builder.rb +170 -0
- data/lib/childprocess/windows/structs.rb +75 -73
- data/spec/childprocess_spec.rb +1 -99
- data/spec/io_spec.rb +128 -0
- data/spec/jruby_spec.rb +0 -0
- data/spec/spec_helper.rb +11 -3
- data/spec/windows_spec.rb +0 -0
- metadata +22 -20
- data/lib/childprocess/windows/api.rb +0 -59
- data/lib/childprocess/windows/constants.rb +0 -36
- data/lib/childprocess/windows/functions.rb +0 -322
@@ -0,0 +1,170 @@
|
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
class ProcessBuilder
|
4
|
+
attr_accessor :inherit, :detach, :duplex, :environment, :stdout, :stderr
|
5
|
+
attr_reader :stdin
|
6
|
+
|
7
|
+
def initialize(args)
|
8
|
+
@args = args
|
9
|
+
|
10
|
+
@inherit = false
|
11
|
+
@detach = false
|
12
|
+
@duplex = false
|
13
|
+
@environment = nil
|
14
|
+
|
15
|
+
@stdout = nil
|
16
|
+
@stderr = nil
|
17
|
+
@stdin = nil
|
18
|
+
|
19
|
+
@flags = 0
|
20
|
+
@cmd_ptr = nil
|
21
|
+
@env_ptr = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def start
|
25
|
+
create_command_pointer
|
26
|
+
create_environment_pointer
|
27
|
+
|
28
|
+
setup_detach
|
29
|
+
setup_io
|
30
|
+
|
31
|
+
pid = create_process
|
32
|
+
close_handles
|
33
|
+
|
34
|
+
pid
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def create_command_pointer
|
40
|
+
string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join ' '
|
41
|
+
@cmd_ptr = FFI::MemoryPointer.from_string string
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_environment_pointer
|
45
|
+
return unless @environment.kind_of?(Hash) && @environment.any?
|
46
|
+
|
47
|
+
# inherited env
|
48
|
+
strings = ENV.map { |k,v| "#{k}=#{v}\0" }
|
49
|
+
|
50
|
+
# extras
|
51
|
+
@environment.each do |key, value|
|
52
|
+
if key.include?("=")
|
53
|
+
raise InvalidEnvironmentVariableName, key
|
54
|
+
end
|
55
|
+
|
56
|
+
strings << "#{key}=#{value}\0"
|
57
|
+
end
|
58
|
+
|
59
|
+
strings << "\0" # terminate the env block
|
60
|
+
env_str = strings.join
|
61
|
+
|
62
|
+
@env_ptr = FFI::MemoryPointer.new(:long, env_str.bytesize)
|
63
|
+
@env_ptr.write_bytes env_str, 0, env_str.bytesize
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_process
|
67
|
+
ok = Lib.create_process(
|
68
|
+
nil, # application name
|
69
|
+
@cmd_ptr, # command line
|
70
|
+
nil, # process attributes
|
71
|
+
nil, # thread attributes
|
72
|
+
@inherit, # inherit handles
|
73
|
+
@flags, # creation flags
|
74
|
+
@env_ptr, # environment
|
75
|
+
nil, # current directory
|
76
|
+
startup_info, # startup info
|
77
|
+
process_info # process info
|
78
|
+
)
|
79
|
+
|
80
|
+
ok or raise LaunchError, Lib.last_error_message
|
81
|
+
|
82
|
+
process_info[:dwProcessId]
|
83
|
+
end
|
84
|
+
|
85
|
+
def startup_info
|
86
|
+
@startup_info ||= StartupInfo.new
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_info
|
90
|
+
@process_info ||= ProcessInfo.new
|
91
|
+
end
|
92
|
+
|
93
|
+
def setup_detach
|
94
|
+
@flags |= DETACHED_PROCESS if @detach
|
95
|
+
end
|
96
|
+
|
97
|
+
def setup_io
|
98
|
+
if @stdout || @stderr
|
99
|
+
startup_info[:dwFlags] ||= 0
|
100
|
+
startup_info[:dwFlags] |= STARTF_USESTDHANDLES
|
101
|
+
|
102
|
+
@inherit = true
|
103
|
+
|
104
|
+
if @stdout
|
105
|
+
startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
|
106
|
+
end
|
107
|
+
|
108
|
+
if @stderr
|
109
|
+
startup_info[:hStdError] = std_stream_handle_for(@stderr)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
setup_stdin if @duplex
|
114
|
+
end
|
115
|
+
|
116
|
+
def setup_stdin
|
117
|
+
read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
118
|
+
write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
119
|
+
sa = SecurityAttributes.new(:inherit => true)
|
120
|
+
|
121
|
+
ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
|
122
|
+
Lib.check_error ok
|
123
|
+
|
124
|
+
@read_pipe = read_pipe_ptr.read_pointer
|
125
|
+
@write_pipe = write_pipe_ptr.read_pointer
|
126
|
+
|
127
|
+
Lib.set_handle_inheritance @write_pipe.address, false
|
128
|
+
|
129
|
+
startup_info[:hStdInput] = @read_pipe
|
130
|
+
end
|
131
|
+
|
132
|
+
def close_handles
|
133
|
+
Lib.close_handle process_info[:hProcess]
|
134
|
+
Lib.close_handle process_info[:hThread]
|
135
|
+
|
136
|
+
if @duplex
|
137
|
+
@stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
|
138
|
+
Lib.close_handle @read_pipe
|
139
|
+
Lib.close_handle @write_pipe
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def std_stream_handle_for(io)
|
144
|
+
handle = Lib.handle_for(io.fileno)
|
145
|
+
|
146
|
+
begin
|
147
|
+
Lib.set_handle_inheritance handle, true
|
148
|
+
rescue ChildProcess::Error
|
149
|
+
# If the IO was set to close on exec previously, this call will fail.
|
150
|
+
# That's probably OK, since the user explicitly asked for it to be
|
151
|
+
# closed (at least I have yet to find other cases where this will
|
152
|
+
# happen...)
|
153
|
+
end
|
154
|
+
|
155
|
+
handle
|
156
|
+
end
|
157
|
+
|
158
|
+
def quote_if_necessary(str)
|
159
|
+
quote = str.start_with?('"') ? "'" : '"'
|
160
|
+
|
161
|
+
case str
|
162
|
+
when /[\s\\'"]/
|
163
|
+
[quote, str, quote].join
|
164
|
+
else
|
165
|
+
str
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end # ProcessBuilder
|
169
|
+
end # Windows
|
170
|
+
end # ChildProcess
|
@@ -1,81 +1,83 @@
|
|
1
|
-
module ChildProcess
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
1
|
+
module ChildProcess
|
2
|
+
module Windows
|
3
|
+
# typedef struct _STARTUPINFO {
|
4
|
+
# DWORD cb;
|
5
|
+
# LPTSTR lpReserved;
|
6
|
+
# LPTSTR lpDesktop;
|
7
|
+
# LPTSTR lpTitle;
|
8
|
+
# DWORD dwX;
|
9
|
+
# DWORD dwY;
|
10
|
+
# DWORD dwXSize;
|
11
|
+
# DWORD dwYSize;
|
12
|
+
# DWORD dwXCountChars;
|
13
|
+
# DWORD dwYCountChars;
|
14
|
+
# DWORD dwFillAttribute;
|
15
|
+
# DWORD dwFlags;
|
16
|
+
# WORD wShowWindow;
|
17
|
+
# WORD cbReserved2;
|
18
|
+
# LPBYTE lpReserved2;
|
19
|
+
# HANDLE hStdInput;
|
20
|
+
# HANDLE hStdOutput;
|
21
|
+
# HANDLE hStdError;
|
22
|
+
# } STARTUPINFO, *LPSTARTUPINFO;
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
24
|
+
class StartupInfo < FFI::Struct
|
25
|
+
layout :cb, :ulong,
|
26
|
+
:lpReserved, :pointer,
|
27
|
+
:lpDesktop, :pointer,
|
28
|
+
:lpTitle, :pointer,
|
29
|
+
:dwX, :ulong,
|
30
|
+
:dwY, :ulong,
|
31
|
+
:dwXSize, :ulong,
|
32
|
+
:dwYSize, :ulong,
|
33
|
+
:dwXCountChars, :ulong,
|
34
|
+
:dwYCountChars, :ulong,
|
35
|
+
:dwFillAttribute, :ulong,
|
36
|
+
:dwFlags, :ulong,
|
37
|
+
:wShowWindow, :ushort,
|
38
|
+
:cbReserved2, :ushort,
|
39
|
+
:lpReserved2, :pointer,
|
40
|
+
:hStdInput, :pointer, # void ptr
|
41
|
+
:hStdOutput, :pointer, # void ptr
|
42
|
+
:hStdError, :pointer # void ptr
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
45
|
+
#
|
46
|
+
# typedef struct _PROCESS_INFORMATION {
|
47
|
+
# HANDLE hProcess;
|
48
|
+
# HANDLE hThread;
|
49
|
+
# DWORD dwProcessId;
|
50
|
+
# DWORD dwThreadId;
|
51
|
+
# } PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
|
52
|
+
#
|
52
53
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
54
|
+
class ProcessInfo < FFI::Struct
|
55
|
+
layout :hProcess, :pointer, # void ptr
|
56
|
+
:hThread, :pointer, # void ptr
|
57
|
+
:dwProcessId, :ulong,
|
58
|
+
:dwThreadId, :ulong
|
59
|
+
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
61
|
+
#
|
62
|
+
# typedef struct _SECURITY_ATTRIBUTES {
|
63
|
+
# DWORD nLength;
|
64
|
+
# LPVOID lpSecurityDescriptor;
|
65
|
+
# BOOL bInheritHandle;
|
66
|
+
# } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
|
67
|
+
#
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
class SecurityAttributes < FFI::Struct
|
70
|
+
layout :nLength, :ulong,
|
71
|
+
:lpSecurityDescriptor, :pointer, # void ptr
|
72
|
+
:bInheritHandle, :int
|
72
73
|
|
73
|
-
|
74
|
-
|
74
|
+
def initialize(opts = {})
|
75
|
+
super()
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
77
|
+
self[:nLength] = self.class.size
|
78
|
+
self[:lpSecurityDescriptor] = nil
|
79
|
+
self[:bInheritHandle] = opts[:inherit] ? 1 : 0
|
80
|
+
end
|
79
81
|
end
|
80
|
-
end
|
81
|
-
end
|
82
|
+
end # Windows
|
83
|
+
end # ChildProcess
|
data/spec/childprocess_spec.rb
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
require File.expand_path('../spec_helper', __FILE__)
|
4
4
|
|
5
5
|
describe ChildProcess do
|
6
|
-
EXIT_TIMEOUT = 10
|
7
|
-
|
8
6
|
it "returns self when started" do
|
9
7
|
process = sleeping_ruby
|
10
8
|
|
@@ -46,7 +44,7 @@ describe ChildProcess do
|
|
46
44
|
|
47
45
|
it "accepts a timeout argument to #stop" do
|
48
46
|
process = sleeping_ruby.start
|
49
|
-
process.stop(
|
47
|
+
process.stop(exit_timeout)
|
50
48
|
end
|
51
49
|
|
52
50
|
it "lets child process inherit the environment of the current process" do
|
@@ -112,102 +110,6 @@ describe ChildProcess do
|
|
112
110
|
pending "how do we spec this?"
|
113
111
|
end
|
114
112
|
|
115
|
-
it "can redirect stdout, stderr" do
|
116
|
-
process = ruby(<<-CODE)
|
117
|
-
[STDOUT, STDERR].each_with_index do |io, idx|
|
118
|
-
io.sync = true
|
119
|
-
io.puts idx
|
120
|
-
end
|
121
|
-
|
122
|
-
sleep 0.2
|
123
|
-
CODE
|
124
|
-
|
125
|
-
out = Tempfile.new("stdout-spec")
|
126
|
-
err = Tempfile.new("stderr-spec")
|
127
|
-
|
128
|
-
begin
|
129
|
-
process.io.stdout = out
|
130
|
-
process.io.stderr = err
|
131
|
-
|
132
|
-
process.start
|
133
|
-
process.io.stdin.should be_nil
|
134
|
-
process.wait
|
135
|
-
|
136
|
-
out.rewind
|
137
|
-
err.rewind
|
138
|
-
|
139
|
-
out.read.should == "0\n"
|
140
|
-
err.read.should == "1\n"
|
141
|
-
ensure
|
142
|
-
out.close
|
143
|
-
err.close
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
it "can redirect stdout only" do
|
148
|
-
process = ruby(<<-CODE)
|
149
|
-
[STDOUT, STDERR].each_with_index do |io, idx|
|
150
|
-
io.sync = true
|
151
|
-
io.puts idx
|
152
|
-
end
|
153
|
-
|
154
|
-
sleep 0.2
|
155
|
-
CODE
|
156
|
-
|
157
|
-
out = Tempfile.new("stdout-spec")
|
158
|
-
|
159
|
-
begin
|
160
|
-
process.io.stdout = out
|
161
|
-
|
162
|
-
process.start
|
163
|
-
process.wait
|
164
|
-
|
165
|
-
out.rewind
|
166
|
-
|
167
|
-
out.read.should == "0\n"
|
168
|
-
ensure
|
169
|
-
out.close
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
it "can write to stdin if duplex = true" do
|
174
|
-
process = cat
|
175
|
-
|
176
|
-
out = Tempfile.new("duplex")
|
177
|
-
out.sync = true
|
178
|
-
|
179
|
-
begin
|
180
|
-
process.io.stdout = out
|
181
|
-
process.io.stderr = out
|
182
|
-
process.duplex = true
|
183
|
-
|
184
|
-
process.start
|
185
|
-
process.io.stdin.puts "hello world"
|
186
|
-
process.io.stdin.close
|
187
|
-
|
188
|
-
process.poll_for_exit(EXIT_TIMEOUT)
|
189
|
-
|
190
|
-
out.rewind
|
191
|
-
out.read.should == "hello world\n"
|
192
|
-
ensure
|
193
|
-
out.close
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
it "can set close-on-exec when IO is inherited" do
|
198
|
-
server = TCPServer.new("localhost", 4433)
|
199
|
-
ChildProcess.close_on_exec server
|
200
|
-
|
201
|
-
process = sleeping_ruby
|
202
|
-
process.io.inherit!
|
203
|
-
|
204
|
-
process.start
|
205
|
-
sleep 0.5 # give the forked process a chance to exec() (which closes the fd)
|
206
|
-
|
207
|
-
server.close
|
208
|
-
lambda { TCPServer.new("localhost", 4433).close }.should_not raise_error
|
209
|
-
end
|
210
|
-
|
211
113
|
it "preserves Dir.pwd in the child" do
|
212
114
|
Tempfile.open("dir-spec-out") do |file|
|
213
115
|
process = ruby("print Dir.pwd")
|
data/spec/io_spec.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe ChildProcess do
|
4
|
+
it "can redirect stdout, stderr" do
|
5
|
+
process = ruby(<<-CODE)
|
6
|
+
[STDOUT, STDERR].each_with_index do |io, idx|
|
7
|
+
io.sync = true
|
8
|
+
io.puts idx
|
9
|
+
end
|
10
|
+
CODE
|
11
|
+
|
12
|
+
out = Tempfile.new("stdout-spec")
|
13
|
+
err = Tempfile.new("stderr-spec")
|
14
|
+
|
15
|
+
begin
|
16
|
+
process.io.stdout = out
|
17
|
+
process.io.stderr = err
|
18
|
+
|
19
|
+
process.start
|
20
|
+
process.io.stdin.should be_nil
|
21
|
+
process.wait
|
22
|
+
|
23
|
+
out.rewind
|
24
|
+
err.rewind
|
25
|
+
|
26
|
+
out.read.should == "0\n"
|
27
|
+
err.read.should == "1\n"
|
28
|
+
ensure
|
29
|
+
out.close
|
30
|
+
err.close
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can redirect stdout only" do
|
35
|
+
process = ruby(<<-CODE)
|
36
|
+
[STDOUT, STDERR].each_with_index do |io, idx|
|
37
|
+
io.sync = true
|
38
|
+
io.puts idx
|
39
|
+
end
|
40
|
+
CODE
|
41
|
+
|
42
|
+
out = Tempfile.new("stdout-spec")
|
43
|
+
|
44
|
+
begin
|
45
|
+
process.io.stdout = out
|
46
|
+
|
47
|
+
process.start
|
48
|
+
process.wait
|
49
|
+
|
50
|
+
out.rewind
|
51
|
+
out.read.should == "0\n"
|
52
|
+
ensure
|
53
|
+
out.close
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "can write to stdin if duplex = true" do
|
58
|
+
process = cat
|
59
|
+
|
60
|
+
out = Tempfile.new("duplex")
|
61
|
+
out.sync = true
|
62
|
+
|
63
|
+
begin
|
64
|
+
process.io.stdout = out
|
65
|
+
process.io.stderr = out
|
66
|
+
process.duplex = true
|
67
|
+
|
68
|
+
process.start
|
69
|
+
process.io.stdin.puts "hello world"
|
70
|
+
process.io.stdin.close
|
71
|
+
|
72
|
+
process.poll_for_exit(exit_timeout)
|
73
|
+
|
74
|
+
out.rewind
|
75
|
+
out.read.should == "hello world\n"
|
76
|
+
ensure
|
77
|
+
out.close
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# this works on JRuby 1.6.5 on my Mac, but for some reason
|
83
|
+
# hangs on Travis (running 1.6.5.1 + OpenJDK).
|
84
|
+
#
|
85
|
+
# http://travis-ci.org/#!/jarib/childprocess/jobs/487331
|
86
|
+
#
|
87
|
+
|
88
|
+
it "works with pipes", :jruby => false do
|
89
|
+
process = ruby(<<-CODE)
|
90
|
+
STDOUT.puts "stdout"
|
91
|
+
STDERR.puts "stderr"
|
92
|
+
CODE
|
93
|
+
|
94
|
+
stdout, stdout_w = IO.pipe
|
95
|
+
stderr, stderr_w = IO.pipe
|
96
|
+
|
97
|
+
process.io.stdout = stdout_w
|
98
|
+
process.io.stderr = stderr_w
|
99
|
+
|
100
|
+
process.duplex = true
|
101
|
+
|
102
|
+
process.start
|
103
|
+
process.wait
|
104
|
+
|
105
|
+
# write streams are closed *after* the process
|
106
|
+
# has exited - otherwise it won't work on JRuby
|
107
|
+
# with the current Process implementation
|
108
|
+
|
109
|
+
stdout_w.close
|
110
|
+
stderr_w.close
|
111
|
+
|
112
|
+
stdout.read.should == "stdout\n"
|
113
|
+
stderr.read.should == "stderr\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
it "can set close-on-exec when IO is inherited" do
|
117
|
+
server = TCPServer.new("localhost", 4433)
|
118
|
+
ChildProcess.close_on_exec server
|
119
|
+
|
120
|
+
process = sleeping_ruby
|
121
|
+
process.io.inherit!
|
122
|
+
|
123
|
+
process.start
|
124
|
+
server.close
|
125
|
+
|
126
|
+
lambda { TCPServer.new("localhost", 4433).close }.should_not raise_error
|
127
|
+
end
|
128
|
+
end
|