childprocess 0.5.9 → 0.6.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.document +6 -6
  3. data/.gitignore +25 -25
  4. data/.rspec +1 -1
  5. data/.travis.yml +20 -18
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile +11 -4
  8. data/LICENSE +20 -20
  9. data/README.md +178 -178
  10. data/Rakefile +61 -61
  11. data/childprocess.gemspec +30 -29
  12. data/lib/childprocess.rb +184 -177
  13. data/lib/childprocess/abstract_io.rb +36 -36
  14. data/lib/childprocess/abstract_process.rb +187 -187
  15. data/lib/childprocess/errors.rb +26 -26
  16. data/lib/childprocess/jruby.rb +56 -56
  17. data/lib/childprocess/jruby/io.rb +16 -16
  18. data/lib/childprocess/jruby/process.rb +159 -159
  19. data/lib/childprocess/jruby/pump.rb +52 -52
  20. data/lib/childprocess/tools/generator.rb +145 -145
  21. data/lib/childprocess/unix.rb +9 -9
  22. data/lib/childprocess/unix/fork_exec_process.rb +70 -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 +89 -89
  31. data/lib/childprocess/version.rb +3 -3
  32. data/lib/childprocess/windows.rb +33 -33
  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 +415 -415
  36. data/lib/childprocess/windows/process.rb +129 -129
  37. data/lib/childprocess/windows/process_builder.rb +174 -174
  38. data/lib/childprocess/windows/structs.rb +148 -148
  39. data/spec/abstract_io_spec.rb +12 -12
  40. data/spec/childprocess_spec.rb +291 -256
  41. data/spec/io_spec.rb +228 -228
  42. data/spec/jruby_spec.rb +24 -24
  43. data/spec/pid_behavior.rb +12 -12
  44. data/spec/spec_helper.rb +253 -253
  45. data/spec/unix_spec.rb +57 -57
  46. data/spec/windows_spec.rb +23 -23
  47. metadata +47 -45
@@ -1,129 +1,129 @@
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.detach = detach?
60
- builder.duplex = duplex?
61
- builder.environment = @environment unless @environment.empty?
62
- builder.cwd = @cwd
63
-
64
- if @io
65
- builder.stdout = @io.stdout
66
- builder.stderr = @io.stderr
67
- end
68
-
69
- @pid = builder.start
70
- @handle = Handle.open @pid
71
-
72
- if leader?
73
- @job = Job.new
74
- @job << @handle
75
- end
76
-
77
- if duplex?
78
- raise Error, "no stdin stream" unless builder.stdin
79
- io._stdin = builder.stdin
80
- end
81
-
82
- self
83
- end
84
-
85
- def close_handle
86
- @handle.close
87
- end
88
-
89
- def close_job_if_necessary
90
- @job.close if leader?
91
- end
92
-
93
-
94
- class Job
95
- def initialize
96
- @pointer = Lib.create_job_object(nil, nil)
97
-
98
- if @pointer.nil? || @pointer.null?
99
- raise Error, "unable to create job object"
100
- end
101
-
102
- basic = JobObjectBasicLimitInformation.new
103
- basic[:LimitFlags] = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
104
-
105
- extended = JobObjectExtendedLimitInformation.new
106
- extended[:BasicLimitInformation] = basic
107
-
108
- ret = Lib.set_information_job_object(
109
- @pointer,
110
- JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
111
- extended,
112
- extended.size
113
- )
114
-
115
- Lib.check_error ret
116
- end
117
-
118
- def <<(handle)
119
- Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
120
- end
121
-
122
- def close
123
- Lib.close_handle @pointer
124
- end
125
- end
126
-
127
- end # Process
128
- end # Windows
129
- 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.detach = detach?
60
+ builder.duplex = duplex?
61
+ builder.environment = @environment unless @environment.empty?
62
+ builder.cwd = @cwd
63
+
64
+ if @io
65
+ builder.stdout = @io.stdout
66
+ builder.stderr = @io.stderr
67
+ end
68
+
69
+ @pid = builder.start
70
+ @handle = Handle.open @pid
71
+
72
+ if leader?
73
+ @job = Job.new
74
+ @job << @handle
75
+ end
76
+
77
+ if duplex?
78
+ raise Error, "no stdin stream" unless builder.stdin
79
+ io._stdin = builder.stdin
80
+ end
81
+
82
+ self
83
+ end
84
+
85
+ def close_handle
86
+ @handle.close
87
+ end
88
+
89
+ def close_job_if_necessary
90
+ @job.close if leader?
91
+ end
92
+
93
+
94
+ class Job
95
+ def initialize
96
+ @pointer = Lib.create_job_object(nil, nil)
97
+
98
+ if @pointer.nil? || @pointer.null?
99
+ raise Error, "unable to create job object"
100
+ end
101
+
102
+ basic = JobObjectBasicLimitInformation.new
103
+ basic[:LimitFlags] = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
104
+
105
+ extended = JobObjectExtendedLimitInformation.new
106
+ extended[:BasicLimitInformation] = basic
107
+
108
+ ret = Lib.set_information_job_object(
109
+ @pointer,
110
+ JOB_OBJECT_EXTENDED_LIMIT_INFORMATION,
111
+ extended,
112
+ extended.size
113
+ )
114
+
115
+ Lib.check_error ret
116
+ end
117
+
118
+ def <<(handle)
119
+ Lib.check_error Lib.assign_process_to_job_object(@pointer, handle.pointer)
120
+ end
121
+
122
+ def close
123
+ Lib.close_handle @pointer
124
+ end
125
+ end
126
+
127
+ end # Process
128
+ end # Windows
129
+ end # ChildProcess
@@ -1,174 +1,174 @@
1
- module ChildProcess
2
- module Windows
3
- class ProcessBuilder
4
- attr_accessor :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
- end
103
-
104
- def setup_io
105
- startup_info[:dwFlags] ||= 0
106
- startup_info[:dwFlags] |= STARTF_USESTDHANDLES
107
-
108
- if @stdout
109
- startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
110
- end
111
-
112
- if @stderr
113
- startup_info[:hStdError] = std_stream_handle_for(@stderr)
114
- end
115
-
116
- if @duplex
117
- read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
118
- write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
119
- sa = SecurityAttributes.new(:inherit => true)
120
-
121
- ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
122
- Lib.check_error ok
123
-
124
- @read_pipe = read_pipe_ptr.read_pointer
125
- @write_pipe = write_pipe_ptr.read_pointer
126
-
127
- Lib.set_handle_inheritance @read_pipe, true
128
- Lib.set_handle_inheritance @write_pipe, false
129
-
130
- startup_info[:hStdInput] = @read_pipe
131
- else
132
- startup_info[:hStdInput] = std_stream_handle_for(STDIN)
133
- end
134
- end
135
-
136
- def std_stream_handle_for(io)
137
- handle = Lib.handle_for(io)
138
-
139
- begin
140
- Lib.set_handle_inheritance handle, true
141
- rescue ChildProcess::Error
142
- # If the IO was set to close on exec previously, this call will fail.
143
- # That's probably OK, since the user explicitly asked for it to be
144
- # closed (at least I have yet to find other cases where this will
145
- # happen...)
146
- end
147
-
148
- handle
149
- end
150
-
151
- def close_handles
152
- Lib.close_handle process_info[:hProcess]
153
- Lib.close_handle process_info[:hThread]
154
-
155
- if @duplex
156
- @stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
157
- Lib.close_handle @read_pipe
158
- Lib.close_handle @write_pipe
159
- end
160
- end
161
-
162
- def quote_if_necessary(str)
163
- quote = str.start_with?('"') ? "'" : '"'
164
-
165
- case str
166
- when /[\s\\'"]/
167
- [quote, str, quote].join
168
- else
169
- str
170
- end
171
- end
172
- end # ProcessBuilder
173
- end # Windows
174
- end # ChildProcess
1
+ module ChildProcess
2
+ module Windows
3
+ class ProcessBuilder
4
+ attr_accessor :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
+ end
103
+
104
+ def setup_io
105
+ startup_info[:dwFlags] ||= 0
106
+ startup_info[:dwFlags] |= STARTF_USESTDHANDLES
107
+
108
+ if @stdout
109
+ startup_info[:hStdOutput] = std_stream_handle_for(@stdout)
110
+ end
111
+
112
+ if @stderr
113
+ startup_info[:hStdError] = std_stream_handle_for(@stderr)
114
+ end
115
+
116
+ if @duplex
117
+ read_pipe_ptr = FFI::MemoryPointer.new(:pointer)
118
+ write_pipe_ptr = FFI::MemoryPointer.new(:pointer)
119
+ sa = SecurityAttributes.new(:inherit => true)
120
+
121
+ ok = Lib.create_pipe(read_pipe_ptr, write_pipe_ptr, sa, 0)
122
+ Lib.check_error ok
123
+
124
+ @read_pipe = read_pipe_ptr.read_pointer
125
+ @write_pipe = write_pipe_ptr.read_pointer
126
+
127
+ Lib.set_handle_inheritance @read_pipe, true
128
+ Lib.set_handle_inheritance @write_pipe, false
129
+
130
+ startup_info[:hStdInput] = @read_pipe
131
+ else
132
+ startup_info[:hStdInput] = std_stream_handle_for(STDIN)
133
+ end
134
+ end
135
+
136
+ def std_stream_handle_for(io)
137
+ handle = Lib.handle_for(io)
138
+
139
+ begin
140
+ Lib.set_handle_inheritance handle, true
141
+ rescue ChildProcess::Error
142
+ # If the IO was set to close on exec previously, this call will fail.
143
+ # That's probably OK, since the user explicitly asked for it to be
144
+ # closed (at least I have yet to find other cases where this will
145
+ # happen...)
146
+ end
147
+
148
+ handle
149
+ end
150
+
151
+ def close_handles
152
+ Lib.close_handle process_info[:hProcess]
153
+ Lib.close_handle process_info[:hThread]
154
+
155
+ if @duplex
156
+ @stdin = Lib.io_for(Lib.duplicate_handle(@write_pipe), File::WRONLY)
157
+ Lib.close_handle @read_pipe
158
+ Lib.close_handle @write_pipe
159
+ end
160
+ end
161
+
162
+ def quote_if_necessary(str)
163
+ quote = str.start_with?('"') ? "'" : '"'
164
+
165
+ case str
166
+ when /[\s\\'"]/
167
+ [quote, str, quote].join
168
+ else
169
+ str
170
+ end
171
+ end
172
+ end # ProcessBuilder
173
+ end # Windows
174
+ end # ChildProcess