childprocess 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +6 -0
- data/.gitignore +28 -0
- data/.rspec +1 -0
- data/.travis.yml +44 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +15 -0
- data/LICENSE +20 -0
- data/README.md +196 -0
- data/Rakefile +61 -0
- data/appveyor.yml +60 -0
- data/childprocess.gemspec +30 -0
- data/lib/childprocess.rb +205 -0
- data/lib/childprocess/abstract_io.rb +36 -0
- data/lib/childprocess/abstract_process.rb +192 -0
- data/lib/childprocess/errors.rb +26 -0
- data/lib/childprocess/jruby.rb +56 -0
- data/lib/childprocess/jruby/io.rb +16 -0
- data/lib/childprocess/jruby/process.rb +159 -0
- data/lib/childprocess/jruby/pump.rb +53 -0
- data/lib/childprocess/tools/generator.rb +146 -0
- data/lib/childprocess/unix.rb +9 -0
- data/lib/childprocess/unix/fork_exec_process.rb +70 -0
- data/lib/childprocess/unix/io.rb +21 -0
- data/lib/childprocess/unix/lib.rb +186 -0
- data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
- data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
- data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
- data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
- data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
- data/lib/childprocess/unix/process.rb +89 -0
- data/lib/childprocess/version.rb +3 -0
- data/lib/childprocess/windows.rb +33 -0
- data/lib/childprocess/windows/handle.rb +91 -0
- data/lib/childprocess/windows/io.rb +25 -0
- data/lib/childprocess/windows/lib.rb +416 -0
- data/lib/childprocess/windows/process.rb +130 -0
- data/lib/childprocess/windows/process_builder.rb +175 -0
- data/lib/childprocess/windows/structs.rb +149 -0
- data/spec/abstract_io_spec.rb +12 -0
- data/spec/childprocess_spec.rb +422 -0
- data/spec/io_spec.rb +228 -0
- data/spec/jruby_spec.rb +24 -0
- data/spec/pid_behavior.rb +12 -0
- data/spec/platform_detection_spec.rb +86 -0
- data/spec/spec_helper.rb +261 -0
- data/spec/unix_spec.rb +57 -0
- data/spec/windows_spec.rb +23 -0
- metadata +179 -0
@@ -0,0 +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
|
@@ -0,0 +1,175 @@
|
|
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 create_command_pointer
|
43
|
+
string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join ' '
|
44
|
+
@cmd_ptr = FFI::MemoryPointer.from_string string
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_environment_pointer
|
48
|
+
return unless @environment.kind_of?(Hash) && @environment.any?
|
49
|
+
|
50
|
+
strings = []
|
51
|
+
|
52
|
+
ENV.to_hash.merge(@environment).each do |key, val|
|
53
|
+
next if val.nil?
|
54
|
+
|
55
|
+
if key.to_s =~ /=|\0/ || val.to_s.include?("\0")
|
56
|
+
raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}"
|
57
|
+
end
|
58
|
+
|
59
|
+
strings << "#{key}=#{val}\0"
|
60
|
+
end
|
61
|
+
|
62
|
+
strings << "\0" # terminate the env block
|
63
|
+
env_str = strings.join
|
64
|
+
|
65
|
+
@env_ptr = FFI::MemoryPointer.new(:long, env_str.bytesize)
|
66
|
+
@env_ptr.put_bytes 0, env_str, 0, env_str.bytesize
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_cwd_pointer
|
70
|
+
@cwd_ptr = FFI::MemoryPointer.from_string(@cwd || Dir.pwd)
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_process
|
74
|
+
ok = Lib.create_process(
|
75
|
+
nil, # application name
|
76
|
+
@cmd_ptr, # command line
|
77
|
+
nil, # process attributes
|
78
|
+
nil, # thread attributes
|
79
|
+
true, # inherit handles
|
80
|
+
@flags, # creation flags
|
81
|
+
@env_ptr, # environment
|
82
|
+
@cwd_ptr, # current directory
|
83
|
+
startup_info, # startup info
|
84
|
+
process_info # process info
|
85
|
+
)
|
86
|
+
|
87
|
+
ok or raise LaunchError, Lib.last_error_message
|
88
|
+
|
89
|
+
process_info[:dwProcessId]
|
90
|
+
end
|
91
|
+
|
92
|
+
def startup_info
|
93
|
+
@startup_info ||= StartupInfo.new
|
94
|
+
end
|
95
|
+
|
96
|
+
def process_info
|
97
|
+
@process_info ||= ProcessInfo.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def setup_flags
|
101
|
+
@flags |= DETACHED_PROCESS if @detach
|
102
|
+
@flags |= CREATE_BREAKAWAY_FROM_JOB if @leader
|
103
|
+
end
|
104
|
+
|
105
|
+
def setup_io
|
106
|
+
startup_info[:dwFlags] ||= 0
|
107
|
+
startup_info[:dwFlags] |= STARTF_USESTDHANDLES
|
108
|
+
|
109
|
+
if @stdout
|
110
|
+
startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
|
111
|
+
end
|
112
|
+
|
113
|
+
if @stderr
|
114
|
+
startup_info[:hStdError] = std_stream_handle_for(@stderr)
|
115
|
+
end
|
116
|
+
|
117
|
+
if @duplex
|
118
|
+
read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
119
|
+
write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
|
120
|
+
sa = SecurityAttributes.new(:inherit => true)
|
121
|
+
|
122
|
+
ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
|
123
|
+
Lib.check_error ok
|
124
|
+
|
125
|
+
@read_pipe = read_pipe_ptr.read_pointer
|
126
|
+
@write_pipe = write_pipe_ptr.read_pointer
|
127
|
+
|
128
|
+
Lib.set_handle_inheritance @read_pipe, true
|
129
|
+
Lib.set_handle_inheritance @write_pipe, false
|
130
|
+
|
131
|
+
startup_info[:hStdInput] = @read_pipe
|
132
|
+
else
|
133
|
+
startup_info[:hStdInput] = std_stream_handle_for(STDIN)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def std_stream_handle_for(io)
|
138
|
+
handle = Lib.handle_for(io)
|
139
|
+
|
140
|
+
begin
|
141
|
+
Lib.set_handle_inheritance handle, true
|
142
|
+
rescue ChildProcess::Error
|
143
|
+
# If the IO was set to close on exec previously, this call will fail.
|
144
|
+
# That's probably OK, since the user explicitly asked for it to be
|
145
|
+
# closed (at least I have yet to find other cases where this will
|
146
|
+
# happen...)
|
147
|
+
end
|
148
|
+
|
149
|
+
handle
|
150
|
+
end
|
151
|
+
|
152
|
+
def close_handles
|
153
|
+
Lib.close_handle process_info[:hProcess]
|
154
|
+
Lib.close_handle process_info[:hThread]
|
155
|
+
|
156
|
+
if @duplex
|
157
|
+
@stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
|
158
|
+
Lib.close_handle @read_pipe
|
159
|
+
Lib.close_handle @write_pipe
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def quote_if_necessary(str)
|
164
|
+
quote = str.start_with?('"') ? "'" : '"'
|
165
|
+
|
166
|
+
case str
|
167
|
+
when /[\s\\'"]/
|
168
|
+
[quote, str, quote].join
|
169
|
+
else
|
170
|
+
str
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end # ProcessBuilder
|
174
|
+
end # Windows
|
175
|
+
end # ChildProcess
|
@@ -0,0 +1,149 @@
|
|
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;
|
23
|
+
|
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
|
44
|
+
|
45
|
+
#
|
46
|
+
# typedef struct _PROCESS_INFORMATION {
|
47
|
+
# HANDLE hProcess;
|
48
|
+
# HANDLE hThread;
|
49
|
+
# DWORD dwProcessId;
|
50
|
+
# DWORD dwThreadId;
|
51
|
+
# } PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
|
52
|
+
#
|
53
|
+
|
54
|
+
class ProcessInfo < FFI::Struct
|
55
|
+
layout :hProcess, :pointer, # void ptr
|
56
|
+
:hThread, :pointer, # void ptr
|
57
|
+
:dwProcessId, :ulong,
|
58
|
+
:dwThreadId, :ulong
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# typedef struct _SECURITY_ATTRIBUTES {
|
63
|
+
# DWORD nLength;
|
64
|
+
# LPVOID lpSecurityDescriptor;
|
65
|
+
# BOOL bInheritHandle;
|
66
|
+
# } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
|
67
|
+
#
|
68
|
+
|
69
|
+
class SecurityAttributes < FFI::Struct
|
70
|
+
layout :nLength, :ulong,
|
71
|
+
:lpSecurityDescriptor, :pointer, # void ptr
|
72
|
+
:bInheritHandle, :int
|
73
|
+
|
74
|
+
def initialize(opts = {})
|
75
|
+
super()
|
76
|
+
|
77
|
+
self[:nLength] = self.class.size
|
78
|
+
self[:lpSecurityDescriptor] = nil
|
79
|
+
self[:bInheritHandle] = opts[:inherit] ? 1 : 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
85
|
+
# LARGE_INTEGER PerProcessUserTimeLimit;
|
86
|
+
# LARGE_INTEGER PerJobUserTimeLimit;
|
87
|
+
# DWORD LimitFlags;
|
88
|
+
# SIZE_T MinimumWorkingSetSize;
|
89
|
+
# SIZE_T MaximumWorkingSetSize;
|
90
|
+
# DWORD ActiveProcessLimit;
|
91
|
+
# ULONG_PTR Affinity;
|
92
|
+
# DWORD PriorityClass;
|
93
|
+
# DWORD SchedulingClass;
|
94
|
+
# } JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;
|
95
|
+
#
|
96
|
+
class JobObjectBasicLimitInformation < FFI::Struct
|
97
|
+
layout :PerProcessUserTimeLimit, :int64,
|
98
|
+
:PerJobUserTimeLimit, :int64,
|
99
|
+
:LimitFlags, :ulong,
|
100
|
+
:MinimumWorkingSetSize, :size_t,
|
101
|
+
:MaximumWorkingSetSize, :size_t,
|
102
|
+
:ActiveProcessLimit, :ulong,
|
103
|
+
:Affinity, :pointer,
|
104
|
+
:PriorityClass, :ulong,
|
105
|
+
:SchedulingClass, :ulong
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# typedef struct _IO_COUNTERS {
|
110
|
+
# ULONGLONG ReadOperationCount;
|
111
|
+
# ULONGLONG WriteOperationCount;
|
112
|
+
# ULONGLONG OtherOperationCount;
|
113
|
+
# ULONGLONG ReadTransferCount;
|
114
|
+
# ULONGLONG WriteTransferCount;
|
115
|
+
# ULONGLONG OtherTransferCount;
|
116
|
+
# } IO_COUNTERS, *PIO_COUNTERS;
|
117
|
+
#
|
118
|
+
|
119
|
+
class IoCounters < FFI::Struct
|
120
|
+
layout :ReadOperationCount, :ulong_long,
|
121
|
+
:WriteOperationCount, :ulong_long,
|
122
|
+
:OtherOperationCount, :ulong_long,
|
123
|
+
:ReadTransferCount, :ulong_long,
|
124
|
+
:WriteTransferCount, :ulong_long,
|
125
|
+
:OtherTransferCount, :ulong_long
|
126
|
+
end
|
127
|
+
#
|
128
|
+
# typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
|
129
|
+
# JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
130
|
+
# IO_COUNTERS IoInfo;
|
131
|
+
# SIZE_T ProcessMemoryLimit;
|
132
|
+
# SIZE_T JobMemoryLimit;
|
133
|
+
# SIZE_T PeakProcessMemoryUsed;
|
134
|
+
# SIZE_T PeakJobMemoryUsed;
|
135
|
+
# } JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION;
|
136
|
+
#
|
137
|
+
|
138
|
+
class JobObjectExtendedLimitInformation < FFI::Struct
|
139
|
+
layout :BasicLimitInformation, JobObjectBasicLimitInformation,
|
140
|
+
:IoInfo, IoCounters,
|
141
|
+
:ProcessMemoryLimit, :size_t,
|
142
|
+
:JobMemoryLimit, :size_t,
|
143
|
+
:PeakProcessMemoryUsed, :size_t,
|
144
|
+
:PeakJobMemoryUsed, :size_t
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
end # Windows
|
149
|
+
end # ChildProcess
|