win32-process 0.7.0 → 0.7.1

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