childprocess 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/.gitignore +28 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +36 -0
  6. data/CHANGELOG.md +44 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE +20 -0
  9. data/README.md +192 -0
  10. data/Rakefile +61 -0
  11. data/appveyor.yml +43 -0
  12. data/childprocess.gemspec +30 -0
  13. data/lib/childprocess.rb +205 -0
  14. data/lib/childprocess/abstract_io.rb +36 -0
  15. data/lib/childprocess/abstract_process.rb +192 -0
  16. data/lib/childprocess/errors.rb +26 -0
  17. data/lib/childprocess/jruby.rb +56 -0
  18. data/lib/childprocess/jruby/io.rb +16 -0
  19. data/lib/childprocess/jruby/process.rb +159 -0
  20. data/lib/childprocess/jruby/pump.rb +53 -0
  21. data/lib/childprocess/tools/generator.rb +146 -0
  22. data/lib/childprocess/unix.rb +9 -0
  23. data/lib/childprocess/unix/fork_exec_process.rb +70 -0
  24. data/lib/childprocess/unix/io.rb +21 -0
  25. data/lib/childprocess/unix/lib.rb +186 -0
  26. data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
  27. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
  28. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
  29. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
  30. data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
  31. data/lib/childprocess/unix/process.rb +89 -0
  32. data/lib/childprocess/version.rb +3 -0
  33. data/lib/childprocess/windows.rb +33 -0
  34. data/lib/childprocess/windows/handle.rb +91 -0
  35. data/lib/childprocess/windows/io.rb +25 -0
  36. data/lib/childprocess/windows/lib.rb +416 -0
  37. data/lib/childprocess/windows/process.rb +130 -0
  38. data/lib/childprocess/windows/process_builder.rb +175 -0
  39. data/lib/childprocess/windows/structs.rb +149 -0
  40. data/spec/abstract_io_spec.rb +12 -0
  41. data/spec/childprocess_spec.rb +391 -0
  42. data/spec/io_spec.rb +228 -0
  43. data/spec/jruby_spec.rb +24 -0
  44. data/spec/pid_behavior.rb +12 -0
  45. data/spec/platform_detection_spec.rb +86 -0
  46. data/spec/spec_helper.rb +261 -0
  47. data/spec/unix_spec.rb +57 -0
  48. data/spec/windows_spec.rb +23 -0
  49. metadata +179 -0
@@ -0,0 +1,89 @@
1
+ module ChildProcess
2
+ module Unix
3
+ class Process < AbstractProcess
4
+ attr_reader :pid
5
+
6
+ def io
7
+ @io ||= Unix::IO.new
8
+ end
9
+
10
+ def stop(timeout = 3)
11
+ assert_started
12
+ send_term
13
+
14
+ begin
15
+ return poll_for_exit(timeout)
16
+ rescue TimeoutError
17
+ # try next
18
+ end
19
+
20
+ send_kill
21
+ wait
22
+ rescue Errno::ECHILD, Errno::ESRCH
23
+ # handle race condition where process dies between timeout
24
+ # and send_kill
25
+ true
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
+ set_exit_code(status)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def send_term
61
+ send_signal 'TERM'
62
+ end
63
+
64
+ def send_kill
65
+ send_signal 'KILL'
66
+ end
67
+
68
+ def send_signal(sig)
69
+ assert_started
70
+
71
+ log "sending #{sig}"
72
+ ::Process.kill sig, _pid
73
+ end
74
+
75
+ def set_exit_code(status)
76
+ @exit_code = status.exitstatus || status.termsig
77
+ end
78
+
79
+ def _pid
80
+ if leader?
81
+ -@pid # negative pid == process group
82
+ else
83
+ @pid
84
+ end
85
+ end
86
+
87
+ end # Process
88
+ end # Unix
89
+ end # ChildProcess
@@ -0,0 +1,3 @@
1
+ module ChildProcess
2
+ VERSION = '0.8.0'
3
+ end
@@ -0,0 +1,33 @@
1
+ require "ffi"
2
+ require "rbconfig"
3
+
4
+ module ChildProcess
5
+ module Windows
6
+ module Lib
7
+ extend FFI::Library
8
+
9
+ def self.msvcrt_name
10
+ host_part = RbConfig::CONFIG['host_os'].split("_")[1]
11
+ manifest = File.join(RbConfig::CONFIG['bindir'], 'ruby.exe.manifest')
12
+
13
+ if host_part && host_part.to_i > 80 && File.exists?(manifest)
14
+ "msvcr#{host_part}"
15
+ else
16
+ "msvcrt"
17
+ end
18
+ end
19
+
20
+ ffi_lib "kernel32", msvcrt_name
21
+ ffi_convention :stdcall
22
+
23
+
24
+ end # Library
25
+ end # Windows
26
+ end # ChildProcess
27
+
28
+ require "childprocess/windows/lib"
29
+ require "childprocess/windows/structs"
30
+ require "childprocess/windows/handle"
31
+ require "childprocess/windows/io"
32
+ require "childprocess/windows/process_builder"
33
+ require "childprocess/windows/process"
@@ -0,0 +1,91 @@
1
+ module ChildProcess
2
+ module Windows
3
+ class Handle
4
+
5
+ class << self
6
+ private :new
7
+
8
+ def open(pid, access = PROCESS_ALL_ACCESS)
9
+ handle = Lib.open_process(access, false, pid)
10
+
11
+ if handle.null?
12
+ raise Error, Lib.last_error_message
13
+ end
14
+
15
+ h = new(handle, pid)
16
+ return h unless block_given?
17
+
18
+ begin
19
+ yield h
20
+ ensure
21
+ h.close
22
+ end
23
+ end
24
+ end
25
+
26
+ attr_reader :pointer
27
+
28
+ def initialize(pointer, pid)
29
+ unless pointer.kind_of?(FFI::Pointer)
30
+ raise TypeError, "invalid handle: #{pointer.inspect}"
31
+ end
32
+
33
+ if pointer.null?
34
+ raise ArgumentError, "handle is null: #{pointer.inspect}"
35
+ end
36
+
37
+ @pid = pid
38
+ @pointer = pointer
39
+ @closed = false
40
+ end
41
+
42
+ def exit_code
43
+ code_pointer = FFI::MemoryPointer.new :ulong
44
+ ok = Lib.get_exit_code(@pointer, code_pointer)
45
+
46
+ if ok
47
+ code_pointer.get_ulong(0)
48
+ else
49
+ close
50
+ raise Error, Lib.last_error_message
51
+ end
52
+ end
53
+
54
+ def send(signal)
55
+ case signal
56
+ when 0
57
+ exit_code == PROCESS_STILL_ALIVE
58
+ when WIN_SIGINT
59
+ Lib.generate_console_ctrl_event(CTRL_C_EVENT, @pid)
60
+ when WIN_SIGBREAK
61
+ Lib.generate_console_ctrl_event(CTRL_BREAK_EVENT, @pid)
62
+ when WIN_SIGKILL
63
+ ok = Lib.terminate_process(@pointer, @pid)
64
+ Lib.check_error ok
65
+ else
66
+ thread_id = FFI::MemoryPointer.new(:ulong)
67
+ module_handle = Lib.get_module_handle("kernel32")
68
+ proc_address = Lib.get_proc_address(module_handle, "ExitProcess")
69
+
70
+ thread = Lib.create_remote_thread(@pointer, 0, 0, proc_address, 0, 0, thread_id)
71
+ check_error thread
72
+
73
+ Lib.wait_for_single_object(thread, 5)
74
+ true
75
+ end
76
+ end
77
+
78
+ def close
79
+ return if @closed
80
+
81
+ Lib.close_handle(@pointer)
82
+ @closed = true
83
+ end
84
+
85
+ def wait(milliseconds = nil)
86
+ Lib.wait_for_single_object(@pointer, milliseconds || INFINITE)
87
+ end
88
+
89
+ end # Handle
90
+ end # Windows
91
+ end # ChildProcess
@@ -0,0 +1,25 @@
1
+ module ChildProcess
2
+ module Windows
3
+ class IO < AbstractIO
4
+ private
5
+
6
+ def check_type(io)
7
+ return if has_fileno?(io)
8
+ return if has_to_io?(io)
9
+
10
+ raise ArgumentError, "#{io.inspect}:#{io.class} must have :fileno or :to_io"
11
+ end
12
+
13
+ def has_fileno?(io)
14
+ io.respond_to?(:fileno) && io.fileno
15
+ end
16
+
17
+ def has_to_io?(io)
18
+ io.respond_to?(:to_io) && io.to_io.kind_of?(::IO)
19
+ end
20
+
21
+ end # IO
22
+ end # Windows
23
+ end # ChildProcess
24
+
25
+
@@ -0,0 +1,416 @@
1
+ module ChildProcess
2
+ module Windows
3
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
4
+ FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
5
+
6
+ PROCESS_ALL_ACCESS = 0x1F0FFF
7
+ PROCESS_QUERY_INFORMATION = 0x0400
8
+ PROCESS_VM_READ = 0x0010
9
+ PROCESS_STILL_ACTIVE = 259
10
+
11
+ INFINITE = 0xFFFFFFFF
12
+
13
+ WIN_SIGINT = 2
14
+ WIN_SIGBREAK = 3
15
+ WIN_SIGKILL = 9
16
+
17
+ CTRL_C_EVENT = 0
18
+ CTRL_BREAK_EVENT = 1
19
+
20
+ CREATE_BREAKAWAY_FROM_JOB = 0x01000000
21
+ DETACHED_PROCESS = 0x00000008
22
+
23
+ STARTF_USESTDHANDLES = 0x00000100
24
+ INVALID_HANDLE_VALUE = -1
25
+ HANDLE_FLAG_INHERIT = 0x00000001
26
+
27
+ DUPLICATE_SAME_ACCESS = 0x00000002
28
+ CREATE_UNICODE_ENVIRONMENT = 0x00000400
29
+
30
+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
31
+ JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
32
+ JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
33
+ JOB_OBJECT_BASIC_LIMIT_INFORMATION = 2
34
+
35
+ module Lib
36
+ enum :wait_status, [
37
+ :wait_object_0, 0,
38
+ :wait_timeout, 0x102,
39
+ :wait_abandoned, 0x80,
40
+ :wait_failed, 0xFFFFFFFF
41
+ ]
42
+
43
+ #
44
+ # BOOL WINAPI CreateProcess(
45
+ # __in_opt LPCTSTR lpApplicationName,
46
+ # __inout_opt LPTSTR lpCommandLine,
47
+ # __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
48
+ # __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
49
+ # __in BOOL bInheritHandles,
50
+ # __in DWORD dwCreationFlags,
51
+ # __in_opt LPVOID lpEnvironment,
52
+ # __in_opt LPCTSTR lpCurrentDirectory,
53
+ # __in LPSTARTUPINFO lpStartupInfo,
54
+ # __out LPPROCESS_INFORMATION lpProcessInformation
55
+ # );
56
+ #
57
+
58
+ attach_function :create_process, :CreateProcessA, [
59
+ :pointer,
60
+ :pointer,
61
+ :pointer,
62
+ :pointer,
63
+ :bool,
64
+ :ulong,
65
+ :pointer,
66
+ :pointer,
67
+ :pointer,
68
+ :pointer], :bool
69
+
70
+ #
71
+ # DWORD WINAPI FormatMessage(
72
+ # __in DWORD dwFlags,
73
+ # __in_opt LPCVOID lpSource,
74
+ # __in DWORD dwMessageId,
75
+ # __in DWORD dwLanguageId,
76
+ # __out LPTSTR lpBuffer,
77
+ # __in DWORD nSize,
78
+ # __in_opt va_list *Arguments
79
+ # );
80
+ #
81
+
82
+ attach_function :format_message, :FormatMessageA, [
83
+ :ulong,
84
+ :pointer,
85
+ :ulong,
86
+ :ulong,
87
+ :pointer,
88
+ :ulong,
89
+ :pointer], :ulong
90
+
91
+
92
+ attach_function :close_handle, :CloseHandle, [:pointer], :bool
93
+
94
+ #
95
+ # HANDLE WINAPI OpenProcess(
96
+ # __in DWORD dwDesiredAccess,
97
+ # __in BOOL bInheritHandle,
98
+ # __in DWORD dwProcessId
99
+ # );
100
+ #
101
+
102
+ attach_function :open_process, :OpenProcess, [:ulong, :bool, :ulong], :pointer
103
+
104
+ #
105
+ # HANDLE WINAPI CreateJobObject(
106
+ # _In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
107
+ # _In_opt_ LPCTSTR lpName
108
+ # );
109
+ #
110
+
111
+ attach_function :create_job_object, :CreateJobObjectA, [:pointer, :pointer], :pointer
112
+
113
+ #
114
+ # BOOL WINAPI AssignProcessToJobObject(
115
+ # _In_ HANDLE hJob,
116
+ # _In_ HANDLE hProcess
117
+ # );
118
+
119
+ attach_function :assign_process_to_job_object, :AssignProcessToJobObject, [:pointer, :pointer], :bool
120
+
121
+ #
122
+ # BOOL WINAPI SetInformationJobObject(
123
+ # _In_ HANDLE hJob,
124
+ # _In_ JOBOBJECTINFOCLASS JobObjectInfoClass,
125
+ # _In_ LPVOID lpJobObjectInfo,
126
+ # _In_ DWORD cbJobObjectInfoLength
127
+ # );
128
+ #
129
+
130
+ attach_function :set_information_job_object, :SetInformationJobObject, [:pointer, :int, :pointer, :ulong], :bool
131
+
132
+ #
133
+ #
134
+ # DWORD WINAPI WaitForSingleObject(
135
+ # __in HANDLE hHandle,
136
+ # __in DWORD dwMilliseconds
137
+ # );
138
+ #
139
+
140
+ attach_function :wait_for_single_object, :WaitForSingleObject, [:pointer, :ulong], :wait_status, :blocking => true
141
+
142
+ #
143
+ # BOOL WINAPI GetExitCodeProcess(
144
+ # __in HANDLE hProcess,
145
+ # __out LPDWORD lpExitCode
146
+ # );
147
+ #
148
+
149
+ attach_function :get_exit_code, :GetExitCodeProcess, [:pointer, :pointer], :bool
150
+
151
+ #
152
+ # BOOL WINAPI GenerateConsoleCtrlEvent(
153
+ # __in DWORD dwCtrlEvent,
154
+ # __in DWORD dwProcessGroupId
155
+ # );
156
+ #
157
+
158
+ attach_function :generate_console_ctrl_event, :GenerateConsoleCtrlEvent, [:ulong, :ulong], :bool
159
+
160
+ #
161
+ # BOOL WINAPI TerminateProcess(
162
+ # __in HANDLE hProcess,
163
+ # __in UINT uExitCode
164
+ # );
165
+ #
166
+
167
+ attach_function :terminate_process, :TerminateProcess, [:pointer, :uint], :bool
168
+
169
+ #
170
+ # intptr_t _get_osfhandle(
171
+ # int fd
172
+ # );
173
+ #
174
+
175
+ attach_function :get_osfhandle, :_get_osfhandle, [:int], :intptr_t
176
+
177
+ #
178
+ # int _open_osfhandle (
179
+ # intptr_t osfhandle,
180
+ # int flags
181
+ # );
182
+ #
183
+
184
+ attach_function :open_osfhandle, :_open_osfhandle, [:pointer, :int], :int
185
+
186
+ # BOOL WINAPI SetHandleInformation(
187
+ # __in HANDLE hObject,
188
+ # __in DWORD dwMask,
189
+ # __in DWORD dwFlags
190
+ # );
191
+
192
+ attach_function :set_handle_information, :SetHandleInformation, [:pointer, :ulong, :ulong], :bool
193
+
194
+ # BOOL WINAPI GetHandleInformation(
195
+ # __in HANDLE hObject,
196
+ # __out LPDWORD lpdwFlags
197
+ # );
198
+
199
+ attach_function :get_handle_information, :GetHandleInformation, [:pointer, :pointer], :bool
200
+
201
+ # BOOL WINAPI CreatePipe(
202
+ # __out PHANDLE hReadPipe,
203
+ # __out PHANDLE hWritePipe,
204
+ # __in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes,
205
+ # __in DWORD nSize
206
+ # );
207
+
208
+ attach_function :create_pipe, :CreatePipe, [:pointer, :pointer, :pointer, :ulong], :bool
209
+
210
+ #
211
+ # HANDLE WINAPI GetCurrentProcess(void);
212
+ #
213
+
214
+ attach_function :current_process, :GetCurrentProcess, [], :pointer
215
+
216
+ #
217
+ # BOOL WINAPI DuplicateHandle(
218
+ # __in HANDLE hSourceProcessHandle,
219
+ # __in HANDLE hSourceHandle,
220
+ # __in HANDLE hTargetProcessHandle,
221
+ # __out LPHANDLE lpTargetHandle,
222
+ # __in DWORD dwDesiredAccess,
223
+ # __in BOOL bInheritHandle,
224
+ # __in DWORD dwOptions
225
+ # );
226
+ #
227
+
228
+ attach_function :_duplicate_handle, :DuplicateHandle, [
229
+ :pointer,
230
+ :pointer,
231
+ :pointer,
232
+ :pointer,
233
+ :ulong,
234
+ :bool,
235
+ :ulong
236
+ ], :bool
237
+
238
+ class << self
239
+ def kill(signal, *pids)
240
+ case signal
241
+ when 'SIGINT', 'INT', :SIGINT, :INT
242
+ signal = WIN_SIGINT
243
+ when 'SIGBRK', 'BRK', :SIGBREAK, :BRK
244
+ signal = WIN_SIGBREAK
245
+ when 'SIGKILL', 'KILL', :SIGKILL, :KILL
246
+ signal = WIN_SIGKILL
247
+ when 0..9
248
+ # Do nothing
249
+ else
250
+ raise Error, "invalid signal #{signal.inspect}"
251
+ end
252
+
253
+ pids.map { |pid| pid if Lib.send_signal(signal, pid) }.compact
254
+ end
255
+
256
+ def waitpid(pid, flags = 0)
257
+ wait_for_pid(pid, no_hang?(flags))
258
+ end
259
+
260
+ def waitpid2(pid, flags = 0)
261
+ code = wait_for_pid(pid, no_hang?(flags))
262
+
263
+ [pid, code] if code
264
+ end
265
+
266
+ def dont_inherit(file)
267
+ unless file.respond_to?(:fileno)
268
+ raise ArgumentError, "expected #{file.inspect} to respond to :fileno"
269
+ end
270
+
271
+ set_handle_inheritance(handle_for(file.fileno), false)
272
+ end
273
+
274
+ def last_error_message
275
+ errnum = FFI.errno
276
+
277
+ buf = FFI::MemoryPointer.new :char, 512
278
+
279
+ size = format_message(
280
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY,
281
+ nil, errnum, 0, buf, buf.size, nil
282
+ )
283
+
284
+ str = buf.read_string(size).strip
285
+ if errnum == 0
286
+ "Unknown error (Windows says #{str.inspect}, but it did not.)"
287
+ else
288
+ "#{str} (#{errnum})"
289
+ end
290
+ end
291
+
292
+ def each_child_of(pid, &blk)
293
+ raise NotImplementedError
294
+
295
+ # http://stackoverflow.com/questions/1173342/terminate-a-process-tree-c-for-windows?rq=1
296
+
297
+ # for each process entry
298
+ # if pe.th32ParentProcessID == pid
299
+ # Handle.open(pe.pe.th32ProcessId, &blk)
300
+ # end
301
+ #
302
+ end
303
+
304
+ def handle_for(fd_or_io)
305
+ if fd_or_io.kind_of?(IO) || fd_or_io.respond_to?(:fileno)
306
+ if ChildProcess.jruby?
307
+ handle = ChildProcess::JRuby.windows_handle_for(fd_or_io)
308
+ else
309
+ handle = get_osfhandle(fd_or_io.fileno)
310
+ end
311
+ elsif fd_or_io.kind_of?(Integer)
312
+ handle = get_osfhandle(fd_or_io)
313
+ elsif fd_or_io.respond_to?(:to_io)
314
+ io = fd_or_io.to_io
315
+
316
+ unless io.kind_of?(IO)
317
+ raise TypeError, "expected #to_io to return an instance of IO"
318
+ end
319
+
320
+ handle = get_osfhandle(io.fileno)
321
+ else
322
+ raise TypeError, "invalid type: #{fd_or_io.inspect}"
323
+ end
324
+
325
+ if handle == INVALID_HANDLE_VALUE
326
+ raise Error, last_error_message
327
+ end
328
+
329
+ FFI::Pointer.new handle
330
+ end
331
+
332
+ def io_for(handle, flags = File::RDONLY)
333
+ fd = open_osfhandle(handle, flags)
334
+ if fd == -1
335
+ raise Error, last_error_message
336
+ end
337
+
338
+ FFI::IO.for_fd fd, flags
339
+ end
340
+
341
+ def duplicate_handle(handle)
342
+ dup = FFI::MemoryPointer.new(:pointer)
343
+ proc = current_process
344
+
345
+ ok = Lib._duplicate_handle(
346
+ proc,
347
+ handle,
348
+ proc,
349
+ dup,
350
+ 0,
351
+ false,
352
+ DUPLICATE_SAME_ACCESS
353
+ )
354
+
355
+ check_error ok
356
+
357
+ dup.read_pointer
358
+ ensure
359
+ close_handle proc
360
+ end
361
+
362
+ def set_handle_inheritance(handle, bool)
363
+ status = set_handle_information(
364
+ handle,
365
+ HANDLE_FLAG_INHERIT,
366
+ bool ? HANDLE_FLAG_INHERIT : 0
367
+ )
368
+
369
+ check_error status
370
+ end
371
+
372
+ def get_handle_inheritance(handle)
373
+ flags = FFI::MemoryPointer.new(:uint)
374
+
375
+ status = get_handle_information(
376
+ handle,
377
+ flags
378
+ )
379
+
380
+ check_error status
381
+
382
+ flags.read_uint
383
+ end
384
+
385
+ def check_error(bool)
386
+ bool or raise Error, last_error_message
387
+ end
388
+
389
+ def alive?(pid)
390
+ handle = Lib.open_process(PROCESS_ALL_ACCESS, false, pid)
391
+ if handle.null?
392
+ false
393
+ else
394
+ ptr = FFI::MemoryPointer.new :ulong
395
+ Lib.check_error Lib.get_exit_code(handle, ptr)
396
+ ptr.read_ulong == PROCESS_STILL_ACTIVE
397
+ end
398
+ end
399
+
400
+ def no_hang?(flags)
401
+ (flags & Process::WNOHANG) == Process::WNOHANG
402
+ end
403
+
404
+ def wait_for_pid(pid, no_hang)
405
+ code = Handle.open(pid) { |handle|
406
+ handle.wait unless no_hang
407
+ handle.exit_code
408
+ }
409
+
410
+ code if code != PROCESS_STILL_ACTIVE
411
+ end
412
+ end
413
+
414
+ end # Lib
415
+ end # Windows
416
+ end # ChildProcess