childprocess 0.8.0 → 3.0.0

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.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/.document +6 -6
  3. data/.gitignore +28 -28
  4. data/.rspec +1 -1
  5. data/.travis.yml +40 -36
  6. data/CHANGELOG.md +73 -44
  7. data/Gemfile +21 -15
  8. data/LICENSE +20 -20
  9. data/README.md +218 -192
  10. data/Rakefile +61 -61
  11. data/appveyor.yml +42 -43
  12. data/childprocess.gemspec +26 -30
  13. data/lib/childprocess.rb +210 -205
  14. data/lib/childprocess/abstract_io.rb +36 -36
  15. data/lib/childprocess/abstract_process.rb +192 -192
  16. data/lib/childprocess/errors.rb +37 -26
  17. data/lib/childprocess/jruby.rb +56 -56
  18. data/lib/childprocess/jruby/io.rb +16 -16
  19. data/lib/childprocess/jruby/process.rb +184 -159
  20. data/lib/childprocess/jruby/pump.rb +53 -53
  21. data/lib/childprocess/tools/generator.rb +145 -145
  22. data/lib/childprocess/unix.rb +9 -9
  23. data/lib/childprocess/unix/fork_exec_process.rb +78 -70
  24. data/lib/childprocess/unix/io.rb +21 -21
  25. data/lib/childprocess/unix/lib.rb +186 -186
  26. data/lib/childprocess/unix/platform/i386-linux.rb +12 -12
  27. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -11
  28. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -12
  29. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -11
  30. data/lib/childprocess/unix/posix_spawn_process.rb +134 -134
  31. data/lib/childprocess/unix/process.rb +90 -89
  32. data/lib/childprocess/version.rb +3 -3
  33. data/lib/childprocess/windows.rb +38 -33
  34. data/lib/childprocess/windows/handle.rb +91 -91
  35. data/lib/childprocess/windows/io.rb +25 -25
  36. data/lib/childprocess/windows/lib.rb +416 -416
  37. data/lib/childprocess/windows/process.rb +130 -130
  38. data/lib/childprocess/windows/process_builder.rb +178 -175
  39. data/lib/childprocess/windows/structs.rb +148 -148
  40. data/spec/abstract_io_spec.rb +12 -12
  41. data/spec/childprocess_spec.rb +447 -391
  42. data/spec/get_env.ps1 +13 -0
  43. data/spec/io_spec.rb +228 -228
  44. data/spec/jruby_spec.rb +24 -24
  45. data/spec/pid_behavior.rb +12 -12
  46. data/spec/platform_detection_spec.rb +86 -86
  47. data/spec/spec_helper.rb +270 -261
  48. data/spec/unix_spec.rb +57 -57
  49. data/spec/windows_spec.rb +23 -23
  50. metadata +8 -39
@@ -1,149 +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
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
149
  end # ChildProcess
@@ -1,12 +1,12 @@
1
- require File.expand_path('../spec_helper', __FILE__)
2
-
3
- describe ChildProcess::AbstractIO do
4
- let(:io) { ChildProcess::AbstractIO.new }
5
-
6
- it "inherits the parent's IO streams" do
7
- io.inherit!
8
-
9
- expect(io.stdout).to eq STDOUT
10
- expect(io.stderr).to eq STDERR
11
- end
12
- end
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe ChildProcess::AbstractIO do
4
+ let(:io) { ChildProcess::AbstractIO.new }
5
+
6
+ it "inherits the parent's IO streams" do
7
+ io.inherit!
8
+
9
+ expect(io.stdout).to eq STDOUT
10
+ expect(io.stderr).to eq STDERR
11
+ end
12
+ end
@@ -1,391 +1,447 @@
1
- # encoding: utf-8
2
-
3
- require File.expand_path('../spec_helper', __FILE__)
4
- require 'rubygems/mock_gem_ui'
5
-
6
-
7
- describe ChildProcess do
8
-
9
- here = File.dirname(__FILE__)
10
-
11
- let(:gemspec) { eval(File.read "#{here}/../childprocess.gemspec") }
12
-
13
- it 'validates cleanly' do
14
- mock_ui = Gem::MockGemUi.new
15
- Gem::DefaultUserInteraction.use_ui(mock_ui) { gemspec.validate }
16
-
17
- expect(mock_ui.error).to_not match(/warn/i)
18
- end
19
-
20
-
21
- it "returns self when started" do
22
- process = sleeping_ruby
23
-
24
- expect(process.start).to eq process
25
- expect(process).to be_alive
26
- end
27
-
28
- # We can't detect failure to execve() when using posix_spawn() on Linux
29
- # without waiting for the child to exit with code 127.
30
- #
31
- # See e.g. http://repo.or.cz/w/glibc.git/blob/669704fd:/sysdeps/posix/spawni.c#l34
32
- #
33
- # We could work around this by doing the PATH search ourselves, but not sure
34
- # it's worth it.
35
- it "raises ChildProcess::LaunchError if the process can't be started", :posix_spawn_on_linux => false do
36
- expect { invalid_process.start }.to raise_error(ChildProcess::LaunchError)
37
- end
38
-
39
- it 'raises ArgumentError if given a non-string argument' do
40
- expect { ChildProcess.build(nil, "unlikelytoexist") }.to raise_error(ArgumentError)
41
- expect { ChildProcess.build("foo", 1) }.to raise_error(ArgumentError)
42
- end
43
-
44
- it "knows if the process crashed" do
45
- process = exit_with(1).start
46
- process.wait
47
-
48
- expect(process).to be_crashed
49
- end
50
-
51
- it "knows if the process didn't crash" do
52
- process = exit_with(0).start
53
- process.wait
54
-
55
- expect(process).to_not be_crashed
56
- end
57
-
58
- it "can wait for a process to finish" do
59
- process = exit_with(0).start
60
- return_value = process.wait
61
-
62
- expect(process).to_not be_alive
63
- expect(return_value).to eq 0
64
- end
65
-
66
- it 'ignores #wait if process already finished' do
67
- process = exit_with(0).start
68
- sleep 0.01 until process.exited?
69
-
70
- expect(process.wait).to eql 0
71
- end
72
-
73
- it "escalates if TERM is ignored" do
74
- process = ignored('TERM').start
75
- process.stop
76
- expect(process).to be_exited
77
- end
78
-
79
- it "accepts a timeout argument to #stop" do
80
- process = sleeping_ruby.start
81
- process.stop(exit_timeout)
82
- end
83
-
84
- it "lets child process inherit the environment of the current process" do
85
- Tempfile.open("env-spec") do |file|
86
- with_env('INHERITED' => 'yes') do
87
- process = write_env(file.path).start
88
- process.wait
89
- end
90
-
91
- child_env = eval rewind_and_read(file)
92
- expect(child_env['INHERITED']).to eql 'yes'
93
- end
94
- end
95
-
96
- it "can override env vars only for the current process" do
97
- Tempfile.open("env-spec") do |file|
98
- process = write_env(file.path)
99
- process.environment['CHILD_ONLY'] = '1'
100
- process.start
101
-
102
- expect(ENV['CHILD_ONLY']).to be_nil
103
-
104
- process.wait
105
-
106
- child_env = eval rewind_and_read(file)
107
- expect(child_env['CHILD_ONLY']).to eql '1'
108
- end
109
- end
110
-
111
- it "inherits the parent's env vars also when some are overridden" do
112
- Tempfile.open("env-spec") do |file|
113
- with_env('INHERITED' => 'yes', 'CHILD_ONLY' => 'no') do
114
- process = write_env(file.path)
115
- process.environment['CHILD_ONLY'] = 'yes'
116
-
117
- process.start
118
- process.wait
119
-
120
- child_env = eval rewind_and_read(file)
121
-
122
- expect(child_env['INHERITED']).to eq 'yes'
123
- expect(child_env['CHILD_ONLY']).to eq 'yes'
124
- end
125
- end
126
- end
127
-
128
- it "can unset env vars" do
129
- Tempfile.open("env-spec") do |file|
130
- ENV['CHILDPROCESS_UNSET'] = '1'
131
- process = write_env(file.path)
132
- process.environment['CHILDPROCESS_UNSET'] = nil
133
- process.start
134
-
135
- process.wait
136
-
137
- child_env = eval rewind_and_read(file)
138
- expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
139
- end
140
- end
141
-
142
- it 'does not see env vars unset in parent' do
143
- Tempfile.open('env-spec') do |file|
144
- ENV['CHILDPROCESS_UNSET'] = nil
145
- process = write_env(file.path)
146
- process.start
147
-
148
- process.wait
149
-
150
- child_env = eval rewind_and_read(file)
151
- expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
152
- end
153
- end
154
-
155
-
156
- it "passes arguments to the child" do
157
- args = ["foo", "bar"]
158
-
159
- Tempfile.open("argv-spec") do |file|
160
- process = write_argv(file.path, *args).start
161
- process.wait
162
-
163
- expect(rewind_and_read(file)).to eql args.inspect
164
- end
165
- end
166
-
167
- it "lets a detached child live on" do
168
- p_pid = nil
169
- c_pid = nil
170
-
171
- Tempfile.open('grandparent_out') do |gp_file|
172
- # Create a parent and detached child process that will spit out their PID. Make sure that the child process lasts longer than the parent.
173
- p_process = ruby("require 'childprocess' ; c_process = ChildProcess.build('ruby', '-e', 'puts \\\"Child PID: \#{Process.pid}\\\" ; sleep 5') ; c_process.io.inherit! ; c_process.detach = true ; c_process.start ; puts \"Child PID: \#{c_process.pid}\" ; puts \"Parent PID: \#{Process.pid}\"")
174
- p_process.io.stdout = p_process.io.stderr = gp_file
175
-
176
- # Let the parent process die
177
- p_process.start
178
- p_process.wait
179
-
180
-
181
- # Gather parent and child PIDs
182
- pids = rewind_and_read(gp_file).split("\n")
183
- pids.collect! { |pid| pid[/\d+/].to_i }
184
- c_pid, p_pid = pids
185
- end
186
-
187
- # Check that the parent process has dies but the child process is still alive
188
- expect(alive?(p_pid)).to_not be true
189
- expect(alive?(c_pid)).to be true
190
- end
191
-
192
- it "preserves Dir.pwd in the child" do
193
- Tempfile.open("dir-spec-out") do |file|
194
- process = ruby("print Dir.pwd")
195
- process.io.stdout = process.io.stderr = file
196
-
197
- expected_dir = nil
198
- Dir.chdir(Dir.tmpdir) do
199
- expected_dir = Dir.pwd
200
- process.start
201
- end
202
-
203
- process.wait
204
-
205
- expect(rewind_and_read(file)).to eq expected_dir
206
- end
207
- end
208
-
209
- it "can handle whitespace, special characters and quotes in arguments" do
210
- args = ["foo bar", 'foo\bar', "'i-am-quoted'", '"i am double quoted"']
211
-
212
- Tempfile.open("argv-spec") do |file|
213
- process = write_argv(file.path, *args).start
214
- process.wait
215
-
216
- expect(rewind_and_read(file)).to eq args.inspect
217
- end
218
- end
219
-
220
- it 'handles whitespace in the executable name' do
221
- path = File.expand_path('foo bar')
222
-
223
- with_executable_at(path) do |proc|
224
- expect(proc.start).to eq proc
225
- expect(proc).to be_alive
226
- end
227
- end
228
-
229
- it "times out when polling for exit" do
230
- process = sleeping_ruby.start
231
- expect { process.poll_for_exit(0.1) }.to raise_error(ChildProcess::TimeoutError)
232
- end
233
-
234
- it "can change working directory" do
235
- process = ruby "print Dir.pwd"
236
-
237
- with_tmpdir { |dir|
238
- process.cwd = dir
239
-
240
- orig_pwd = Dir.pwd
241
-
242
- Tempfile.open('cwd') do |file|
243
- process.io.stdout = file
244
-
245
- process.start
246
- process.wait
247
-
248
- expect(rewind_and_read(file)).to eq dir
249
- end
250
-
251
- expect(Dir.pwd).to eq orig_pwd
252
- }
253
- end
254
-
255
- it 'kills the full process tree', :process_builder => false do
256
- Tempfile.open('kill-process-tree') do |file|
257
- process = write_pid_in_sleepy_grand_child(file.path)
258
- process.leader = true
259
- process.start
260
-
261
- pid = wait_until(30) do
262
- Integer(rewind_and_read(file)) rescue nil
263
- end
264
-
265
- process.stop
266
- wait_until(3) { expect(alive?(pid)).to eql(false) }
267
- end
268
- end
269
-
270
- it 'releases the GIL while waiting for the process' do
271
- time = Time.now
272
- threads = []
273
-
274
- threads << Thread.new { sleeping_ruby(1).start.wait }
275
- threads << Thread.new(time) { expect(Time.now - time).to be < 0.5 }
276
-
277
- threads.each { |t| t.join }
278
- end
279
-
280
- it 'can check if a detached child is alive' do
281
- proc = ruby_process("-e", "sleep")
282
- proc.detach = true
283
-
284
- proc.start
285
-
286
- expect(proc).to be_alive
287
- proc.stop(0)
288
-
289
- expect(proc).to be_exited
290
- end
291
-
292
-
293
- it 'has a logger' do
294
- expect(ChildProcess).to respond_to(:logger)
295
- end
296
-
297
- it 'can change its logger' do
298
- expect(ChildProcess).to respond_to(:logger=)
299
-
300
- original_logger = ChildProcess.logger
301
- begin
302
- ChildProcess.logger = :some_other_logger
303
- expect(ChildProcess.logger).to eq(:some_other_logger)
304
- ensure
305
- ChildProcess.logger = original_logger
306
- end
307
- end
308
-
309
-
310
- describe 'logger' do
311
-
312
- before(:each) do
313
- ChildProcess.logger = logger
314
- end
315
-
316
- after(:all) do
317
- ChildProcess.logger = nil
318
- end
319
-
320
-
321
- context 'with the default logger' do
322
-
323
- let(:logger) { nil }
324
-
325
-
326
- it 'logs at INFO level by default' do
327
- expect(ChildProcess.logger.level).to eq(Logger::INFO)
328
- end
329
-
330
- it 'logs at DEBUG level by default if $DEBUG is on' do
331
- original_debug = $DEBUG
332
-
333
- begin
334
- $DEBUG = true
335
-
336
- expect(ChildProcess.logger.level).to eq(Logger::DEBUG)
337
- ensure
338
- $DEBUG = original_debug
339
- end
340
- end
341
-
342
- it "logs to stderr by default" do
343
- cap = capture_std { generate_log_messages }
344
-
345
- expect(cap.stdout).to be_empty
346
- expect(cap.stderr).to_not be_empty
347
- end
348
-
349
- end
350
-
351
- context 'with a custom logger' do
352
-
353
- let(:logger) { Logger.new($stdout) }
354
-
355
- it "logs to configured logger" do
356
- cap = capture_std { generate_log_messages }
357
-
358
- expect(cap.stdout).to_not be_empty
359
- expect(cap.stderr).to be_empty
360
- end
361
-
362
- end
363
-
364
- end
365
-
366
- describe '#started?' do
367
- subject { process.started? }
368
-
369
- context 'when not started' do
370
- let(:process) { sleeping_ruby(1) }
371
-
372
- it { is_expected.to be false }
373
- end
374
-
375
- context 'when started' do
376
- let(:process) { sleeping_ruby(1).start }
377
-
378
- it { is_expected.to be true }
379
- end
380
-
381
- context 'when finished' do
382
- before(:each) { process.wait }
383
-
384
- let(:process) { sleeping_ruby(0).start }
385
-
386
- it { is_expected.to be true }
387
- end
388
-
389
- end
390
-
391
- end
1
+ # encoding: utf-8
2
+
3
+ require File.expand_path('../spec_helper', __FILE__)
4
+ require 'rubygems/mock_gem_ui'
5
+
6
+
7
+ describe ChildProcess do
8
+
9
+ here = File.dirname(__FILE__)
10
+
11
+ let(:gemspec) { eval(File.read "#{here}/../childprocess.gemspec") }
12
+
13
+ it 'validates cleanly' do
14
+ mock_ui = Gem::MockGemUi.new
15
+ Gem::DefaultUserInteraction.use_ui(mock_ui) { gemspec.validate }
16
+
17
+ expect(mock_ui.error).to_not match(/warn/i)
18
+ end
19
+
20
+
21
+ it "returns self when started" do
22
+ process = sleeping_ruby
23
+
24
+ expect(process.start).to eq process
25
+ expect(process).to be_alive
26
+ end
27
+
28
+ # We can't detect failure to execve() when using posix_spawn() on Linux
29
+ # without waiting for the child to exit with code 127.
30
+ #
31
+ # See e.g. http://repo.or.cz/w/glibc.git/blob/669704fd:/sysdeps/posix/spawni.c#l34
32
+ #
33
+ # We could work around this by doing the PATH search ourselves, but not sure
34
+ # it's worth it.
35
+ it "raises ChildProcess::LaunchError if the process can't be started", :posix_spawn_on_linux => false do
36
+ expect { invalid_process.start }.to raise_error(ChildProcess::LaunchError)
37
+ end
38
+
39
+ it 'raises ArgumentError if given a non-string argument' do
40
+ expect { ChildProcess.build(nil, "unlikelytoexist") }.to raise_error(ArgumentError)
41
+ expect { ChildProcess.build("foo", 1) }.to raise_error(ArgumentError)
42
+ end
43
+
44
+ it "knows if the process crashed" do
45
+ process = exit_with(1).start
46
+ process.wait
47
+
48
+ expect(process).to be_crashed
49
+ end
50
+
51
+ it "knows if the process didn't crash" do
52
+ process = exit_with(0).start
53
+ process.wait
54
+
55
+ expect(process).to_not be_crashed
56
+ end
57
+
58
+ it "can wait for a process to finish" do
59
+ process = exit_with(0).start
60
+ return_value = process.wait
61
+
62
+ expect(process).to_not be_alive
63
+ expect(return_value).to eq 0
64
+ end
65
+
66
+ it 'ignores #wait if process already finished' do
67
+ process = exit_with(0).start
68
+ sleep 0.01 until process.exited?
69
+
70
+ expect(process.wait).to eql 0
71
+ end
72
+
73
+ it "escalates if TERM is ignored" do
74
+ process = ignored('TERM').start
75
+ process.stop
76
+ expect(process).to be_exited
77
+ end
78
+
79
+ it "accepts a timeout argument to #stop" do
80
+ process = sleeping_ruby.start
81
+ process.stop(exit_timeout)
82
+ end
83
+
84
+ it "lets child process inherit the environment of the current process" do
85
+ Tempfile.open("env-spec") do |file|
86
+ file.close
87
+ with_env('INHERITED' => 'yes') do
88
+ process = write_env(file.path).start
89
+ process.wait
90
+ end
91
+
92
+ file.open
93
+ child_env = eval rewind_and_read(file)
94
+ expect(child_env['INHERITED']).to eql 'yes'
95
+ end
96
+ end
97
+
98
+ it "can override env vars only for the current process" do
99
+ Tempfile.open("env-spec") do |file|
100
+ file.close
101
+ process = write_env(file.path)
102
+ process.environment['CHILD_ONLY'] = '1'
103
+ process.start
104
+
105
+ expect(ENV['CHILD_ONLY']).to be_nil
106
+
107
+ process.wait
108
+
109
+ file.open
110
+ child_env = eval rewind_and_read(file)
111
+ expect(child_env['CHILD_ONLY']).to eql '1'
112
+ end
113
+ end
114
+
115
+ it 'allows unicode characters in the environment' do
116
+ Tempfile.open("env-spec") do |file|
117
+ file.close
118
+ process = write_env(file.path)
119
+ process.environment['FOö'] = 'baör'
120
+ process.start
121
+ process.wait
122
+
123
+ file.open
124
+ child_env = eval rewind_and_read(file)
125
+
126
+ expect(child_env['FOö']).to eql 'baör'
127
+ end
128
+ end
129
+
130
+ it "inherits the parent's env vars also when some are overridden" do
131
+ Tempfile.open("env-spec") do |file|
132
+ file.close
133
+ with_env('INHERITED' => 'yes', 'CHILD_ONLY' => 'no') do
134
+ process = write_env(file.path)
135
+ process.environment['CHILD_ONLY'] = 'yes'
136
+
137
+ process.start
138
+ process.wait
139
+
140
+ file.open
141
+ child_env = eval rewind_and_read(file)
142
+
143
+ expect(child_env['INHERITED']).to eq 'yes'
144
+ expect(child_env['CHILD_ONLY']).to eq 'yes'
145
+ end
146
+ end
147
+ end
148
+
149
+ it "can unset env vars" do
150
+ Tempfile.open("env-spec") do |file|
151
+ file.close
152
+ ENV['CHILDPROCESS_UNSET'] = '1'
153
+ process = write_env(file.path)
154
+ process.environment['CHILDPROCESS_UNSET'] = nil
155
+ process.start
156
+
157
+ process.wait
158
+
159
+ file.open
160
+ child_env = eval rewind_and_read(file)
161
+ expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
162
+ end
163
+ end
164
+
165
+ it 'does not see env vars unset in parent' do
166
+ Tempfile.open('env-spec') do |file|
167
+ file.close
168
+ ENV['CHILDPROCESS_UNSET'] = nil
169
+ process = write_env(file.path)
170
+ process.start
171
+
172
+ process.wait
173
+
174
+ file.open
175
+ child_env = eval rewind_and_read(file)
176
+ expect(child_env).to_not have_key('CHILDPROCESS_UNSET')
177
+ end
178
+ end
179
+
180
+
181
+ it "passes arguments to the child" do
182
+ args = ["foo", "bar"]
183
+
184
+ Tempfile.open("argv-spec") do |file|
185
+ process = write_argv(file.path, *args).start
186
+ process.wait
187
+
188
+ expect(rewind_and_read(file)).to eql args.inspect
189
+ end
190
+ end
191
+
192
+ it "lets a detached child live on" do
193
+ p_pid = nil
194
+ c_pid = nil
195
+
196
+ Tempfile.open('grandparent_out') do |gp_file|
197
+ # Create a parent and detached child process that will spit out their PID. Make sure that the child process lasts longer than the parent.
198
+ p_process = ruby("require 'childprocess' ; c_process = ChildProcess.build('ruby', '-e', 'puts \\\"Child PID: \#{Process.pid}\\\" ; sleep 5') ; c_process.io.inherit! ; c_process.detach = true ; c_process.start ; puts \"Child PID: \#{c_process.pid}\" ; puts \"Parent PID: \#{Process.pid}\"")
199
+ p_process.io.stdout = p_process.io.stderr = gp_file
200
+
201
+ # Let the parent process die
202
+ p_process.start
203
+ p_process.wait
204
+
205
+
206
+ # Gather parent and child PIDs
207
+ pids = rewind_and_read(gp_file).split("\n")
208
+ pids.collect! { |pid| pid[/\d+/].to_i }
209
+ c_pid, p_pid = pids
210
+ end
211
+
212
+ # Check that the parent process has dies but the child process is still alive
213
+ expect(alive?(p_pid)).to_not be true
214
+ expect(alive?(c_pid)).to be true
215
+ end
216
+
217
+ it "preserves Dir.pwd in the child" do
218
+ Tempfile.open("dir-spec-out") do |file|
219
+ process = ruby("print Dir.pwd")
220
+ process.io.stdout = process.io.stderr = file
221
+
222
+ expected_dir = nil
223
+ Dir.chdir(Dir.tmpdir) do
224
+ expected_dir = Dir.pwd
225
+ process.start
226
+ end
227
+
228
+ process.wait
229
+
230
+ expect(rewind_and_read(file)).to eq expected_dir
231
+ end
232
+ end
233
+
234
+ it "can handle whitespace, special characters and quotes in arguments" do
235
+ args = ["foo bar", 'foo\bar', "'i-am-quoted'", '"i am double quoted"']
236
+
237
+ Tempfile.open("argv-spec") do |file|
238
+ process = write_argv(file.path, *args).start
239
+ process.wait
240
+
241
+ expect(rewind_and_read(file)).to eq args.inspect
242
+ end
243
+ end
244
+
245
+ it 'handles whitespace in the executable name' do
246
+ path = File.expand_path('foo bar')
247
+
248
+ with_executable_at(path) do |proc|
249
+ expect(proc.start).to eq proc
250
+ expect(proc).to be_alive
251
+ end
252
+ end
253
+
254
+ it "times out when polling for exit" do
255
+ process = sleeping_ruby.start
256
+ expect { process.poll_for_exit(0.1) }.to raise_error(ChildProcess::TimeoutError)
257
+ end
258
+
259
+ it "can change working directory" do
260
+ process = ruby "print Dir.pwd"
261
+
262
+ with_tmpdir { |dir|
263
+ process.cwd = dir
264
+
265
+ orig_pwd = Dir.pwd
266
+
267
+ Tempfile.open('cwd') do |file|
268
+ process.io.stdout = file
269
+
270
+ process.start
271
+ process.wait
272
+
273
+ expect(rewind_and_read(file)).to eq dir
274
+ end
275
+
276
+ expect(Dir.pwd).to eq orig_pwd
277
+ }
278
+ end
279
+
280
+ it 'kills the full process tree', :process_builder => false do
281
+ Tempfile.open('kill-process-tree') do |file|
282
+ process = write_pid_in_sleepy_grand_child(file.path)
283
+ process.leader = true
284
+ process.start
285
+
286
+ pid = wait_until(30) do
287
+ Integer(rewind_and_read(file)) rescue nil
288
+ end
289
+
290
+ process.stop
291
+ wait_until(3) { expect(alive?(pid)).to eql(false) }
292
+ end
293
+ end
294
+
295
+ it 'releases the GIL while waiting for the process' do
296
+ time = Time.now
297
+ threads = []
298
+
299
+ threads << Thread.new { sleeping_ruby(1).start.wait }
300
+ threads << Thread.new(time) { expect(Time.now - time).to be < 0.5 }
301
+
302
+ threads.each { |t| t.join }
303
+ end
304
+
305
+ it 'can check if a detached child is alive' do
306
+ proc = ruby_process("-e", "sleep")
307
+ proc.detach = true
308
+
309
+ proc.start
310
+
311
+ expect(proc).to be_alive
312
+ proc.stop(0)
313
+
314
+ expect(proc).to be_exited
315
+ end
316
+
317
+ describe 'OS detection' do
318
+
319
+ before(:all) do
320
+ # Save off original OS so that it can be restored later
321
+ @original_host_os = RbConfig::CONFIG['host_os']
322
+ end
323
+
324
+ after(:each) do
325
+ # Restore things to the real OS instead of the fake test OS
326
+ RbConfig::CONFIG['host_os'] = @original_host_os
327
+ ChildProcess.instance_variable_set(:@os, nil)
328
+ end
329
+
330
+
331
+ # TODO: add tests for other OSs
332
+ context 'on a BSD system' do
333
+
334
+ let(:bsd_patterns) { ['bsd', 'dragonfly'] }
335
+
336
+ it 'correctly identifies BSD systems' do
337
+ bsd_patterns.each do |pattern|
338
+ RbConfig::CONFIG['host_os'] = pattern
339
+ ChildProcess.instance_variable_set(:@os, nil)
340
+
341
+ expect(ChildProcess.os).to eq(:bsd)
342
+ end
343
+ end
344
+
345
+ end
346
+
347
+ end
348
+
349
+ it 'has a logger' do
350
+ expect(ChildProcess).to respond_to(:logger)
351
+ end
352
+
353
+ it 'can change its logger' do
354
+ expect(ChildProcess).to respond_to(:logger=)
355
+
356
+ original_logger = ChildProcess.logger
357
+ begin
358
+ ChildProcess.logger = :some_other_logger
359
+ expect(ChildProcess.logger).to eq(:some_other_logger)
360
+ ensure
361
+ ChildProcess.logger = original_logger
362
+ end
363
+ end
364
+
365
+
366
+ describe 'logger' do
367
+
368
+ before(:each) do
369
+ ChildProcess.logger = logger
370
+ end
371
+
372
+ after(:all) do
373
+ ChildProcess.logger = nil
374
+ end
375
+
376
+
377
+ context 'with the default logger' do
378
+
379
+ let(:logger) { nil }
380
+
381
+
382
+ it 'logs at INFO level by default' do
383
+ expect(ChildProcess.logger.level).to eq(Logger::INFO)
384
+ end
385
+
386
+ it 'logs at DEBUG level by default if $DEBUG is on' do
387
+ original_debug = $DEBUG
388
+
389
+ begin
390
+ $DEBUG = true
391
+
392
+ expect(ChildProcess.logger.level).to eq(Logger::DEBUG)
393
+ ensure
394
+ $DEBUG = original_debug
395
+ end
396
+ end
397
+
398
+ it "logs to stderr by default" do
399
+ cap = capture_std { generate_log_messages }
400
+
401
+ expect(cap.stdout).to be_empty
402
+ expect(cap.stderr).to_not be_empty
403
+ end
404
+
405
+ end
406
+
407
+ context 'with a custom logger' do
408
+
409
+ let(:logger) { Logger.new($stdout) }
410
+
411
+ it "logs to configured logger" do
412
+ cap = capture_std { generate_log_messages }
413
+
414
+ expect(cap.stdout).to_not be_empty
415
+ expect(cap.stderr).to be_empty
416
+ end
417
+
418
+ end
419
+
420
+ end
421
+
422
+ describe '#started?' do
423
+ subject { process.started? }
424
+
425
+ context 'when not started' do
426
+ let(:process) { sleeping_ruby(1) }
427
+
428
+ it { is_expected.to be false }
429
+ end
430
+
431
+ context 'when started' do
432
+ let(:process) { sleeping_ruby(1).start }
433
+
434
+ it { is_expected.to be true }
435
+ end
436
+
437
+ context 'when finished' do
438
+ before(:each) { process.wait }
439
+
440
+ let(:process) { sleeping_ruby(0).start }
441
+
442
+ it { is_expected.to be true }
443
+ end
444
+
445
+ end
446
+
447
+ end