childprocess 0.8.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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