win32-process 0.6.2 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
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