win32-process 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6b32a752eceb46f4043479e5141a308c481c9244
4
- data.tar.gz: a18b21a741d649fd07287629a161f9b43ce97aca
2
+ SHA256:
3
+ metadata.gz: 5f5cdcabac1da0aebf77bd252b0647899dfee2632e34e626f18a9951a9d17e62
4
+ data.tar.gz: 39382a91f43eba2e0c00f1b92e3f53de3a49b87e689b22683cf5c08f395077b7
5
5
  SHA512:
6
- metadata.gz: c1796d614519ad5bbd7d783b910136a78042ed31e933aa5269a3f56558af3b982aa867b283d5ea93bb98edaef4b8d384f8d3e6717b185c338fe980d35e553255
7
- data.tar.gz: d2c023c4bae8162ac235131ed9bcc6a097403cf822b4ac7098fc2f5c059440915a074acf5f4038b2368f4c2ada467c425ee11364a0832029d7f1de19aec96eb5
6
+ metadata.gz: fe1ba6453b70b3712322a588b83708c2eb5f657d97e443c974162be290f009488c28ddb57de1212bcb141a57006f6d0db2e59244b19583da21265df2cab35d83
7
+ data.tar.gz: b38df396da190521cbf3fa0975e8087d6f7435956996716b3fe3416ae0d5565e32787c3794a943a7f74c98cb4d31b8c704882b53f6a88d265c7e334a1da1923b
@@ -0,0 +1,74 @@
1
+ # win32-process
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/win32-process.svg)](https://badge.fury.io/rb/win32-process)
4
+
5
+ This library provides analogues of the :getpriority, :setpriority, :getrlimit, :setrlimit and :uid methods for MS Windows. It also adds the new methods :job?, :get_affinity, and :create, and redefines the :kill method.
6
+
7
+ ## Prerequisites
8
+
9
+ - ffi
10
+ - sys-proctable (dev only)
11
+ - test-unit 2 (dev only)
12
+
13
+ ## Supported Platforms
14
+
15
+ This library is supported on Windows 2000 or later.
16
+
17
+ ## Installation
18
+
19
+ ```
20
+ gem install win32-process
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```ruby
26
+ require 'win32/process'
27
+
28
+ p Process.job? # => true or false
29
+
30
+ info = Process.create( :app_name => "notepad.exe", :creation_flags => Process::DETACHED_PROCESS, :process_inherit => false, :thread_inherit => true, :cwd => "C:\" )
31
+
32
+ p info.process_id
33
+ ```
34
+
35
+ ## Developer's Notes
36
+
37
+ ### Removal of Process.fork in release 0.7.0
38
+
39
+ The Process.fork method was originally experimental but it has never been particularly useful in practice. On top of that, it required special implementations of the Process.waitXXX methods, so it was a maintenance issue as well.
40
+
41
+ With Ruby 1.9 now becoming standard and its addition of Process.spawn and friends (and concomitant support for the Process.waitXXX methods) I felt it was time to remove it.
42
+
43
+ You can still simulate Process.fork if you like using Process.create, which is how it was implemented internally anyway. A better solution might be to follow in the footsteps of ActiveState Perl, which uses native threads to simulate fork on Windows.
44
+
45
+ ### Changes in the custom Process.kill method for 0.7.0
46
+
47
+ The Process.kill method in 0.7.0 more closely matches the spec now, but the internal method for killing processes is still nicer for most signals. With the release of 0.7.0 users can now specify options that provide finer control over how a process is killed. See the documentation for details.
48
+
49
+ ## The removal of the custom Process.ppid method
50
+
51
+ This was added at some point in the Ruby 1.9 dev cycle so it was removed from this library.
52
+
53
+ ## Known Issues
54
+
55
+ JRuby doesn't seem to like SIGBRK for Process.kill.
56
+
57
+ Any issues or bugs should be reported on the project page at <https://github.com/djberg96/win32-process>.
58
+
59
+ ## License
60
+
61
+ Artistic 2.0
62
+
63
+ ## Copyright
64
+
65
+ (C) 2003-2015 Daniel J. Berger All Rights Reserved
66
+
67
+ ## Warranty
68
+
69
+ This library is provided "as is" and without any express or implied warranties, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose.
70
+
71
+ ## Author(s)
72
+
73
+ - Park Heesob
74
+ - Daniel J. Berger
@@ -1 +1 @@
1
- require_relative 'win32/process'
1
+ require_relative 'win32/process'
@@ -1,1141 +1,1139 @@
1
- require_relative 'process/functions'
2
- require_relative 'process/constants'
3
- require_relative 'process/structs'
4
- require_relative 'process/helper'
5
-
6
- module Process
7
- include Process::Constants
8
- extend Process::Functions
9
- extend Process::Structs
10
- extend Process::Constants
11
-
12
- # The version of the win32-process library.
13
- WIN32_PROCESS_VERSION = '0.8.3'
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.
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 == 0
64
- raise SystemCallError, FFI.errno, "OpenProcess"
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, 0, int)
103
-
104
- if handle == 0
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, 0 , int)
145
-
146
- if handle == 0
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[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
197
-
198
- if sid
199
- string_sid
200
- else
201
- psid = FFI::MemoryPointer.new(:uintptr_t)
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, 1, @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
- # WARNING: Exceeding the limit you set with this method could segfault
336
- # the interpreter. Consider this method experimental.
337
- #
338
- def setrlimit(resource, current_limit, max_limit = nil)
339
- max_limit = current_limit
340
-
341
- handle = nil
342
- in_job = Process.job?
343
-
344
- unless [RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS, RLIMIT_CPU].include?(resource)
345
- raise ArgumentError, "unsupported resource type: '#{resource}'"
346
- end
347
-
348
- # Put the current process in a job if it's not already in one
349
- if in_job && defined? @win32_process_job_name
350
- handle = OpenJobObjectA(JOB_OBJECT_SET_ATTRIBUTES, 1, @win32_process_job_name)
351
- raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
352
- else
353
- @win32_process_job_name = 'ruby_' + Process.pid.to_s
354
- handle = CreateJobObjectA(nil, @win32_process_job_name)
355
- raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
356
- end
357
-
358
- begin
359
- unless in_job
360
- unless AssignProcessToJobObject(handle, GetCurrentProcess())
361
- raise SystemCallError, FFI.errno, "AssignProcessToJobObject"
362
- end
363
- end
364
-
365
- ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
366
-
367
- # Set the LimitFlags and relevant members of the struct
368
- if resource == RLIMIT_CPU
369
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
370
- ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = max_limit
371
- else
372
- ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
373
- ptr[:ProcessMemoryLimit] = max_limit
374
- end
375
-
376
- bool = SetInformationJobObject(
377
- handle,
378
- JobObjectExtendedLimitInformation,
379
- ptr,
380
- ptr.size
381
- )
382
-
383
- unless bool
384
- raise SystemCallError, FFI.errno, "SetInformationJobObject"
385
- end
386
- ensure
387
- at_exit{ CloseHandle(handle) if handle }
388
- end
389
- end
390
-
391
- # Process.create(key => value, ...) => ProcessInfo
392
- #
393
- # This is a wrapper for the CreateProcess() function. It executes a process,
394
- # returning a ProcessInfo struct. It accepts a hash as an argument.
395
- # There are several primary keys:
396
- #
397
- # * command_line (this or app_name must be present)
398
- # * app_name (default: nil)
399
- # * inherit (default: false)
400
- # * process_inherit (default: false)
401
- # * thread_inherit (default: false)
402
- # * creation_flags (default: 0)
403
- # * cwd (default: Dir.pwd)
404
- # * startup_info (default: nil)
405
- # * environment (default: nil)
406
- # * close_handles (default: true)
407
- # * with_logon (default: nil)
408
- # * domain (default: nil)
409
- # * password (default: nil, mandatory if with_logon)
410
- #
411
- # Of these, the 'command_line' or 'app_name' must be specified or an
412
- # error is raised. Both may be set individually, but 'command_line' should
413
- # be preferred if only one of them is set because it does not (necessarily)
414
- # require an explicit path or extension to work.
415
- #
416
- # The 'domain' and 'password' options are only relevent in the context
417
- # of 'with_logon'. If 'with_logon' is set, then the 'password' option is
418
- # mandatory.
419
- #
420
- # The startup_info key takes a hash. Its keys are attributes that are
421
- # part of the StartupInfo struct, and are generally only meaningful for
422
- # GUI or console processes. See the documentation on CreateProcess()
423
- # and the StartupInfo struct on MSDN for more information.
424
- #
425
- # * desktop
426
- # * title
427
- # * x
428
- # * y
429
- # * x_size
430
- # * y_size
431
- # * x_count_chars
432
- # * y_count_chars
433
- # * fill_attribute
434
- # * sw_flags
435
- # * startf_flags
436
- # * stdin
437
- # * stdout
438
- # * stderr
439
- #
440
- # Note that the 'stdin', 'stdout' and 'stderr' options can be either Ruby
441
- # IO objects or file descriptors (i.e. a fileno). However, StringIO objects
442
- # are not currently supported. Unfortunately, setting these is not currently
443
- # an option for JRuby.
444
- #
445
- # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
446
- # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
447
- # automatically OR'd to the +startf_flags+ value.
448
- #
449
- # The ProcessInfo struct contains the following members:
450
- #
451
- # * process_handle - The handle to the newly created process.
452
- # * thread_handle - The handle to the primary thread of the process.
453
- # * process_id - Process ID.
454
- # * thread_id - Thread ID.
455
- #
456
- # If the 'close_handles' option is set to true (the default) then the
457
- # process_handle and the thread_handle are automatically closed for you
458
- # before the ProcessInfo struct is returned.
459
- #
460
- # If the 'with_logon' option is set, then the process runs the specified
461
- # executable file in the security context of the specified credentials.
462
- #
463
- # To simulate Process.wait you can use this approach:
464
- #
465
- # sleep 0.1 while !Process.get_exitcode(info.process_id)
466
- #
467
- # If you really to use Process.wait, then you should use the
468
- # Process.spawn method instead of Process.create where possible.
469
- #
470
- def create(args)
471
- unless args.kind_of?(Hash)
472
- raise TypeError, 'hash keyword arguments expected'
473
- end
474
-
475
- valid_keys = %w[
476
- app_name command_line inherit creation_flags cwd environment
477
- startup_info thread_inherit process_inherit close_handles with_logon
478
- domain password
479
- ]
480
-
481
- valid_si_keys = %w[
482
- startf_flags desktop title x y x_size y_size x_count_chars
483
- y_count_chars fill_attribute sw_flags stdin stdout stderr
484
- ]
485
-
486
- # Set default values
487
- hash = {
488
- 'app_name' => nil,
489
- 'creation_flags' => 0,
490
- 'close_handles' => true
491
- }
492
-
493
- # Validate the keys, and convert symbols and case to lowercase strings.
494
- args.each{ |key, val|
495
- key = key.to_s.downcase
496
- unless valid_keys.include?(key)
497
- raise ArgumentError, "invalid key '#{key}'"
498
- end
499
- hash[key] = val
500
- }
501
-
502
- si_hash = {}
503
-
504
- # If the startup_info key is present, validate its subkeys
505
- if hash['startup_info']
506
- hash['startup_info'].each{ |key, val|
507
- key = key.to_s.downcase
508
- unless valid_si_keys.include?(key)
509
- raise ArgumentError, "invalid startup_info key '#{key}'"
510
- end
511
- si_hash[key] = val
512
- }
513
- end
514
-
515
- # The +command_line+ key is mandatory unless the +app_name+ key
516
- # is specified.
517
- unless hash['command_line']
518
- if hash['app_name']
519
- hash['command_line'] = hash['app_name']
520
- hash['app_name'] = nil
521
- else
522
- raise ArgumentError, 'command_line or app_name must be specified'
523
- end
524
- end
525
-
526
- env = nil
527
-
528
- # The env string should be passed as a string of ';' separated paths.
529
- if hash['environment']
530
- env = hash['environment']
531
-
532
- unless env.respond_to?(:join)
533
- env = hash['environment'].split(File::PATH_SEPARATOR)
534
- end
535
-
536
- env = env.map{ |e| e + 0.chr }.join('') + 0.chr
537
- env.to_wide_string! if hash['with_logon']
538
- end
539
-
540
- # Process SECURITY_ATTRIBUTE structure
541
- process_security = nil
542
-
543
- if hash['process_inherit']
544
- process_security = SECURITY_ATTRIBUTES.new
545
- process_security[:nLength] = 12
546
- process_security[:bInheritHandle] = 1
547
- end
548
-
549
- # Thread SECURITY_ATTRIBUTE structure
550
- thread_security = nil
551
-
552
- if hash['thread_inherit']
553
- thread_security = SECURITY_ATTRIBUTES.new
554
- thread_security[:nLength] = 12
555
- thread_security[:bInheritHandle] = 1
556
- end
557
-
558
- # Automatically handle stdin, stdout and stderr as either IO objects
559
- # or file descriptors. This won't work for StringIO, however. It also
560
- # will not work on JRuby because of the way it handles internal file
561
- # descriptors.
562
- #
563
- ['stdin', 'stdout', 'stderr'].each{ |io|
564
- if si_hash[io]
565
- if si_hash[io].respond_to?(:fileno)
566
- handle = get_osfhandle(si_hash[io].fileno)
567
- else
568
- handle = get_osfhandle(si_hash[io])
569
- end
570
-
571
- if handle == INVALID_HANDLE_VALUE
572
- ptr = FFI::MemoryPointer.new(:int)
573
-
574
- if windows_version >= 6 && get_errno(ptr) == 0
575
- errno = ptr.read_int
576
- else
577
- errno = FFI.errno
578
- end
579
-
580
- raise SystemCallError.new("get_osfhandle", errno)
581
- end
582
-
583
- # Most implementations of Ruby on Windows create inheritable
584
- # handles by default, but some do not. RF bug #26988.
585
- bool = SetHandleInformation(
586
- handle,
587
- HANDLE_FLAG_INHERIT,
588
- HANDLE_FLAG_INHERIT
589
- )
590
-
591
- raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
592
-
593
- si_hash[io] = handle
594
- si_hash['startf_flags'] ||= 0
595
- si_hash['startf_flags'] |= STARTF_USESTDHANDLES
596
- hash['inherit'] = true
597
- end
598
- }
599
-
600
- procinfo = PROCESS_INFORMATION.new
601
- startinfo = STARTUPINFO.new
602
-
603
- unless si_hash.empty?
604
- startinfo[:cb] = startinfo.size
605
- startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
606
- startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
607
- startinfo[:dwX] = si_hash['x'] if si_hash['x']
608
- startinfo[:dwY] = si_hash['y'] if si_hash['y']
609
- startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
610
- startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
611
- startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
612
- startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
613
- startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
614
- startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
615
- startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
616
- startinfo[:cbReserved2] = 0
617
- startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
618
- startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
619
- startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
620
- end
621
-
622
- app = nil
623
- cmd = nil
624
-
625
- # Convert strings to wide character strings if present
626
- if hash['app_name']
627
- app = hash['app_name'].to_wide_string
628
- end
629
-
630
- if hash['command_line']
631
- cmd = hash['command_line'].to_wide_string
632
- end
633
-
634
- if hash['cwd']
635
- cwd = hash['cwd'].to_wide_string
636
- end
637
-
638
- if hash['with_logon']
639
- logon = hash['with_logon'].to_wide_string
640
-
641
- if hash['password']
642
- passwd = hash['password'].to_wide_string
643
- else
644
- raise ArgumentError, 'password must be specified if with_logon is used'
645
- end
646
-
647
- if hash['domain']
648
- domain = hash['domain'].to_wide_string
649
- end
650
-
651
- hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
652
-
653
- bool = CreateProcessWithLogonW(
654
- logon, # User
655
- domain, # Domain
656
- passwd, # Password
657
- LOGON_WITH_PROFILE, # Logon flags
658
- app, # App name
659
- cmd, # Command line
660
- hash['creation_flags'], # Creation flags
661
- env, # Environment
662
- cwd, # Working directory
663
- startinfo, # Startup Info
664
- procinfo # Process Info
665
- )
666
-
667
- unless bool
668
- raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
669
- end
670
- else
671
- inherit = hash['inherit'] ? 1 : 0
672
-
673
- bool = CreateProcessW(
674
- app, # App name
675
- cmd, # Command line
676
- process_security, # Process attributes
677
- thread_security, # Thread attributes
678
- inherit, # Inherit handles?
679
- hash['creation_flags'], # Creation flags
680
- env, # Environment
681
- cwd, # Working directory
682
- startinfo, # Startup Info
683
- procinfo # Process Info
684
- )
685
-
686
- unless bool
687
- raise SystemCallError.new("CreateProcess", FFI.errno)
688
- end
689
- end
690
-
691
- # Automatically close the process and thread handles in the
692
- # PROCESS_INFORMATION struct unless explicitly told not to.
693
- if hash['close_handles']
694
- CloseHandle(procinfo[:hProcess])
695
- CloseHandle(procinfo[:hThread])
696
- end
697
-
698
- ProcessInfo.new(
699
- procinfo[:hProcess],
700
- procinfo[:hThread],
701
- procinfo[:dwProcessId],
702
- procinfo[:dwThreadId]
703
- )
704
- end
705
-
706
- remove_method :kill
707
-
708
- # Kill a given process with a specific signal. This overrides the default
709
- # implementation of Process.kill. The differences mainly reside in the way
710
- # it kills processes, but this version also gives you finer control over
711
- # behavior.
712
- #
713
- # Internally, signals 2 and 3 will generate a console control event, using
714
- # a ctrl-c or ctrl-break event, respectively. Signal 9 terminates the
715
- # process harshly, given that process no chance to do any internal cleanup.
716
- # Signals 1 and 4-8 kill the process more nicely, giving the process a
717
- # chance to do internal cleanup before being killed. Signal 0 behaves the
718
- # same as the default implementation.
719
- #
720
- # When using signals 1 or 4-8 you may specify additional options that
721
- # allow finer control over how that process is killed and how your program
722
- # behaves.
723
- #
724
- # Possible options for signals 1 and 4-8.
725
- #
726
- # :exit_proc => The name of the exit function called when signal 1 or 4-8
727
- # is used. The default is 'ExitProcess'.
728
- #
729
- # :dll_module => The name of the .dll (or .exe) that contains :exit_proc.
730
- # The default is 'kernel32'.
731
- #
732
- # :wait_time => The time, in milliseconds, to wait for the process to
733
- # actually die. The default is 5ms. If you specify 0 here
734
- # then the process does not wait if the process is not
735
- # signaled and instead returns immediately. Alternatively,
736
- # you may specify Process::INFINITE, and your code will
737
- # block until the process is actually signaled.
738
- #
739
- # Example:
740
- #
741
- # Process.kill(1, 12345, :exit_proc => 'ExitProcess', :module => 'kernel32')
742
- #
743
- def kill(signal, *pids)
744
- raise SecurityError if $SAFE && $SAFE >= 2 # Match the spec
745
-
746
- # Match the spec, require at least 2 arguments
747
- if pids.length == 0
748
- raise ArgumentError, "wrong number of arguments (1 for at least 2)"
749
- end
750
-
751
- # Match the spec, signal may not be less than zero if numeric
752
- if signal.is_a?(Numeric) && signal < 0 # EINVAL
753
- raise SystemCallError.new(22)
754
- end
755
-
756
- # Match the spec, signal must be a numeric, string or symbol
757
- unless signal.is_a?(String) || signal.is_a?(Numeric) || signal.is_a?(Symbol)
758
- raise ArgumentError, "bad signal type #{signal.class}"
759
- end
760
-
761
- # Match the spec, making an exception for BRK/SIGBRK, if the signal name is invalid.
762
- # Older versions of JRuby did not include KILL, so we've made an explicit exception
763
- # for that here, too.
764
- if signal.is_a?(String) || signal.is_a?(Symbol)
765
- signal = signal.to_s.sub('SIG', '')
766
- unless Signal.list.keys.include?(signal) || ['KILL', 'BRK'].include?(signal)
767
- raise ArgumentError, "unsupported name '#{signal}'"
768
- end
769
- end
770
-
771
- # If the last argument is a hash, pop it and assume it's a hash of options
772
- if pids.last.is_a?(Hash)
773
- hash = pids.pop
774
- opts = {}
775
-
776
- valid = %w[exit_proc dll_module wait_time]
777
-
778
- hash.each{ |k,v|
779
- k = k.to_s.downcase
780
- unless valid.include?(k)
781
- raise ArgumentError, "invalid option '#{k}'"
782
- end
783
- opts[k] = v
784
- }
785
-
786
- exit_proc = opts['exit_proc'] || 'ExitProcess'
787
- dll_module = opts['dll_module'] || 'kernel32'
788
- wait_time = opts['wait_time'] || 5
789
- else
790
- wait_time = 5
791
- exit_proc = 'ExitProcess'
792
- dll_module = 'kernel32'
793
- end
794
-
795
- count = 0
796
-
797
- pids.each{ |pid|
798
- raise TypeError unless pid.is_a?(Numeric) # Match spec, pid must be a number
799
- raise SystemCallError.new(22) if pid < 0 # Match spec, EINVAL if pid less than zero
800
-
801
- sigint = [Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2]
802
-
803
- # Match the spec
804
- if pid == 0 && !sigint.include?(signal)
805
- raise SystemCallError.new(22)
806
- end
807
-
808
- if signal == 0
809
- access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
810
- elsif signal == 9
811
- access = PROCESS_TERMINATE
812
- else
813
- access = PROCESS_ALL_ACCESS
814
- end
815
-
816
- begin
817
- handle = OpenProcess(access, 0, pid)
818
-
819
- if signal != 0 && handle == 0
820
- raise SystemCallError, FFI.errno, "OpenProcess"
821
- end
822
-
823
- case signal
824
- when 0
825
- if handle != 0
826
- count += 1
827
- else
828
- if FFI.errno == ERROR_ACCESS_DENIED
829
- count += 1
830
- else
831
- raise SystemCallError.new(3) # ESRCH
832
- end
833
- end
834
- when Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2
835
- if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
836
- count += 1
837
- else
838
- raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
839
- end
840
- when Signal.list['BRK'], 'BRK', 'SIGBRK', :BRK, :SIGBRK, 3
841
- if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
842
- count += 1
843
- else
844
- raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
845
- end
846
- when Signal.list['KILL'], 'KILL', 'SIGKILL', :KILL, :SIGKILL, 9
847
- if TerminateProcess(handle, pid)
848
- count += 1
849
- else
850
- raise SystemCallError.new("TerminateProcess", FFI.errno)
851
- end
852
- else
853
- thread_id = FFI::MemoryPointer.new(:ulong)
854
-
855
- mod = GetModuleHandle(dll_module)
856
-
857
- if mod == 0
858
- raise SystemCallError.new("GetModuleHandle: '#{dll_module}'", FFI.errno)
859
- end
860
-
861
- proc_addr = GetProcAddress(mod, exit_proc)
862
-
863
- if proc_addr == 0
864
- raise SystemCallError.new("GetProcAddress: '#{exit_proc}'", FFI.errno)
865
- end
866
-
867
- thread = CreateRemoteThread(handle, nil, 0, proc_addr, nil, 0, thread_id)
868
-
869
- if thread > 0
870
- WaitForSingleObject(thread, wait_time)
871
- count += 1
872
- else
873
- raise SystemCallError.new("CreateRemoteThread", FFI.errno)
874
- end
875
- end
876
- ensure
877
- CloseHandle(handle) if handle
878
- end
879
- }
880
-
881
- count
882
- end
883
-
884
- # Returns the exitcode of the process with given +pid+ or nil if the process
885
- # is still running. Note that the process doesn't have to be a child process.
886
- #
887
- # This method is very handy for finding out if a process started with Process.create
888
- # is still running. The usual way of calling Process.wait doesn't work when
889
- # the process isn't recognized as a child process (ECHILD). This happens for example
890
- # when stdin, stdout or stderr are set to custom values.
891
- #
892
- def get_exitcode(pid)
893
- handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
894
-
895
- if handle == INVALID_HANDLE_VALUE
896
- raise SystemCallError.new("OpenProcess", FFI.errno)
897
- end
898
-
899
- begin
900
- buf = FFI::MemoryPointer.new(:ulong, 1)
901
-
902
- unless GetExitCodeProcess(handle, buf)
903
- raise SystemCallError.new("GetExitCodeProcess", FFI.errno)
904
- end
905
- ensure
906
- CloseHandle(handle)
907
- end
908
-
909
- exitcode = buf.read_int
910
-
911
- if exitcode == STILL_ACTIVE
912
- nil
913
- else
914
- exitcode
915
- end
916
- end
917
-
918
- # Returns a list of process information structs in the form of a hash,
919
- # with the pid as the key, and an array of information as the value of
920
- # that key. The type of information in that array depends on the
921
- # +info_type+ parameter. The possible values for +info_type+, and the
922
- # type of information they each return is as follows:
923
- #
924
- # :thread => ThreadSnapInfo[:thread_id, :process_id, :base_priority]
925
- # :heap => HeapSnapInfo[:address, :block_size, :flags, :process_id, :heap_id]
926
- # :module => ModuleSnapInfo[:process_id, :address, :module_size, :handle, :name, :path]
927
- # :process => ProcessSnapInfo[:process_id, :threads, :parent_process_id, :priority, :flags, :path]
928
- #
929
- # If no argument is provided, then :thread is assumed. Note that it is up
930
- # to you to filter by pid if you wish.
931
- #
932
- # Example:
933
- #
934
- # # Get all thread info
935
- # Process.snapshot.each{ |pid, v|
936
- # puts "PID: #{pid}"
937
- # p v
938
- # }
939
- #
940
- # # Get module info for just the current process
941
- # p Process.snapshot(:module)[Process.pid]
942
- #
943
- # # Get heap info for just the current process
944
- # p Process.snapshot(:heap)[Process.pid]
945
- #
946
- # # Show pids of all running processes
947
- # p Process.snapshot(:process).keys
948
- #
949
- def snapshot(info_type = 'thread')
950
- case info_type.to_s.downcase
951
- when 'thread'
952
- flag = TH32CS_SNAPTHREAD
953
- when 'heap'
954
- flag = TH32CS_SNAPHEAPLIST
955
- when 'module'
956
- flag = TH32CS_SNAPMODULE
957
- when 'process'
958
- flag = TH32CS_SNAPPROCESS
959
- else
960
- raise ArgumentError, "info_type '#{info_type}' unsupported"
961
- end
962
-
963
- begin
964
- handle = CreateToolhelp32Snapshot(flag, Process.pid)
965
-
966
- if handle == INVALID_HANDLE_VALUE
967
- raise SystemCallError.new('CreateToolhelp32Snapshot', FFI.errno)
968
- end
969
-
970
- case info_type.to_s.downcase
971
- when 'thread'
972
- array = get_thread_info(handle)
973
- when 'heap'
974
- array = get_heap_info(handle)
975
- when 'module'
976
- array = get_module_info(handle)
977
- when 'process'
978
- array = get_process_info(handle)
979
- end
980
-
981
- array
982
- ensure
983
- CloseHandle(handle) if handle
984
- end
985
- end
986
- end
987
-
988
- class << self
989
- private
990
-
991
- # Private method that returns the volume type, e.g. "NTFS", etc.
992
- def volume_type
993
- buf = FFI::MemoryPointer.new(:char, 32)
994
- bool = GetVolumeInformationA(nil, nil, 0, nil, nil, nil, buf, buf.size)
995
- bool ? buf.read_string : nil
996
- end
997
-
998
- # Return thread info for Process.snapshot
999
- def get_thread_info(handle, pid = nil)
1000
- lpte = THREADENTRY32.new
1001
- lpte[:dwSize] = lpte.size
1002
-
1003
- hash = Hash.new{ |h,k| h[k] = [] }
1004
-
1005
- if Thread32First(handle, lpte)
1006
- hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri])
1007
- else
1008
- if FFI.errno == ERROR_NO_MORE_FILES
1009
- return hash
1010
- else
1011
- raise SystemCallError.new('Thread32First', FFI.errno)
1012
- end
1013
- end
1014
-
1015
- while Thread32Next(handle, lpte)
1016
- hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri])
1017
- end
1018
-
1019
- hash
1020
- end
1021
-
1022
- # Return heap info for Process.snapshot
1023
- def get_heap_info(handle)
1024
- hash = Hash.new{ |h,k| h[k] = [] }
1025
-
1026
- hl = HEAPLIST32.new
1027
- hl[:dwSize] = hl.size
1028
-
1029
- if Heap32ListFirst(handle, hl)
1030
- while Heap32ListNext(handle, hl)
1031
- he = HEAPENTRY32.new
1032
- he[:dwSize] = he.size
1033
-
1034
- if Heap32First(he, Process.pid, hl[:th32HeapID])
1035
- hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID])
1036
- else
1037
- if FFI.errno == ERROR_NO_MORE_FILES
1038
- break
1039
- else
1040
- raise SystemCallError.new('Heap32First', FFI.errno)
1041
- end
1042
- end
1043
-
1044
- while Heap32Next(he)
1045
- hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID])
1046
- end
1047
- end
1048
- end
1049
-
1050
- hash
1051
- end
1052
-
1053
- # Return module info for Process.snapshot
1054
- def get_module_info(handle)
1055
- hash = Hash.new{ |h,k| h[k] = [] }
1056
-
1057
- me = MODULEENTRY32.new
1058
- me[:dwSize] = me.size
1059
-
1060
- if Module32First(handle, me)
1061
- hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
1062
- me[:th32ProcessID],
1063
- me[:modBaseAddr].to_i,
1064
- me[:modBaseSize],
1065
- me[:hModule],
1066
- me[:szModule].to_s,
1067
- me[:szExePath].to_s
1068
- )
1069
- else
1070
- if FFI.errno == ERROR_NO_MORE_FILES
1071
- return hash
1072
- else
1073
- raise SystemCallError.new('Module32First', FFI.errno)
1074
- end
1075
- end
1076
-
1077
- while Module32Next(handle, me)
1078
- hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
1079
- me[:th32ProcessID],
1080
- me[:modBaseAddr].to_i,
1081
- me[:modBaseSize],
1082
- me[:hModule],
1083
- me[:szModule].to_s,
1084
- me[:szExePath].to_s
1085
- )
1086
- end
1087
-
1088
- hash
1089
- end
1090
-
1091
- # Return process info for Process.snapshot
1092
- def get_process_info(handle)
1093
- hash = Hash.new{ |h,k| h[k] = [] }
1094
-
1095
- pe = PROCESSENTRY32.new
1096
- pe[:dwSize] = pe.size
1097
-
1098
- if Process32First(handle, pe)
1099
- hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
1100
- pe[:th32ProcessID],
1101
- pe[:cntThreads],
1102
- pe[:th32ParentProcessID],
1103
- pe[:pcPriClassBase],
1104
- pe[:dwFlags],
1105
- pe[:szExeFile].to_s
1106
- )
1107
- else
1108
- if FFI.errno == ERROR_NO_MORE_FILES
1109
- return hash
1110
- else
1111
- raise SystemCallError.new('Process32First', FFI.errno)
1112
- end
1113
- end
1114
-
1115
- while Process32Next(handle, pe)
1116
- hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
1117
- pe[:th32ProcessID],
1118
- pe[:cntThreads],
1119
- pe[:th32ParentProcessID],
1120
- pe[:pcPriClassBase],
1121
- pe[:dwFlags],
1122
- pe[:szExeFile].to_s
1123
- )
1124
- end
1125
-
1126
- hash
1127
- end
1128
-
1129
- # Private method that returns the Windows major version number.
1130
- def windows_version
1131
- ver = OSVERSIONINFO.new
1132
- ver[:dwOSVersionInfoSize] = ver.size
1133
-
1134
- unless GetVersionExA(ver)
1135
- raise SystemCallError.new("GetVersionEx", FFI.errno)
1136
- end
1137
-
1138
- ver[:dwMajorVersion]
1139
- end
1140
- end
1141
- end
1
+ require_relative 'process/functions'
2
+ require_relative 'process/constants'
3
+ require_relative 'process/structs'
4
+ require_relative 'process/helper'
5
+
6
+ module Process
7
+ include Process::Constants
8
+ extend Process::Functions
9
+ extend Process::Structs
10
+ extend Process::Constants
11
+
12
+ # The version of the win32-process library.
13
+ WIN32_PROCESS_VERSION = '0.9.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.
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 == 0
64
+ raise SystemCallError, FFI.errno, "OpenProcess"
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, 0, int)
103
+
104
+ if handle == 0
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, 0 , int)
145
+
146
+ if handle == 0
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[FFI.type_size(:pointer)*2, (rlength.read_ulong - FFI.type_size(:pointer)*2)]
197
+
198
+ if sid
199
+ string_sid
200
+ else
201
+ psid = FFI::MemoryPointer.new(:uintptr_t)
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, 1, @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
+ # WARNING: Exceeding the limit you set with this method could segfault
336
+ # the interpreter. Consider this method experimental.
337
+ #
338
+ def setrlimit(resource, current_limit, max_limit = nil)
339
+ max_limit = current_limit
340
+
341
+ handle = nil
342
+ in_job = Process.job?
343
+
344
+ unless [RLIMIT_AS, RLIMIT_VMEM, RLIMIT_RSS, RLIMIT_CPU].include?(resource)
345
+ raise ArgumentError, "unsupported resource type: '#{resource}'"
346
+ end
347
+
348
+ # Put the current process in a job if it's not already in one
349
+ if in_job && defined? @win32_process_job_name
350
+ handle = OpenJobObjectA(JOB_OBJECT_SET_ATTRIBUTES, 1, @win32_process_job_name)
351
+ raise SystemCallError, FFI.errno, "OpenJobObject" if handle == 0
352
+ else
353
+ @win32_process_job_name = 'ruby_' + Process.pid.to_s
354
+ handle = CreateJobObjectA(nil, @win32_process_job_name)
355
+ raise SystemCallError, FFI.errno, "CreateJobObject" if handle == 0
356
+ end
357
+
358
+ begin
359
+ unless in_job
360
+ unless AssignProcessToJobObject(handle, GetCurrentProcess())
361
+ raise SystemCallError, FFI.errno, "AssignProcessToJobObject"
362
+ end
363
+ end
364
+
365
+ ptr = JOBJECT_EXTENDED_LIMIT_INFORMATION.new
366
+
367
+ # Set the LimitFlags and relevant members of the struct
368
+ if resource == RLIMIT_CPU
369
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_TIME
370
+ ptr[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = max_limit
371
+ else
372
+ ptr[:BasicLimitInformation][:LimitFlags] = JOB_OBJECT_LIMIT_PROCESS_MEMORY
373
+ ptr[:ProcessMemoryLimit] = max_limit
374
+ end
375
+
376
+ bool = SetInformationJobObject(
377
+ handle,
378
+ JobObjectExtendedLimitInformation,
379
+ ptr,
380
+ ptr.size
381
+ )
382
+
383
+ unless bool
384
+ raise SystemCallError, FFI.errno, "SetInformationJobObject"
385
+ end
386
+ ensure
387
+ at_exit{ CloseHandle(handle) if handle }
388
+ end
389
+ end
390
+
391
+ # Process.create(key => value, ...) => ProcessInfo
392
+ #
393
+ # This is a wrapper for the CreateProcess() function. It executes a process,
394
+ # returning a ProcessInfo struct. It accepts a hash as an argument.
395
+ # There are several primary keys:
396
+ #
397
+ # * command_line (this or app_name must be present)
398
+ # * app_name (default: nil)
399
+ # * inherit (default: false)
400
+ # * process_inherit (default: false)
401
+ # * thread_inherit (default: false)
402
+ # * creation_flags (default: 0)
403
+ # * cwd (default: Dir.pwd)
404
+ # * startup_info (default: nil)
405
+ # * environment (default: nil)
406
+ # * close_handles (default: true)
407
+ # * with_logon (default: nil)
408
+ # * domain (default: nil)
409
+ # * password (default: nil, mandatory if with_logon)
410
+ #
411
+ # Of these, the 'command_line' or 'app_name' must be specified or an
412
+ # error is raised. Both may be set individually, but 'command_line' should
413
+ # be preferred if only one of them is set because it does not (necessarily)
414
+ # require an explicit path or extension to work.
415
+ #
416
+ # The 'domain' and 'password' options are only relevent in the context
417
+ # of 'with_logon'. If 'with_logon' is set, then the 'password' option is
418
+ # mandatory.
419
+ #
420
+ # The startup_info key takes a hash. Its keys are attributes that are
421
+ # part of the StartupInfo struct, and are generally only meaningful for
422
+ # GUI or console processes. See the documentation on CreateProcess()
423
+ # and the StartupInfo struct on MSDN for more information.
424
+ #
425
+ # * desktop
426
+ # * title
427
+ # * x
428
+ # * y
429
+ # * x_size
430
+ # * y_size
431
+ # * x_count_chars
432
+ # * y_count_chars
433
+ # * fill_attribute
434
+ # * sw_flags
435
+ # * startf_flags
436
+ # * stdin
437
+ # * stdout
438
+ # * stderr
439
+ #
440
+ # Note that the 'stdin', 'stdout' and 'stderr' options can be either Ruby
441
+ # IO objects or file descriptors (i.e. a fileno). However, StringIO objects
442
+ # are not currently supported. Unfortunately, setting these is not currently
443
+ # an option for JRuby.
444
+ #
445
+ # If 'stdin', 'stdout' or 'stderr' are specified, then the +inherit+ value
446
+ # is automatically set to true and the Process::STARTF_USESTDHANDLES flag is
447
+ # automatically OR'd to the +startf_flags+ value.
448
+ #
449
+ # The ProcessInfo struct contains the following members:
450
+ #
451
+ # * process_handle - The handle to the newly created process.
452
+ # * thread_handle - The handle to the primary thread of the process.
453
+ # * process_id - Process ID.
454
+ # * thread_id - Thread ID.
455
+ #
456
+ # If the 'close_handles' option is set to true (the default) then the
457
+ # process_handle and the thread_handle are automatically closed for you
458
+ # before the ProcessInfo struct is returned.
459
+ #
460
+ # If the 'with_logon' option is set, then the process runs the specified
461
+ # executable file in the security context of the specified credentials.
462
+ #
463
+ # To simulate Process.wait you can use this approach:
464
+ #
465
+ # sleep 0.1 while !Process.get_exitcode(info.process_id)
466
+ #
467
+ # If you really to use Process.wait, then you should use the
468
+ # Process.spawn method instead of Process.create where possible.
469
+ #
470
+ def create(args)
471
+ unless args.kind_of?(Hash)
472
+ raise TypeError, 'hash keyword arguments expected'
473
+ end
474
+
475
+ valid_keys = %w[
476
+ app_name command_line inherit creation_flags cwd environment
477
+ startup_info thread_inherit process_inherit close_handles with_logon
478
+ domain password
479
+ ]
480
+
481
+ valid_si_keys = %w[
482
+ startf_flags desktop title x y x_size y_size x_count_chars
483
+ y_count_chars fill_attribute sw_flags stdin stdout stderr
484
+ ]
485
+
486
+ # Set default values
487
+ hash = {
488
+ 'app_name' => nil,
489
+ 'creation_flags' => 0,
490
+ 'close_handles' => true
491
+ }
492
+
493
+ # Validate the keys, and convert symbols and case to lowercase strings.
494
+ args.each{ |key, val|
495
+ key = key.to_s.downcase
496
+ unless valid_keys.include?(key)
497
+ raise ArgumentError, "invalid key '#{key}'"
498
+ end
499
+ hash[key] = val
500
+ }
501
+
502
+ si_hash = {}
503
+
504
+ # If the startup_info key is present, validate its subkeys
505
+ if hash['startup_info']
506
+ hash['startup_info'].each{ |key, val|
507
+ key = key.to_s.downcase
508
+ unless valid_si_keys.include?(key)
509
+ raise ArgumentError, "invalid startup_info key '#{key}'"
510
+ end
511
+ si_hash[key] = val
512
+ }
513
+ end
514
+
515
+ # The +command_line+ key is mandatory unless the +app_name+ key
516
+ # is specified.
517
+ unless hash['command_line']
518
+ if hash['app_name']
519
+ hash['command_line'] = hash['app_name']
520
+ hash['app_name'] = nil
521
+ else
522
+ raise ArgumentError, 'command_line or app_name must be specified'
523
+ end
524
+ end
525
+
526
+ env = nil
527
+
528
+ # The env string should be passed as a string of ';' separated paths.
529
+ if hash['environment']
530
+ env = hash['environment']
531
+
532
+ unless env.respond_to?(:join)
533
+ env = hash['environment'].split(File::PATH_SEPARATOR)
534
+ end
535
+
536
+ env = env.map{ |e| e + 0.chr }.join('') + 0.chr
537
+ env.to_wide_string! if hash['with_logon']
538
+ end
539
+
540
+ # Process SECURITY_ATTRIBUTE structure
541
+ process_security = nil
542
+
543
+ if hash['process_inherit']
544
+ process_security = SECURITY_ATTRIBUTES.new
545
+ process_security[:nLength] = 12
546
+ process_security[:bInheritHandle] = 1
547
+ end
548
+
549
+ # Thread SECURITY_ATTRIBUTE structure
550
+ thread_security = nil
551
+
552
+ if hash['thread_inherit']
553
+ thread_security = SECURITY_ATTRIBUTES.new
554
+ thread_security[:nLength] = 12
555
+ thread_security[:bInheritHandle] = 1
556
+ end
557
+
558
+ # Automatically handle stdin, stdout and stderr as either IO objects
559
+ # or file descriptors. This won't work for StringIO, however. It also
560
+ # will not work on JRuby because of the way it handles internal file
561
+ # descriptors.
562
+ #
563
+ ['stdin', 'stdout', 'stderr'].each{ |io|
564
+ if si_hash[io]
565
+ if si_hash[io].respond_to?(:fileno)
566
+ handle = get_osfhandle(si_hash[io].fileno)
567
+ else
568
+ handle = get_osfhandle(si_hash[io])
569
+ end
570
+
571
+ if handle == INVALID_HANDLE_VALUE
572
+ ptr = FFI::MemoryPointer.new(:int)
573
+
574
+ if windows_version >= 6 && get_errno(ptr) == 0
575
+ errno = ptr.read_int
576
+ else
577
+ errno = FFI.errno
578
+ end
579
+
580
+ raise SystemCallError.new("get_osfhandle", errno)
581
+ end
582
+
583
+ # Most implementations of Ruby on Windows create inheritable
584
+ # handles by default, but some do not. RF bug #26988.
585
+ bool = SetHandleInformation(
586
+ handle,
587
+ HANDLE_FLAG_INHERIT,
588
+ HANDLE_FLAG_INHERIT
589
+ )
590
+
591
+ raise SystemCallError.new("SetHandleInformation", FFI.errno) unless bool
592
+
593
+ si_hash[io] = handle
594
+ si_hash['startf_flags'] ||= 0
595
+ si_hash['startf_flags'] |= STARTF_USESTDHANDLES
596
+ hash['inherit'] = true
597
+ end
598
+ }
599
+
600
+ procinfo = PROCESS_INFORMATION.new
601
+ startinfo = STARTUPINFO.new
602
+
603
+ unless si_hash.empty?
604
+ startinfo[:cb] = startinfo.size
605
+ startinfo[:lpDesktop] = si_hash['desktop'] if si_hash['desktop']
606
+ startinfo[:lpTitle] = si_hash['title'] if si_hash['title']
607
+ startinfo[:dwX] = si_hash['x'] if si_hash['x']
608
+ startinfo[:dwY] = si_hash['y'] if si_hash['y']
609
+ startinfo[:dwXSize] = si_hash['x_size'] if si_hash['x_size']
610
+ startinfo[:dwYSize] = si_hash['y_size'] if si_hash['y_size']
611
+ startinfo[:dwXCountChars] = si_hash['x_count_chars'] if si_hash['x_count_chars']
612
+ startinfo[:dwYCountChars] = si_hash['y_count_chars'] if si_hash['y_count_chars']
613
+ startinfo[:dwFillAttribute] = si_hash['fill_attribute'] if si_hash['fill_attribute']
614
+ startinfo[:dwFlags] = si_hash['startf_flags'] if si_hash['startf_flags']
615
+ startinfo[:wShowWindow] = si_hash['sw_flags'] if si_hash['sw_flags']
616
+ startinfo[:cbReserved2] = 0
617
+ startinfo[:hStdInput] = si_hash['stdin'] if si_hash['stdin']
618
+ startinfo[:hStdOutput] = si_hash['stdout'] if si_hash['stdout']
619
+ startinfo[:hStdError] = si_hash['stderr'] if si_hash['stderr']
620
+ end
621
+
622
+ app = nil
623
+ cmd = nil
624
+
625
+ # Convert strings to wide character strings if present
626
+ if hash['app_name']
627
+ app = hash['app_name'].to_wide_string
628
+ end
629
+
630
+ if hash['command_line']
631
+ cmd = hash['command_line'].to_wide_string
632
+ end
633
+
634
+ if hash['cwd']
635
+ cwd = hash['cwd'].to_wide_string
636
+ end
637
+
638
+ if hash['with_logon']
639
+ logon = hash['with_logon'].to_wide_string
640
+
641
+ if hash['password']
642
+ passwd = hash['password'].to_wide_string
643
+ else
644
+ raise ArgumentError, 'password must be specified if with_logon is used'
645
+ end
646
+
647
+ if hash['domain']
648
+ domain = hash['domain'].to_wide_string
649
+ end
650
+
651
+ hash['creation_flags'] |= CREATE_UNICODE_ENVIRONMENT
652
+
653
+ bool = CreateProcessWithLogonW(
654
+ logon, # User
655
+ domain, # Domain
656
+ passwd, # Password
657
+ LOGON_WITH_PROFILE, # Logon flags
658
+ app, # App name
659
+ cmd, # Command line
660
+ hash['creation_flags'], # Creation flags
661
+ env, # Environment
662
+ cwd, # Working directory
663
+ startinfo, # Startup Info
664
+ procinfo # Process Info
665
+ )
666
+
667
+ unless bool
668
+ raise SystemCallError.new("CreateProcessWithLogonW", FFI.errno)
669
+ end
670
+ else
671
+ inherit = hash['inherit'] ? 1 : 0
672
+
673
+ bool = CreateProcessW(
674
+ app, # App name
675
+ cmd, # Command line
676
+ process_security, # Process attributes
677
+ thread_security, # Thread attributes
678
+ inherit, # Inherit handles?
679
+ hash['creation_flags'], # Creation flags
680
+ env, # Environment
681
+ cwd, # Working directory
682
+ startinfo, # Startup Info
683
+ procinfo # Process Info
684
+ )
685
+
686
+ unless bool
687
+ raise SystemCallError.new("CreateProcess", FFI.errno)
688
+ end
689
+ end
690
+
691
+ # Automatically close the process and thread handles in the
692
+ # PROCESS_INFORMATION struct unless explicitly told not to.
693
+ if hash['close_handles']
694
+ CloseHandle(procinfo[:hProcess])
695
+ CloseHandle(procinfo[:hThread])
696
+ end
697
+
698
+ ProcessInfo.new(
699
+ procinfo[:hProcess],
700
+ procinfo[:hThread],
701
+ procinfo[:dwProcessId],
702
+ procinfo[:dwThreadId]
703
+ )
704
+ end
705
+
706
+ remove_method :kill
707
+
708
+ # Kill a given process with a specific signal. This overrides the default
709
+ # implementation of Process.kill. The differences mainly reside in the way
710
+ # it kills processes, but this version also gives you finer control over
711
+ # behavior.
712
+ #
713
+ # Internally, signals 2 and 3 will generate a console control event, using
714
+ # a ctrl-c or ctrl-break event, respectively. Signal 9 terminates the
715
+ # process harshly, given that process no chance to do any internal cleanup.
716
+ # Signals 1 and 4-8 kill the process more nicely, giving the process a
717
+ # chance to do internal cleanup before being killed. Signal 0 behaves the
718
+ # same as the default implementation.
719
+ #
720
+ # When using signals 1 or 4-8 you may specify additional options that
721
+ # allow finer control over how that process is killed and how your program
722
+ # behaves.
723
+ #
724
+ # Possible options for signals 1 and 4-8.
725
+ #
726
+ # :exit_proc => The name of the exit function called when signal 1 or 4-8
727
+ # is used. The default is 'ExitProcess'.
728
+ #
729
+ # :dll_module => The name of the .dll (or .exe) that contains :exit_proc.
730
+ # The default is 'kernel32'.
731
+ #
732
+ # :wait_time => The time, in milliseconds, to wait for the process to
733
+ # actually die. The default is 5ms. If you specify 0 here
734
+ # then the process does not wait if the process is not
735
+ # signaled and instead returns immediately. Alternatively,
736
+ # you may specify Process::INFINITE, and your code will
737
+ # block until the process is actually signaled.
738
+ #
739
+ # Example:
740
+ #
741
+ # Process.kill(1, 12345, :exit_proc => 'ExitProcess', :module => 'kernel32')
742
+ #
743
+ def kill(signal, *pids)
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
+ # Older versions of JRuby did not include KILL, so we've made an explicit exception
761
+ # for that here, too.
762
+ if signal.is_a?(String) || signal.is_a?(Symbol)
763
+ signal = signal.to_s.sub('SIG', '')
764
+ unless Signal.list.keys.include?(signal) || ['KILL', 'BRK'].include?(signal)
765
+ raise ArgumentError, "unsupported name '#{signal}'"
766
+ end
767
+ end
768
+
769
+ # If the last argument is a hash, pop it and assume it's a hash of options
770
+ if pids.last.is_a?(Hash)
771
+ hash = pids.pop
772
+ opts = {}
773
+
774
+ valid = %w[exit_proc dll_module wait_time]
775
+
776
+ hash.each{ |k,v|
777
+ k = k.to_s.downcase
778
+ unless valid.include?(k)
779
+ raise ArgumentError, "invalid option '#{k}'"
780
+ end
781
+ opts[k] = v
782
+ }
783
+
784
+ exit_proc = opts['exit_proc'] || 'ExitProcess'
785
+ dll_module = opts['dll_module'] || 'kernel32'
786
+ wait_time = opts['wait_time'] || 5
787
+ else
788
+ wait_time = 5
789
+ exit_proc = 'ExitProcess'
790
+ dll_module = 'kernel32'
791
+ end
792
+
793
+ count = 0
794
+
795
+ pids.each{ |pid|
796
+ raise TypeError unless pid.is_a?(Numeric) # Match spec, pid must be a number
797
+ raise SystemCallError.new(22) if pid < 0 # Match spec, EINVAL if pid less than zero
798
+
799
+ sigint = [Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2]
800
+
801
+ # Match the spec
802
+ if pid == 0 && !sigint.include?(signal)
803
+ raise SystemCallError.new(22)
804
+ end
805
+
806
+ if signal == 0
807
+ access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
808
+ elsif signal == 9
809
+ access = PROCESS_TERMINATE
810
+ else
811
+ access = PROCESS_ALL_ACCESS
812
+ end
813
+
814
+ begin
815
+ handle = OpenProcess(access, 0, pid)
816
+
817
+ if signal != 0 && handle == 0
818
+ raise SystemCallError, FFI.errno, "OpenProcess"
819
+ end
820
+
821
+ case signal
822
+ when 0
823
+ if handle != 0
824
+ count += 1
825
+ else
826
+ if FFI.errno == ERROR_ACCESS_DENIED
827
+ count += 1
828
+ else
829
+ raise SystemCallError.new(3) # ESRCH
830
+ end
831
+ end
832
+ when Signal.list['INT'], 'INT', 'SIGINT', :INT, :SIGINT, 2
833
+ if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
834
+ count += 1
835
+ else
836
+ raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
837
+ end
838
+ when Signal.list['BRK'], 'BRK', 'SIGBRK', :BRK, :SIGBRK, 3
839
+ if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
840
+ count += 1
841
+ else
842
+ raise SystemCallError.new("GenerateConsoleCtrlEvent", FFI.errno)
843
+ end
844
+ when Signal.list['KILL'], 'KILL', 'SIGKILL', :KILL, :SIGKILL, 9
845
+ if TerminateProcess(handle, pid)
846
+ count += 1
847
+ else
848
+ raise SystemCallError.new("TerminateProcess", FFI.errno)
849
+ end
850
+ else
851
+ thread_id = FFI::MemoryPointer.new(:ulong)
852
+
853
+ mod = GetModuleHandle(dll_module)
854
+
855
+ if mod == 0
856
+ raise SystemCallError.new("GetModuleHandle: '#{dll_module}'", FFI.errno)
857
+ end
858
+
859
+ proc_addr = GetProcAddress(mod, exit_proc)
860
+
861
+ if proc_addr == 0
862
+ raise SystemCallError.new("GetProcAddress: '#{exit_proc}'", FFI.errno)
863
+ end
864
+
865
+ thread = CreateRemoteThread(handle, nil, 0, proc_addr, nil, 0, thread_id)
866
+
867
+ if thread > 0
868
+ WaitForSingleObject(thread, wait_time)
869
+ count += 1
870
+ else
871
+ raise SystemCallError.new("CreateRemoteThread", FFI.errno)
872
+ end
873
+ end
874
+ ensure
875
+ CloseHandle(handle) if handle
876
+ end
877
+ }
878
+
879
+ count
880
+ end
881
+
882
+ # Returns the exitcode of the process with given +pid+ or nil if the process
883
+ # is still running. Note that the process doesn't have to be a child process.
884
+ #
885
+ # This method is very handy for finding out if a process started with Process.create
886
+ # is still running. The usual way of calling Process.wait doesn't work when
887
+ # the process isn't recognized as a child process (ECHILD). This happens for example
888
+ # when stdin, stdout or stderr are set to custom values.
889
+ #
890
+ def get_exitcode(pid)
891
+ handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid)
892
+
893
+ if handle == INVALID_HANDLE_VALUE
894
+ raise SystemCallError.new("OpenProcess", FFI.errno)
895
+ end
896
+
897
+ begin
898
+ buf = FFI::MemoryPointer.new(:ulong, 1)
899
+
900
+ unless GetExitCodeProcess(handle, buf)
901
+ raise SystemCallError.new("GetExitCodeProcess", FFI.errno)
902
+ end
903
+ ensure
904
+ CloseHandle(handle)
905
+ end
906
+
907
+ exitcode = buf.read_int
908
+
909
+ if exitcode == STILL_ACTIVE
910
+ nil
911
+ else
912
+ exitcode
913
+ end
914
+ end
915
+
916
+ # Returns a list of process information structs in the form of a hash,
917
+ # with the pid as the key, and an array of information as the value of
918
+ # that key. The type of information in that array depends on the
919
+ # +info_type+ parameter. The possible values for +info_type+, and the
920
+ # type of information they each return is as follows:
921
+ #
922
+ # :thread => ThreadSnapInfo[:thread_id, :process_id, :base_priority]
923
+ # :heap => HeapSnapInfo[:address, :block_size, :flags, :process_id, :heap_id]
924
+ # :module => ModuleSnapInfo[:process_id, :address, :module_size, :handle, :name, :path]
925
+ # :process => ProcessSnapInfo[:process_id, :threads, :parent_process_id, :priority, :flags, :path]
926
+ #
927
+ # If no argument is provided, then :thread is assumed. Note that it is up
928
+ # to you to filter by pid if you wish.
929
+ #
930
+ # Example:
931
+ #
932
+ # # Get all thread info
933
+ # Process.snapshot.each{ |pid, v|
934
+ # puts "PID: #{pid}"
935
+ # p v
936
+ # }
937
+ #
938
+ # # Get module info for just the current process
939
+ # p Process.snapshot(:module)[Process.pid]
940
+ #
941
+ # # Get heap info for just the current process
942
+ # p Process.snapshot(:heap)[Process.pid]
943
+ #
944
+ # # Show pids of all running processes
945
+ # p Process.snapshot(:process).keys
946
+ #
947
+ def snapshot(info_type = 'thread')
948
+ case info_type.to_s.downcase
949
+ when 'thread'
950
+ flag = TH32CS_SNAPTHREAD
951
+ when 'heap'
952
+ flag = TH32CS_SNAPHEAPLIST
953
+ when 'module'
954
+ flag = TH32CS_SNAPMODULE
955
+ when 'process'
956
+ flag = TH32CS_SNAPPROCESS
957
+ else
958
+ raise ArgumentError, "info_type '#{info_type}' unsupported"
959
+ end
960
+
961
+ begin
962
+ handle = CreateToolhelp32Snapshot(flag, Process.pid)
963
+
964
+ if handle == INVALID_HANDLE_VALUE
965
+ raise SystemCallError.new('CreateToolhelp32Snapshot', FFI.errno)
966
+ end
967
+
968
+ case info_type.to_s.downcase
969
+ when 'thread'
970
+ array = get_thread_info(handle)
971
+ when 'heap'
972
+ array = get_heap_info(handle)
973
+ when 'module'
974
+ array = get_module_info(handle)
975
+ when 'process'
976
+ array = get_process_info(handle)
977
+ end
978
+
979
+ array
980
+ ensure
981
+ CloseHandle(handle) if handle
982
+ end
983
+ end
984
+ end
985
+
986
+ class << self
987
+ private
988
+
989
+ # Private method that returns the volume type, e.g. "NTFS", etc.
990
+ def volume_type
991
+ buf = FFI::MemoryPointer.new(:char, 32)
992
+ bool = GetVolumeInformationA(nil, nil, 0, nil, nil, nil, buf, buf.size)
993
+ bool ? buf.read_string : nil
994
+ end
995
+
996
+ # Return thread info for Process.snapshot
997
+ def get_thread_info(handle, pid = nil)
998
+ lpte = THREADENTRY32.new
999
+ lpte[:dwSize] = lpte.size
1000
+
1001
+ hash = Hash.new{ |h,k| h[k] = [] }
1002
+
1003
+ if Thread32First(handle, lpte)
1004
+ hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri])
1005
+ else
1006
+ if FFI.errno == ERROR_NO_MORE_FILES
1007
+ return hash
1008
+ else
1009
+ raise SystemCallError.new('Thread32First', FFI.errno)
1010
+ end
1011
+ end
1012
+
1013
+ while Thread32Next(handle, lpte)
1014
+ hash[lpte[:th32OwnerProcessID]] << ThreadSnapInfo.new(lpte[:th32ThreadID], lpte[:th32OwnerProcessID], lpte[:tpBasePri])
1015
+ end
1016
+
1017
+ hash
1018
+ end
1019
+
1020
+ # Return heap info for Process.snapshot
1021
+ def get_heap_info(handle)
1022
+ hash = Hash.new{ |h,k| h[k] = [] }
1023
+
1024
+ hl = HEAPLIST32.new
1025
+ hl[:dwSize] = hl.size
1026
+
1027
+ if Heap32ListFirst(handle, hl)
1028
+ while Heap32ListNext(handle, hl)
1029
+ he = HEAPENTRY32.new
1030
+ he[:dwSize] = he.size
1031
+
1032
+ if Heap32First(he, Process.pid, hl[:th32HeapID])
1033
+ hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID])
1034
+ else
1035
+ if FFI.errno == ERROR_NO_MORE_FILES
1036
+ break
1037
+ else
1038
+ raise SystemCallError.new('Heap32First', FFI.errno)
1039
+ end
1040
+ end
1041
+
1042
+ while Heap32Next(he)
1043
+ hash[he[:th32ProcessID]] << HeapSnapInfo.new(he[:dwAddress], he[:dwBlockSize], he[:dwFlags], he[:th32ProcessID], he[:th32HeapID])
1044
+ end
1045
+ end
1046
+ end
1047
+
1048
+ hash
1049
+ end
1050
+
1051
+ # Return module info for Process.snapshot
1052
+ def get_module_info(handle)
1053
+ hash = Hash.new{ |h,k| h[k] = [] }
1054
+
1055
+ me = MODULEENTRY32.new
1056
+ me[:dwSize] = me.size
1057
+
1058
+ if Module32First(handle, me)
1059
+ hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
1060
+ me[:th32ProcessID],
1061
+ me[:modBaseAddr].to_i,
1062
+ me[:modBaseSize],
1063
+ me[:hModule],
1064
+ me[:szModule].to_s,
1065
+ me[:szExePath].to_s
1066
+ )
1067
+ else
1068
+ if FFI.errno == ERROR_NO_MORE_FILES
1069
+ return hash
1070
+ else
1071
+ raise SystemCallError.new('Module32First', FFI.errno)
1072
+ end
1073
+ end
1074
+
1075
+ while Module32Next(handle, me)
1076
+ hash[me[:th32ProcessID]] << ModuleSnapInfo.new(
1077
+ me[:th32ProcessID],
1078
+ me[:modBaseAddr].to_i,
1079
+ me[:modBaseSize],
1080
+ me[:hModule],
1081
+ me[:szModule].to_s,
1082
+ me[:szExePath].to_s
1083
+ )
1084
+ end
1085
+
1086
+ hash
1087
+ end
1088
+
1089
+ # Return process info for Process.snapshot
1090
+ def get_process_info(handle)
1091
+ hash = Hash.new{ |h,k| h[k] = [] }
1092
+
1093
+ pe = PROCESSENTRY32.new
1094
+ pe[:dwSize] = pe.size
1095
+
1096
+ if Process32First(handle, pe)
1097
+ hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
1098
+ pe[:th32ProcessID],
1099
+ pe[:cntThreads],
1100
+ pe[:th32ParentProcessID],
1101
+ pe[:pcPriClassBase],
1102
+ pe[:dwFlags],
1103
+ pe[:szExeFile].to_s
1104
+ )
1105
+ else
1106
+ if FFI.errno == ERROR_NO_MORE_FILES
1107
+ return hash
1108
+ else
1109
+ raise SystemCallError.new('Process32First', FFI.errno)
1110
+ end
1111
+ end
1112
+
1113
+ while Process32Next(handle, pe)
1114
+ hash[pe[:th32ProcessID]] = ProcessSnapInfo.new(
1115
+ pe[:th32ProcessID],
1116
+ pe[:cntThreads],
1117
+ pe[:th32ParentProcessID],
1118
+ pe[:pcPriClassBase],
1119
+ pe[:dwFlags],
1120
+ pe[:szExeFile].to_s
1121
+ )
1122
+ end
1123
+
1124
+ hash
1125
+ end
1126
+
1127
+ # Private method that returns the Windows major version number.
1128
+ def windows_version
1129
+ ver = OSVERSIONINFO.new
1130
+ ver[:dwOSVersionInfoSize] = ver.size
1131
+
1132
+ unless GetVersionExA(ver)
1133
+ raise SystemCallError.new("GetVersionEx", FFI.errno)
1134
+ end
1135
+
1136
+ ver[:dwMajorVersion]
1137
+ end
1138
+ end
1139
+ end