childprocess 0.9.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.document +6 -0
  3. data/.gitignore +28 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +44 -0
  6. data/CHANGELOG.md +49 -0
  7. data/Gemfile +15 -0
  8. data/LICENSE +20 -0
  9. data/README.md +196 -0
  10. data/Rakefile +61 -0
  11. data/appveyor.yml +60 -0
  12. data/childprocess.gemspec +30 -0
  13. data/lib/childprocess.rb +205 -0
  14. data/lib/childprocess/abstract_io.rb +36 -0
  15. data/lib/childprocess/abstract_process.rb +192 -0
  16. data/lib/childprocess/errors.rb +26 -0
  17. data/lib/childprocess/jruby.rb +56 -0
  18. data/lib/childprocess/jruby/io.rb +16 -0
  19. data/lib/childprocess/jruby/process.rb +159 -0
  20. data/lib/childprocess/jruby/pump.rb +53 -0
  21. data/lib/childprocess/tools/generator.rb +146 -0
  22. data/lib/childprocess/unix.rb +9 -0
  23. data/lib/childprocess/unix/fork_exec_process.rb +70 -0
  24. data/lib/childprocess/unix/io.rb +21 -0
  25. data/lib/childprocess/unix/lib.rb +186 -0
  26. data/lib/childprocess/unix/platform/i386-linux.rb +12 -0
  27. data/lib/childprocess/unix/platform/i386-solaris.rb +11 -0
  28. data/lib/childprocess/unix/platform/x86_64-linux.rb +12 -0
  29. data/lib/childprocess/unix/platform/x86_64-macosx.rb +11 -0
  30. data/lib/childprocess/unix/posix_spawn_process.rb +134 -0
  31. data/lib/childprocess/unix/process.rb +89 -0
  32. data/lib/childprocess/version.rb +3 -0
  33. data/lib/childprocess/windows.rb +33 -0
  34. data/lib/childprocess/windows/handle.rb +91 -0
  35. data/lib/childprocess/windows/io.rb +25 -0
  36. data/lib/childprocess/windows/lib.rb +416 -0
  37. data/lib/childprocess/windows/process.rb +130 -0
  38. data/lib/childprocess/windows/process_builder.rb +175 -0
  39. data/lib/childprocess/windows/structs.rb +149 -0
  40. data/spec/abstract_io_spec.rb +12 -0
  41. data/spec/childprocess_spec.rb +422 -0
  42. data/spec/io_spec.rb +228 -0
  43. data/spec/jruby_spec.rb +24 -0
  44. data/spec/pid_behavior.rb +12 -0
  45. data/spec/platform_detection_spec.rb +86 -0
  46. data/spec/spec_helper.rb +261 -0
  47. data/spec/unix_spec.rb +57 -0
  48. data/spec/windows_spec.rb +23 -0
  49. metadata +179 -0
@@ -0,0 +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
@@ -0,0 +1,175 @@
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
@@ -0,0 +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
149
+ end # ChildProcess