aggkit 0.2.5
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/.dockerignore +20 -0
- data/.gitignore +109 -0
- data/.gitlab-ci.yml +66 -0
- data/.rspec +4 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +30 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +55 -0
- data/README.md +96 -0
- data/aggkit.gemspec +38 -0
- data/bin/agg +167 -0
- data/bin/aggconsul +222 -0
- data/bin/agglock +71 -0
- data/bin/aggmerge +118 -0
- data/bin/aggwait +262 -0
- data/bin/consul.rb +222 -0
- data/bin/locker.rb +71 -0
- data/bin/merger.rb +118 -0
- data/bin/terminator.rb +71 -0
- data/bin/waiter.rb +262 -0
- data/docker/Dockerfile +112 -0
- data/docker/docker-compose.yml +12 -0
- data/docker/down.sh +4 -0
- data/docker/run_tests.sh +23 -0
- data/lib/aggkit/childprocess/abstract_io.rb +38 -0
- data/lib/aggkit/childprocess/abstract_process.rb +194 -0
- data/lib/aggkit/childprocess/errors.rb +28 -0
- data/lib/aggkit/childprocess/jruby/io.rb +17 -0
- data/lib/aggkit/childprocess/jruby/process.rb +161 -0
- data/lib/aggkit/childprocess/jruby/pump.rb +55 -0
- data/lib/aggkit/childprocess/jruby.rb +58 -0
- data/lib/aggkit/childprocess/tools/generator.rb +148 -0
- data/lib/aggkit/childprocess/unix/fork_exec_process.rb +72 -0
- data/lib/aggkit/childprocess/unix/io.rb +22 -0
- data/lib/aggkit/childprocess/unix/lib.rb +188 -0
- data/lib/aggkit/childprocess/unix/platform/i386-linux.rb +14 -0
- data/lib/aggkit/childprocess/unix/platform/i386-solaris.rb +13 -0
- data/lib/aggkit/childprocess/unix/platform/x86_64-linux.rb +14 -0
- data/lib/aggkit/childprocess/unix/platform/x86_64-macosx.rb +13 -0
- data/lib/aggkit/childprocess/unix/posix_spawn_process.rb +135 -0
- data/lib/aggkit/childprocess/unix/process.rb +91 -0
- data/lib/aggkit/childprocess/unix.rb +11 -0
- data/lib/aggkit/childprocess/version.rb +5 -0
- data/lib/aggkit/childprocess/windows/handle.rb +93 -0
- data/lib/aggkit/childprocess/windows/io.rb +25 -0
- data/lib/aggkit/childprocess/windows/lib.rb +418 -0
- data/lib/aggkit/childprocess/windows/process.rb +132 -0
- data/lib/aggkit/childprocess/windows/process_builder.rb +177 -0
- data/lib/aggkit/childprocess/windows/structs.rb +151 -0
- data/lib/aggkit/childprocess/windows.rb +35 -0
- data/lib/aggkit/childprocess.rb +213 -0
- data/lib/aggkit/env.rb +219 -0
- data/lib/aggkit/runner.rb +80 -0
- data/lib/aggkit/version.rb +5 -0
- data/lib/aggkit/watcher.rb +239 -0
- data/lib/aggkit.rb +15 -0
- metadata +196 -0
@@ -0,0 +1,418 @@
|
|
1
|
+
module Aggkit
|
2
|
+
module ChildProcess
|
3
|
+
module Windows
|
4
|
+
FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
|
5
|
+
FORMAT_MESSAGE_ARGUMENT_ARRAY = 0x00002000
|
6
|
+
|
7
|
+
PROCESS_ALL_ACCESS = 0x1F0FFF
|
8
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
9
|
+
PROCESS_VM_READ = 0x0010
|
10
|
+
PROCESS_STILL_ACTIVE = 259
|
11
|
+
|
12
|
+
INFINITE = 0xFFFFFFFF
|
13
|
+
|
14
|
+
WIN_SIGINT = 2
|
15
|
+
WIN_SIGBREAK = 3
|
16
|
+
WIN_SIGKILL = 9
|
17
|
+
|
18
|
+
CTRL_C_EVENT = 0
|
19
|
+
CTRL_BREAK_EVENT = 1
|
20
|
+
|
21
|
+
CREATE_BREAKAWAY_FROM_JOB = 0x01000000
|
22
|
+
DETACHED_PROCESS = 0x00000008
|
23
|
+
|
24
|
+
STARTF_USESTDHANDLES = 0x00000100
|
25
|
+
INVALID_HANDLE_VALUE = -1
|
26
|
+
HANDLE_FLAG_INHERIT = 0x00000001
|
27
|
+
|
28
|
+
DUPLICATE_SAME_ACCESS = 0x00000002
|
29
|
+
CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
30
|
+
|
31
|
+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
|
32
|
+
JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
|
33
|
+
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION = 9
|
34
|
+
JOB_OBJECT_BASIC_LIMIT_INFORMATION = 2
|
35
|
+
|
36
|
+
module Lib
|
37
|
+
enum :wait_status, [
|
38
|
+
:wait_object_0, 0,
|
39
|
+
:wait_timeout, 0x102,
|
40
|
+
:wait_abandoned, 0x80,
|
41
|
+
:wait_failed, 0xFFFFFFFF
|
42
|
+
]
|
43
|
+
|
44
|
+
#
|
45
|
+
# BOOL WINAPI CreateProcess(
|
46
|
+
# __in_opt LPCTSTR lpApplicationName,
|
47
|
+
# __inout_opt LPTSTR lpCommandLine,
|
48
|
+
# __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
|
49
|
+
# __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
|
50
|
+
# __in BOOL bInheritHandles,
|
51
|
+
# __in DWORD dwCreationFlags,
|
52
|
+
# __in_opt LPVOID lpEnvironment,
|
53
|
+
# __in_opt LPCTSTR lpCurrentDirectory,
|
54
|
+
# __in LPSTARTUPINFO lpStartupInfo,
|
55
|
+
# __out LPPROCESS_INFORMATION lpProcessInformation
|
56
|
+
# );
|
57
|
+
#
|
58
|
+
|
59
|
+
attach_function :create_process, :CreateProcessA, [
|
60
|
+
:pointer,
|
61
|
+
:pointer,
|
62
|
+
:pointer,
|
63
|
+
:pointer,
|
64
|
+
:bool,
|
65
|
+
:ulong,
|
66
|
+
:pointer,
|
67
|
+
:pointer,
|
68
|
+
:pointer,
|
69
|
+
:pointer], :bool
|
70
|
+
|
71
|
+
#
|
72
|
+
# DWORD WINAPI FormatMessage(
|
73
|
+
# __in DWORD dwFlags,
|
74
|
+
# __in_opt LPCVOID lpSource,
|
75
|
+
# __in DWORD dwMessageId,
|
76
|
+
# __in DWORD dwLanguageId,
|
77
|
+
# __out LPTSTR lpBuffer,
|
78
|
+
# __in DWORD nSize,
|
79
|
+
# __in_opt va_list *Arguments
|
80
|
+
# );
|
81
|
+
#
|
82
|
+
|
83
|
+
attach_function :format_message, :FormatMessageA, [
|
84
|
+
:ulong,
|
85
|
+
:pointer,
|
86
|
+
:ulong,
|
87
|
+
:ulong,
|
88
|
+
:pointer,
|
89
|
+
:ulong,
|
90
|
+
:pointer], :ulong
|
91
|
+
|
92
|
+
|
93
|
+
attach_function :close_handle, :CloseHandle, [:pointer], :bool
|
94
|
+
|
95
|
+
#
|
96
|
+
# HANDLE WINAPI OpenProcess(
|
97
|
+
# __in DWORD dwDesiredAccess,
|
98
|
+
# __in BOOL bInheritHandle,
|
99
|
+
# __in DWORD dwProcessId
|
100
|
+
# );
|
101
|
+
#
|
102
|
+
|
103
|
+
attach_function :open_process, :OpenProcess, [:ulong, :bool, :ulong], :pointer
|
104
|
+
|
105
|
+
#
|
106
|
+
# HANDLE WINAPI CreateJobObject(
|
107
|
+
# _In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
|
108
|
+
# _In_opt_ LPCTSTR lpName
|
109
|
+
# );
|
110
|
+
#
|
111
|
+
|
112
|
+
attach_function :create_job_object, :CreateJobObjectA, [:pointer, :pointer], :pointer
|
113
|
+
|
114
|
+
#
|
115
|
+
# BOOL WINAPI AssignProcessToJobObject(
|
116
|
+
# _In_ HANDLE hJob,
|
117
|
+
# _In_ HANDLE hProcess
|
118
|
+
# );
|
119
|
+
|
120
|
+
attach_function :assign_process_to_job_object, :AssignProcessToJobObject, [:pointer, :pointer], :bool
|
121
|
+
|
122
|
+
#
|
123
|
+
# BOOL WINAPI SetInformationJobObject(
|
124
|
+
# _In_ HANDLE hJob,
|
125
|
+
# _In_ JOBOBJECTINFOCLASS JobObjectInfoClass,
|
126
|
+
# _In_ LPVOID lpJobObjectInfo,
|
127
|
+
# _In_ DWORD cbJobObjectInfoLength
|
128
|
+
# );
|
129
|
+
#
|
130
|
+
|
131
|
+
attach_function :set_information_job_object, :SetInformationJobObject, [:pointer, :int, :pointer, :ulong], :bool
|
132
|
+
|
133
|
+
#
|
134
|
+
#
|
135
|
+
# DWORD WINAPI WaitForSingleObject(
|
136
|
+
# __in HANDLE hHandle,
|
137
|
+
# __in DWORD dwMilliseconds
|
138
|
+
# );
|
139
|
+
#
|
140
|
+
|
141
|
+
attach_function :wait_for_single_object, :WaitForSingleObject, [:pointer, :ulong], :wait_status, :blocking => true
|
142
|
+
|
143
|
+
#
|
144
|
+
# BOOL WINAPI GetExitCodeProcess(
|
145
|
+
# __in HANDLE hProcess,
|
146
|
+
# __out LPDWORD lpExitCode
|
147
|
+
# );
|
148
|
+
#
|
149
|
+
|
150
|
+
attach_function :get_exit_code, :GetExitCodeProcess, [:pointer, :pointer], :bool
|
151
|
+
|
152
|
+
#
|
153
|
+
# BOOL WINAPI GenerateConsoleCtrlEvent(
|
154
|
+
# __in DWORD dwCtrlEvent,
|
155
|
+
# __in DWORD dwProcessGroupId
|
156
|
+
# );
|
157
|
+
#
|
158
|
+
|
159
|
+
attach_function :generate_console_ctrl_event, :GenerateConsoleCtrlEvent, [:ulong, :ulong], :bool
|
160
|
+
|
161
|
+
#
|
162
|
+
# BOOL WINAPI TerminateProcess(
|
163
|
+
# __in HANDLE hProcess,
|
164
|
+
# __in UINT uExitCode
|
165
|
+
# );
|
166
|
+
#
|
167
|
+
|
168
|
+
attach_function :terminate_process, :TerminateProcess, [:pointer, :uint], :bool
|
169
|
+
|
170
|
+
#
|
171
|
+
# intptr_t _get_osfhandle(
|
172
|
+
# int fd
|
173
|
+
# );
|
174
|
+
#
|
175
|
+
|
176
|
+
attach_function :get_osfhandle, :_get_osfhandle, [:int], :intptr_t
|
177
|
+
|
178
|
+
#
|
179
|
+
# int _open_osfhandle (
|
180
|
+
# intptr_t osfhandle,
|
181
|
+
# int flags
|
182
|
+
# );
|
183
|
+
#
|
184
|
+
|
185
|
+
attach_function :open_osfhandle, :_open_osfhandle, [:pointer, :int], :int
|
186
|
+
|
187
|
+
# BOOL WINAPI SetHandleInformation(
|
188
|
+
# __in HANDLE hObject,
|
189
|
+
# __in DWORD dwMask,
|
190
|
+
# __in DWORD dwFlags
|
191
|
+
# );
|
192
|
+
|
193
|
+
attach_function :set_handle_information, :SetHandleInformation, [:pointer, :ulong, :ulong], :bool
|
194
|
+
|
195
|
+
# BOOL WINAPI GetHandleInformation(
|
196
|
+
# __in HANDLE hObject,
|
197
|
+
# __out LPDWORD lpdwFlags
|
198
|
+
# );
|
199
|
+
|
200
|
+
attach_function :get_handle_information, :GetHandleInformation, [:pointer, :pointer], :bool
|
201
|
+
|
202
|
+
# BOOL WINAPI CreatePipe(
|
203
|
+
# __out PHANDLE hReadPipe,
|
204
|
+
# __out PHANDLE hWritePipe,
|
205
|
+
# __in_opt LPSECURITY_ATTRIBUTES lpPipeAttributes,
|
206
|
+
# __in DWORD nSize
|
207
|
+
# );
|
208
|
+
|
209
|
+
attach_function :create_pipe, :CreatePipe, [:pointer, :pointer, :pointer, :ulong], :bool
|
210
|
+
|
211
|
+
#
|
212
|
+
# HANDLE WINAPI GetCurrentProcess(void);
|
213
|
+
#
|
214
|
+
|
215
|
+
attach_function :current_process, :GetCurrentProcess, [], :pointer
|
216
|
+
|
217
|
+
#
|
218
|
+
# BOOL WINAPI DuplicateHandle(
|
219
|
+
# __in HANDLE hSourceProcessHandle,
|
220
|
+
# __in HANDLE hSourceHandle,
|
221
|
+
# __in HANDLE hTargetProcessHandle,
|
222
|
+
# __out LPHANDLE lpTargetHandle,
|
223
|
+
# __in DWORD dwDesiredAccess,
|
224
|
+
# __in BOOL bInheritHandle,
|
225
|
+
# __in DWORD dwOptions
|
226
|
+
# );
|
227
|
+
#
|
228
|
+
|
229
|
+
attach_function :_duplicate_handle, :DuplicateHandle, [
|
230
|
+
:pointer,
|
231
|
+
:pointer,
|
232
|
+
:pointer,
|
233
|
+
:pointer,
|
234
|
+
:ulong,
|
235
|
+
:bool,
|
236
|
+
:ulong
|
237
|
+
], :bool
|
238
|
+
|
239
|
+
class << self
|
240
|
+
def kill(signal, *pids)
|
241
|
+
case signal
|
242
|
+
when 'SIGINT', 'INT', :SIGINT, :INT
|
243
|
+
signal = WIN_SIGINT
|
244
|
+
when 'SIGBRK', 'BRK', :SIGBREAK, :BRK
|
245
|
+
signal = WIN_SIGBREAK
|
246
|
+
when 'SIGKILL', 'KILL', :SIGKILL, :KILL
|
247
|
+
signal = WIN_SIGKILL
|
248
|
+
when 0..9
|
249
|
+
# Do nothing
|
250
|
+
else
|
251
|
+
raise Error, "invalid signal #{signal.inspect}"
|
252
|
+
end
|
253
|
+
|
254
|
+
pids.map { |pid| pid if Lib.send_signal(signal, pid) }.compact
|
255
|
+
end
|
256
|
+
|
257
|
+
def waitpid(pid, flags = 0)
|
258
|
+
wait_for_pid(pid, no_hang?(flags))
|
259
|
+
end
|
260
|
+
|
261
|
+
def waitpid2(pid, flags = 0)
|
262
|
+
code = wait_for_pid(pid, no_hang?(flags))
|
263
|
+
|
264
|
+
[pid, code] if code
|
265
|
+
end
|
266
|
+
|
267
|
+
def dont_inherit(file)
|
268
|
+
unless file.respond_to?(:fileno)
|
269
|
+
raise ArgumentError, "expected #{file.inspect} to respond to :fileno"
|
270
|
+
end
|
271
|
+
|
272
|
+
set_handle_inheritance(handle_for(file.fileno), false)
|
273
|
+
end
|
274
|
+
|
275
|
+
def last_error_message
|
276
|
+
errnum = FFI.errno
|
277
|
+
|
278
|
+
buf = FFI::MemoryPointer.new :char, 512
|
279
|
+
|
280
|
+
size = format_message(
|
281
|
+
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY,
|
282
|
+
nil, errnum, 0, buf, buf.size, nil
|
283
|
+
)
|
284
|
+
|
285
|
+
str = buf.read_string(size).strip
|
286
|
+
if errnum == 0
|
287
|
+
"Unknown error (Windows says #{str.inspect}, but it did not.)"
|
288
|
+
else
|
289
|
+
"#{str} (#{errnum})"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def each_child_of(pid, &blk)
|
294
|
+
raise NotImplementedError
|
295
|
+
|
296
|
+
# http://stackoverflow.com/questions/1173342/terminate-a-process-tree-c-for-windows?rq=1
|
297
|
+
|
298
|
+
# for each process entry
|
299
|
+
# if pe.th32ParentProcessID == pid
|
300
|
+
# Handle.open(pe.pe.th32ProcessId, &blk)
|
301
|
+
# end
|
302
|
+
#
|
303
|
+
end
|
304
|
+
|
305
|
+
def handle_for(fd_or_io)
|
306
|
+
if fd_or_io.kind_of?(IO) || fd_or_io.respond_to?(:fileno)
|
307
|
+
if ChildProcess.jruby?
|
308
|
+
handle = ChildProcess::JRuby.windows_handle_for(fd_or_io)
|
309
|
+
else
|
310
|
+
handle = get_osfhandle(fd_or_io.fileno)
|
311
|
+
end
|
312
|
+
elsif fd_or_io.kind_of?(Integer)
|
313
|
+
handle = get_osfhandle(fd_or_io)
|
314
|
+
elsif fd_or_io.respond_to?(:to_io)
|
315
|
+
io = fd_or_io.to_io
|
316
|
+
|
317
|
+
unless io.kind_of?(IO)
|
318
|
+
raise TypeError, "expected #to_io to return an instance of IO"
|
319
|
+
end
|
320
|
+
|
321
|
+
handle = get_osfhandle(io.fileno)
|
322
|
+
else
|
323
|
+
raise TypeError, "invalid type: #{fd_or_io.inspect}"
|
324
|
+
end
|
325
|
+
|
326
|
+
if handle == INVALID_HANDLE_VALUE
|
327
|
+
raise Error, last_error_message
|
328
|
+
end
|
329
|
+
|
330
|
+
FFI::Pointer.new handle
|
331
|
+
end
|
332
|
+
|
333
|
+
def io_for(handle, flags = File::RDONLY)
|
334
|
+
fd = open_osfhandle(handle, flags)
|
335
|
+
if fd == -1
|
336
|
+
raise Error, last_error_message
|
337
|
+
end
|
338
|
+
|
339
|
+
FFI::IO.for_fd fd, flags
|
340
|
+
end
|
341
|
+
|
342
|
+
def duplicate_handle(handle)
|
343
|
+
dup = FFI::MemoryPointer.new(:pointer)
|
344
|
+
proc = current_process
|
345
|
+
|
346
|
+
ok = Lib._duplicate_handle(
|
347
|
+
proc,
|
348
|
+
handle,
|
349
|
+
proc,
|
350
|
+
dup,
|
351
|
+
0,
|
352
|
+
false,
|
353
|
+
DUPLICATE_SAME_ACCESS
|
354
|
+
)
|
355
|
+
|
356
|
+
check_error ok
|
357
|
+
|
358
|
+
dup.read_pointer
|
359
|
+
ensure
|
360
|
+
close_handle proc
|
361
|
+
end
|
362
|
+
|
363
|
+
def set_handle_inheritance(handle, bool)
|
364
|
+
status = set_handle_information(
|
365
|
+
handle,
|
366
|
+
HANDLE_FLAG_INHERIT,
|
367
|
+
bool ? HANDLE_FLAG_INHERIT : 0
|
368
|
+
)
|
369
|
+
|
370
|
+
check_error status
|
371
|
+
end
|
372
|
+
|
373
|
+
def get_handle_inheritance(handle)
|
374
|
+
flags = FFI::MemoryPointer.new(:uint)
|
375
|
+
|
376
|
+
status = get_handle_information(
|
377
|
+
handle,
|
378
|
+
flags
|
379
|
+
)
|
380
|
+
|
381
|
+
check_error status
|
382
|
+
|
383
|
+
flags.read_uint
|
384
|
+
end
|
385
|
+
|
386
|
+
def check_error(bool)
|
387
|
+
bool or raise Error, last_error_message
|
388
|
+
end
|
389
|
+
|
390
|
+
def alive?(pid)
|
391
|
+
handle = Lib.open_process(PROCESS_ALL_ACCESS, false, pid)
|
392
|
+
if handle.null?
|
393
|
+
false
|
394
|
+
else
|
395
|
+
ptr = FFI::MemoryPointer.new :ulong
|
396
|
+
Lib.check_error Lib.get_exit_code(handle, ptr)
|
397
|
+
ptr.read_ulong == PROCESS_STILL_ACTIVE
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def no_hang?(flags)
|
402
|
+
(flags & Process::WNOHANG) == Process::WNOHANG
|
403
|
+
end
|
404
|
+
|
405
|
+
def wait_for_pid(pid, no_hang)
|
406
|
+
code = Handle.open(pid) { |handle|
|
407
|
+
handle.wait unless no_hang
|
408
|
+
handle.exit_code
|
409
|
+
}
|
410
|
+
|
411
|
+
code if code != PROCESS_STILL_ACTIVE
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
end # Lib
|
416
|
+
end # Windows
|
417
|
+
end # ChildProcess
|
418
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Aggkit
|
2
|
+
module ChildProcess
|
3
|
+
module Windows
|
4
|
+
class Process < AbstractProcess
|
5
|
+
|
6
|
+
attr_reader :pid
|
7
|
+
|
8
|
+
def io
|
9
|
+
@io ||= Windows::IO.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def stop(timeout = 3)
|
13
|
+
assert_started
|
14
|
+
|
15
|
+
log "sending KILL"
|
16
|
+
@handle.send(WIN_SIGKILL)
|
17
|
+
|
18
|
+
poll_for_exit(timeout)
|
19
|
+
ensure
|
20
|
+
close_handle
|
21
|
+
close_job_if_necessary
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait
|
25
|
+
if exited?
|
26
|
+
exit_code
|
27
|
+
else
|
28
|
+
@handle.wait
|
29
|
+
@exit_code = @handle.exit_code
|
30
|
+
|
31
|
+
close_handle
|
32
|
+
close_job_if_necessary
|
33
|
+
|
34
|
+
@exit_code
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def exited?
|
39
|
+
return true if @exit_code
|
40
|
+
assert_started
|
41
|
+
|
42
|
+
code = @handle.exit_code
|
43
|
+
exited = code != PROCESS_STILL_ACTIVE
|
44
|
+
|
45
|
+
log(:exited? => exited, :code => code)
|
46
|
+
|
47
|
+
if exited
|
48
|
+
@exit_code = code
|
49
|
+
close_handle
|
50
|
+
close_job_if_necessary
|
51
|
+
end
|
52
|
+
|
53
|
+
exited
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def launch_process
|
59
|
+
builder = ProcessBuilder.new(@args)
|
60
|
+
builder.leader = leader?
|
61
|
+
builder.detach = detach?
|
62
|
+
builder.duplex = duplex?
|
63
|
+
builder.environment = @environment unless @environment.empty?
|
64
|
+
builder.cwd = @cwd
|
65
|
+
|
66
|
+
if @io
|
67
|
+
builder.stdout = @io.stdout
|
68
|
+
builder.stderr = @io.stderr
|
69
|
+
end
|
70
|
+
|
71
|
+
@pid = builder.start
|
72
|
+
@handle = Handle.open @pid
|
73
|
+
|
74
|
+
if leader?
|
75
|
+
@job = Job.new
|
76
|
+
@job << @handle
|
77
|
+
end
|
78
|
+
|
79
|
+
if duplex?
|
80
|
+
raise Error, "no stdin stream" unless builder.stdin
|
81
|
+
io._stdin = builder.stdin
|
82
|
+
end
|
83
|
+
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def close_handle
|
88
|
+
@handle.close
|
89
|
+
end
|
90
|
+
|
91
|
+
def close_job_if_necessary
|
92
|
+
@job.close if leader?
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
class Job
|
97
|
+
def initialize
|
98
|
+
@pointer = Lib.create_job_object(nil, nil)
|
99
|
+
|
100
|
+
if @pointer.nil? || @pointer.null?
|
101
|
+
raise Error, "unable to create job object"
|
102
|
+
end
|
103
|
+
|
104
|
+
basic = JobObjectBasicLimitInformation.new
|
105
|
+
basic[:LimitFlags] = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
106
|
+
|
107
|
+
extended = JobObjectExtendedLimitInformation.new
|
108
|
+
extended[:BasicLimitInformation] = basic
|
109
|
+
|
110
|
+
ret = Lib.set_information_job_object(
|
111
|
+
@pointer,
|
112
|
+
JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
|
113
|
+
extended,
|
114
|
+
extended.size
|
115
|
+
)
|
116
|
+
|
117
|
+
Lib.check_error ret
|
118
|
+
end
|
119
|
+
|
120
|
+
def <<(handle)
|
121
|
+
Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
|
122
|
+
end
|
123
|
+
|
124
|
+
def close
|
125
|
+
Lib.close_handle @pointer
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end # Process
|
130
|
+
end # Windows
|
131
|
+
end # ChildProcess
|
132
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Aggkit
|
2
|
+
module ChildProcess
|
3
|
+
module Windows
|
4
|
+
class ProcessBuilder
|
5
|
+
attr_accessor :leader, :detach, :duplex, :environment, :stdout, :stderr, :cwd
|
6
|
+
attr_reader :stdin
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@args = args
|
10
|
+
|
11
|
+
@detach = false
|
12
|
+
@duplex = false
|
13
|
+
@environment = nil
|
14
|
+
@cwd = nil
|
15
|
+
|
16
|
+
@stdout = nil
|
17
|
+
@stderr = nil
|
18
|
+
@stdin = nil
|
19
|
+
|
20
|
+
@flags = 0
|
21
|
+
@job_ptr = nil
|
22
|
+
@cmd_ptr = nil
|
23
|
+
@env_ptr = nil
|
24
|
+
@cwd_ptr = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def start
|
28
|
+
create_command_pointer
|
29
|
+
create_environment_pointer
|
30
|
+
create_cwd_pointer
|
31
|
+
|
32
|
+
setup_flags
|
33
|
+
setup_io
|
34
|
+
|
35
|
+
pid = create_process
|
36
|
+
close_handles
|
37
|
+
|
38
|
+
pid
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def create_command_pointer
|
44
|
+
string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join ' '
|
45
|
+
@cmd_ptr = FFI::MemoryPointer.from_string string
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_environment_pointer
|
49
|
+
return unless @environment.kind_of?(Hash) && @environment.any?
|
50
|
+
|
51
|
+
strings = []
|
52
|
+
|
53
|
+
ENV.to_hash.merge(@environment).each do |key, val|
|
54
|
+
next if val.nil?
|
55
|
+
|
56
|
+
if key.to_s =~ /=|\0/ || val.to_s.include?("\0")
|
57
|
+
raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}"
|
58
|
+
end
|
59
|
+
|
60
|
+
strings << "#{key}=#{val}\0"
|
61
|
+
end
|
62
|
+
|
63
|
+
strings << "\0" # terminate the env block
|
64
|
+
env_str = strings.join
|
65
|
+
|
66
|
+
@env_ptr = FFI::MemoryPointer.new(:long, env_str.bytesize)
|
67
|
+
@env_ptr.put_bytes 0, env_str, 0, env_str.bytesize
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_cwd_pointer
|
71
|
+
@cwd_ptr = FFI::MemoryPointer.from_string(@cwd || Dir.pwd)
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_process
|
75
|
+
ok = Lib.create_process(
|
76
|
+
nil, # application name
|
77
|
+
@cmd_ptr, # command line
|
78
|
+
nil, # process attributes
|
79
|
+
nil, # thread attributes
|
80
|
+
true, # inherit handles
|
81
|
+
@flags, # creation flags
|
82
|
+
@env_ptr, # environment
|
83
|
+
@cwd_ptr, # current directory
|
84
|
+
startup_info, # startup info
|
85
|
+
process_info # process info
|
86
|
+
)
|
87
|
+
|
88
|
+
ok or raise LaunchError, Lib.last_error_message
|
89
|
+
|
90
|
+
process_info[:dwProcessId]
|
91
|
+
end
|
92
|
+
|
93
|
+
def startup_info
|
94
|
+
@startup_info ||= StartupInfo.new
|
95
|
+
end
|
96
|
+
|
97
|
+
def process_info
|
98
|
+
@process_info ||= ProcessInfo.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def setup_flags
|
102
|
+
@flags |= DETACHED_PROCESS if @detach
|
103
|
+
@flags |= CREATE_BREAKAWAY_FROM_JOB if @leader
|
104
|
+
end
|
105
|
+
|
106
|
+
def setup_io
|
107
|
+
startup_info[:dwFlags] ||= 0
|
108
|
+
startup_info[:dwFlags] |= STARTF_USESTDHANDLES
|
109
|
+
|
110
|
+
if @stdout
|
111
|
+
startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
|
112
|
+
end
|
113
|
+
|
114
|
+
if @stderr
|
115
|
+
startup_info[:hStdError] = std_stream_handle_for(@stderr)
|
116
|
+
end
|
117
|
+
|
118
|
+
if @duplex
|
119
|
+
read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
120
|
+
write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
121
|
+
sa = SecurityAttributes.new(:inherit => true)
|
122
|
+
|
123
|
+
ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
|
124
|
+
Lib.check_error ok
|
125
|
+
|
126
|
+
@read_pipe = read_pipe_ptr.read_pointer
|
127
|
+
@write_pipe = write_pipe_ptr.read_pointer
|
128
|
+
|
129
|
+
Lib.set_handle_inheritance @read_pipe, true
|
130
|
+
Lib.set_handle_inheritance @write_pipe, false
|
131
|
+
|
132
|
+
startup_info[:hStdInput] = @read_pipe
|
133
|
+
else
|
134
|
+
startup_info[:hStdInput] = std_stream_handle_for(STDIN)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def std_stream_handle_for(io)
|
139
|
+
handle = Lib.handle_for(io)
|
140
|
+
|
141
|
+
begin
|
142
|
+
Lib.set_handle_inheritance handle, true
|
143
|
+
rescue ChildProcess::Error
|
144
|
+
# If the IO was set to close on exec previously, this call will fail.
|
145
|
+
# That's probably OK, since the user explicitly asked for it to be
|
146
|
+
# closed (at least I have yet to find other cases where this will
|
147
|
+
# happen...)
|
148
|
+
end
|
149
|
+
|
150
|
+
handle
|
151
|
+
end
|
152
|
+
|
153
|
+
def close_handles
|
154
|
+
Lib.close_handle process_info[:hProcess]
|
155
|
+
Lib.close_handle process_info[:hThread]
|
156
|
+
|
157
|
+
if @duplex
|
158
|
+
@stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
|
159
|
+
Lib.close_handle @read_pipe
|
160
|
+
Lib.close_handle @write_pipe
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def quote_if_necessary(str)
|
165
|
+
quote = str.start_with?('"') ? "'" : '"'
|
166
|
+
|
167
|
+
case str
|
168
|
+
when /[\s\\'"]/
|
169
|
+
[quote, str, quote].join
|
170
|
+
else
|
171
|
+
str
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end # ProcessBuilder
|
175
|
+
end # Windows
|
176
|
+
end # ChildProcess
|
177
|
+
end
|