win32-process 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,1141 +1,1141 @@
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.1'
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, false, 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, false , 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, 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
- # 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, true, @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] = true
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] = true
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'] || false
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, false, 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, false, 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.8.2'
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