win32-process 0.6.2 → 0.6.3

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.
data/Rakefile CHANGED
@@ -1,35 +1,28 @@
1
- require 'rake'
2
- require 'rake/testtask'
3
- require 'rbconfig'
4
- include Config
5
-
6
- desc 'Install the win32-process library (non-gem)'
7
- task :install do
8
- install_dir = File.join(CONFIG['sitelibdir'], 'win32')
9
- Dir.mkdir(install_dir) unless File.exists?(install_dir)
10
- cp 'lib/win32/process.rb', install_dir, :verbose => true
11
- end
12
-
13
- desc 'Removes the win32-process library (non-gem)'
14
- task :uninstall do
15
- file = File.join(CONFIG['sitelibdir'], 'win32', 'process.rb')
16
- if File.exists?(file)
17
- rm_f file, :verbose => true
18
- end
19
- end
20
-
21
- desc 'Builds a gem for the win32-process library'
22
- task :gem do
23
- spec = eval(IO.read('win32-process.gemspec'))
24
- Gem::Builder.new(spec).build
25
- end
26
-
27
- task :install_gem => [:gem] do
28
- file = Dir["*.gem"].first
29
- sh "gem install #{file}"
30
- end
31
-
32
- Rake::TestTask.new do |t|
33
- t.verbose = true
34
- t.warning = true
35
- end
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+ require 'rbconfig'
5
+ include Config
6
+
7
+ CLEAN.include('**/*.gem', '**/*.rbc')
8
+
9
+ namespace :gem do
10
+ desc 'Create the win32-process gem'
11
+ task :create do
12
+ spec = eval(IO.read('win32-process.gemspec'))
13
+ Gem::Builder.new(spec).build
14
+ end
15
+
16
+ desc 'Install the win32-process gem'
17
+ task :install => [:create] do
18
+ file = Dir["*.gem"].first
19
+ sh "gem install #{file}"
20
+ end
21
+ end
22
+
23
+ Rake::TestTask.new do |t|
24
+ t.verbose = true
25
+ t.warning = true
26
+ end
27
+
28
+ task :default => :test
@@ -1,39 +1,39 @@
1
- ##########################################################################
2
- # test_create.rb
3
- #
4
- # Simple test program for the Process.create() method.
5
- ##########################################################################
6
- Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
7
- $LOAD_PATH.unshift Dir.pwd
8
- $LOAD_PATH.unshift Dir.pwd + '/lib'
9
- Dir.chdir('examples') rescue nil
10
-
11
- require "win32/process"
12
-
13
- p Process::WIN32_PROCESS_VERSION
14
-
15
- struct = Process.create(
16
- :app_name => "notepad.exe",
17
- :creation_flags => Process::DETACHED_PROCESS,
18
- :process_inherit => false,
19
- :thread_inherit => true,
20
- :cwd => "C:\\",
21
- :inherit => true,
22
- :environment => "SYSTEMROOT=#{ENV['SYSTEMROOT']};PATH=C:\\"
23
- )
24
-
25
- p struct
26
-
27
- =begin
28
- # Don't run this from an existing terminal
29
- pid = Process.create(
30
- :app_name => "cmd.exe",
31
- :creation_flags => Process::DETACHED_PROCESS,
32
- :startf_flags => Process::USEPOSITION,
33
- :x => 0,
34
- :y => 0,
35
- :title => "Hi Dan"
36
- )
37
-
38
- puts "Pid of new process: #{pid}"
39
- =end
1
+ ##########################################################################
2
+ # test_create.rb
3
+ #
4
+ # Simple test program for the Process.create() method.
5
+ ##########################################################################
6
+ Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
7
+ $LOAD_PATH.unshift Dir.pwd
8
+ $LOAD_PATH.unshift Dir.pwd + '/lib'
9
+ Dir.chdir('examples') rescue nil
10
+
11
+ require "win32/process"
12
+
13
+ p Process::WIN32_PROCESS_VERSION
14
+
15
+ struct = Process.create(
16
+ :app_name => "notepad.exe",
17
+ :creation_flags => Process::DETACHED_PROCESS,
18
+ :process_inherit => false,
19
+ :thread_inherit => true,
20
+ :cwd => "C:\\",
21
+ :inherit => true,
22
+ :environment => "SYSTEMROOT=#{ENV['SYSTEMROOT']};PATH=C:\\"
23
+ )
24
+
25
+ p struct
26
+
27
+ =begin
28
+ # Don't run this from an existing terminal
29
+ pid = Process.create(
30
+ :app_name => "cmd.exe",
31
+ :creation_flags => Process::DETACHED_PROCESS,
32
+ :startf_flags => Process::USEPOSITION,
33
+ :x => 0,
34
+ :y => 0,
35
+ :title => "Hi Dan"
36
+ )
37
+
38
+ puts "Pid of new process: #{pid}"
39
+ =end
@@ -1,33 +1,33 @@
1
- ##########################################################################
2
- # test_fork_wait.rb
3
- #
4
- # Generic test script for futzing around with the block form of fork/wait
5
- ##########################################################################
6
- Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
7
- $LOAD_PATH.unshift Dir.pwd
8
- $LOAD_PATH.unshift Dir.pwd + '/lib'
9
- Dir.chdir('examples') rescue nil
10
-
11
- require 'win32/process'
12
-
13
- puts "VERSION: " + Process::WIN32_PROCESS_VERSION
14
-
15
- # In the child, using block form
16
- fork{
17
- 7.times { |i|
18
- puts "Child: #{i}"
19
- sleep 1
20
- }
21
- }
22
-
23
- # Back in the parent
24
- 4.times{ |i|
25
- puts "Parent: #{i}"
26
- sleep 1
27
- }
28
-
29
- # Wait for the children
30
- Process.wait
31
-
32
- # Children should be done here before continuing on
33
- puts "Continuing on..."
1
+ ##########################################################################
2
+ # test_fork_wait.rb
3
+ #
4
+ # Generic test script for futzing around with the block form of fork/wait
5
+ ##########################################################################
6
+ Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
7
+ $LOAD_PATH.unshift Dir.pwd
8
+ $LOAD_PATH.unshift Dir.pwd + '/lib'
9
+ Dir.chdir('examples') rescue nil
10
+
11
+ require 'win32/process'
12
+
13
+ puts "VERSION: " + Process::WIN32_PROCESS_VERSION
14
+
15
+ # In the child, using block form
16
+ fork{
17
+ 7.times { |i|
18
+ puts "Child: #{i}"
19
+ sleep 1
20
+ }
21
+ }
22
+
23
+ # Back in the parent
24
+ 4.times{ |i|
25
+ puts "Parent: #{i}"
26
+ sleep 1
27
+ }
28
+
29
+ # Wait for the children
30
+ Process.wait
31
+
32
+ # Children should be done here before continuing on
33
+ puts "Continuing on..."
@@ -1,50 +1,50 @@
1
- ##########################################################################
2
- # test_fork_waitpid.rb
3
- #
4
- # Generic test script for futzing around with the traditional form of
5
- # fork/wait, plus waitpid and waitpid2.
6
- ##########################################################################
7
- Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
8
- $LOAD_PATH.unshift Dir.pwd
9
- $LOAD_PATH.unshift Dir.pwd + '/lib'
10
- Dir.chdir('examples') rescue nil
11
-
12
- require 'win32/process'
13
-
14
- puts "VERSION: " + Process::WIN32_PROCESS_VERSION
15
-
16
- pid = Process.fork
17
- puts "PID1: #{pid}"
18
-
19
- #child
20
- if pid.nil?
21
- 7.times{ |i|
22
- puts "Child: #{i}"
23
- sleep 1
24
- }
25
- exit(-1)
26
- end
27
-
28
- pid2 = Process.fork
29
- puts "PID2: #{pid2}"
30
-
31
- #child2
32
- if pid2.nil?
33
- 7.times{ |i|
34
- puts "Child2: #{i}"
35
- sleep 1
36
- }
37
- exit(1)
38
- end
39
-
40
- #parent
41
- 2.times { |i|
42
- puts "Parent: #{i}"
43
- sleep 1
44
- }
45
-
46
- p Process.waitpid2(pid)
47
- p Process.waitpid2(pid2)
48
-
49
- puts "Continuing on..."
50
-
1
+ ##########################################################################
2
+ # test_fork_waitpid.rb
3
+ #
4
+ # Generic test script for futzing around with the traditional form of
5
+ # fork/wait, plus waitpid and waitpid2.
6
+ ##########################################################################
7
+ Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
8
+ $LOAD_PATH.unshift Dir.pwd
9
+ $LOAD_PATH.unshift Dir.pwd + '/lib'
10
+ Dir.chdir('examples') rescue nil
11
+
12
+ require 'win32/process'
13
+
14
+ puts "VERSION: " + Process::WIN32_PROCESS_VERSION
15
+
16
+ pid = Process.fork
17
+ puts "PID1: #{pid}"
18
+
19
+ #child
20
+ if pid.nil?
21
+ 7.times{ |i|
22
+ puts "Child: #{i}"
23
+ sleep 1
24
+ }
25
+ exit(-1)
26
+ end
27
+
28
+ pid2 = Process.fork
29
+ puts "PID2: #{pid2}"
30
+
31
+ #child2
32
+ if pid2.nil?
33
+ 7.times{ |i|
34
+ puts "Child2: #{i}"
35
+ sleep 1
36
+ }
37
+ exit(1)
38
+ end
39
+
40
+ #parent
41
+ 2.times { |i|
42
+ puts "Parent: #{i}"
43
+ sleep 1
44
+ }
45
+
46
+ p Process.waitpid2(pid)
47
+ p Process.waitpid2(pid2)
48
+
49
+ puts "Continuing on..."
50
+
@@ -1,35 +1,35 @@
1
- ##########################################################################
2
- # test_kill.rb
3
- #
4
- # Generic test script for futzing around Process.kill. This script
5
- # requires the sys-proctable package (available on the RAA).
6
- ##########################################################################
7
- Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
8
- $LOAD_PATH.unshift Dir.pwd
9
- $LOAD_PATH.unshift Dir.pwd + '/lib'
10
- Dir.chdir('examples') rescue nil
11
-
12
- require "win32/process"
13
-
14
- begin
15
- require "sys/proctable"
16
- rescue LoadError
17
- STDERR.puts "Whoa there!"
18
- STDERR.puts "This script requires the sys-proctable package to work."
19
- STDERR.puts "You can find it at http://ruby-sysutils.sf.net"
20
- STDERR.puts "Exiting..."
21
- exit
22
- end
23
- include Sys
24
-
25
- puts "VERSION: " + Process::WIN32_PROCESS_VERSION
26
-
27
- IO.popen("notepad")
28
- sleep 1 # Give it a chance to start before checking for its pid
29
-
30
- pids = []
31
- ProcTable.ps{ |s|
32
- pids.push(s.pid) if s.cmdline =~ /notepad/i
33
- }
34
-
1
+ ##########################################################################
2
+ # test_kill.rb
3
+ #
4
+ # Generic test script for futzing around Process.kill. This script
5
+ # requires the sys-proctable package (available on the RAA).
6
+ ##########################################################################
7
+ Dir.chdir('..') if File.basename(Dir.pwd) == 'examples'
8
+ $LOAD_PATH.unshift Dir.pwd
9
+ $LOAD_PATH.unshift Dir.pwd + '/lib'
10
+ Dir.chdir('examples') rescue nil
11
+
12
+ require "win32/process"
13
+
14
+ begin
15
+ require "sys/proctable"
16
+ rescue LoadError
17
+ STDERR.puts "Whoa there!"
18
+ STDERR.puts "This script requires the sys-proctable package to work."
19
+ STDERR.puts "You can find it at http://ruby-sysutils.sf.net"
20
+ STDERR.puts "Exiting..."
21
+ exit
22
+ end
23
+ include Sys
24
+
25
+ puts "VERSION: " + Process::WIN32_PROCESS_VERSION
26
+
27
+ IO.popen("notepad")
28
+ sleep 1 # Give it a chance to start before checking for its pid
29
+
30
+ pids = []
31
+ ProcTable.ps{ |s|
32
+ pids.push(s.pid) if s.cmdline =~ /notepad/i
33
+ }
34
+
35
35
  p Process.kill(9,pids.last)
@@ -1,1004 +1,1098 @@
1
- require 'windows/error'
2
- require 'windows/process'
3
- require 'windows/thread'
4
- require 'windows/synchronize'
5
- require 'windows/handle'
6
- require 'windows/library'
7
- require 'windows/console'
8
- require 'windows/window'
9
- require 'windows/unicode'
10
- require 'windows/tool_helper'
11
- require 'windows/security'
12
- require 'windows/msvcrt/string'
13
-
14
- module Process
15
- # The Process::Error class is typically raised if any of the custom
16
- # Process methods fail.
17
- class Error < RuntimeError; end
18
-
19
- # Eliminates redefinition warnings.
20
- undef_method :getpriority, :kill, :getrlimit, :ppid, :setpriority
21
- undef_method :wait, :wait2, :waitpid, :waitpid2, :uid
22
-
23
- # The version of the win32-process library
24
- WIN32_PROCESS_VERSION = '0.6.2'
25
-
26
- include Windows::Process
27
- include Windows::Thread
28
- include Windows::Error
29
- include Windows::Library
30
- include Windows::Console
31
- include Windows::Handle
32
- include Windows::Security
33
- include Windows::Synchronize
34
- include Windows::Window
35
- include Windows::Unicode
36
- include Windows::ToolHelper
37
- include Windows::MSVCRT::String
38
-
39
- extend Windows::Error
40
- extend Windows::Process
41
- extend Windows::Thread
42
- extend Windows::Security
43
- extend Windows::Synchronize
44
- extend Windows::Handle
45
- extend Windows::Library
46
- extend Windows::Console
47
- extend Windows::Unicode
48
- extend Windows::ToolHelper
49
- extend Windows::MSVCRT::String
50
-
51
- # :stopdoc:
52
-
53
- # Used by Process.create
54
- ProcessInfo = Struct.new("ProcessInfo",
55
- :process_handle,
56
- :thread_handle,
57
- :process_id,
58
- :thread_id
59
- )
60
-
61
- @child_pids = [] # Static variable used for Process.fork
62
- @i = -1 # Static variable used for Process.fork
63
-
64
- # These are probably not defined on MS Windows by default
65
- unless defined? RLIMIT_CPU
66
- RLIMIT_CPU = 0 # PerProcessUserTimeLimit
67
- RLIMIT_FSIZE = 1 # Hard coded at 4TB - 64K (assumes NTFS)
68
- RLIMIT_AS = 5 # ProcessMemoryLimit
69
- RLIMIT_RSS = 5 # ProcessMemoryLimit
70
- RLIMIT_VMEM = 5 # ProcessMemoryLimit
71
- end
72
-
73
- # :startdoc:
74
-
75
- # Returns the process and system affinity mask for the given +pid+, or the
76
- # current process if no pid is provided. The return value is a two element
77
- # array, with the first containing the process affinity mask, and the second
78
- # containing the system affinity mask. Both are decimal values.
79
- #
80
- # A process affinity mask is a bit vector indicating the processors that a
81
- # process is allowed to run on. A system affinity mask is a bit vector in
82
- # which each bit represents the processors that are configured into a
83
- # system.
84
- #
85
- # Example:
86
- #
87
- # # System has 4 processors, current process is allowed to run on all
88
- # Process.get_affinity # => [[15], [15]]
89
- #
90
- # # System has 4 processors, current process only allowed on 1 and 4 only
91
- # Process.get_affinity # => [[9], [15]]
92
- #
93
- # If you want to convert a decimal bit vector into an array of 0's and 1's
94
- # indicating the flag value of each processor, you can use something like
95
- # this approach:
96
- #
97
- # mask = Process.get_affinity.first
98
- # (0..mask).to_a.map{ |n| mask[n] }
99
- #
100
- def get_affinity(int = Process.pid)
101
- pmask = 0.chr * 4
102
- smask = 0.chr * 4
103
-
104
- if int == Process.pid
105
- unless GetProcessAffinityMask(GetCurrentProcess(), pmask, smask)
106
- raise Error, get_last_error
107
- end
108
- else
109
- begin
110
- handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
111
-
112
- if handle == INVALID_HANDLE_VALUE
113
- raise Error, get_last_error
114
- end
115
-
116
- unless GetProcessAffinityMask(handle, pmask, smask)
117
- raise Error, get_last_error
118
- end
119
- ensure
120
- CloseHandle(handle) if handle != INVALID_HANDLE_VALUE
121
- end
122
- end
123
-
124
- pmask = pmask.unpack('L').first
125
- smask = smask.unpack('L').first
126
-
127
- [pmask, smask]
128
- end
129
-
130
- # Gets the resource limit of the current process. Only a limited number
131
- # of flags are supported.
132
- #
133
- # Process::RLIMIT_CPU
134
- # Process::RLIMIT_FSIZE
135
- # Process::RLIMIT_AS
136
- # Process::RLIMIT_RSS
137
- # Process::RLIMIT_VMEM
138
- #
139
- # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
140
- # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
141
- # refers to the per process user time limit. The Process::RLIMIT_FSIZE
142
- # constant is hard coded to the maximum file size on an NTFS filesystem,
143
- # approximately 4TB.
144
- #
145
- # While a two element array is returned in order to comply with the spec,
146
- # there is no separate hard and soft limit. The values will always be the
147
- # same.
148
- #
149
- # If [0,0] is returned then it means no limit has been set.
150
- #
151
- def getrlimit(resource)
152
- # Strictly for API compatibility (actually 4 GB on FAT32)
153
- # TODO: Check FS type. If FAT32, return 4 GB.
154
- if resource == RLIMIT_FSIZE
155
- return ((1024**4) * 4) - (1024 * 64)
156
- end
157
-
158
- # Put the current Ruby process in its own job unless it's already
159
- # part of a job.
160
- bool = 0.chr * 4
161
- IsProcessInJob(GetCurrentProcess(), nil, bool)
162
- bool = bool.unpack('L').first == 0 ? false : true
163
-
164
- unless bool
165
- job_name = 'ruby_' + Time.now.to_s
166
-
167
- # Create a job object and add the current process to it
168
- handle = CreateJobObject(nil, job_name)
169
-
170
- if handle == 0
171
- raise Error, get_last_error
172
- end
173
- end
174
-
175
- begin
176
- unless bool
177
- unless AssignProcessToJobObject(handle, GetCurrentProcess())
178
- raise Error, get_last_error
179
- end
180
- end
181
-
182
- buf = 0.chr * 112 # sizeof(struct JOBJECT_EXTENDED_LIMIT_INFORMATION)
183
- val = nil # value returned at end of method
184
-
185
- case resource
186
- when RLIMIT_CPU
187
- buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_TIME].pack('L')
188
- when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
189
- buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_MEMORY].pack('L')
190
- end
191
-
192
- bool = QueryInformationJobObject(
193
- handle,
194
- JobObjectExtendedLimitInformation,
195
- buf,
196
- buf.size,
197
- nil
198
- )
199
-
200
- unless bool
201
- raise Error, get_last_error
202
- end
203
-
204
- case resource
205
- when Process::RLIMIT_CPU
206
- val = buf[0,8].unpack('Q').first
207
- when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
208
- val = buf[96,4].unpack('L').first
209
- end
210
- ensure
211
- CloseHandle(handle)
212
- end
213
-
214
- [val, val] # Return an array of two values to comply with spec
215
- end
216
-
217
- # Retrieves the priority class for the specified process id +int+. Unlike
218
- # the default implementation, lower values do not necessarily correspond to
219
- # higher priority classes.
220
- #
221
- # The +kind+ parameter is ignored but present for API compatibility.
222
- # You can only retrieve process information, not process group or user
223
- # information, so it is effectively always Process::PRIO_PROCESS.
224
- #
225
- # Possible return values are:
226
- #
227
- # 32 - Process::NORMAL_PRIORITY_CLASS
228
- # 64 - Process::IDLE_PRIORITY_CLASS
229
- # 128 - Process::HIGH_PRIORITY_CLASS
230
- # 256 - Process::REALTIME_PRIORITY_CLASS
231
- # 16384 - Process::BELOW_NORMAL_PRIORITY_CLASS
232
- # 32768 - Process::ABOVE_NORMAL_PRIORITY_CLASS
233
- #
234
- def getpriority(kind = Process::PRIO_PROCESS, int = nil)
235
- raise ArgumentError unless int
236
-
237
- handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
238
-
239
- if handle == INVALID_HANDLE_VALUE
240
- raise Error, get_last_error
241
- end
242
-
243
- priority_class = GetPriorityClass(handle)
244
-
245
- if priority_class == 0
246
- raise Error, get_last_error
247
- end
248
-
249
- priority_class
250
- end
251
-
252
- # Sets the priority class for the specified process id +int+.
253
- #
254
- # The +kind+ parameter is ignored but present for API compatibility.
255
- # You can only retrieve process information, not process group or user
256
- # information, so it is effectively always Process::PRIO_PROCESS.
257
- #
258
- # Possible +int_priority+ values are:
259
- #
260
- # * Process::NORMAL_PRIORITY_CLASS
261
- # * Process::IDLE_PRIORITY_CLASS
262
- # * Process::HIGH_PRIORITY_CLASS
263
- # * Process::REALTIME_PRIORITY_CLASS
264
- # * Process::BELOW_NORMAL_PRIORITY_CLASS
265
- # * Process::ABOVE_NORMAL_PRIORITY_CLASS
266
- #
267
- def setpriority(kind = nil, int = nil, int_priority = nil)
268
- raise ArgumentError unless int
269
- raise ArgumentError unless int_priority
270
-
271
- handle = OpenProcess(PROCESS_SET_INFORMATION, 0 , int)
272
-
273
- if handle == INVALID_HANDLE_VALUE
274
- raise Error, get_last_error
275
- end
276
-
277
- unless SetPriorityClass(handle, int_priority)
278
- raise Error, get_last_error
279
- end
280
-
281
- return 0 # Match the spec
282
- end
283
-
284
- # Returns the uid of the current process. Specifically, it returns the
285
- # RID of the SID associated with the owner of the process.
286
- #
287
- # If +sid+ is set to true, then a binary sid is returned. Otherwise, a
288
- # numeric id is returned (the default).
289
- #--
290
- # The Process.uid method in core Ruby always returns 0 on MS Windows.
291
- #
292
- def uid(sid = false)
293
- token = 0.chr * 4
294
-
295
- unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
296
- raise Error, get_last_error
297
- end
298
-
299
- token = token.unpack('V')[0]
300
- rlength = 0.chr * 4
301
- tuser = 0.chr * 512
302
-
303
- bool = GetTokenInformation(
304
- token,
305
- TokenUser,
306
- tuser,
307
- tuser.size,
308
- rlength
309
- )
310
-
311
- unless bool
312
- raise Error, get_last_error
313
- end
314
-
315
- lsid = tuser[8, (rlength.unpack('L').first - 8)]
316
-
317
- if sid
318
- lsid
319
- else
320
- sid_addr = [lsid].pack('p*').unpack('L')[0]
321
- sid_buf = 0.chr * 80
322
- sid_ptr = 0.chr * 4
323
-
324
- unless ConvertSidToStringSid(sid_addr, sid_ptr)
325
- raise Error, get_last_error
326
- end
327
-
328
- strcpy(sid_buf, sid_ptr.unpack('L')[0])
329
- sid_buf.strip.split('-').last.to_i
330
- end
331
- end
332
-
333
- # Waits for the given child process to exit and returns that pid.
334
- #
335
- # Note that the $? (Process::Status) global variable is NOT set. This
336
- # may be addressed in a future release.
337
- #
338
- def waitpid(pid)
339
- exit_code = [0].pack('L')
340
- handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
341
-
342
- if handle == INVALID_HANDLE_VALUE
343
- raise Error, get_last_error
344
- end
345
-
346
- # TODO: update the $? global variable (if/when possible)
347
- status = WaitForSingleObject(handle, INFINITE)
348
-
349
- begin
350
- unless GetExitCodeProcess(handle, exit_code)
351
- raise Error, get_last_error
352
- end
353
- ensure
354
- CloseHandle(handle)
355
- end
356
-
357
- @child_pids.delete(pid)
358
-
359
- # TODO: update the $? global variable (if/when possible)
360
- exit_code = exit_code.unpack('L').first
361
-
362
- pid
363
- end
364
-
365
- # Waits for the given child process to exit and returns an array containing
366
- # the process id and the exit status.
367
- #
368
- # Note that the $? (Process::Status) global variable is NOT set. This
369
- # may be addressed in a future release if/when possible.
370
- #--
371
- # Ruby does not provide a way to hook into $? so there's no way for us
372
- # to set it.
373
- #
374
- def waitpid2(pid)
375
- exit_code = [0].pack('L')
376
- handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
377
-
378
- if handle == INVALID_HANDLE_VALUE
379
- raise Error, get_last_error
380
- end
381
-
382
- # TODO: update the $? global variable (if/when possible)
383
- status = WaitForSingleObject(handle, INFINITE)
384
-
385
- begin
386
- unless GetExitCodeProcess(handle, exit_code)
387
- raise Error, get_last_error
388
- end
389
- ensure
390
- CloseHandle(handle)
391
- end
392
-
393
- @child_pids.delete(pid)
394
-
395
- # TODO: update the $? global variable (if/when possible)
396
- exit_code = exit_code.unpack('L').first
397
-
398
- [pid, exit_code]
399
- end
400
-
401
- # Sends the given +signal+ to an array of process id's. The +signal+ may
402
- # be any value from 0 to 9, or the special strings 'SIGINT' (or 'INT'),
403
- # 'SIGBRK' (or 'BRK') and 'SIGKILL' (or 'KILL'). An array of successfully
404
- # killed pids is returned.
405
- #
406
- # Signal 0 merely tests if the process is running without killing it.
407
- # Signal 2 sends a CTRL_C_EVENT to the process.
408
- # Signal 3 sends a CTRL_BRK_EVENT to the process.
409
- # Signal 9 kills the process in a harsh manner.
410
- # Signals 1 and 4-8 kill the process in a nice manner.
411
- #
412
- # SIGINT/INT corresponds to signal 2
413
- # SIGBRK/BRK corresponds to signal 3
414
- # SIGKILL/KILL corresponds to signal 9
415
- #
416
- # Signals 2 and 3 only affect console processes, and then only if the
417
- # process was created with the CREATE_NEW_PROCESS_GROUP flag.
418
- #
419
- def kill(signal, *pids)
420
- case signal
421
- when 'SIGINT', 'INT'
422
- signal = 2
423
- when 'SIGBRK', 'BRK'
424
- signal = 3
425
- when 'SIGKILL', 'KILL'
426
- signal = 9
427
- when 0..9
428
- # Do nothing
429
- else
430
- raise Error, "Invalid signal '#{signal}'"
431
- end
432
-
433
- killed_pids = []
434
-
435
- pids.each{ |pid|
436
- # Send the signal to the current process if the pid is zero
437
- if pid == 0
438
- pid = Process.pid
439
- end
440
-
441
- # No need for full access if the signal is zero
442
- if signal == 0
443
- access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
444
- handle = OpenProcess(access, 0 , pid)
445
- else
446
- handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
447
- end
448
-
449
- begin
450
- case signal
451
- when 0
452
- if handle != 0
453
- killed_pids.push(pid)
454
- else
455
- # If ERROR_ACCESS_DENIED is returned, we know it's running
456
- if GetLastError() == ERROR_ACCESS_DENIED
457
- killed_pids.push(pid)
458
- else
459
- raise Error, get_last_error
460
- end
461
- end
462
- when 2
463
- if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
464
- killed_pids.push(pid)
465
- end
466
- when 3
467
- if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
468
- killed_pids.push(pid)
469
- end
470
- when 9
471
- if TerminateProcess(handle, pid)
472
- killed_pids.push(pid)
473
- @child_pids.delete(pid)
474
- else
475
- raise Error, get_last_error
476
- end
477
- else
478
- if handle != 0
479
- thread_id = [0].pack('L')
480
- dll = 'kernel32'
481
- eproc = 'ExitProcess'
482
-
483
- thread = CreateRemoteThread(
484
- handle,
485
- 0,
486
- 0,
487
- GetProcAddress(GetModuleHandle(dll), eproc),
488
- 0,
489
- 0,
490
- thread_id
491
- )
492
-
493
- if thread
494
- WaitForSingleObject(thread, 5)
495
- killed_pids.push(pid)
496
- @child_pids.delete(pid)
497
- else
498
- raise Error, get_last_error
499
- end
500
- else
501
- raise Error, get_last_error
502
- end
503
-
504
- @child_pids.delete(pid)
505
- end
506
- ensure
507
- CloseHandle(handle) unless handle == INVALID_HANDLE_VALUE
508
- end
509
- }
510
-
511
- killed_pids
512
- end
513
-
514
- # Process.create(key => value, ...) => ProcessInfo
515
- #
516
- # This is a wrapper for the CreateProcess() function. It executes a process,
517
- # returning a ProcessInfo struct. It accepts a hash as an argument.
518
- # There are several primary keys:
519
- #
520
- # * command_line (mandatory)
521
- # * app_name (default: nil)
522
- # * inherit (default: false)
523
- # * process_inherit (default: false)
524
- # * thread_inherit (default: false)
525
- # * creation_flags (default: 0)
526
- # * cwd (default: Dir.pwd)
527
- # * startup_info (default: nil)
528
- # * environment (default: nil)
529
- # * close_handles (default: true)
530
- # * with_logon (default: nil)
531
- # * domain (default: nil)
532
- # * password (default: nil)
533
- #
534
- # Of these, the 'command_line' or 'app_name' must be specified or an
535
- # error is raised. Both may be set individually, but 'command_line' should
536
- # be preferred if only one of them is set because it does not (necessarily)
537
- # require an explicit path or extension to work.
538
- #
539
- # The 'domain' and 'password' options are only relevent in the context
540
- # of 'with_logon'.
541
- #
542
- # The startup_info key takes a hash. Its keys are attributes that are
543
- # part of the StartupInfo struct, and are generally only meaningful for
544
- # GUI or console processes. See the documentation on CreateProcess()
545
- # and the StartupInfo struct on MSDN for more information.
546
- #
547
- # * desktop
548
- # * title
549
- # * x
550
- # * y
551
- # * x_size
552
- # * y_size
553
- # * x_count_chars
554
- # * y_count_chars
555
- # * fill_attribute
556
- # * sw_flags
557
- # * startf_flags
558
- # * stdin
559
- # * stdout
560
- # * stderr
561
- #
562
- # The relevant constants for 'creation_flags', 'sw_flags' and 'startf_flags'
563
- # are included in the Windows::Process, Windows::Console and Windows::Window
564
- # modules. These come with the windows-pr library, a prerequisite of this
565
- # library. Note that the 'stdin', 'stdout' and 'stderr' options can be
566
- # either Ruby IO objects or file descriptors (i.e. a fileno). However,
567
- # StringIO objects are not currently supported.
568
- #
569
- # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
570
- # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
571
- # automatically OR'd to the +startf_flags+ value.
572
- #
573
- # The ProcessInfo struct contains the following members:
574
- #
575
- # * process_handle - The handle to the newly created process.
576
- # * thread_handle - The handle to the primary thread of the process.
577
- # * process_id - Process ID.
578
- # * thread_id - Thread ID.
579
- #
580
- # If the 'close_handles' option is set to true (the default) then the
581
- # process_handle and the thread_handle are automatically closed for you
582
- # before the ProcessInfo struct is returned.
583
- #
584
- # If the 'with_logon' option is set, then the process runs the specified
585
- # executable file in the security context of the specified credentials.
586
- #
587
- def create(args)
588
- unless args.kind_of?(Hash)
589
- raise TypeError, 'Expecting hash-style keyword arguments'
590
- end
591
-
592
- valid_keys = %w/
593
- app_name command_line inherit creation_flags cwd environment
594
- startup_info thread_inherit process_inherit close_handles with_logon
595
- domain password
596
- /
597
-
598
- valid_si_keys = %/
599
- startf_flags desktop title x y x_size y_size x_count_chars
600
- y_count_chars fill_attribute sw_flags stdin stdout stderr
601
- /
602
-
603
- # Set default values
604
- hash = {
605
- 'app_name' => nil,
606
- 'creation_flags' => 0,
607
- 'close_handles' => true
608
- }
609
-
610
- # Validate the keys, and convert symbols and case to lowercase strings.
611
- args.each{ |key, val|
612
- key = key.to_s.downcase
613
- unless valid_keys.include?(key)
614
- raise Error, "invalid key '#{key}'"
615
- end
616
- hash[key] = val
617
- }
618
-
619
- si_hash = {}
620
-
621
- # If the startup_info key is present, validate its subkeys
622
- if hash['startup_info']
623
- hash['startup_info'].each{ |key, val|
624
- key = key.to_s.downcase
625
- unless valid_si_keys.include?(key)
626
- raise Error, "invalid startup_info key '#{key}'"
627
- end
628
- si_hash[key] = val
629
- }
630
- end
631
-
632
- # The +command_line+ key is mandatory unless the +app_name+ key
633
- # is specified.
634
- unless hash['command_line']
635
- if hash['app_name']
636
- hash['command_line'] = hash['app_name']
637
- hash['app_name'] = nil
638
- else
639
- raise Error, 'command_line or app_name must be specified'
640
- end
641
- end
642
-
643
- # The environment string should be passed as a string of ';' separated
644
- # paths.
645
- if hash['environment']
646
- env = hash['environment'].split(File::PATH_SEPARATOR) << 0.chr
647
- if hash['with_logon']
648
- env = env.map{ |e| multi_to_wide(e) }
649
- env = [env.join("\0\0")].pack('p*').unpack('L').first
650
- else
651
- env = [env.join("\0")].pack('p*').unpack('L').first
652
- end
653
- else
654
- env = nil
655
- end
656
-
657
- startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
658
- startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
659
- procinfo = [0,0,0,0].pack('LLLL')
660
-
661
- # Process SECURITY_ATTRIBUTE structure
662
- process_security = 0
663
- if hash['process_inherit']
664
- process_security = [0,0,0].pack('LLL')
665
- process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
666
- process_security[8,4] = [1].pack('L') # TRUE
667
- end
668
-
669
- # Thread SECURITY_ATTRIBUTE structure
670
- thread_security = 0
671
- if hash['thread_inherit']
672
- thread_security = [0,0,0].pack('LLL')
673
- thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
674
- thread_security[8,4] = [1].pack('L') # TRUE
675
- end
676
-
677
- # Automatically handle stdin, stdout and stderr as either IO objects
678
- # or file descriptors. This won't work for StringIO, however.
679
- ['stdin', 'stdout', 'stderr'].each{ |io|
680
- if si_hash[io]
681
- if si_hash[io].respond_to?(:fileno)
682
- handle = get_osfhandle(si_hash[io].fileno)
683
- else
684
- handle = get_osfhandle(si_hash[io])
685
- end
686
-
687
- if handle == INVALID_HANDLE_VALUE
688
- raise Error, get_last_error
689
- end
690
-
691
- # Most implementations of Ruby on Windows create inheritable
692
- # handles by default, but some do not. RF bug #26988.
693
- bool = SetHandleInformation(
694
- handle,
695
- HANDLE_FLAG_INHERIT,
696
- HANDLE_FLAG_INHERIT
697
- )
698
-
699
- raise Error, get_last_error unless bool
700
-
701
- si_hash[io] = handle
702
- si_hash['startf_flags'] ||= 0
703
- si_hash['startf_flags'] |= STARTF_USESTDHANDLES
704
- hash['inherit'] = true
705
- end
706
- }
707
-
708
- # The bytes not covered here are reserved (null)
709
- unless si_hash.empty?
710
- startinfo[0,4] = [startinfo.size].pack('L')
711
- startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop']
712
- startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title']
713
- startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x']
714
- startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y']
715
- startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size']
716
- startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size']
717
- startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars']
718
- startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars']
719
- startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute']
720
- startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags']
721
- startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags']
722
- startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin']
723
- startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout']
724
- startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr']
725
- end
726
-
727
- if hash['with_logon']
728
- logon = multi_to_wide(hash['with_logon'])
729
- domain = multi_to_wide(hash['domain'])
730
- app = hash['app_name'].nil? ? nil : multi_to_wide(hash['app_name'])
731
- cmd = hash['command_line'].nil? ? nil : multi_to_wide(hash['command_line'])
732
- cwd = multi_to_wide(hash['cwd'])
733
- passwd = multi_to_wide(hash['password'])
734
-
735
- hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
736
-
737
- bool = CreateProcessWithLogonW(
738
- logon, # User
739
- domain, # Domain
740
- passwd, # Password
741
- LOGON_WITH_PROFILE, # Logon flags
742
- app, # App name
743
- cmd, # Command line
744
- hash['creation_flags'], # Creation flags
745
- env, # Environment
746
- cwd, # Working directory
747
- startinfo, # Startup Info
748
- procinfo # Process Info
749
- )
750
- else
751
- bool = CreateProcess(
752
- hash['app_name'], # App name
753
- hash['command_line'], # Command line
754
- process_security, # Process attributes
755
- thread_security, # Thread attributes
756
- hash['inherit'], # Inherit handles?
757
- hash['creation_flags'], # Creation flags
758
- env, # Environment
759
- hash['cwd'], # Working directory
760
- startinfo, # Startup Info
761
- procinfo # Process Info
762
- )
763
- end
764
-
765
- # TODO: Close stdin, stdout and stderr handles in the si_hash unless
766
- # they're pointing to one of the standard handles already. [Maybe]
767
- unless bool
768
- raise Error, "CreateProcess() failed: ", get_last_error
769
- end
770
-
771
- # Automatically close the process and thread handles in the
772
- # PROCESS_INFORMATION struct unless explicitly told not to.
773
- if hash['close_handles']
774
- CloseHandle(procinfo[0,4].unpack('L').first)
775
- CloseHandle(procinfo[4,4].unpack('L').first)
776
- end
777
-
778
- ProcessInfo.new(
779
- procinfo[0,4].unpack('L').first, # hProcess
780
- procinfo[4,4].unpack('L').first, # hThread
781
- procinfo[8,4].unpack('L').first, # hProcessId
782
- procinfo[12,4].unpack('L').first # hThreadId
783
- )
784
- end
785
-
786
- # Waits for any child process to exit and returns the process id of that
787
- # child.
788
- #
789
- # Note that the $? (Process::Status) global variable is NOT set. This
790
- # may be addressed in a future release.
791
- #--
792
- # The GetProcessId() function is not defined in Windows 2000 or earlier
793
- # so we have to do some extra work for those platforms.
794
- #
795
- def wait
796
- handles = []
797
-
798
- # Windows 2000 or earlier
799
- unless defined? GetProcessId
800
- pids = []
801
- end
802
-
803
- @child_pids.each_with_index{ |pid, i|
804
- handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
805
-
806
- if handles[i] == INVALID_HANDLE_VALUE
807
- err = "unable to get HANDLE on process associated with pid #{pid}"
808
- raise Error, err
809
- end
810
-
811
- unless defined? GetProcessId
812
- pids[i] = pid
813
- end
814
- }
815
-
816
- wait = WaitForMultipleObjects(
817
- handles.size,
818
- handles.pack('L*'),
819
- 0,
820
- INFINITE
821
- )
822
-
823
- if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
824
- index = wait - WAIT_OBJECT_0
825
- handle = handles[index]
826
-
827
- if defined? GetProcessId
828
- pid = GetProcessId(handle)
829
- else
830
- pid = pids[index]
831
- end
832
-
833
- @child_pids.delete(pid)
834
- handles.each{ |handle| CloseHandle(handle) }
835
- return pid
836
- end
837
-
838
- nil
839
- end
840
-
841
- # Waits for any child process to exit and returns an array containing the
842
- # process id and the exit status of that child.
843
- #
844
- # Note that the $? (Process::Status) global variable is NOT set. This
845
- # may be addressed in a future release.
846
- #--
847
- # The GetProcessId() function is not defined in Windows 2000 or earlier
848
- # so we have to do some extra work for those platforms.
849
- #
850
- def wait2
851
- handles = []
852
-
853
- # Windows 2000 or earlier
854
- unless defined? GetProcessId
855
- pids = []
856
- end
857
-
858
- @child_pids.each_with_index{ |pid, i|
859
- handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
860
-
861
- if handles[i] == INVALID_HANDLE_VALUE
862
- err = "unable to get HANDLE on process associated with pid #{pid}"
863
- raise Error, err
864
- end
865
-
866
- unless defined? GetProcessId
867
- pids[i] = pid
868
- end
869
- }
870
-
871
- wait = WaitForMultipleObjects(
872
- handles.size,
873
- handles.pack('L*'),
874
- 0,
875
- INFINITE
876
- )
877
-
878
- if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
879
- index = wait - WAIT_OBJECT_0
880
- handle = handles[index]
881
-
882
- if defined? GetProcessId
883
- pid = GetProcessId(handle)
884
- else
885
- pid = pids[index]
886
- end
887
-
888
- exit_code = [0].pack('l')
889
-
890
- unless GetExitCodeProcess(handle, exit_code)
891
- raise get_last_error
892
- end
893
-
894
- @child_pids.delete(pid)
895
-
896
- handles.each{ |handle| CloseHandle(handle) }
897
- return [pid, exit_code.unpack('l').first]
898
- end
899
-
900
- nil
901
- end
902
-
903
- # Returns the process ID of the parent of this process.
904
- #--
905
- # In MRI this method always returns 0.
906
- #
907
- def ppid
908
- ppid = 0
909
-
910
- return ppid if Process.pid == 0 # Paranoia
911
-
912
- handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
913
-
914
- if handle == INVALID_HANDLE_VALUE
915
- raise Error, get_last_error
916
- end
917
-
918
- proc_entry = 0.chr * 296 # 36 + 260
919
- proc_entry[0, 4] = [proc_entry.size].pack('L') # Set dwSize member
920
-
921
- begin
922
- unless Process32First(handle, proc_entry)
923
- error = get_last_error
924
- raise Error, error
925
- end
926
-
927
- while Process32Next(handle, proc_entry)
928
- if proc_entry[8, 4].unpack('L')[0] == Process.pid
929
- ppid = proc_entry[24, 4].unpack('L')[0] # th32ParentProcessID
930
- break
931
- end
932
- end
933
- ensure
934
- CloseHandle(handle)
935
- end
936
-
937
- ppid
938
- end
939
-
940
- # Creates the equivalent of a subshell via the CreateProcess() function.
941
- # This behaves in a manner that is similar, but not identical to, the
942
- # Kernel.fork method for Unix. Unlike the Unix fork, this method starts
943
- # from the top of the script rather than the point of the call.
944
- #
945
- # WARNING: This implementation should be considered experimental. It is
946
- # not recommended for production use.
947
- #
948
- def fork
949
- last_arg = ARGV.last
950
-
951
- # Look for the 'child#xxx' tag
952
- if last_arg =~ /child#\d+/
953
- @i += 1
954
- num = last_arg.split('#').last.to_i
955
- if num == @i
956
- if block_given?
957
- status = 0
958
- begin
959
- yield
960
- rescue Exception
961
- status = -1 # Any non-zero result is failure
962
- ensure
963
- return status
964
- end
965
- end
966
- return nil
967
- else
968
- return false
969
- end
970
- end
971
-
972
- # Tag the command with the word 'child#xxx' to distinguish it
973
- # from the calling process.
974
- cmd = 'ruby -I "' + $LOAD_PATH.join(File::PATH_SEPARATOR) << '" "'
975
- cmd << File.expand_path($PROGRAM_NAME) << '" ' << ARGV.join(' ')
976
- cmd << ' child#' << @child_pids.length.to_s
977
-
978
- startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
979
- startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
980
- procinfo = [0,0,0,0].pack('LLLL')
981
-
982
- rv = CreateProcess(0, cmd, 0, 0, 1, 0, 0, 0, startinfo, procinfo)
983
-
984
- if rv == 0
985
- raise Error, get_last_error
986
- end
987
-
988
- pid = procinfo[8,4].unpack('L').first
989
- @child_pids.push(pid)
990
-
991
- pid
992
- end
993
-
994
- module_function :create, :fork, :get_affinity, :getrlimit, :getpriority, :kill, :ppid
995
- module_function :setpriority, :wait, :wait2, :waitpid, :waitpid2, :uid
996
- end
997
-
998
- # Create a global fork method
999
- module Kernel
1000
- undef_method :fork # Eliminate redefinition warning
1001
- def fork(&block)
1002
- Process.fork(&block)
1003
- end
1004
- end
1
+ require 'windows/error'
2
+ require 'windows/process'
3
+ require 'windows/thread'
4
+ require 'windows/synchronize'
5
+ require 'windows/handle'
6
+ require 'windows/library'
7
+ require 'windows/console'
8
+ require 'windows/window'
9
+ require 'windows/unicode'
10
+ require 'windows/tool_helper'
11
+ require 'windows/security'
12
+ require 'windows/msvcrt/string'
13
+
14
+ module Process
15
+ # The Process::Error class is typically raised if any of the custom
16
+ # Process methods fail.
17
+ class Error < RuntimeError; end
18
+
19
+ # Eliminates redefinition warnings.
20
+ undef_method :getpriority, :kill, :getrlimit, :ppid, :setrlimit
21
+ undef_method :setpriority, :wait, :wait2, :waitpid, :waitpid2, :uid
22
+
23
+ # The version of the win32-process library
24
+ WIN32_PROCESS_VERSION = '0.6.3'
25
+
26
+ include Windows::Process
27
+ include Windows::Thread
28
+ include Windows::Error
29
+ include Windows::Library
30
+ include Windows::Console
31
+ include Windows::Handle
32
+ include Windows::Security
33
+ include Windows::Synchronize
34
+ include Windows::Window
35
+ include Windows::Unicode
36
+ include Windows::ToolHelper
37
+ include Windows::MSVCRT::String
38
+
39
+ extend Windows::Error
40
+ extend Windows::Process
41
+ extend Windows::Thread
42
+ extend Windows::Security
43
+ extend Windows::Synchronize
44
+ extend Windows::Handle
45
+ extend Windows::Library
46
+ extend Windows::Console
47
+ extend Windows::Unicode
48
+ extend Windows::ToolHelper
49
+ extend Windows::MSVCRT::String
50
+
51
+ # :stopdoc:
52
+
53
+ # Used by Process.create
54
+ ProcessInfo = Struct.new("ProcessInfo",
55
+ :process_handle,
56
+ :thread_handle,
57
+ :process_id,
58
+ :thread_id
59
+ )
60
+
61
+ @child_pids = [] # Static variable used for Process.fork
62
+ @i = -1 # Static variable used for Process.fork
63
+
64
+ # These are probably not defined on MS Windows by default
65
+ unless defined? RLIMIT_CPU
66
+ RLIMIT_CPU = 0 # PerProcessUserTimeLimit
67
+ RLIMIT_FSIZE = 1 # Hard coded at 4TB - 64K (assumes NTFS)
68
+ RLIMIT_AS = 5 # ProcessMemoryLimit
69
+ RLIMIT_RSS = 5 # ProcessMemoryLimit
70
+ RLIMIT_VMEM = 5 # ProcessMemoryLimit
71
+ end
72
+
73
+ # :startdoc:
74
+
75
+ # Returns the process and system affinity mask for the given +pid+, or the
76
+ # current process if no pid is provided. The return value is a two element
77
+ # array, with the first containing the process affinity mask, and the second
78
+ # containing the system affinity mask. Both are decimal values.
79
+ #
80
+ # A process affinity mask is a bit vector indicating the processors that a
81
+ # process is allowed to run on. A system affinity mask is a bit vector in
82
+ # which each bit represents the processors that are configured into a
83
+ # system.
84
+ #
85
+ # Example:
86
+ #
87
+ # # System has 4 processors, current process is allowed to run on all
88
+ # Process.get_affinity # => [[15], [15]]
89
+ #
90
+ # # System has 4 processors, current process only allowed on 1 and 4 only
91
+ # Process.get_affinity # => [[9], [15]]
92
+ #
93
+ # If you want to convert a decimal bit vector into an array of 0's and 1's
94
+ # indicating the flag value of each processor, you can use something like
95
+ # this approach:
96
+ #
97
+ # mask = Process.get_affinity.first
98
+ # (0..mask).to_a.map{ |n| mask[n] }
99
+ #
100
+ def get_affinity(int = Process.pid)
101
+ pmask = 0.chr * 4
102
+ smask = 0.chr * 4
103
+
104
+ if int == Process.pid
105
+ unless GetProcessAffinityMask(GetCurrentProcess(), pmask, smask)
106
+ raise Error, get_last_error
107
+ end
108
+ else
109
+ begin
110
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
111
+
112
+ if handle == INVALID_HANDLE_VALUE
113
+ raise Error, get_last_error
114
+ end
115
+
116
+ unless GetProcessAffinityMask(handle, pmask, smask)
117
+ raise Error, get_last_error
118
+ end
119
+ ensure
120
+ CloseHandle(handle) if handle != INVALID_HANDLE_VALUE
121
+ end
122
+ end
123
+
124
+ pmask = pmask.unpack('L').first
125
+ smask = smask.unpack('L').first
126
+
127
+ [pmask, smask]
128
+ end
129
+
130
+ # Returns whether or not the current process is part of a Job.
131
+ #
132
+ def job?
133
+ pbool = 0.chr * 4
134
+ IsProcessInJob(GetCurrentProcess(), nil, pbool)
135
+ pbool.unpack('L').first == 0 ? false : true
136
+ end
137
+
138
+ # Gets the resource limit of the current process. Only a limited number
139
+ # of flags are supported.
140
+ #
141
+ # Process::RLIMIT_CPU
142
+ # Process::RLIMIT_FSIZE
143
+ # Process::RLIMIT_AS
144
+ # Process::RLIMIT_RSS
145
+ # Process::RLIMIT_VMEM
146
+ #
147
+ # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
148
+ # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
149
+ # refers to the per process user time limit. The Process::RLIMIT_FSIZE
150
+ # constant is hard coded to the maximum file size on an NTFS filesystem,
151
+ # approximately 4TB (or 4GB if not NTFS).
152
+ #
153
+ # While a two element array is returned in order to comply with the spec,
154
+ # there is no separate hard and soft limit. The values will always be the
155
+ # same.
156
+ #
157
+ # If [0,0] is returned then it means no limit has been set.
158
+ #
159
+ # Example:
160
+ #
161
+ # Process.getrlimit(Process::RLIMIT_VMEM) # => [0, 0]
162
+ #--
163
+ # NOTE: Both the getrlimit and setrlimit method use an at_exit handler
164
+ # to close a job handle. This is necessary because simply calling it
165
+ # at the end of the block, while marking it for closure, would also make
166
+ # it unavailable even within the same process since it would no longer
167
+ # be associated with the job.
168
+ #
169
+ def getrlimit(resource)
170
+ if resource == RLIMIT_FSIZE
171
+ if get_volume_type == 'NTFS'
172
+ return ((1024**4) * 4) - (1024 * 64) # ~4 TB
173
+ else
174
+ return (1024**3) * 4 # 4 GB
175
+ end
176
+ end
177
+
178
+ handle = nil
179
+ in_job = Process.job?
180
+
181
+ # Put the current process in a job if it's not already in one
182
+ if in_job && defined?(@job_name)
183
+ handle = OpenJobObject(JOB_OBJECT_QUERY, true, @job_name)
184
+ raise Error, get_last_error if handle == 0
185
+ else
186
+ @job_name = 'ruby_' + Process.pid.to_s
187
+ handle = CreateJobObject(nil, @job_name)
188
+ raise Error, get_last_error if handle == 0
189
+ end
190
+
191
+ begin
192
+ unless in_job
193
+ unless AssignProcessToJobObject(handle, GetCurrentProcess())
194
+ raise Error, get_last_error
195
+ end
196
+ end
197
+
198
+ buf = 0.chr * 112 # sizeof(struct JOBJECT_EXTENDED_LIMIT_INFORMATION)
199
+ val = nil # value returned at end of method
200
+
201
+ # Set the LimitFlags member of the struct
202
+ case resource
203
+ when RLIMIT_CPU
204
+ buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_TIME].pack('L')
205
+ when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
206
+ buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_MEMORY].pack('L')
207
+ else
208
+ raise Error, "unsupported resource type"
209
+ end
210
+
211
+ bool = QueryInformationJobObject(
212
+ handle,
213
+ JobObjectExtendedLimitInformation,
214
+ buf,
215
+ buf.size,
216
+ nil
217
+ )
218
+
219
+ unless bool
220
+ raise Error, get_last_error
221
+ end
222
+
223
+ case resource
224
+ when Process::RLIMIT_CPU
225
+ val = buf[0,8].unpack('Q').first
226
+ when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
227
+ val = buf[96,4].unpack('L').first
228
+ end
229
+ ensure
230
+ at_exit{ CloseHandle(handle) if handle }
231
+ end
232
+
233
+ [val, val] # Return an array of two values to comply with spec
234
+ end
235
+
236
+ # Sets the resource limit of the current process. Only a limited number
237
+ # of flags are supported.
238
+ #
239
+ # Process::RLIMIT_CPU
240
+ # Process::RLIMIT_AS
241
+ # Process::RLIMIT_RSS
242
+ # Process::RLIMIT_VMEM
243
+ #
244
+ # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
245
+ # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
246
+ # refers to the per process user time limit.
247
+ #
248
+ # The +max_limit+ parameter is provided for interface compatibility only.
249
+ # It is always set to the current_limit value.
250
+ #
251
+ # Example:
252
+ #
253
+ # Process.setrlimit(Process::RLIMIT_VMEM, 1024 * 4) # => nil
254
+ # Process.getrlimit(Process::RLIMIT_VMEM) # => [4096, 4096]
255
+ #
256
+ def setrlimit(resource, current_limit, max_limit = nil)
257
+ max_limit = current_limit
258
+
259
+ handle = nil
260
+ in_job = Process.job?
261
+
262
+ # Put the current process in a job if it's not already in one
263
+ if in_job && defined? @job_name
264
+ handle = OpenJobObject(JOB_OBJECT_SET_ATTRIBUTES, true, @job_name)
265
+ raise Error, get_last_error if handle == 0
266
+ else
267
+ @job_name = 'ruby_' + Process.pid.to_s
268
+ handle = CreateJobObject(nil, job_name)
269
+ raise Error, get_last_error if handle == 0
270
+ end
271
+
272
+ begin
273
+ unless in_job
274
+ unless AssignProcessToJobObject(handle, GetCurrentProcess())
275
+ raise Error, get_last_error
276
+ end
277
+ end
278
+
279
+ # sizeof(struct JOBJECT_EXTENDED_LIMIT_INFORMATION)
280
+ buf = 0.chr * 112
281
+
282
+ # Set the LimitFlags and relevant members of the struct
283
+ case resource
284
+ when RLIMIT_CPU
285
+ buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_TIME].pack('L')
286
+ buf[0,8] = [max_limit].pack('Q') # PerProcessUserTimeLimit
287
+ when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
288
+ buf[16,4] = [JOB_OBJECT_LIMIT_PROCESS_MEMORY].pack('L')
289
+ buf[96,4] = [max_limit].pack('L') # ProcessMemoryLimit
290
+ else
291
+ raise Error, "unsupported resource type"
292
+ end
293
+
294
+ bool = SetInformationJobObject(
295
+ handle,
296
+ JobObjectExtendedLimitInformation,
297
+ buf,
298
+ buf.size
299
+ )
300
+
301
+ unless bool
302
+ raise Error, get_last_error
303
+ end
304
+ ensure
305
+ at_exit{ CloseHandle(handle) if handle }
306
+ end
307
+ end
308
+
309
+ # Retrieves the priority class for the specified process id +int+. Unlike
310
+ # the default implementation, lower values do not necessarily correspond to
311
+ # higher priority classes.
312
+ #
313
+ # The +kind+ parameter is ignored but present for API compatibility.
314
+ # You can only retrieve process information, not process group or user
315
+ # information, so it is effectively always Process::PRIO_PROCESS.
316
+ #
317
+ # Possible return values are:
318
+ #
319
+ # 32 - Process::NORMAL_PRIORITY_CLASS
320
+ # 64 - Process::IDLE_PRIORITY_CLASS
321
+ # 128 - Process::HIGH_PRIORITY_CLASS
322
+ # 256 - Process::REALTIME_PRIORITY_CLASS
323
+ # 16384 - Process::BELOW_NORMAL_PRIORITY_CLASS
324
+ # 32768 - Process::ABOVE_NORMAL_PRIORITY_CLASS
325
+ #
326
+ def getpriority(kind = Process::PRIO_PROCESS, int = nil)
327
+ raise ArgumentError unless int
328
+
329
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
330
+
331
+ if handle == INVALID_HANDLE_VALUE
332
+ raise Error, get_last_error
333
+ end
334
+
335
+ priority_class = GetPriorityClass(handle)
336
+
337
+ if priority_class == 0
338
+ raise Error, get_last_error
339
+ end
340
+
341
+ priority_class
342
+ end
343
+
344
+ # Sets the priority class for the specified process id +int+.
345
+ #
346
+ # The +kind+ parameter is ignored but present for API compatibility.
347
+ # You can only retrieve process information, not process group or user
348
+ # information, so it is effectively always Process::PRIO_PROCESS.
349
+ #
350
+ # Possible +int_priority+ values are:
351
+ #
352
+ # * Process::NORMAL_PRIORITY_CLASS
353
+ # * Process::IDLE_PRIORITY_CLASS
354
+ # * Process::HIGH_PRIORITY_CLASS
355
+ # * Process::REALTIME_PRIORITY_CLASS
356
+ # * Process::BELOW_NORMAL_PRIORITY_CLASS
357
+ # * Process::ABOVE_NORMAL_PRIORITY_CLASS
358
+ #
359
+ def setpriority(kind = nil, int = nil, int_priority = nil)
360
+ raise ArgumentError unless int
361
+ raise ArgumentError unless int_priority
362
+
363
+ handle = OpenProcess(PROCESS_SET_INFORMATION, 0 , int)
364
+
365
+ if handle == INVALID_HANDLE_VALUE
366
+ raise Error, get_last_error
367
+ end
368
+
369
+ unless SetPriorityClass(handle, int_priority)
370
+ raise Error, get_last_error
371
+ end
372
+
373
+ return 0 # Match the spec
374
+ end
375
+
376
+ # Returns the uid of the current process. Specifically, it returns the
377
+ # RID of the SID associated with the owner of the process.
378
+ #
379
+ # If +sid+ is set to true, then a binary sid is returned. Otherwise, a
380
+ # numeric id is returned (the default).
381
+ #--
382
+ # The Process.uid method in core Ruby always returns 0 on MS Windows.
383
+ #
384
+ def uid(sid = false)
385
+ token = 0.chr * 4
386
+
387
+ unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
388
+ raise Error, get_last_error
389
+ end
390
+
391
+ token = token.unpack('V')[0]
392
+ rlength = 0.chr * 4
393
+ tuser = 0.chr * 512
394
+
395
+ bool = GetTokenInformation(
396
+ token,
397
+ TokenUser,
398
+ tuser,
399
+ tuser.size,
400
+ rlength
401
+ )
402
+
403
+ unless bool
404
+ raise Error, get_last_error
405
+ end
406
+
407
+ lsid = tuser[8, (rlength.unpack('L').first - 8)]
408
+
409
+ if sid
410
+ lsid
411
+ else
412
+ sid_addr = [lsid].pack('p*').unpack('L')[0]
413
+ sid_buf = 0.chr * 80
414
+ sid_ptr = 0.chr * 4
415
+
416
+ unless ConvertSidToStringSid(sid_addr, sid_ptr)
417
+ raise Error, get_last_error
418
+ end
419
+
420
+ strcpy(sid_buf, sid_ptr.unpack('L')[0])
421
+ sid_buf.strip.split('-').last.to_i
422
+ end
423
+ end
424
+
425
+ # Waits for the given child process to exit and returns that pid.
426
+ #
427
+ # Note that the $? (Process::Status) global variable is NOT set. This
428
+ # may be addressed in a future release.
429
+ #
430
+ def waitpid(pid)
431
+ exit_code = [0].pack('L')
432
+ handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
433
+
434
+ if handle == INVALID_HANDLE_VALUE
435
+ raise Error, get_last_error
436
+ end
437
+
438
+ # TODO: update the $? global variable (if/when possible)
439
+ status = WaitForSingleObject(handle, INFINITE)
440
+
441
+ begin
442
+ unless GetExitCodeProcess(handle, exit_code)
443
+ raise Error, get_last_error
444
+ end
445
+ ensure
446
+ CloseHandle(handle)
447
+ end
448
+
449
+ @child_pids.delete(pid)
450
+
451
+ # TODO: update the $? global variable (if/when possible)
452
+ exit_code = exit_code.unpack('L').first
453
+
454
+ pid
455
+ end
456
+
457
+ # Waits for the given child process to exit and returns an array containing
458
+ # the process id and the exit status.
459
+ #
460
+ # Note that the $? (Process::Status) global variable is NOT set. This
461
+ # may be addressed in a future release if/when possible.
462
+ #--
463
+ # Ruby does not provide a way to hook into $? so there's no way for us
464
+ # to set it.
465
+ #
466
+ def waitpid2(pid)
467
+ exit_code = [0].pack('L')
468
+ handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
469
+
470
+ if handle == INVALID_HANDLE_VALUE
471
+ raise Error, get_last_error
472
+ end
473
+
474
+ # TODO: update the $? global variable (if/when possible)
475
+ status = WaitForSingleObject(handle, INFINITE)
476
+
477
+ begin
478
+ unless GetExitCodeProcess(handle, exit_code)
479
+ raise Error, get_last_error
480
+ end
481
+ ensure
482
+ CloseHandle(handle)
483
+ end
484
+
485
+ @child_pids.delete(pid)
486
+
487
+ # TODO: update the $? global variable (if/when possible)
488
+ exit_code = exit_code.unpack('L').first
489
+
490
+ [pid, exit_code]
491
+ end
492
+
493
+ # Sends the given +signal+ to an array of process id's. The +signal+ may
494
+ # be any value from 0 to 9, or the special strings 'SIGINT' (or 'INT'),
495
+ # 'SIGBRK' (or 'BRK') and 'SIGKILL' (or 'KILL'). An array of successfully
496
+ # killed pids is returned.
497
+ #
498
+ # Signal 0 merely tests if the process is running without killing it.
499
+ # Signal 2 sends a CTRL_C_EVENT to the process.
500
+ # Signal 3 sends a CTRL_BRK_EVENT to the process.
501
+ # Signal 9 kills the process in a harsh manner.
502
+ # Signals 1 and 4-8 kill the process in a nice manner.
503
+ #
504
+ # SIGINT/INT corresponds to signal 2
505
+ # SIGBRK/BRK corresponds to signal 3
506
+ # SIGKILL/KILL corresponds to signal 9
507
+ #
508
+ # Signals 2 and 3 only affect console processes, and then only if the
509
+ # process was created with the CREATE_NEW_PROCESS_GROUP flag.
510
+ #
511
+ def kill(signal, *pids)
512
+ case signal
513
+ when 'SIGINT', 'INT'
514
+ signal = 2
515
+ when 'SIGBRK', 'BRK'
516
+ signal = 3
517
+ when 'SIGKILL', 'KILL'
518
+ signal = 9
519
+ when 0..9
520
+ # Do nothing
521
+ else
522
+ raise Error, "Invalid signal '#{signal}'"
523
+ end
524
+
525
+ killed_pids = []
526
+
527
+ pids.each{ |pid|
528
+ # Send the signal to the current process if the pid is zero
529
+ if pid == 0
530
+ pid = Process.pid
531
+ end
532
+
533
+ # No need for full access if the signal is zero
534
+ if signal == 0
535
+ access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
536
+ handle = OpenProcess(access, 0 , pid)
537
+ else
538
+ handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
539
+ end
540
+
541
+ begin
542
+ case signal
543
+ when 0
544
+ if handle != 0
545
+ killed_pids.push(pid)
546
+ else
547
+ # If ERROR_ACCESS_DENIED is returned, we know it's running
548
+ if GetLastError() == ERROR_ACCESS_DENIED
549
+ killed_pids.push(pid)
550
+ else
551
+ raise Error, get_last_error
552
+ end
553
+ end
554
+ when 2
555
+ if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
556
+ killed_pids.push(pid)
557
+ end
558
+ when 3
559
+ if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
560
+ killed_pids.push(pid)
561
+ end
562
+ when 9
563
+ if TerminateProcess(handle, pid)
564
+ killed_pids.push(pid)
565
+ @child_pids.delete(pid)
566
+ else
567
+ raise Error, get_last_error
568
+ end
569
+ else
570
+ if handle != 0
571
+ thread_id = [0].pack('L')
572
+ begin
573
+ thread = CreateRemoteThread(
574
+ handle,
575
+ 0,
576
+ 0,
577
+ GetProcAddress(GetModuleHandle('kernel32'), 'ExitProcess'),
578
+ 0,
579
+ 0,
580
+ thread_id
581
+ )
582
+
583
+ if thread
584
+ WaitForSingleObject(thread, 5)
585
+ killed_pids.push(pid)
586
+ @child_pids.delete(pid)
587
+ else
588
+ raise Error, get_last_error
589
+ end
590
+ ensure
591
+ CloseHandle(thread) if thread
592
+ end
593
+ else
594
+ raise Error, get_last_error
595
+ end # case
596
+
597
+ @child_pids.delete(pid)
598
+ end
599
+ ensure
600
+ CloseHandle(handle) unless handle == INVALID_HANDLE_VALUE
601
+ end
602
+ }
603
+
604
+ killed_pids
605
+ end
606
+
607
+ # Process.create(key => value, ...) => ProcessInfo
608
+ #
609
+ # This is a wrapper for the CreateProcess() function. It executes a process,
610
+ # returning a ProcessInfo struct. It accepts a hash as an argument.
611
+ # There are several primary keys:
612
+ #
613
+ # * command_line (mandatory)
614
+ # * app_name (default: nil)
615
+ # * inherit (default: false)
616
+ # * process_inherit (default: false)
617
+ # * thread_inherit (default: false)
618
+ # * creation_flags (default: 0)
619
+ # * cwd (default: Dir.pwd)
620
+ # * startup_info (default: nil)
621
+ # * environment (default: nil)
622
+ # * close_handles (default: true)
623
+ # * with_logon (default: nil)
624
+ # * domain (default: nil)
625
+ # * password (default: nil)
626
+ #
627
+ # Of these, the 'command_line' or 'app_name' must be specified or an
628
+ # error is raised. Both may be set individually, but 'command_line' should
629
+ # be preferred if only one of them is set because it does not (necessarily)
630
+ # require an explicit path or extension to work.
631
+ #
632
+ # The 'domain' and 'password' options are only relevent in the context
633
+ # of 'with_logon'.
634
+ #
635
+ # The startup_info key takes a hash. Its keys are attributes that are
636
+ # part of the StartupInfo struct, and are generally only meaningful for
637
+ # GUI or console processes. See the documentation on CreateProcess()
638
+ # and the StartupInfo struct on MSDN for more information.
639
+ #
640
+ # * desktop
641
+ # * title
642
+ # * x
643
+ # * y
644
+ # * x_size
645
+ # * y_size
646
+ # * x_count_chars
647
+ # * y_count_chars
648
+ # * fill_attribute
649
+ # * sw_flags
650
+ # * startf_flags
651
+ # * stdin
652
+ # * stdout
653
+ # * stderr
654
+ #
655
+ # The relevant constants for 'creation_flags', 'sw_flags' and 'startf_flags'
656
+ # are included in the Windows::Process, Windows::Console and Windows::Window
657
+ # modules. These come with the windows-pr library, a prerequisite of this
658
+ # library. Note that the 'stdin', 'stdout' and 'stderr' options can be
659
+ # either Ruby IO objects or file descriptors (i.e. a fileno). However,
660
+ # StringIO objects are not currently supported.
661
+ #
662
+ # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
663
+ # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
664
+ # automatically OR'd to the +startf_flags+ value.
665
+ #
666
+ # The ProcessInfo struct contains the following members:
667
+ #
668
+ # * process_handle - The handle to the newly created process.
669
+ # * thread_handle - The handle to the primary thread of the process.
670
+ # * process_id - Process ID.
671
+ # * thread_id - Thread ID.
672
+ #
673
+ # If the 'close_handles' option is set to true (the default) then the
674
+ # process_handle and the thread_handle are automatically closed for you
675
+ # before the ProcessInfo struct is returned.
676
+ #
677
+ # If the 'with_logon' option is set, then the process runs the specified
678
+ # executable file in the security context of the specified credentials.
679
+ #
680
+ def create(args)
681
+ unless args.kind_of?(Hash)
682
+ raise TypeError, 'Expecting hash-style keyword arguments'
683
+ end
684
+
685
+ valid_keys = %w/
686
+ app_name command_line inherit creation_flags cwd environment
687
+ startup_info thread_inherit process_inherit close_handles with_logon
688
+ domain password
689
+ /
690
+
691
+ valid_si_keys = %/
692
+ startf_flags desktop title x y x_size y_size x_count_chars
693
+ y_count_chars fill_attribute sw_flags stdin stdout stderr
694
+ /
695
+
696
+ # Set default values
697
+ hash = {
698
+ 'app_name' => nil,
699
+ 'creation_flags' => 0,
700
+ 'close_handles' => true
701
+ }
702
+
703
+ # Validate the keys, and convert symbols and case to lowercase strings.
704
+ args.each{ |key, val|
705
+ key = key.to_s.downcase
706
+ unless valid_keys.include?(key)
707
+ raise Error, "invalid key '#{key}'"
708
+ end
709
+ hash[key] = val
710
+ }
711
+
712
+ si_hash = {}
713
+
714
+ # If the startup_info key is present, validate its subkeys
715
+ if hash['startup_info']
716
+ hash['startup_info'].each{ |key, val|
717
+ key = key.to_s.downcase
718
+ unless valid_si_keys.include?(key)
719
+ raise Error, "invalid startup_info key '#{key}'"
720
+ end
721
+ si_hash[key] = val
722
+ }
723
+ end
724
+
725
+ # The +command_line+ key is mandatory unless the +app_name+ key
726
+ # is specified.
727
+ unless hash['command_line']
728
+ if hash['app_name']
729
+ hash['command_line'] = hash['app_name']
730
+ hash['app_name'] = nil
731
+ else
732
+ raise Error, 'command_line or app_name must be specified'
733
+ end
734
+ end
735
+
736
+ # The environment string should be passed as a string of ';' separated
737
+ # paths.
738
+ if hash['environment']
739
+ env = hash['environment'].split(File::PATH_SEPARATOR) << 0.chr
740
+ if hash['with_logon']
741
+ env = env.map{ |e| multi_to_wide(e) }
742
+ env = [env.join("\0\0")].pack('p*').unpack('L').first
743
+ else
744
+ env = [env.join("\0")].pack('p*').unpack('L').first
745
+ end
746
+ else
747
+ env = nil
748
+ end
749
+
750
+ startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
751
+ startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
752
+ procinfo = [0,0,0,0].pack('LLLL')
753
+
754
+ # Process SECURITY_ATTRIBUTE structure
755
+ process_security = 0
756
+ if hash['process_inherit']
757
+ process_security = [0,0,0].pack('LLL')
758
+ process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
759
+ process_security[8,4] = [1].pack('L') # TRUE
760
+ end
761
+
762
+ # Thread SECURITY_ATTRIBUTE structure
763
+ thread_security = 0
764
+ if hash['thread_inherit']
765
+ thread_security = [0,0,0].pack('LLL')
766
+ thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
767
+ thread_security[8,4] = [1].pack('L') # TRUE
768
+ end
769
+
770
+ # Automatically handle stdin, stdout and stderr as either IO objects
771
+ # or file descriptors. This won't work for StringIO, however.
772
+ ['stdin', 'stdout', 'stderr'].each{ |io|
773
+ if si_hash[io]
774
+ if si_hash[io].respond_to?(:fileno)
775
+ handle = get_osfhandle(si_hash[io].fileno)
776
+ else
777
+ handle = get_osfhandle(si_hash[io])
778
+ end
779
+
780
+ if handle == INVALID_HANDLE_VALUE
781
+ raise Error, get_last_error
782
+ end
783
+
784
+ # Most implementations of Ruby on Windows create inheritable
785
+ # handles by default, but some do not. RF bug #26988.
786
+ bool = SetHandleInformation(
787
+ handle,
788
+ HANDLE_FLAG_INHERIT,
789
+ HANDLE_FLAG_INHERIT
790
+ )
791
+
792
+ raise Error, get_last_error unless bool
793
+
794
+ si_hash[io] = handle
795
+ si_hash['startf_flags'] ||= 0
796
+ si_hash['startf_flags'] |= STARTF_USESTDHANDLES
797
+ hash['inherit'] = true
798
+ end
799
+ }
800
+
801
+ # The bytes not covered here are reserved (null)
802
+ unless si_hash.empty?
803
+ startinfo[0,4] = [startinfo.size].pack('L')
804
+ startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop']
805
+ startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title']
806
+ startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x']
807
+ startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y']
808
+ startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size']
809
+ startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size']
810
+ startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars']
811
+ startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars']
812
+ startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute']
813
+ startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags']
814
+ startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags']
815
+ startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin']
816
+ startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout']
817
+ startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr']
818
+ end
819
+
820
+ if hash['with_logon']
821
+ logon = multi_to_wide(hash['with_logon'])
822
+ domain = multi_to_wide(hash['domain'])
823
+ app = hash['app_name'].nil? ? nil : multi_to_wide(hash['app_name'])
824
+ cmd = hash['command_line'].nil? ? nil : multi_to_wide(hash['command_line'])
825
+ cwd = multi_to_wide(hash['cwd'])
826
+ passwd = multi_to_wide(hash['password'])
827
+
828
+ hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
829
+
830
+ bool = CreateProcessWithLogonW(
831
+ logon, # User
832
+ domain, # Domain
833
+ passwd, # Password
834
+ LOGON_WITH_PROFILE, # Logon flags
835
+ app, # App name
836
+ cmd, # Command line
837
+ hash['creation_flags'], # Creation flags
838
+ env, # Environment
839
+ cwd, # Working directory
840
+ startinfo, # Startup Info
841
+ procinfo # Process Info
842
+ )
843
+ else
844
+ bool = CreateProcess(
845
+ hash['app_name'], # App name
846
+ hash['command_line'], # Command line
847
+ process_security, # Process attributes
848
+ thread_security, # Thread attributes
849
+ hash['inherit'], # Inherit handles?
850
+ hash['creation_flags'], # Creation flags
851
+ env, # Environment
852
+ hash['cwd'], # Working directory
853
+ startinfo, # Startup Info
854
+ procinfo # Process Info
855
+ )
856
+ end
857
+
858
+ # TODO: Close stdin, stdout and stderr handles in the si_hash unless
859
+ # they're pointing to one of the standard handles already. [Maybe]
860
+ unless bool
861
+ raise Error, "CreateProcess() failed: ", get_last_error
862
+ end
863
+
864
+ # Automatically close the process and thread handles in the
865
+ # PROCESS_INFORMATION struct unless explicitly told not to.
866
+ if hash['close_handles']
867
+ CloseHandle(procinfo[0,4].unpack('L').first)
868
+ CloseHandle(procinfo[4,4].unpack('L').first)
869
+ end
870
+
871
+ ProcessInfo.new(
872
+ procinfo[0,4].unpack('L').first, # hProcess
873
+ procinfo[4,4].unpack('L').first, # hThread
874
+ procinfo[8,4].unpack('L').first, # hProcessId
875
+ procinfo[12,4].unpack('L').first # hThreadId
876
+ )
877
+ end
878
+
879
+ # Waits for any child process to exit and returns the process id of that
880
+ # child.
881
+ #
882
+ # Note that the $? (Process::Status) global variable is NOT set. This
883
+ # may be addressed in a future release.
884
+ #--
885
+ # The GetProcessId() function is not defined in Windows 2000 or earlier
886
+ # so we have to do some extra work for those platforms.
887
+ #
888
+ def wait
889
+ handles = []
890
+
891
+ # Windows 2000 or earlier
892
+ unless defined? GetProcessId
893
+ pids = []
894
+ end
895
+
896
+ @child_pids.each_with_index{ |pid, i|
897
+ handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
898
+
899
+ if handles[i] == INVALID_HANDLE_VALUE
900
+ err = "unable to get HANDLE on process associated with pid #{pid}"
901
+ raise Error, err
902
+ end
903
+
904
+ unless defined? GetProcessId
905
+ pids[i] = pid
906
+ end
907
+ }
908
+
909
+ wait = WaitForMultipleObjects(
910
+ handles.size,
911
+ handles.pack('L*'),
912
+ 0,
913
+ INFINITE
914
+ )
915
+
916
+ if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
917
+ index = wait - WAIT_OBJECT_0
918
+ handle = handles[index]
919
+
920
+ if defined? GetProcessId
921
+ pid = GetProcessId(handle)
922
+ else
923
+ pid = pids[index]
924
+ end
925
+
926
+ @child_pids.delete(pid)
927
+ handles.each{ |handle| CloseHandle(handle) }
928
+ return pid
929
+ end
930
+
931
+ nil
932
+ end
933
+
934
+ # Waits for any child process to exit and returns an array containing the
935
+ # process id and the exit status of that child.
936
+ #
937
+ # Note that the $? (Process::Status) global variable is NOT set. This
938
+ # may be addressed in a future release.
939
+ #--
940
+ # The GetProcessId() function is not defined in Windows 2000 or earlier
941
+ # so we have to do some extra work for those platforms.
942
+ #
943
+ def wait2
944
+ handles = []
945
+
946
+ # Windows 2000 or earlier
947
+ unless defined? GetProcessId
948
+ pids = []
949
+ end
950
+
951
+ @child_pids.each_with_index{ |pid, i|
952
+ handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
953
+
954
+ if handles[i] == INVALID_HANDLE_VALUE
955
+ err = "unable to get HANDLE on process associated with pid #{pid}"
956
+ raise Error, err
957
+ end
958
+
959
+ unless defined? GetProcessId
960
+ pids[i] = pid
961
+ end
962
+ }
963
+
964
+ wait = WaitForMultipleObjects(
965
+ handles.size,
966
+ handles.pack('L*'),
967
+ 0,
968
+ INFINITE
969
+ )
970
+
971
+ if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
972
+ index = wait - WAIT_OBJECT_0
973
+ handle = handles[index]
974
+
975
+ if defined? GetProcessId
976
+ pid = GetProcessId(handle)
977
+ else
978
+ pid = pids[index]
979
+ end
980
+
981
+ exit_code = [0].pack('l')
982
+
983
+ unless GetExitCodeProcess(handle, exit_code)
984
+ raise get_last_error
985
+ end
986
+
987
+ @child_pids.delete(pid)
988
+
989
+ handles.each{ |handle| CloseHandle(handle) }
990
+ return [pid, exit_code.unpack('l').first]
991
+ end
992
+
993
+ nil
994
+ end
995
+
996
+ # Returns the process ID of the parent of this process.
997
+ #--
998
+ # In MRI this method always returns 0.
999
+ #
1000
+ def ppid
1001
+ ppid = 0
1002
+
1003
+ return ppid if Process.pid == 0 # Paranoia
1004
+
1005
+ handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
1006
+
1007
+ if handle == INVALID_HANDLE_VALUE
1008
+ raise Error, get_last_error
1009
+ end
1010
+
1011
+ proc_entry = 0.chr * 296 # 36 + 260
1012
+ proc_entry[0, 4] = [proc_entry.size].pack('L') # Set dwSize member
1013
+
1014
+ begin
1015
+ unless Process32First(handle, proc_entry)
1016
+ error = get_last_error
1017
+ raise Error, error
1018
+ end
1019
+
1020
+ while Process32Next(handle, proc_entry)
1021
+ if proc_entry[8, 4].unpack('L')[0] == Process.pid
1022
+ ppid = proc_entry[24, 4].unpack('L')[0] # th32ParentProcessID
1023
+ break
1024
+ end
1025
+ end
1026
+ ensure
1027
+ CloseHandle(handle)
1028
+ end
1029
+
1030
+ ppid
1031
+ end
1032
+
1033
+ # Creates the equivalent of a subshell via the CreateProcess() function.
1034
+ # This behaves in a manner that is similar, but not identical to, the
1035
+ # Kernel.fork method for Unix. Unlike the Unix fork, this method starts
1036
+ # from the top of the script rather than the point of the call.
1037
+ #
1038
+ # WARNING: This implementation should be considered experimental. It is
1039
+ # not recommended for production use.
1040
+ #
1041
+ def fork
1042
+ last_arg = ARGV.last
1043
+
1044
+ # Look for the 'child#xxx' tag
1045
+ if last_arg =~ /child#\d+/
1046
+ @i += 1
1047
+ num = last_arg.split('#').last.to_i
1048
+ if num == @i
1049
+ if block_given?
1050
+ status = 0
1051
+ begin
1052
+ yield
1053
+ rescue Exception
1054
+ status = -1 # Any non-zero result is failure
1055
+ ensure
1056
+ return status
1057
+ end
1058
+ end
1059
+ return nil
1060
+ else
1061
+ return false
1062
+ end
1063
+ end
1064
+
1065
+ # Tag the command with the word 'child#xxx' to distinguish it
1066
+ # from the calling process.
1067
+ cmd = 'ruby -I "' + $LOAD_PATH.join(File::PATH_SEPARATOR) << '" "'
1068
+ cmd << File.expand_path($PROGRAM_NAME) << '" ' << ARGV.join(' ')
1069
+ cmd << ' child#' << @child_pids.length.to_s
1070
+
1071
+ startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
1072
+ startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
1073
+ procinfo = [0,0,0,0].pack('LLLL')
1074
+
1075
+ rv = CreateProcess(0, cmd, 0, 0, 1, 0, 0, 0, startinfo, procinfo)
1076
+
1077
+ if rv == 0
1078
+ raise Error, get_last_error
1079
+ end
1080
+
1081
+ pid = procinfo[8,4].unpack('L').first
1082
+ @child_pids.push(pid)
1083
+
1084
+ pid
1085
+ end
1086
+
1087
+ module_function :create, :fork, :get_affinity, :getrlimit, :getpriority
1088
+ module_function :job?, :kill, :ppid, :setpriority, :setrlimit
1089
+ module_function :wait, :wait2, :waitpid, :waitpid2, :uid
1090
+ end
1091
+
1092
+ # Create a global fork method
1093
+ module Kernel
1094
+ undef_method :fork # Eliminate redefinition warning
1095
+ def fork(&block)
1096
+ Process.fork(&block)
1097
+ end
1098
+ end