childprocess 0.8.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) 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 +42 -36
  6. data/CHANGELOG.md +67 -44
  7. data/Gemfile +18 -15
  8. data/LICENSE +20 -20
  9. data/README.md +216 -192
  10. data/Rakefile +61 -61
  11. data/appveyor.yml +42 -43
  12. data/childprocess.gemspec +32 -30
  13. data/ext/mkrf_conf.rb +24 -0
  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/io.rb +16 -16
  18. data/lib/childprocess/jruby/process.rb +184 -159
  19. data/lib/childprocess/jruby/pump.rb +53 -53
  20. data/lib/childprocess/jruby.rb +56 -56
  21. data/lib/childprocess/tools/generator.rb +145 -145
  22. data/lib/childprocess/unix/fork_exec_process.rb +78 -70
  23. data/lib/childprocess/unix/io.rb +21 -21
  24. data/lib/childprocess/unix/lib.rb +186 -186
  25. data/lib/childprocess/unix/platform/i386-linux.rb +12 -12
  26. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -11
  27. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -12
  28. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -11
  29. data/lib/childprocess/unix/posix_spawn_process.rb +134 -134
  30. data/lib/childprocess/unix/process.rb +90 -89
  31. data/lib/childprocess/unix.rb +9 -9
  32. data/lib/childprocess/version.rb +3 -3
  33. data/lib/childprocess/windows/handle.rb +91 -91
  34. data/lib/childprocess/windows/io.rb +25 -25
  35. data/lib/childprocess/windows/lib.rb +416 -416
  36. data/lib/childprocess/windows/process.rb +130 -130
  37. data/lib/childprocess/windows/process_builder.rb +178 -175
  38. data/lib/childprocess/windows/structs.rb +148 -148
  39. data/lib/childprocess/windows.rb +33 -33
  40. data/lib/childprocess.rb +210 -205
  41. data/spec/abstract_io_spec.rb +12 -12
  42. data/spec/childprocess_spec.rb +447 -391
  43. data/spec/get_env.ps1 +13 -0
  44. data/spec/io_spec.rb +228 -228
  45. data/spec/jruby_spec.rb +24 -24
  46. data/spec/pid_behavior.rb +12 -12
  47. data/spec/platform_detection_spec.rb +86 -86
  48. data/spec/spec_helper.rb +270 -261
  49. data/spec/unix_spec.rb +57 -57
  50. data/spec/windows_spec.rb +23 -23
  51. metadata +18 -33
@@ -1,130 +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
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
@@ -1,175 +1,178 @@
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
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 to_wide_string(str)
43
+ newstr = str + "\0".encode(str.encoding)
44
+ newstr.encode!('UTF-16LE')
45
+ end
46
+
47
+ def create_command_pointer
48
+ string = @args.map { |arg| quote_if_necessary(arg.to_s) }.join(' ')
49
+ @cmd_ptr = to_wide_string(string)
50
+ end
51
+
52
+ def create_environment_pointer
53
+ return unless @environment.kind_of?(Hash) && @environment.any?
54
+
55
+ strings = []
56
+
57
+ ENV.to_hash.merge(@environment).each do |key, val|
58
+ next if val.nil?
59
+
60
+ if key.to_s =~ /=|\0/ || val.to_s.include?("\0")
61
+ raise InvalidEnvironmentVariable, "#{key.inspect} => #{val.inspect}"
62
+ end
63
+
64
+ strings << "#{key}=#{val}\0"
65
+ end
66
+
67
+ env_str = to_wide_string(strings.join)
68
+ @env_ptr = FFI::MemoryPointer.from_string(env_str)
69
+ end
70
+
71
+ def create_cwd_pointer
72
+ @cwd_ptr = FFI::MemoryPointer.from_string(to_wide_string(@cwd || Dir.pwd))
73
+ end
74
+
75
+ def create_process
76
+ ok = Lib.create_process(
77
+ nil, # application name
78
+ @cmd_ptr, # command line
79
+ nil, # process attributes
80
+ nil, # thread attributes
81
+ true, # inherit handles
82
+ @flags, # creation flags
83
+ @env_ptr, # environment
84
+ @cwd_ptr, # current directory
85
+ startup_info, # startup info
86
+ process_info # process info
87
+ )
88
+
89
+ ok or raise LaunchError, Lib.last_error_message
90
+
91
+ process_info[:dwProcessId]
92
+ end
93
+
94
+ def startup_info
95
+ @startup_info ||= StartupInfo.new
96
+ end
97
+
98
+ def process_info
99
+ @process_info ||= ProcessInfo.new
100
+ end
101
+
102
+ def setup_flags
103
+ @flags |= CREATE_UNICODE_ENVIRONMENT
104
+ @flags |= DETACHED_PROCESS if @detach
105
+ @flags |= CREATE_BREAKAWAY_FROM_JOB if @leader
106
+ end
107
+
108
+ def setup_io
109
+ startup_info[:dwFlags] ||= 0
110
+ startup_info[:dwFlags] |= STARTF_USESTDHANDLES
111
+
112
+ if @stdout
113
+ startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
114
+ end
115
+
116
+ if @stderr
117
+ startup_info[:hStdError] = std_stream_handle_for(@stderr)
118
+ end
119
+
120
+ if @duplex
121
+ read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
122
+ write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
123
+ sa = SecurityAttributes.new(:inherit => true)
124
+
125
+ ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
126
+ Lib.check_error ok
127
+
128
+ @read_pipe = read_pipe_ptr.read_pointer
129
+ @write_pipe = write_pipe_ptr.read_pointer
130
+
131
+ Lib.set_handle_inheritance @read_pipe, true
132
+ Lib.set_handle_inheritance @write_pipe, false
133
+
134
+ startup_info[:hStdInput] = @read_pipe
135
+ else
136
+ startup_info[:hStdInput] = std_stream_handle_for(STDIN)
137
+ end
138
+ end
139
+
140
+ def std_stream_handle_for(io)
141
+ handle = Lib.handle_for(io)
142
+
143
+ begin
144
+ Lib.set_handle_inheritance handle, true
145
+ rescue ChildProcess::Error
146
+ # If the IO was set to close on exec previously, this call will fail.
147
+ # That's probably OK, since the user explicitly asked for it to be
148
+ # closed (at least I have yet to find other cases where this will
149
+ # happen...)
150
+ end
151
+
152
+ handle
153
+ end
154
+
155
+ def close_handles
156
+ Lib.close_handle process_info[:hProcess]
157
+ Lib.close_handle process_info[:hThread]
158
+
159
+ if @duplex
160
+ @stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
161
+ Lib.close_handle @read_pipe
162
+ Lib.close_handle @write_pipe
163
+ end
164
+ end
165
+
166
+ def quote_if_necessary(str)
167
+ quote = str.start_with?('"') ? "'" : '"'
168
+
169
+ case str
170
+ when /[\s\\'"]/
171
+ [quote, str, quote].join
172
+ else
173
+ str
174
+ end
175
+ end
176
+ end # ProcessBuilder
177
+ end # Windows
178
+ end # ChildProcess