childprocess 1.0.0 → 4.1.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.document +6 -6
  3. data/.gitignore +28 -28
  4. data/.rspec +1 -1
  5. data/.travis.yml +37 -52
  6. data/CHANGELOG.md +83 -56
  7. data/Gemfile +9 -18
  8. data/LICENSE +20 -20
  9. data/README.md +230 -200
  10. data/Rakefile +61 -61
  11. data/appveyor.yml +36 -60
  12. data/childprocess.gemspec +26 -29
  13. data/lib/childprocess.rb +210 -210
  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 -37
  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 -184
  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/arm64-macosx.rb +11 -0
  27. data/lib/childprocess/unix/platform/i386-linux.rb +12 -12
  28. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -11
  29. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -12
  30. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -11
  31. data/lib/childprocess/unix/posix_spawn_process.rb +134 -134
  32. data/lib/childprocess/unix/process.rb +90 -89
  33. data/lib/childprocess/version.rb +3 -3
  34. data/lib/childprocess/windows.rb +38 -33
  35. data/lib/childprocess/windows/handle.rb +91 -91
  36. data/lib/childprocess/windows/io.rb +25 -25
  37. data/lib/childprocess/windows/lib.rb +416 -416
  38. data/lib/childprocess/windows/process.rb +131 -130
  39. data/lib/childprocess/windows/process_builder.rb +178 -178
  40. data/lib/childprocess/windows/structs.rb +148 -148
  41. data/spec/abstract_io_spec.rb +12 -12
  42. data/spec/childprocess_spec.rb +447 -447
  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 -270
  48. data/spec/unix_spec.rb +57 -57
  49. data/spec/windows_spec.rb +23 -23
  50. metadata +12 -26
  51. data/ext/mkrf_conf.rb +0 -24
@@ -1,130 +1,131 @@
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(detach?, true)
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(detach, leader)
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 if !detach
105
+ basic[:LimitFlags] |= JOB_OBJECT_LIMIT_BREAKAWAY_OK if leader
106
+
107
+ extended = JobObjectExtendedLimitInformation.new
108
+ extended[:BasicLimitInformation] = basic
109
+
110
+ ret = Lib.set_information_job_object(
111
+ @pointer,
112
+ JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
113
+ extended,
114
+ extended.size
115
+ )
116
+
117
+ Lib.check_error ret
118
+ end
119
+
120
+ def <<(handle)
121
+ Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
122
+ end
123
+
124
+ def close
125
+ Lib.close_handle @pointer
126
+ end
127
+ end
128
+
129
+ end # Process
130
+ end # Windows
131
+ end # ChildProcess
@@ -1,178 +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 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
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