win32-process 0.7.2 → 0.7.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/MANIFEST CHANGED
@@ -1,9 +1,9 @@
1
- * CHANGES
2
- * README
3
- * MANIFEST
4
- * Rakefile
5
- * win32-process.gemspec
6
- * examples/example_create.rb
7
- * examples/example_kill.rb
8
- * lib/win32/process.rb
9
- * test/test_process.rb
1
+ * CHANGES
2
+ * README
3
+ * MANIFEST
4
+ * Rakefile
5
+ * win32-process.gemspec
6
+ * examples/example_create.rb
7
+ * examples/example_kill.rb
8
+ * lib/win32/process.rb
9
+ * test/test_process.rb
data/README CHANGED
@@ -1,76 +1,76 @@
1
- = Description
2
- This library provides analogues of the :getpriority, :setpriority, :getrlimit,
3
- :setrlimit and :uid methods for MS Windows. It also adds the new methods :job?,
4
- :get_affinity, and :create, and redefines the :kill method.
5
-
6
- = Prerequisites
7
- * ffi
8
- * sys-proctable (dev only)
9
- * test-unit 2 (dev only)
10
-
11
- = Supported Platforms
12
- This library is supported on Windows 2000 or later.
13
-
14
- = Installation
15
- gem install win32-process
16
-
17
- = Synopsis
18
- require 'win32/process'
19
-
20
- p Process.job? # => true or false
21
-
22
- info = Process.create(
23
- :app_name => "notepad.exe",
24
- :creation_flags => Process::DETACHED_PROCESS,
25
- :process_inherit => false,
26
- :thread_inherit => true,
27
- :cwd => "C:\\"
28
- )
29
-
30
- p info.process_id
31
-
32
- = Developer's Notes
33
- == Removal of Process.fork in release 0.7.0
34
- The Process.fork method was originally experimental but it has never
35
- been particularly useful in practice. On top of that, it required special
36
- implementations of the Process.waitXXX methods, so it was a maintenance
37
- issue as well.
38
-
39
- With Ruby 1.9 now becoming standard and its addition of Process.spawn
40
- and friends (and concomitant support for the Process.waitXXX methods) I
41
- felt it was time to remove it.
42
-
43
- You can still simulate Process.fork if you like using Process.create, which
44
- is how it was implemented internally anyway. A better solution might be
45
- to follow in the footsteps of ActiveState Perl, which uses native threads
46
- to simulate fork on Windows.
47
-
48
- == Changes in the custom Process.kill method for 0.7.0
49
- The Process.kill method in 0.7.0 more closely matches the spec now, but
50
- the internal method for killing processes is still nicer for most signals.
51
- With the release of 0.7.0 users can now specify options that provide finer
52
- control over how a process is killed. See the documentation for details.
53
-
54
- == The removal of the custom Process.ppid method
55
- This was added at some point in the Ruby 1.9 dev cycle so it was removed
56
- from this library.
57
-
58
- = Known Bugs
59
- None known. Any bugs should be reported on the project page at
60
- https://github.com/djberg96/win32-process.
61
-
62
- = License
63
- Artistic 2.0
64
-
65
- = Copyright
66
- (C) 2003-2013 Daniel J. Berger
67
- All Rights Reserved
68
-
69
- = Warranty
70
- This library is provided "as is" and without any express or
71
- implied warranties, including, without limitation, the implied
72
- warranties of merchantability and fitness for a particular purpose.
73
-
74
- = Author(s)
75
- Park Heesob
76
- Daniel J. Berger
1
+ = Description
2
+ This library provides analogues of the :getpriority, :setpriority, :getrlimit,
3
+ :setrlimit and :uid methods for MS Windows. It also adds the new methods :job?,
4
+ :get_affinity, and :create, and redefines the :kill method.
5
+
6
+ = Prerequisites
7
+ * ffi
8
+ * sys-proctable (dev only)
9
+ * test-unit 2 (dev only)
10
+
11
+ = Supported Platforms
12
+ This library is supported on Windows 2000 or later.
13
+
14
+ = Installation
15
+ gem install win32-process
16
+
17
+ = Synopsis
18
+ require 'win32/process'
19
+
20
+ p Process.job? # => true or false
21
+
22
+ info = Process.create(
23
+ :app_name => "notepad.exe",
24
+ :creation_flags => Process::DETACHED_PROCESS,
25
+ :process_inherit => false,
26
+ :thread_inherit => true,
27
+ :cwd => "C:\\"
28
+ )
29
+
30
+ p info.process_id
31
+
32
+ = Developer's Notes
33
+ == Removal of Process.fork in release 0.7.0
34
+ The Process.fork method was originally experimental but it has never
35
+ been particularly useful in practice. On top of that, it required special
36
+ implementations of the Process.waitXXX methods, so it was a maintenance
37
+ issue as well.
38
+
39
+ With Ruby 1.9 now becoming standard and its addition of Process.spawn
40
+ and friends (and concomitant support for the Process.waitXXX methods) I
41
+ felt it was time to remove it.
42
+
43
+ You can still simulate Process.fork if you like using Process.create, which
44
+ is how it was implemented internally anyway. A better solution might be
45
+ to follow in the footsteps of ActiveState Perl, which uses native threads
46
+ to simulate fork on Windows.
47
+
48
+ == Changes in the custom Process.kill method for 0.7.0
49
+ The Process.kill method in 0.7.0 more closely matches the spec now, but
50
+ the internal method for killing processes is still nicer for most signals.
51
+ With the release of 0.7.0 users can now specify options that provide finer
52
+ control over how a process is killed. See the documentation for details.
53
+
54
+ == The removal of the custom Process.ppid method
55
+ This was added at some point in the Ruby 1.9 dev cycle so it was removed
56
+ from this library.
57
+
58
+ = Known Bugs
59
+ None known. Any bugs should be reported on the project page at
60
+ https://github.com/djberg96/win32-process.
61
+
62
+ = License
63
+ Artistic 2.0
64
+
65
+ = Copyright
66
+ (C) 2003-2013 Daniel J. Berger
67
+ All Rights Reserved
68
+
69
+ = Warranty
70
+ This library is provided "as is" and without any express or
71
+ implied warranties, including, without limitation, the implied
72
+ warranties of merchantability and fitness for a particular purpose.
73
+
74
+ = Author(s)
75
+ Park Heesob
76
+ Daniel J. Berger
data/Rakefile CHANGED
@@ -1,58 +1,63 @@
1
- require 'rake'
2
- require 'rake/clean'
3
- require 'rake/testtask'
4
- require 'rbconfig'
5
- include RbConfig
6
-
7
- CLEAN.include('**/*.gem', '**/*.rbc', '**/*.log')
8
-
9
- namespace :gem do
10
- desc 'Create the win32-process gem'
11
- task :create => [:clean] 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
- namespace :example do
24
- desc 'Run the fork + wait example'
25
- task :fork_wait do
26
- sh "ruby -Ilib examples/example_fork_wait.rb"
27
- end
28
-
29
- desc 'Run the fork + waitpid example'
30
- task :fork_waitpid do
31
- sh "ruby -Ilib examples/example_fork_waitpid.rb"
32
- end
33
-
34
- desc 'Run the kill example'
35
- task :kill do
36
- sh "ruby -Ilib examples/example_kill.rb"
37
- end
38
-
39
- desc 'Run the create example'
40
- task :create do
41
- sh "ruby -Ilib examples/example_create.rb"
42
- end
43
- end
44
-
45
- namespace :test do
46
- Rake::TestTask.new(:kill) do |t|
47
- t.verbose = true
48
- t.warning = true
49
- t.test_files = FileList['test/test_win32_process_kill.rb']
50
- end
51
- end
52
-
53
- Rake::TestTask.new do |t|
54
- t.verbose = true
55
- t.warning = true
56
- end
57
-
58
- task :default => :test
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+ require 'rbconfig'
5
+ include RbConfig
6
+
7
+ CLEAN.include('**/*.gem', '**/*.rbc', '**/*.log')
8
+
9
+ namespace :gem do
10
+ desc 'Create the win32-process gem'
11
+ task :create => [:clean] do
12
+ spec = eval(IO.read('win32-process.gemspec'))
13
+ if Gem::VERSION.to_f < 2.0
14
+ Gem::Builder.new(spec).build
15
+ else
16
+ require 'rubygems/package'
17
+ Gem::Package.build(spec)
18
+ end
19
+ end
20
+
21
+ desc 'Install the win32-process gem'
22
+ task :install => [:create] do
23
+ file = Dir["*.gem"].first
24
+ sh "gem install #{file}"
25
+ end
26
+ end
27
+
28
+ namespace :example do
29
+ desc 'Run the fork + wait example'
30
+ task :fork_wait do
31
+ sh "ruby -Ilib examples/example_fork_wait.rb"
32
+ end
33
+
34
+ desc 'Run the fork + waitpid example'
35
+ task :fork_waitpid do
36
+ sh "ruby -Ilib examples/example_fork_waitpid.rb"
37
+ end
38
+
39
+ desc 'Run the kill example'
40
+ task :kill do
41
+ sh "ruby -Ilib examples/example_kill.rb"
42
+ end
43
+
44
+ desc 'Run the create example'
45
+ task :create do
46
+ sh "ruby -Ilib examples/example_create.rb"
47
+ end
48
+ end
49
+
50
+ namespace :test do
51
+ Rake::TestTask.new(:kill) do |t|
52
+ t.verbose = true
53
+ t.warning = true
54
+ t.test_files = FileList['test/test_win32_process_kill.rb']
55
+ end
56
+ end
57
+
58
+ Rake::TestTask.new do |t|
59
+ t.verbose = true
60
+ t.warning = true
61
+ end
62
+
63
+ task :default => :test
data/lib/win32/process.rb CHANGED
@@ -1,894 +1,937 @@
1
- require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'functions')
2
- require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'constants')
3
- require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'structs')
4
- require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'helper')
5
-
6
-
7
- module Process
8
- include Process::Constants
9
- extend Process::Functions
10
- extend Process::Structs
11
- extend Process::Constants
12
-
13
- # The version of the win32-process library.
14
- WIN32_PROCESS_VERSION = '0.7.2'
15
-
16
- # Disable popups. This mostly affects the Process.kill method.
17
- SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX)
18
-
19
- class << self
20
- # Returns whether or not the current process is part of a Job (process group).
21
- def job?
22
- pbool = FFI::MemoryPointer.new(:int)
23
- IsProcessInJob(GetCurrentProcess(), nil, pbool)
24
- pbool.read_int == 1 ? true : false
25
- end
26
-
27
- # Returns the process and system affinity mask for the given +pid+, or the
28
- # current process if no pid is provided. The return value is a two element
29
- # array, with the first containing the process affinity mask, and the second
30
- # containing the system affinity mask. Both are decimal values.
31
- #
32
- # A process affinity mask is a bit vector indicating the processors that a
33
- # process is allowed to run on. A system affinity mask is a bit vector in
34
- # which each bit represents the processors that are configured into a
35
- # system.
36
- #
37
- # Example:
38
- #
39
- # # System has 4 processors, current process is allowed to run on all
40
- # Process.get_affinity # => [[15], [15]], where '15' is 1 + 2 + 4 + 8
41
- #
42
- # # System has 4 processors, current process only allowed on 1 and 4 only
43
- # Process.get_affinity # => [[9], [15]]
44
- #
45
- # If you want to convert a decimal bit vector into an array of 0's and 1's
46
- # indicating the flag value of each processor, you can use something like
47
- # this approach:
48
- #
49
- # mask = Process.get_affinity.first
50
- # (0..mask).to_a.map{ |n| mask[n] }
51
- #
52
- def get_affinity(int = Process.pid)
53
- pmask = FFI::MemoryPointer.new(:ulong)
54
- smask = FFI::MemoryPointer.new(:ulong)
55
-
56
- if int == Process.pid
57
- unless GetProcessAffinityMask(GetCurrentProcess(), pmask, smask)
58
- raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
59
- end
60
- else
61
- begin
62
- handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
63
-
64
- if handle == INVALID_HANDLE_VALUE
65
- raise SystemCallError, FFI.errno, "OpeProcess"
66
- end
67
-
68
- unless GetProcessAffinityMask(handle, pmask, smask)
69
- raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
70
- end
71
- ensure
72
- CloseHandle(handle)
73
- end
74
- end
75
-
76
- [pmask.read_ulong, smask.read_ulong]
77
- end
78
-
79
- remove_method :getpriority
80
-
81
- # Retrieves the priority class for the specified process id +int+. Unlike
82
- # the default implementation, lower return values do not necessarily
83
- # correspond to higher priority classes.
84
- #
85
- # The +kind+ parameter is ignored but required for API compatibility.
86
- # You can only retrieve process information, not process group or user
87
- # information, so it is effectively always Process::PRIO_PROCESS.
88
- #
89
- # Possible return values are:
90
- #
91
- # 32 => Process::NORMAL_PRIORITY_CLASS
92
- # 64 => Process::IDLE_PRIORITY_CLASS
93
- # 128 => Process::HIGH_PRIORITY_CLASS
94
- # 256 => Process::REALTIME_PRIORITY_CLASS
95
- # 16384 => Process::BELOW_NORMAL_PRIORITY_CLASS
96
- # 32768 => Process::ABOVE_NORMAL_PRIORITY_CLASS
97
- #
98
- def getpriority(kind, int)
99
- raise TypeError, kind unless kind.is_a?(Fixnum) # Match spec
100
- raise TypeError, int unless int.is_a?(Fixnum) # Match spec
101
- int = Process.pid if int == 0 # Match spec
102
-
103
- handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, int)
104
-
105
- if handle == INVALID_HANDLE_VALUE
106
- raise SystemCallError, FFI.errno, "OpenProcess"
107
- end
108
-
109
- begin
110
- priority = GetPriorityClass(handle)
111
-
112
- if priority == 0
113
- raise SystemCallError, FFI.errno, "GetPriorityClass"
114
- end
115
- ensure
116
- CloseHandle(handle)
117
- end
118
-
119
- priority
120
- end
121
-
122
- remove_method :setpriority
123
-
124
- # Sets the priority class for the specified process id +int+.
125
- #
126
- # The +kind+ parameter is ignored but present for API compatibility.
127
- # You can only retrieve process information, not process group or user
128
- # information, so it is effectively always Process::PRIO_PROCESS.
129
- #
130
- # Possible +int_priority+ values are:
131
- #
132
- # * Process::NORMAL_PRIORITY_CLASS
133
- # * Process::IDLE_PRIORITY_CLASS
134
- # * Process::HIGH_PRIORITY_CLASS
135
- # * Process::REALTIME_PRIORITY_CLASS
136
- # * Process::BELOW_NORMAL_PRIORITY_CLASS
137
- # * Process::ABOVE_NORMAL_PRIORITY_CLASS
138
- #
139
- def setpriority(kind, int, int_priority)
140
- raise TypeError unless kind.is_a?(Integer) # Match spec
141
- raise TypeError unless int.is_a?(Integer) # Match spec
142
- raise TypeError unless int_priority.is_a?(Integer) # Match spec
143
- int = Process.pid if int == 0 # Match spec
144
-
145
- handle = OpenProcess(PROCESS_SET_INFORMATION, false , int)
146
-
147
- if handle == INVALID_HANDLE_VALUE
148
- raise SystemCallError, FFI.errno, "OpenProcess"
149
- end
150
-
151
- begin
152
- unless SetPriorityClass(handle, int_priority)
153
- raise SystemCallError, FFI.errno, "SetPriorityClass"
154
- end
155
- ensure
156
- CloseHandle(handle)
157
- end
158
-
159
- return 0 # Match the spec
160
- end
161
-
162
- remove_method :uid
163
-
164
- # Returns the uid of the current process. Specifically, it returns the
165
- # RID of the SID associated with the owner of the process.
166
- #
167
- # If +sid+ is set to true, then a binary sid is returned. Otherwise, a
168
- # numeric id is returned (the default).
169
- #--
170
- # The Process.uid method in core Ruby always returns 0 on MS Windows.
171
- #
172
- def uid(sid = false)
173
- token = FFI::MemoryPointer.new(:ulong)
174
-
175
- raise TypeError unless sid.is_a?(TrueClass) || sid.is_a?(FalseClass)
176
-
177
- unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
178
- raise SystemCallError, FFI.errno, "OpenProcessToken"
179
- end
180
-
181
- token = token.read_ulong
182
- rlength = FFI::MemoryPointer.new(:ulong)
183
- tuser = 0.chr * 512
184
-
185
- bool = GetTokenInformation(
186
- token,
187
- TokenUser,
188
- tuser,
189
- tuser.size,
190
- rlength
191
- )
192
-
193
- unless bool
194
- raise SystemCallError, FFI.errno, "GetTokenInformation"
195
- end
196
-
197
- string_sid = tuser[8, (rlength.read_ulong - 8)]
198
-
199
- if sid
200
- string_sid
201
- else
202
- psid = FFI::MemoryPointer.new(:ulong)
203
-
204
- unless ConvertSidToStringSidA(string_sid, psid)
205
- raise SystemCallError, FFI.errno, "ConvertSidToStringSid"
206
- end
207
-
208
- psid.read_pointer.read_string.split('-').last.to_i
209
- end
210
- end
211
-
212
- remove_method :getrlimit
213
-
214
- # Gets the resource limit of the current process. Only a limited number
215
- # of flags are supported.
216
- #
217
- # Process::RLIMIT_CPU
218
- # Process::RLIMIT_FSIZE
219
- # Process::RLIMIT_AS
220
- # Process::RLIMIT_RSS
221
- # Process::RLIMIT_VMEM
222
- #
223
- # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
224
- # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
225
- # refers to the per process user time limit. The Process::RLIMIT_FSIZE
226
- # constant is hard coded to the maximum file size on an NTFS filesystem,
227
- # approximately 4TB (or 4GB if not NTFS).
228
- #
229
- # While a two element array is returned in order to comply with the spec,
230
- # there is no separate hard and soft limit. The values will always be the
231
- # same.
232
- #
233
- # If [0,0] is returned then it means no limit has been set.
234
- #
235
- # Example:
236
- #
237
- # Process.getrlimit(Process::RLIMIT_VMEM) # => [0, 0]
238
- #--
239
- # NOTE: Both the getrlimit and setrlimit method use an at_exit handler
240
- # to close a job handle. This is necessary because simply calling it
241
- # at the end of the block, while marking it for closure, would also make
242
- # it unavailable within the same process again since it would no longer
243
- # be associated with the job. In other words, trying to call it more than
244
- # once within the same program would fail.
245
- #
246
- def getrlimit(resource)
247
- if resource == RLIMIT_FSIZE
248
- if volume_type == 'NTFS'
249
- return ((1024**4) * 4) - (1024 * 64) # ~ 4TB
250
- else
251
- return (1024**3) * 4 # 4 GB
252
- end
253
- end
254
-
255
- handle = nil
256
- in_job = Process.job?
257
-
258
- # Put the current process in a job if it's not already in one
259
- if in_job && defined?(@win32_process_job_name)
260
- handle = OpenJobObjectA(JOB_OBJECT_QUERY, true, @win32_process_job_name)
261
- raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
262
- else
263
- @win32_process_job_name = 'ruby_' + Process.pid.to_s
264
- handle = CreateJobObjectA(nil, @win32_process_job_name)
265
- raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
266
- end
267
-
268
- begin
269
- unless in_job
270
- unless AssignProcessToJobObject(handle, GetCurrentProcess())
271
- raise Error, get_last_error
272
- end
273
- end
274
-
275
- ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
276
- val = nil
277
-
278
- # Set the LimitFlags member of the struct
279
- case resource
280
- when RLIMIT_CPU
281
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
282
- when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
283
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
284
- else
285
- raise ArgumentError, "unsupported resource type: '#{resource}'"
286
- end
287
-
288
- bool = QueryInformationJobObject(
289
- handle,
290
- JobObjectExtendedLimitInformation,
291
- ptr,
292
- ptr.size,
293
- nil
294
- )
295
-
296
- unless bool
297
- raise SystemCallError, FFI.errno, "QueryInformationJobObject"
298
- end
299
-
300
- case resource
301
- when Process::RLIMIT_CPU
302
- val = ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart]
303
- when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
304
- val = ptr[:ProcessMemoryLimit]
305
- end
306
-
307
- ensure
308
- at_exit{ CloseHandle(handle) if handle }
309
- end
310
-
311
- [val, val]
312
- end
313
-
314
- remove_method :setrlimit
315
-
316
- # Sets the resource limit of the current process. Only a limited number
317
- # of flags are supported.
318
- #
319
- # Process::RLIMIT_CPU
320
- # Process::RLIMIT_AS
321
- # Process::RLIMIT_RSS
322
- # Process::RLIMIT_VMEM
323
- #
324
- # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
325
- # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
326
- # refers to the per process user time limit.
327
- #
328
- # The +max_limit+ parameter is provided for interface compatibility only.
329
- # It is always set to the current_limit value.
330
- #
331
- # Example:
332
- #
333
- # Process.setrlimit(Process::RLIMIT_VMEM, 1024 * 4) # => nil
334
- # Process.getrlimit(Process::RLIMIT_VMEM) # => [4096, 4096]
335
- #
336
- def setrlimit(resource, current_limit, max_limit = nil)
337
- max_limit = current_limit
338
-
339
- handle = nil
340
- in_job = Process.job?
341
-
342
- unless [RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS, RLIMIT_CPU].include?(resource)
343
- raise ArgumentError, "unsupported resource type: '#{resource}'"
344
- end
345
-
346
- # Put the current process in a job if it's not already in one
347
- if in_job && defined? @win32_process_job_name
348
- handle = OpenJobObjectA(JOB_OBJECT_SET_ATTRIBUTES, true, @win32_process_job_name)
349
- raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
350
- else
351
- @job_name = 'ruby_' + Process.pid.to_s
352
- handle = CreateJobObjectA(nil, job_name)
353
- raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
354
- end
355
-
356
- begin
357
- unless in_job
358
- unless AssignProcessToJobObject(handle, GetCurrentProcess())
359
- raise SystemCallError, FFI.errno, "AssignProcessToJobObject"
360
- end
361
- end
362
-
363
- ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
364
-
365
- # Set the LimitFlags and relevant members of the struct
366
- if resource == RLIMIT_CPU
367
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
368
- ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = max_limit
369
- else
370
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
371
- ptr[:ProcessMemoryLimit] = max_limit
372
- end
373
-
374
- bool = SetInformationJobObject(
375
- handle,
376
- JobObjectExtendedLimitInformation,
377
- ptr,
378
- ptr.size
379
- )
380
-
381
- unless bool
382
- raise SystemCallError, FFI.errno, "SetInformationJobObject"
383
- end
384
- ensure
385
- at_exit{ CloseHandle(handle) if handle }
386
- end
387
- end
388
-
389
- # Process.create(key => value, ...) => ProcessInfo
390
- #
391
- # This is a wrapper for the CreateProcess() function. It executes a process,
392
- # returning a ProcessInfo struct. It accepts a hash as an argument.
393
- # There are several primary keys:
394
- #
395
- # * command_line (this or app_name must be present)
396
- # * app_name (default: nil)
397
- # * inherit (default: false)
398
- # * process_inherit (default: false)
399
- # * thread_inherit (default: false)
400
- # * creation_flags (default: 0)
401
- # * cwd (default: Dir.pwd)
402
- # * startup_info (default: nil)
403
- # * environment (default: nil)
404
- # * close_handles (default: true)
405
- # * with_logon (default: nil)
406
- # * domain (default: nil)
407
- # * password (default: nil, mandatory if with_logon)
408
- #
409
- # Of these, the 'command_line' or 'app_name' must be specified or an
410
- # error is raised. Both may be set individually, but 'command_line' should
411
- # be preferred if only one of them is set because it does not (necessarily)
412
- # require an explicit path or extension to work.
413
- #
414
- # The 'domain' and 'password' options are only relevent in the context
415
- # of 'with_logon'. If 'with_logon' is set, then the 'password' option is
416
- # mandatory.
417
- #
418
- # The startup_info key takes a hash. Its keys are attributes that are
419
- # part of the StartupInfo struct, and are generally only meaningful for
420
- # GUI or console processes. See the documentation on CreateProcess()
421
- # and the StartupInfo struct on MSDN for more information.
422
- #
423
- # * desktop
424
- # * title
425
- # * x
426
- # * y
427
- # * x_size
428
- # * y_size
429
- # * x_count_chars
430
- # * y_count_chars
431
- # * fill_attribute
432
- # * sw_flags
433
- # * startf_flags
434
- # * stdin
435
- # * stdout
436
- # * stderr
437
- #
438
- # Note that the 'stdin', 'stdout' and 'stderr' options can be either Ruby
439
- # IO objects or file descriptors (i.e. a fileno). However, StringIO objects
440
- # are not currently supported. Unfortunately, setting these is not currently
441
- # an option for JRuby.
442
- #
443
- # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
444
- # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
445
- # automatically OR'd to the +startf_flags+ value.
446
- #
447
- # The ProcessInfo struct contains the following members:
448
- #
449
- # * process_handle - The handle to the newly created process.
450
- # * thread_handle - The handle to the primary thread of the process.
451
- # * process_id - Process ID.
452
- # * thread_id - Thread ID.
453
- #
454
- # If the 'close_handles' option is set to true (the default) then the
455
- # process_handle and the thread_handle are automatically closed for you
456
- # before the ProcessInfo struct is returned.
457
- #
458
- # If the 'with_logon' option is set, then the process runs the specified
459
- # executable file in the security context of the specified credentials.
460
- #
461
- def create(args)
462
- unless args.kind_of?(Hash)
463
- raise TypeError, 'hash keyword arguments expected'
464
- end
465
-
466
- valid_keys = %w[
467
- app_name command_line inherit creation_flags cwd environment
468
- startup_info thread_inherit process_inherit close_handles with_logon
469
- domain password
470
- ]
471
-
472
- valid_si_keys = %w[
473
- startf_flags desktop title x y x_size y_size x_count_chars
474
- y_count_chars fill_attribute sw_flags stdin stdout stderr
475
- ]
476
-
477
- # Set default values
478
- hash = {
479
- 'app_name' => nil,
480
- 'creation_flags' => 0,
481
- 'close_handles' => true
482
- }
483
-
484
- # Validate the keys, and convert symbols and case to lowercase strings.
485
- args.each{ |key, val|
486
- key = key.to_s.downcase
487
- unless valid_keys.include?(key)
488
- raise ArgumentError, "invalid key '#{key}'"
489
- end
490
- hash[key] = val
491
- }
492
-
493
- si_hash = {}
494
-
495
- # If the startup_info key is present, validate its subkeys
496
- if hash['startup_info']
497
- hash['startup_info'].each{ |key, val|
498
- key = key.to_s.downcase
499
- unless valid_si_keys.include?(key)
500
- raise ArgumentError, "invalid startup_info key '#{key}'"
501
- end
502
- si_hash[key] = val
503
- }
504
- end
505
-
506
- # The +command_line+ key is mandatory unless the +app_name+ key
507
- # is specified.
508
- unless hash['command_line']
509
- if hash['app_name']
510
- hash['command_line'] = hash['app_name']
511
- hash['app_name'] = nil
512
- else
513
- raise ArgumentError, 'command_line or app_name must be specified'
514
- end
515
- end
516
-
517
- env = nil
518
-
519
- # The env string should be passed as a string of ';' separated paths.
520
- if hash['environment']
521
- env = hash['environment']
522
-
523
- unless env.respond_to?(:join)
524
- env = hash['environment'].split(File::PATH_SEPARATOR)
525
- end
526
-
527
- env = env.map{ |e| e + 0.chr }.join('') + 0.chr
528
- env.to_wide_string! if hash['with_logon']
529
- end
530
-
531
- # Process SECURITY_ATTRIBUTE structure
532
- process_security = nil
533
-
534
- if hash['process_inherit']
535
- process_security = SECURITY_ATTRIBUTES.new
536
- process_security[:nLength] = 12
537
- process_security[:bInheritHandle] = true
538
- end
539
-
540
- # Thread SECURITY_ATTRIBUTE structure
541
- thread_security = nil
542
-
543
- if hash['thread_inherit']
544
- thread_security = SECURITY_ATTRIBUTES.new
545
- thread_security[:nLength] = 12
546
- thread_security[:bInheritHandle] = true
547
- end
548
-
549
- # Automatically handle stdin, stdout and stderr as either IO objects
550
- # or file descriptors. This won't work for StringIO, however. It also
551
- # will not work on JRuby because of the way it handles internal file
552
- # descriptors.
553
- #
554
- ['stdin', 'stdout', 'stderr'].each{ |io|
555
- if si_hash[io]
556
- if si_hash[io].respond_to?(:fileno)
557
- handle = get_osfhandle(si_hash[io].fileno)
558
- else
559
- handle = get_osfhandle(si_hash[io])
560
- end
561
-
562
- if handle == INVALID_HANDLE_VALUE
563
- ptr = FFI::MemoryPointer.new(:int)
564
-
565
- if windows_version >= 6 && get_errno(ptr) == 0
566
- errno = ptr.read_int
567
- else
568
- errno = FFI.errno
569
- end
570
-
571
- raise SystemCallError.new("get_osfhandle", errno)
572
- end
573
-
574
- # Most implementations of Ruby on Windows create inheritable
575
- # handles by default, but some do not. RF bug #26988.
576
- bool = SetHandleInformation(
577
- handle,
578
- HANDLE_FLAG_INHERIT,
579
- HANDLE_FLAG_INHERIT
580
- )
581
-
582
- raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
583
-
584
- si_hash[io] = handle
585
- si_hash['startf_flags'] ||= 0
586
- si_hash['startf_flags'] |= STARTF_USESTDHANDLES
587
- hash['inherit'] = true
588
- end
589
- }
590
-
591
- procinfo = PROCESS_INFORMATION.new
592
- startinfo = STARTUPINFO.new
593
-
594
- unless si_hash.empty?
595
- startinfo[:cb] = startinfo.size
596
- startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
597
- startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
598
- startinfo[:dwX] = si_hash['x'] if si_hash['x']
599
- startinfo[:dwY] = si_hash['y'] if si_hash['y']
600
- startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
601
- startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
602
- startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
603
- startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
604
- startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
605
- startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
606
- startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
607
- startinfo[:cbReserved2] = 0
608
- startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
609
- startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
610
- startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
611
- end
612
-
613
- app = nil
614
- cmd = nil
615
-
616
- # Convert strings to wide character strings if present
617
- if hash['app_name']
618
- app = hash['app_name'].to_wide_string
619
- end
620
-
621
- if hash['command_line']
622
- cmd = hash['command_line'].to_wide_string
623
- end
624
-
625
- if hash['cwd']
626
- cwd = hash['cwd'].to_wide_string
627
- end
628
-
629
- if hash['with_logon']
630
- logon = hash['with_logon'].to_wide_string
631
-
632
- if hash['password']
633
- passwd = hash['password'].to_wide_string
634
- else
635
- raise ArgumentError, 'password must be specified if with_logon is used'
636
- end
637
-
638
- if hash['domain']
639
- domain = hash['domain'].to_wide_string
640
- end
641
-
642
- hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
643
-
644
- bool = CreateProcessWithLogonW(
645
- logon, # User
646
- domain, # Domain
647
- passwd, # Password
648
- LOGON_WITH_PROFILE, # Logon flags
649
- app, # App name
650
- cmd, # Command line
651
- hash['creation_flags'], # Creation flags
652
- env, # Environment
653
- cwd, # Working directory
654
- startinfo, # Startup Info
655
- procinfo # Process Info
656
- )
657
-
658
- unless bool
659
- raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
660
- end
661
- else
662
- inherit = hash['inherit'] || false
663
-
664
- bool = CreateProcessW(
665
- app, # App name
666
- cmd, # Command line
667
- process_security, # Process attributes
668
- thread_security, # Thread attributes
669
- inherit, # Inherit handles?
670
- hash['creation_flags'], # Creation flags
671
- env, # Environment
672
- cwd, # Working directory
673
- startinfo, # Startup Info
674
- procinfo # Process Info
675
- )
676
-
677
- unless bool
678
- raise SystemCallError.new("CreateProcess", FFI.errno)
679
- end
680
- end
681
-
682
- # Automatically close the process and thread handles in the
683
- # PROCESS_INFORMATION struct unless explicitly told not to.
684
- if hash['close_handles']
685
- CloseHandle(procinfo[:hProcess])
686
- CloseHandle(procinfo[:hThread])
687
- end
688
-
689
- ProcessInfo.new(
690
- procinfo[:hProcess],
691
- procinfo[:hThread],
692
- procinfo[:dwProcessId],
693
- procinfo[:dwThreadId]
694
- )
695
- end
696
-
697
- remove_method :kill
698
-
699
- # Kill a given process with a specific signal. This overrides the default
700
- # implementation of Process.kill. The differences mainly reside in the way
701
- # it kills processes, but this version also gives you finer control over
702
- # behavior.
703
- #
704
- # Internally, signals 2 and 3 will generate a console control event, using
705
- # a ctrl-c or ctrl-break event, respectively. Signal 9 terminates the
706
- # process harshly, given that process no chance to do any internal cleanup.
707
- # Signals 1 and 4-8 kill the process more nicely, giving the process a
708
- # chance to do internal cleanup before being killed. Signal 0 behaves the
709
- # same as the default implementation.
710
- #
711
- # When using signals 1 or 4-8 you may specify additional options that
712
- # allow finer control over how that process is killed and how your program
713
- # behaves.
714
- #
715
- # Possible options for signals 1 and 4-8.
716
- #
717
- # :exit_proc => The name of the exit function called when signal 1 or 4-8
718
- # is used. The default is 'ExitProcess'.
719
- #
720
- # :dll_module => The name of the .dll (or .exe) that contains :exit_proc.
721
- # The default is 'kernel32'.
722
- #
723
- # :wait_time => The time, in milliseconds, to wait for the process to
724
- # actually die. The default is 5ms. If you specify 0 here
725
- # then the process does not wait if the process is not
726
- # signaled and instead returns immediately. Alternatively,
727
- # you may specify Process::INFINITE, and your code will
728
- # block until the process is actually signaled.
729
- #
730
- # Example:
731
- #
732
- # Process.kill(1, 12345, :exit_proc => 'ExitProcess', :module => 'kernel32')
733
- #
734
- def kill(signal, *pids)
735
- # Match the spec, require at least 2 arguments
736
- if pids.length == 0
737
- raise ArgumentError, "wrong number of arguments (1 for at least 2)"
738
- end
739
-
740
- # Match the spec, signal may not be less than zero if numeric
741
- if signal.is_a?(Numeric) && signal < 0 # EINVAL
742
- raise SystemCallError.new(22)
743
- end
744
-
745
- # Match the spec, signal must be a numeric, string or symbol
746
- unless signal.is_a?(String) || signal.is_a?(Numeric) || signal.is_a?(Symbol)
747
- raise ArgumentError, "bad signal type #{signal.class}"
748
- end
749
-
750
- # Match the spec, making an exception for BRK/SIGBRK, if the signal name is invalid
751
- if signal.is_a?(String) || signal.is_a?(Symbol)
752
- signal = signal.to_s.sub('SIG', '')
753
- unless Signal.list.keys.include?(signal) || ['BRK', 'BRK'].include?(signal)
754
- raise ArgumentError, "unsupported name '#{signal}'"
755
- end
756
- end
757
-
758
- # If the last argument is a hash, pop it and assume it's a hash of options
759
- if pids.last.is_a?(Hash)
760
- hash = pids.pop
761
- opts = {}
762
-
763
- valid = %w[exit_proc dll_module wait_time]
764
-
765
- hash.each{ |k,v|
766
- k = k.to_s.downcase
767
- unless valid.include?(k)
768
- raise ArgumentError, "invalid option '#{k}'"
769
- end
770
- opts[k] = v
771
- }
772
-
773
- exit_proc = opts['exit_proc'] || 'ExitProcess'
774
- dll_module = opts['dll_module'] || 'kernel32'
775
- wait_time = opts['wait_time'] || 5
776
- else
777
- wait_time = 5
778
- exit_proc = 'ExitProcess'
779
- dll_module = 'kernel32'
780
- end
781
-
782
- count = 0
783
-
784
- pids.each{ |pid|
785
- raise TypeError unless pid.is_a?(Numeric) # Match spec, pid must be a number
786
- raise SystemCallError.new(22) if pid < 0 # Match spec, EINVAL if pid less than zero
787
-
788
- sigint = [Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2]
789
-
790
- # Match the spec
791
- if pid == 0 && !sigint.include?(signal)
792
- raise SystemCallError.new(22)
793
- end
794
-
795
- if signal == 0
796
- access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
797
- elsif signal == 9
798
- access = PROCESS_TERMINATE
799
- else
800
- access = PROCESS_ALL_ACCESS
801
- end
802
-
803
- begin
804
- handle = OpenProcess(access, false, pid)
805
-
806
- if signal != 0 && handle == 0
807
- raise SystemCallError, FFI.errno, "OpenProcess"
808
- end
809
-
810
- case signal
811
- when 0
812
- if handle != 0
813
- count += 1
814
- else
815
- if FFI.errno == ERROR_ACCESS_DENIED
816
- count += 1
817
- else
818
- raise SystemCallError.new(3) # ESRCH
819
- end
820
- end
821
- when Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2
822
- if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
823
- count += 1
824
- else
825
- raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
826
- end
827
- when Signal.list['BRK'], 'BRK', 'SIGBRK', :BRK, :SIGBRK, 3
828
- if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
829
- count += 1
830
- else
831
- raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
832
- end
833
- when Signal.list['KILL'], 'KILL', 'SIGKILL', :KILL, :SIGKILL, 9
834
- if TerminateProcess(handle, pid)
835
- count += 1
836
- else
837
- raise SystemCallError.new("TerminateProcess", FFI.errno)
838
- end
839
- else
840
- thread_id = FFI::MemoryPointer.new(:ulong)
841
-
842
- mod = GetModuleHandle(dll_module)
843
-
844
- if mod == 0
845
- raise SystemCallError.new("GetModuleHandle: '#{dll_module}'", FFI.errno)
846
- end
847
-
848
- proc_addr = GetProcAddress(mod, exit_proc)
849
-
850
- if proc_addr == 0
851
- raise SystemCallError.new("GetProcAddress: '#{exit_proc}'", FFI.errno)
852
- end
853
-
854
- thread = CreateRemoteThread(handle, nil, 0, proc_addr, nil, 0, thread_id)
855
-
856
- if thread > 0
857
- WaitForSingleObject(thread, wait_time)
858
- count += 1
859
- else
860
- raise SystemCallError.new("CreateRemoteThread", FFI.errno)
861
- end
862
- end
863
- ensure
864
- CloseHandle(handle) if handle
865
- end
866
- }
867
-
868
- count
869
- end
870
- end
871
-
872
- class << self
873
- private
874
-
875
- # Private method that returns the volume type, e.g. "NTFS", etc.
876
- def volume_type
877
- buf = FFI::MemoryPointer.new(:char, 32)
878
- bool = GetVolumeInformationA(nil, nil, 0, nil, nil, nil, buf, buf.size)
879
- bool ? buf.read_string : nil
880
- end
881
-
882
- # Private method that returns the Windows major version number.
883
- def windows_version
884
- ver = OSVERSIONINFO.new
885
- ver[:dwOSVersionInfoSize] = ver.size
886
-
887
- unless GetVersionExA(ver)
888
- raise SystemCallError.new("GetVersionEx", FFI.errno)
889
- end
890
-
891
- ver[:dwMajorVersion]
892
- end
893
- end
894
- end
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'functions')
2
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'constants')
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'structs')
4
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'process', 'helper')
5
+
6
+
7
+ module Process
8
+ include Process::Constants
9
+ extend Process::Functions
10
+ extend Process::Structs
11
+ extend Process::Constants
12
+
13
+ # The version of the win32-process library.
14
+ WIN32_PROCESS_VERSION = '0.7.3'
15
+
16
+ # Disable popups. This mostly affects the Process.kill method.
17
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX)
18
+
19
+ class << self
20
+ # Returns whether or not the current process is part of a Job (process group).
21
+ def job?
22
+ pbool = FFI::MemoryPointer.new(:int)
23
+ IsProcessInJob(GetCurrentProcess(), nil, pbool)
24
+ pbool.read_int == 1 ? true : false
25
+ end
26
+
27
+ # Returns the process and system affinity mask for the given +pid+, or the
28
+ # current process if no pid is provided. The return value is a two element
29
+ # array, with the first containing the process affinity mask, and the second
30
+ # containing the system affinity mask. Both are decimal values.
31
+ #
32
+ # A process affinity mask is a bit vector indicating the processors that a
33
+ # process is allowed to run on. A system affinity mask is a bit vector in
34
+ # which each bit represents the processors that are configured into a
35
+ # system.
36
+ #
37
+ # Example:
38
+ #
39
+ # # System has 4 processors, current process is allowed to run on all.
40
+ # Process.get_affinity # => [[15], [15]], where '15' is 1 + 2 + 4 + 8
41
+ #
42
+ # # System has 4 processors, current process only allowed on 1 and 4.
43
+ # Process.get_affinity # => [[9], [15]]
44
+ #
45
+ # If you want to convert a decimal bit vector into an array of 0's and 1's
46
+ # indicating the flag value of each processor, you can use something like
47
+ # this approach:
48
+ #
49
+ # mask = Process.get_affinity.first
50
+ # (0..mask).to_a.map{ |n| mask[n] }
51
+ #
52
+ def get_affinity(int = Process.pid)
53
+ pmask = FFI::MemoryPointer.new(:ulong)
54
+ smask = FFI::MemoryPointer.new(:ulong)
55
+
56
+ if int == Process.pid
57
+ unless GetProcessAffinityMask(GetCurrentProcess(), pmask, smask)
58
+ raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
59
+ end
60
+ else
61
+ begin
62
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0 , int)
63
+
64
+ if handle == 0
65
+ raise SystemCallError, FFI.errno, "OpenProcess"
66
+ end
67
+
68
+ unless GetProcessAffinityMask(handle, pmask, smask)
69
+ raise SystemCallError, FFI.errno, "GetProcessAffinityMask"
70
+ end
71
+ ensure
72
+ CloseHandle(handle)
73
+ end
74
+ end
75
+
76
+ [pmask.read_ulong, smask.read_ulong]
77
+ end
78
+
79
+ remove_method :getpriority
80
+
81
+ # Retrieves the priority class for the specified process id +int+. Unlike
82
+ # the default implementation, lower return values do not necessarily
83
+ # correspond to higher priority classes.
84
+ #
85
+ # The +kind+ parameter is ignored but required for API compatibility.
86
+ # You can only retrieve process information, not process group or user
87
+ # information, so it is effectively always Process::PRIO_PROCESS.
88
+ #
89
+ # Possible return values are:
90
+ #
91
+ # 32 => Process::NORMAL_PRIORITY_CLASS
92
+ # 64 => Process::IDLE_PRIORITY_CLASS
93
+ # 128 => Process::HIGH_PRIORITY_CLASS
94
+ # 256 => Process::REALTIME_PRIORITY_CLASS
95
+ # 16384 => Process::BELOW_NORMAL_PRIORITY_CLASS
96
+ # 32768 => Process::ABOVE_NORMAL_PRIORITY_CLASS
97
+ #
98
+ def getpriority(kind, int)
99
+ raise TypeError, kind unless kind.is_a?(Fixnum) # Match spec
100
+ raise TypeError, int unless int.is_a?(Fixnum) # Match spec
101
+ int = Process.pid if int == 0 # Match spec
102
+
103
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, int)
104
+
105
+ if handle == 0
106
+ raise SystemCallError, FFI.errno, "OpenProcess"
107
+ end
108
+
109
+ begin
110
+ priority = GetPriorityClass(handle)
111
+
112
+ if priority == 0
113
+ raise SystemCallError, FFI.errno, "GetPriorityClass"
114
+ end
115
+ ensure
116
+ CloseHandle(handle)
117
+ end
118
+
119
+ priority
120
+ end
121
+
122
+ remove_method :setpriority
123
+
124
+ # Sets the priority class for the specified process id +int+.
125
+ #
126
+ # The +kind+ parameter is ignored but present for API compatibility.
127
+ # You can only retrieve process information, not process group or user
128
+ # information, so it is effectively always Process::PRIO_PROCESS.
129
+ #
130
+ # Possible +int_priority+ values are:
131
+ #
132
+ # * Process::NORMAL_PRIORITY_CLASS
133
+ # * Process::IDLE_PRIORITY_CLASS
134
+ # * Process::HIGH_PRIORITY_CLASS
135
+ # * Process::REALTIME_PRIORITY_CLASS
136
+ # * Process::BELOW_NORMAL_PRIORITY_CLASS
137
+ # * Process::ABOVE_NORMAL_PRIORITY_CLASS
138
+ #
139
+ def setpriority(kind, int, int_priority)
140
+ raise TypeError unless kind.is_a?(Integer) # Match spec
141
+ raise TypeError unless int.is_a?(Integer) # Match spec
142
+ raise TypeError unless int_priority.is_a?(Integer) # Match spec
143
+ int = Process.pid if int == 0 # Match spec
144
+
145
+ handle = OpenProcess(PROCESS_SET_INFORMATION, false , int)
146
+
147
+ if handle == 0
148
+ raise SystemCallError, FFI.errno, "OpenProcess"
149
+ end
150
+
151
+ begin
152
+ unless SetPriorityClass(handle, int_priority)
153
+ raise SystemCallError, FFI.errno, "SetPriorityClass"
154
+ end
155
+ ensure
156
+ CloseHandle(handle)
157
+ end
158
+
159
+ return 0 # Match the spec
160
+ end
161
+
162
+ remove_method :uid
163
+
164
+ # Returns the uid of the current process. Specifically, it returns the
165
+ # RID of the SID associated with the owner of the process.
166
+ #
167
+ # If +sid+ is set to true, then a binary sid is returned. Otherwise, a
168
+ # numeric id is returned (the default).
169
+ #--
170
+ # The Process.uid method in core Ruby always returns 0 on MS Windows.
171
+ #
172
+ def uid(sid = false)
173
+ token = FFI::MemoryPointer.new(:ulong)
174
+
175
+ raise TypeError unless sid.is_a?(TrueClass) || sid.is_a?(FalseClass)
176
+
177
+ unless OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, token)
178
+ raise SystemCallError, FFI.errno, "OpenProcessToken"
179
+ end
180
+
181
+ token = token.read_ulong
182
+ rlength = FFI::MemoryPointer.new(:ulong)
183
+ tuser = 0.chr * 512
184
+
185
+ bool = GetTokenInformation(
186
+ token,
187
+ TokenUser,
188
+ tuser,
189
+ tuser.size,
190
+ rlength
191
+ )
192
+
193
+ unless bool
194
+ raise SystemCallError, FFI.errno, "GetTokenInformation"
195
+ end
196
+
197
+ string_sid = tuser[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
198
+
199
+ if sid
200
+ string_sid
201
+ else
202
+ psid = FFI::MemoryPointer.new(:uintptr_t)
203
+
204
+ unless ConvertSidToStringSidA(string_sid, psid)
205
+ raise SystemCallError, FFI.errno, "ConvertSidToStringSid"
206
+ end
207
+
208
+ psid.read_pointer.read_string.split('-').last.to_i
209
+ end
210
+ end
211
+
212
+ remove_method :getrlimit
213
+
214
+ # Gets the resource limit of the current process. Only a limited number
215
+ # of flags are supported.
216
+ #
217
+ # Process::RLIMIT_CPU
218
+ # Process::RLIMIT_FSIZE
219
+ # Process::RLIMIT_AS
220
+ # Process::RLIMIT_RSS
221
+ # Process::RLIMIT_VMEM
222
+ #
223
+ # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
224
+ # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
225
+ # refers to the per process user time limit. The Process::RLIMIT_FSIZE
226
+ # constant is hard coded to the maximum file size on an NTFS filesystem,
227
+ # approximately 4TB (or 4GB if not NTFS).
228
+ #
229
+ # While a two element array is returned in order to comply with the spec,
230
+ # there is no separate hard and soft limit. The values will always be the
231
+ # same.
232
+ #
233
+ # If [0,0] is returned then it means no limit has been set.
234
+ #
235
+ # Example:
236
+ #
237
+ # Process.getrlimit(Process::RLIMIT_VMEM) # => [0, 0]
238
+ #--
239
+ # NOTE: Both the getrlimit and setrlimit method use an at_exit handler
240
+ # to close a job handle. This is necessary because simply calling it
241
+ # at the end of the block, while marking it for closure, would also make
242
+ # it unavailable within the same process again since it would no longer
243
+ # be associated with the job. In other words, trying to call it more than
244
+ # once within the same program would fail.
245
+ #
246
+ def getrlimit(resource)
247
+ if resource == RLIMIT_FSIZE
248
+ if volume_type == 'NTFS'
249
+ return ((1024**4) * 4) - (1024 * 64) # ~ 4TB
250
+ else
251
+ return (1024**3) * 4 # 4 GB
252
+ end
253
+ end
254
+
255
+ handle = nil
256
+ in_job = Process.job?
257
+
258
+ # Put the current process in a job if it's not already in one
259
+ if in_job && defined?(@win32_process_job_name)
260
+ handle = OpenJobObjectA(JOB_OBJECT_QUERY, true, @win32_process_job_name)
261
+ raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
262
+ else
263
+ @win32_process_job_name = 'ruby_' + Process.pid.to_s
264
+ handle = CreateJobObjectA(nil, @win32_process_job_name)
265
+ raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
266
+ end
267
+
268
+ begin
269
+ unless in_job
270
+ unless AssignProcessToJobObject(handle, GetCurrentProcess())
271
+ raise Error, get_last_error
272
+ end
273
+ end
274
+
275
+ ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
276
+ val = nil
277
+
278
+ # Set the LimitFlags member of the struct
279
+ case resource
280
+ when RLIMIT_CPU
281
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
282
+ when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
283
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
284
+ else
285
+ raise ArgumentError, "unsupported resource type: '#{resource}'"
286
+ end
287
+
288
+ bool = QueryInformationJobObject(
289
+ handle,
290
+ JobObjectExtendedLimitInformation,
291
+ ptr,
292
+ ptr.size,
293
+ nil
294
+ )
295
+
296
+ unless bool
297
+ raise SystemCallError, FFI.errno, "QueryInformationJobObject"
298
+ end
299
+
300
+ case resource
301
+ when Process::RLIMIT_CPU
302
+ val = ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart]
303
+ when RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS
304
+ val = ptr[:ProcessMemoryLimit]
305
+ end
306
+
307
+ ensure
308
+ at_exit{ CloseHandle(handle) if handle }
309
+ end
310
+
311
+ [val, val]
312
+ end
313
+
314
+ remove_method :setrlimit
315
+
316
+ # Sets the resource limit of the current process. Only a limited number
317
+ # of flags are supported.
318
+ #
319
+ # Process::RLIMIT_CPU
320
+ # Process::RLIMIT_AS
321
+ # Process::RLIMIT_RSS
322
+ # Process::RLIMIT_VMEM
323
+ #
324
+ # The Process:RLIMIT_AS, Process::RLIMIT_RSS and Process::VMEM constants
325
+ # all refer to the Process memory limit. The Process::RLIMIT_CPU constant
326
+ # refers to the per process user time limit.
327
+ #
328
+ # The +max_limit+ parameter is provided for interface compatibility only.
329
+ # It is always set to the current_limit value.
330
+ #
331
+ # Example:
332
+ #
333
+ # Process.setrlimit(Process::RLIMIT_VMEM, 1024 * 4) # => nil
334
+ # Process.getrlimit(Process::RLIMIT_VMEM) # => [4096, 4096]
335
+ #
336
+ def setrlimit(resource, current_limit, max_limit = nil)
337
+ max_limit = current_limit
338
+
339
+ handle = nil
340
+ in_job = Process.job?
341
+
342
+ unless [RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS, RLIMIT_CPU].include?(resource)
343
+ raise ArgumentError, "unsupported resource type: '#{resource}'"
344
+ end
345
+
346
+ # Put the current process in a job if it's not already in one
347
+ if in_job && defined? @win32_process_job_name
348
+ handle = OpenJobObjectA(JOB_OBJECT_SET_ATTRIBUTES, true, @win32_process_job_name)
349
+ raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
350
+ else
351
+ @job_name = 'ruby_' + Process.pid.to_s
352
+ handle = CreateJobObjectA(nil, job_name)
353
+ raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
354
+ end
355
+
356
+ begin
357
+ unless in_job
358
+ unless AssignProcessToJobObject(handle, GetCurrentProcess())
359
+ raise SystemCallError, FFI.errno, "AssignProcessToJobObject"
360
+ end
361
+ end
362
+
363
+ ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
364
+
365
+ # Set the LimitFlags and relevant members of the struct
366
+ if resource == RLIMIT_CPU
367
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
368
+ ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = max_limit
369
+ else
370
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
371
+ ptr[:ProcessMemoryLimit] = max_limit
372
+ end
373
+
374
+ bool = SetInformationJobObject(
375
+ handle,
376
+ JobObjectExtendedLimitInformation,
377
+ ptr,
378
+ ptr.size
379
+ )
380
+
381
+ unless bool
382
+ raise SystemCallError, FFI.errno, "SetInformationJobObject"
383
+ end
384
+ ensure
385
+ at_exit{ CloseHandle(handle) if handle }
386
+ end
387
+ end
388
+
389
+ # Process.create(key => value, ...) => ProcessInfo
390
+ #
391
+ # This is a wrapper for the CreateProcess() function. It executes a process,
392
+ # returning a ProcessInfo struct. It accepts a hash as an argument.
393
+ # There are several primary keys:
394
+ #
395
+ # * command_line (this or app_name must be present)
396
+ # * app_name (default: nil)
397
+ # * inherit (default: false)
398
+ # * process_inherit (default: false)
399
+ # * thread_inherit (default: false)
400
+ # * creation_flags (default: 0)
401
+ # * cwd (default: Dir.pwd)
402
+ # * startup_info (default: nil)
403
+ # * environment (default: nil)
404
+ # * close_handles (default: true)
405
+ # * with_logon (default: nil)
406
+ # * domain (default: nil)
407
+ # * password (default: nil, mandatory if with_logon)
408
+ #
409
+ # Of these, the 'command_line' or 'app_name' must be specified or an
410
+ # error is raised. Both may be set individually, but 'command_line' should
411
+ # be preferred if only one of them is set because it does not (necessarily)
412
+ # require an explicit path or extension to work.
413
+ #
414
+ # The 'domain' and 'password' options are only relevent in the context
415
+ # of 'with_logon'. If 'with_logon' is set, then the 'password' option is
416
+ # mandatory.
417
+ #
418
+ # The startup_info key takes a hash. Its keys are attributes that are
419
+ # part of the StartupInfo struct, and are generally only meaningful for
420
+ # GUI or console processes. See the documentation on CreateProcess()
421
+ # and the StartupInfo struct on MSDN for more information.
422
+ #
423
+ # * desktop
424
+ # * title
425
+ # * x
426
+ # * y
427
+ # * x_size
428
+ # * y_size
429
+ # * x_count_chars
430
+ # * y_count_chars
431
+ # * fill_attribute
432
+ # * sw_flags
433
+ # * startf_flags
434
+ # * stdin
435
+ # * stdout
436
+ # * stderr
437
+ #
438
+ # Note that the 'stdin', 'stdout' and 'stderr' options can be either Ruby
439
+ # IO objects or file descriptors (i.e. a fileno). However, StringIO objects
440
+ # are not currently supported. Unfortunately, setting these is not currently
441
+ # an option for JRuby.
442
+ #
443
+ # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
444
+ # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
445
+ # automatically OR'd to the +startf_flags+ value.
446
+ #
447
+ # The ProcessInfo struct contains the following members:
448
+ #
449
+ # * process_handle - The handle to the newly created process.
450
+ # * thread_handle - The handle to the primary thread of the process.
451
+ # * process_id - Process ID.
452
+ # * thread_id - Thread ID.
453
+ #
454
+ # If the 'close_handles' option is set to true (the default) then the
455
+ # process_handle and the thread_handle are automatically closed for you
456
+ # before the ProcessInfo struct is returned.
457
+ #
458
+ # If the 'with_logon' option is set, then the process runs the specified
459
+ # executable file in the security context of the specified credentials.
460
+ #
461
+ # To simulate Process.wait you can use this approach:
462
+ #
463
+ # sleep 0.1 while !Process.get_exitcode(info.process_id)
464
+ #
465
+ # If you really to use Process.wait, then you should use the
466
+ # Process.spawn method instead of Process.create where possible.
467
+ #
468
+ def create(args)
469
+ unless args.kind_of?(Hash)
470
+ raise TypeError, 'hash keyword arguments expected'
471
+ end
472
+
473
+ valid_keys = %w[
474
+ app_name command_line inherit creation_flags cwd environment
475
+ startup_info thread_inherit process_inherit close_handles with_logon
476
+ domain password
477
+ ]
478
+
479
+ valid_si_keys = %w[
480
+ startf_flags desktop title x y x_size y_size x_count_chars
481
+ y_count_chars fill_attribute sw_flags stdin stdout stderr
482
+ ]
483
+
484
+ # Set default values
485
+ hash = {
486
+ 'app_name' => nil,
487
+ 'creation_flags' => 0,
488
+ 'close_handles' => true
489
+ }
490
+
491
+ # Validate the keys, and convert symbols and case to lowercase strings.
492
+ args.each{ |key, val|
493
+ key = key.to_s.downcase
494
+ unless valid_keys.include?(key)
495
+ raise ArgumentError, "invalid key '#{key}'"
496
+ end
497
+ hash[key] = val
498
+ }
499
+
500
+ si_hash = {}
501
+
502
+ # If the startup_info key is present, validate its subkeys
503
+ if hash['startup_info']
504
+ hash['startup_info'].each{ |key, val|
505
+ key = key.to_s.downcase
506
+ unless valid_si_keys.include?(key)
507
+ raise ArgumentError, "invalid startup_info key '#{key}'"
508
+ end
509
+ si_hash[key] = val
510
+ }
511
+ end
512
+
513
+ # The +command_line+ key is mandatory unless the +app_name+ key
514
+ # is specified.
515
+ unless hash['command_line']
516
+ if hash['app_name']
517
+ hash['command_line'] = hash['app_name']
518
+ hash['app_name'] = nil
519
+ else
520
+ raise ArgumentError, 'command_line or app_name must be specified'
521
+ end
522
+ end
523
+
524
+ env = nil
525
+
526
+ # The env string should be passed as a string of ';' separated paths.
527
+ if hash['environment']
528
+ env = hash['environment']
529
+
530
+ unless env.respond_to?(:join)
531
+ env = hash['environment'].split(File::PATH_SEPARATOR)
532
+ end
533
+
534
+ env = env.map{ |e| e + 0.chr }.join('') + 0.chr
535
+ env.to_wide_string! if hash['with_logon']
536
+ end
537
+
538
+ # Process SECURITY_ATTRIBUTE structure
539
+ process_security = nil
540
+
541
+ if hash['process_inherit']
542
+ process_security = SECURITY_ATTRIBUTES.new
543
+ process_security[:nLength] = 12
544
+ process_security[:bInheritHandle] = true
545
+ end
546
+
547
+ # Thread SECURITY_ATTRIBUTE structure
548
+ thread_security = nil
549
+
550
+ if hash['thread_inherit']
551
+ thread_security = SECURITY_ATTRIBUTES.new
552
+ thread_security[:nLength] = 12
553
+ thread_security[:bInheritHandle] = true
554
+ end
555
+
556
+ # Automatically handle stdin, stdout and stderr as either IO objects
557
+ # or file descriptors. This won't work for StringIO, however. It also
558
+ # will not work on JRuby because of the way it handles internal file
559
+ # descriptors.
560
+ #
561
+ ['stdin', 'stdout', 'stderr'].each{ |io|
562
+ if si_hash[io]
563
+ if si_hash[io].respond_to?(:fileno)
564
+ handle = get_osfhandle(si_hash[io].fileno)
565
+ else
566
+ handle = get_osfhandle(si_hash[io])
567
+ end
568
+
569
+ if handle == INVALID_HANDLE_VALUE
570
+ ptr = FFI::MemoryPointer.new(:int)
571
+
572
+ if windows_version >= 6 && get_errno(ptr) == 0
573
+ errno = ptr.read_int
574
+ else
575
+ errno = FFI.errno
576
+ end
577
+
578
+ raise SystemCallError.new("get_osfhandle", errno)
579
+ end
580
+
581
+ # Most implementations of Ruby on Windows create inheritable
582
+ # handles by default, but some do not. RF bug #26988.
583
+ bool = SetHandleInformation(
584
+ handle,
585
+ HANDLE_FLAG_INHERIT,
586
+ HANDLE_FLAG_INHERIT
587
+ )
588
+
589
+ raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
590
+
591
+ si_hash[io] = handle
592
+ si_hash['startf_flags'] ||= 0
593
+ si_hash['startf_flags'] |= STARTF_USESTDHANDLES
594
+ hash['inherit'] = true
595
+ end
596
+ }
597
+
598
+ procinfo = PROCESS_INFORMATION.new
599
+ startinfo = STARTUPINFO.new
600
+
601
+ unless si_hash.empty?
602
+ startinfo[:cb] = startinfo.size
603
+ startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
604
+ startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
605
+ startinfo[:dwX] = si_hash['x'] if si_hash['x']
606
+ startinfo[:dwY] = si_hash['y'] if si_hash['y']
607
+ startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
608
+ startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
609
+ startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
610
+ startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
611
+ startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
612
+ startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
613
+ startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
614
+ startinfo[:cbReserved2] = 0
615
+ startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
616
+ startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
617
+ startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
618
+ end
619
+
620
+ app = nil
621
+ cmd = nil
622
+
623
+ # Convert strings to wide character strings if present
624
+ if hash['app_name']
625
+ app = hash['app_name'].to_wide_string
626
+ end
627
+
628
+ if hash['command_line']
629
+ cmd = hash['command_line'].to_wide_string
630
+ end
631
+
632
+ if hash['cwd']
633
+ cwd = hash['cwd'].to_wide_string
634
+ end
635
+
636
+ if hash['with_logon']
637
+ logon = hash['with_logon'].to_wide_string
638
+
639
+ if hash['password']
640
+ passwd = hash['password'].to_wide_string
641
+ else
642
+ raise ArgumentError, 'password must be specified if with_logon is used'
643
+ end
644
+
645
+ if hash['domain']
646
+ domain = hash['domain'].to_wide_string
647
+ end
648
+
649
+ hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
650
+
651
+ bool = CreateProcessWithLogonW(
652
+ logon, # User
653
+ domain, # Domain
654
+ passwd, # Password
655
+ LOGON_WITH_PROFILE, # Logon flags
656
+ app, # App name
657
+ cmd, # Command line
658
+ hash['creation_flags'], # Creation flags
659
+ env, # Environment
660
+ cwd, # Working directory
661
+ startinfo, # Startup Info
662
+ procinfo # Process Info
663
+ )
664
+
665
+ unless bool
666
+ raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
667
+ end
668
+ else
669
+ inherit = hash['inherit'] || false
670
+
671
+ bool = CreateProcessW(
672
+ app, # App name
673
+ cmd, # Command line
674
+ process_security, # Process attributes
675
+ thread_security, # Thread attributes
676
+ inherit, # Inherit handles?
677
+ hash['creation_flags'], # Creation flags
678
+ env, # Environment
679
+ cwd, # Working directory
680
+ startinfo, # Startup Info
681
+ procinfo # Process Info
682
+ )
683
+
684
+ unless bool
685
+ raise SystemCallError.new("CreateProcess", FFI.errno)
686
+ end
687
+ end
688
+
689
+ # Automatically close the process and thread handles in the
690
+ # PROCESS_INFORMATION struct unless explicitly told not to.
691
+ if hash['close_handles']
692
+ CloseHandle(procinfo[:hProcess])
693
+ CloseHandle(procinfo[:hThread])
694
+ end
695
+
696
+ ProcessInfo.new(
697
+ procinfo[:hProcess],
698
+ procinfo[:hThread],
699
+ procinfo[:dwProcessId],
700
+ procinfo[:dwThreadId]
701
+ )
702
+ end
703
+
704
+ remove_method :kill
705
+
706
+ # Kill a given process with a specific signal. This overrides the default
707
+ # implementation of Process.kill. The differences mainly reside in the way
708
+ # it kills processes, but this version also gives you finer control over
709
+ # behavior.
710
+ #
711
+ # Internally, signals 2 and 3 will generate a console control event, using
712
+ # a ctrl-c or ctrl-break event, respectively. Signal 9 terminates the
713
+ # process harshly, given that process no chance to do any internal cleanup.
714
+ # Signals 1 and 4-8 kill the process more nicely, giving the process a
715
+ # chance to do internal cleanup before being killed. Signal 0 behaves the
716
+ # same as the default implementation.
717
+ #
718
+ # When using signals 1 or 4-8 you may specify additional options that
719
+ # allow finer control over how that process is killed and how your program
720
+ # behaves.
721
+ #
722
+ # Possible options for signals 1 and 4-8.
723
+ #
724
+ # :exit_proc => The name of the exit function called when signal 1 or 4-8
725
+ # is used. The default is 'ExitProcess'.
726
+ #
727
+ # :dll_module => The name of the .dll (or .exe) that contains :exit_proc.
728
+ # The default is 'kernel32'.
729
+ #
730
+ # :wait_time => The time, in milliseconds, to wait for the process to
731
+ # actually die. The default is 5ms. If you specify 0 here
732
+ # then the process does not wait if the process is not
733
+ # signaled and instead returns immediately. Alternatively,
734
+ # you may specify Process::INFINITE, and your code will
735
+ # block until the process is actually signaled.
736
+ #
737
+ # Example:
738
+ #
739
+ # Process.kill(1, 12345, :exit_proc => 'ExitProcess', :module => 'kernel32')
740
+ #
741
+ def kill(signal, *pids)
742
+ raise SecurityError if $SAFE && $SAFE >= 2 # Match the spec
743
+
744
+ # Match the spec, require at least 2 arguments
745
+ if pids.length == 0
746
+ raise ArgumentError, "wrong number of arguments (1 for at least 2)"
747
+ end
748
+
749
+ # Match the spec, signal may not be less than zero if numeric
750
+ if signal.is_a?(Numeric) && signal < 0 # EINVAL
751
+ raise SystemCallError.new(22)
752
+ end
753
+
754
+ # Match the spec, signal must be a numeric, string or symbol
755
+ unless signal.is_a?(String) || signal.is_a?(Numeric) || signal.is_a?(Symbol)
756
+ raise ArgumentError, "bad signal type #{signal.class}"
757
+ end
758
+
759
+ # Match the spec, making an exception for BRK/SIGBRK, if the signal name is invalid
760
+ if signal.is_a?(String) || signal.is_a?(Symbol)
761
+ signal = signal.to_s.sub('SIG', '')
762
+ unless Signal.list.keys.include?(signal) || ['BRK', 'BRK'].include?(signal)
763
+ raise ArgumentError, "unsupported name '#{signal}'"
764
+ end
765
+ end
766
+
767
+ # If the last argument is a hash, pop it and assume it's a hash of options
768
+ if pids.last.is_a?(Hash)
769
+ hash = pids.pop
770
+ opts = {}
771
+
772
+ valid = %w[exit_proc dll_module wait_time]
773
+
774
+ hash.each{ |k,v|
775
+ k = k.to_s.downcase
776
+ unless valid.include?(k)
777
+ raise ArgumentError, "invalid option '#{k}'"
778
+ end
779
+ opts[k] = v
780
+ }
781
+
782
+ exit_proc = opts['exit_proc'] || 'ExitProcess'
783
+ dll_module = opts['dll_module'] || 'kernel32'
784
+ wait_time = opts['wait_time'] || 5
785
+ else
786
+ wait_time = 5
787
+ exit_proc = 'ExitProcess'
788
+ dll_module = 'kernel32'
789
+ end
790
+
791
+ count = 0
792
+
793
+ pids.each{ |pid|
794
+ raise TypeError unless pid.is_a?(Numeric) # Match spec, pid must be a number
795
+ raise SystemCallError.new(22) if pid < 0 # Match spec, EINVAL if pid less than zero
796
+
797
+ sigint = [Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2]
798
+
799
+ # Match the spec
800
+ if pid == 0 && !sigint.include?(signal)
801
+ raise SystemCallError.new(22)
802
+ end
803
+
804
+ if signal == 0
805
+ access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
806
+ elsif signal == 9
807
+ access = PROCESS_TERMINATE
808
+ else
809
+ access = PROCESS_ALL_ACCESS
810
+ end
811
+
812
+ begin
813
+ handle = OpenProcess(access, false, pid)
814
+
815
+ if signal != 0 && handle == 0
816
+ raise SystemCallError, FFI.errno, "OpenProcess"
817
+ end
818
+
819
+ case signal
820
+ when 0
821
+ if handle != 0
822
+ count += 1
823
+ else
824
+ if FFI.errno == ERROR_ACCESS_DENIED
825
+ count += 1
826
+ else
827
+ raise SystemCallError.new(3) # ESRCH
828
+ end
829
+ end
830
+ when Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2
831
+ if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
832
+ count += 1
833
+ else
834
+ raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
835
+ end
836
+ when Signal.list['BRK'], 'BRK', 'SIGBRK', :BRK, :SIGBRK, 3
837
+ if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
838
+ count += 1
839
+ else
840
+ raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
841
+ end
842
+ when Signal.list['KILL'], 'KILL', 'SIGKILL', :KILL, :SIGKILL, 9
843
+ if TerminateProcess(handle, pid)
844
+ count += 1
845
+ else
846
+ raise SystemCallError.new("TerminateProcess", FFI.errno)
847
+ end
848
+ else
849
+ thread_id = FFI::MemoryPointer.new(:ulong)
850
+
851
+ mod = GetModuleHandle(dll_module)
852
+
853
+ if mod == 0
854
+ raise SystemCallError.new("GetModuleHandle: '#{dll_module}'", FFI.errno)
855
+ end
856
+
857
+ proc_addr = GetProcAddress(mod, exit_proc)
858
+
859
+ if proc_addr == 0
860
+ raise SystemCallError.new("GetProcAddress: '#{exit_proc}'", FFI.errno)
861
+ end
862
+
863
+ thread = CreateRemoteThread(handle, nil, 0, proc_addr, nil, 0, thread_id)
864
+
865
+ if thread > 0
866
+ WaitForSingleObject(thread, wait_time)
867
+ count += 1
868
+ else
869
+ raise SystemCallError.new("CreateRemoteThread", FFI.errno)
870
+ end
871
+ end
872
+ ensure
873
+ CloseHandle(handle) if handle
874
+ end
875
+ }
876
+
877
+ count
878
+ end
879
+
880
+ # Returns the exitcode of the process with given +pid+ or nil if the process
881
+ # is still running. Note that the process doesn't have to be a child process.
882
+ #
883
+ # This method is very handy for finding out if a process started with Process.create
884
+ # is still running. The usual way of calling Process.wait doesn't work when
885
+ # the process isn't recognized as a child process (ECHILD). This happens for example
886
+ # when stdin, stdout or stderr are set to custom values.
887
+ #
888
+ def get_exitcode(pid)
889
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid)
890
+
891
+ if handle == INVALID_HANDLE_VALUE
892
+ raise SystemCallError.new("OpenProcess", FFI.errno)
893
+ end
894
+
895
+ begin
896
+ buf = FFI::MemoryPointer.new(:ulong, 1)
897
+
898
+ unless GetExitCodeProcess(handle, buf)
899
+ raise SystemCallError.new("GetExitCodeProcess", FFI.errno)
900
+ end
901
+ ensure
902
+ CloseHandle(handle)
903
+ end
904
+
905
+ exitcode = buf.read_int
906
+
907
+ if exitcode == STILL_ACTIVE
908
+ nil
909
+ else
910
+ exitcode
911
+ end
912
+ end
913
+ end
914
+
915
+ class << self
916
+ private
917
+
918
+ # Private method that returns the volume type, e.g. "NTFS", etc.
919
+ def volume_type
920
+ buf = FFI::MemoryPointer.new(:char, 32)
921
+ bool = GetVolumeInformationA(nil, nil, 0, nil, nil, nil, buf, buf.size)
922
+ bool ? buf.read_string : nil
923
+ end
924
+
925
+ # Private method that returns the Windows major version number.
926
+ def windows_version
927
+ ver = OSVERSIONINFO.new
928
+ ver[:dwOSVersionInfoSize] = ver.size
929
+
930
+ unless GetVersionExA(ver)
931
+ raise SystemCallError.new("GetVersionEx", FFI.errno)
932
+ end
933
+
934
+ ver[:dwMajorVersion]
935
+ end
936
+ end
937
+ end