win32-job 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 616cc46c7f759058ebeb168cb3bc4056ad7f6a45
4
+ data.tar.gz: 1b11d76a96473a5329595841e75385a14c6444cf
5
+ SHA512:
6
+ metadata.gz: 19e1fc1b49f54627771f717ad838a013409ec3cd13f667036aaf06c9113c8de0d82f95c378a23248460eb2f14cdfb13f85cc6a9f15069b1da4d03596a97d07a3
7
+ data.tar.gz: 7d2bd91f2104817cd31197c8f65815af65b88449bf96a3c48604c7a6f43ec8d61980c8115beee9dc2cd14a204f6ccafe5ad0fc2e01f0fddfda63338be4ab02bd
data/CHANGES ADDED
@@ -0,0 +1,2 @@
1
+ = 0.1.0 - 4-Feb-2014
2
+ * Initial release.
data/MANIFEST ADDED
@@ -0,0 +1,12 @@
1
+ * CHANGES
2
+ * MANIFEST
3
+ * Rakefile
4
+ * README
5
+ * win32-job.gemspec
6
+ * examples\example_job.rb
7
+ * lib\win32\job.rb
8
+ * lib\win32\job\constants.rb
9
+ * lib\win32\job\functions.rb
10
+ * lib\win32\job\helper.rb
11
+ * lib\win32\job\structs.rb
12
+ * test\test_win32_job.rb
data/README ADDED
@@ -0,0 +1,50 @@
1
+ == Description
2
+ A Ruby interface to Windows' jobs, process groups for Windows.
3
+
4
+ == Installation
5
+ gem install win32-job
6
+
7
+ == Prerequisites
8
+ ffi
9
+
10
+ == Synopsis
11
+ require 'win32/job'
12
+ include Win32
13
+
14
+ job = Job.create('test')
15
+
16
+ j.configure_limit(
17
+ :breakaway_ok => true,
18
+ :kill_on_job_close => true,
19
+ :process_memory => 1024 * 8,
20
+ :process_time => 1000
21
+ )
22
+
23
+ job.add_process(1234)
24
+ job.add_process(1235)
25
+
26
+ job.close
27
+
28
+ == Future Plans
29
+ Add a wait method that waits for all processes to complete.
30
+
31
+ == Known Issues
32
+ None known.
33
+
34
+ Please file any bug reports on the project page at
35
+ http://github.com/djberg96/win32-job
36
+
37
+ == License
38
+ Artistic 2.0
39
+
40
+ == Copyright
41
+ (C) 2003-2013 Daniel J. Berger
42
+ All Rights Reserved
43
+
44
+ == Warranty
45
+ This package is provided "as is" and without any express or
46
+ implied warranties, including, without limitation, the implied
47
+ warranties of merchantability and fitness for a particular purpose.
48
+
49
+ == Author
50
+ Daniel J. Berger
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+
5
+ CLEAN.include("**/*.gem", "**/*.rbc", "**/*.rbx")
6
+
7
+ namespace :gem do
8
+ desc 'Create the win32-job gem'
9
+ task :create do
10
+ spec = eval(IO.read('win32-job.gemspec'))
11
+ if Gem::VERSION < "2.0"
12
+ Gem::Builder.new(spec).build
13
+ else
14
+ require 'rubygems/package'
15
+ Gem::Package.build(spec)
16
+ end
17
+ end
18
+
19
+ desc 'Install the win32-job gem'
20
+ task :install => [:create] do
21
+ file = Dir["*.gem"].first
22
+ sh "gem install #{file}"
23
+ end
24
+ end
25
+
26
+ Rake::TestTask.new do |t|
27
+ t.verbose = true
28
+ t.warning = true
29
+ end
30
+
31
+ task :default => :test
@@ -0,0 +1,29 @@
1
+ #######################################################################
2
+ # example_job.rb
3
+ #
4
+ # Simple example script for futzing with Windows Jobs. This will fire
5
+ # up two instances of notepad and add them to the job, then close
6
+ # them.
7
+ #######################################################################
8
+ require 'win32/job'
9
+ include Win32
10
+
11
+ pid1 = Process.spawn("notepad.exe")
12
+ pid2 = Process.spawn("notepad.exe")
13
+ sleep 0.5
14
+
15
+ j = Job.new('test')
16
+
17
+ j.configure_limit(
18
+ :breakaway_ok => true,
19
+ :kill_on_job_close => true,
20
+ :process_memory => 1024 * 8,
21
+ :process_time => 1000
22
+ )
23
+
24
+ j.add_process(pid1)
25
+ j.add_process(pid2)
26
+
27
+ sleep 0.5
28
+
29
+ j.close # Notepad instances should terminate here, too.
data/lib/win32/job.rb ADDED
@@ -0,0 +1,509 @@
1
+ require File.join(File.dirname(__FILE__), 'job', 'constants')
2
+ require File.join(File.dirname(__FILE__), 'job', 'functions')
3
+ require File.join(File.dirname(__FILE__), 'job', 'structs')
4
+ require File.join(File.dirname(__FILE__), 'job', 'helper')
5
+
6
+ # The Win32 module serves as a namespace only.
7
+ module Win32
8
+
9
+ # The Job class encapsulates a Windows Job object.
10
+ class Job
11
+ include Windows::Constants
12
+ include Windows::Functions
13
+ include Windows::Structs
14
+ extend Windows::Functions
15
+
16
+ # The version of the win32-job library
17
+ VERSION = '0.1.0'
18
+
19
+ private
20
+
21
+ # Valid options for the configure method
22
+ VALID_OPTIONS = %w[
23
+ active_process
24
+ affinity
25
+ breakaway_ok
26
+ die_on_unhandled_exception
27
+ job_memory
28
+ job_time
29
+ kill_on_job_close
30
+ limit_job_time
31
+ limit_affinity
32
+ minimum_working_set
33
+ maximum_working_set
34
+ preserve_job_time
35
+ priority_class
36
+ process_memory
37
+ process_time
38
+ scheduling_class
39
+ silent_breakaway_ok
40
+ ]
41
+
42
+ public
43
+
44
+ attr_reader :job_name
45
+
46
+ alias :name :job_name
47
+
48
+ # Create a new Job object identified by +name+. If no name is provided
49
+ # then an anonymous job is created.
50
+ #
51
+ # If the +kill_on_close+ argument is true, all associated processes are
52
+ # terminated and the job object then destroys itself. Otherwise, the job
53
+ # object will not be destroyed until all associated processes have exited.
54
+ #
55
+ def initialize(name = nil, security = nil)
56
+ raise TypeError unless name.is_a?(String) if name
57
+
58
+ @job_name = name
59
+ @process_list = []
60
+ @closed = false
61
+
62
+ @job_handle = CreateJobObject(security, name)
63
+
64
+ if @job_handle == 0
65
+ FFI.raise_windows_error('CreateJobObject', FFI.errno)
66
+ end
67
+
68
+ if block_given?
69
+ begin
70
+ yield self
71
+ ensure
72
+ close
73
+ end
74
+ end
75
+
76
+ ObjectSpace.define_finalizer(self, self.class.finalize(@job_handle, @closed))
77
+ end
78
+
79
+ # Add process +pid+ to the job object. Process ID's added to the
80
+ # job are tracked via the Job#process_list accessor.
81
+ #
82
+ def add_process(pid)
83
+ if @process_list.size > 99
84
+ raise ArgumentError, "maximum number of processes reached"
85
+ end
86
+
87
+ phandle = OpenProcess(PROCESS_ALL_ACCESS, false, pid)
88
+
89
+ if phandle == 0
90
+ FFI.raise_windows_error('OpenProcess', FFI.errno)
91
+ end
92
+
93
+ pbool = FFI::MemoryPointer.new(:int)
94
+
95
+ IsProcessInJob(phandle, 0, pbool)
96
+
97
+ if pbool.read_int == 0
98
+ unless AssignProcessToJobObject(@job_handle, phandle)
99
+ FFI.raise_windows_error('AssignProcessToJobObject', FFI.errno)
100
+ end
101
+ @process_list << pid
102
+ else
103
+ raise ArgumentError, "pid #{pid} is already part of a job"
104
+ end
105
+
106
+ pid
107
+ end
108
+
109
+ # Close the job object.
110
+ #
111
+ def close
112
+ CloseHandle(@job_handle) if @job_handle
113
+ @closed = true
114
+ end
115
+
116
+ # Kill all processes associated with the job object that are
117
+ # associated with the current process.
118
+ #
119
+ def kill
120
+ unless TerminateJobObject(@job_handle, Process.pid)
121
+ FFI.raise_windows_error('TerminateJobObject', FFI.errno)
122
+ end
123
+
124
+ @process_list = []
125
+ end
126
+
127
+ alias terminate kill
128
+
129
+ # Set various job limits. Possible options are:
130
+ #
131
+ # * active_process => Numeric
132
+ # Establishes a maximum number of simultaneously active processes
133
+ # associated with the job.
134
+ #
135
+ # * affinity => Numeric
136
+ # Causes all processes associated with the job to use the same
137
+ # processor affinity.
138
+ #
139
+ # * breakaway_ok => Boolean
140
+ # If any process associated with the job creates a child process using
141
+ # the CREATE_BREAKAWAY_FROM_JOB flag while this limit is in effect, the
142
+ # child process is not associated with the job.
143
+ #
144
+ # * die_on_unhandled_exception => Boolean
145
+ # Forces a call to the SetErrorMode function with the SEM_NOGPFAULTERRORBOX
146
+ # flag for each process associated with the job. If an exception occurs
147
+ # and the system calls the UnhandledExceptionFilter function, the debugger
148
+ # will be given a chance to act. If there is no debugger, the functions
149
+ # returns EXCEPTION_EXECUTE_HANDLER. Normally, this will cause termination
150
+ # of the process with the exception code as the exit status.
151
+ #
152
+ # * job_memory => Numeric
153
+ # Causes all processes associated with the job to limit the job-wide
154
+ # sum of their committed memory. When a process attempts to commit
155
+ # memory that would exceed the job-wide limit, it fails. If the job
156
+ # object is associated with a completion port, a
157
+ # JOB_OBJECT_MSG_JOB_MEMORY_LIMIT message is sent to the completion
158
+ # port.
159
+ #
160
+ # * job_time => Numeric
161
+ # Establishes a user-mode execution time limit for the job.
162
+ #
163
+ # * kill_on_job_close => Boolean
164
+ # Causes all processes associated with the job to terminate when the
165
+ # last handle to the job is closed.
166
+ #
167
+ # * minimum_working_set_size => Numeric
168
+ # Causes all processes associated with the job to use the same minimum
169
+ # set size. If the job is nested, the effective working set size is the
170
+ # smallest working set size in the job chain.
171
+ #
172
+ # * maximum_working_set_size => Numeric
173
+ # Causes all processes associated with the job to use the same maximum
174
+ # set size. If the job is nested, the effective working set size is the
175
+ # smallest working set size in the job chain.
176
+ #
177
+ # * per_job_user_time_limit
178
+ # The per-job user-mode execution time limit, in 100-nanosecond ticks.
179
+ # The system adds the current time of the processes associated with the
180
+ # job to this limit.
181
+ #
182
+ # For example, if you set this limit to 1 minute, and the job has a
183
+ # process that has accumulated 5 minutes of user-mode time, the limit
184
+ # actually enforced is 6 minutes.
185
+ #
186
+ # The system periodically checks to determine whether the sum of the
187
+ # user-mode execution time for all processes is greater than this
188
+ # end-of-job limit. If so all processes are terminated.
189
+ #
190
+ # * per_process_user_time_limit
191
+ # The per-process user-mode execution time limit, in 100-nanosecond
192
+ # ticks. The system periodically checks to determine whether each
193
+ # process associated with the job has accumulated more user-mode time
194
+ # than the set limit. If it has, the process is terminated.
195
+ # If the job is nested, the effective limit is the most restrictive
196
+ # limit in the job chain.
197
+ #
198
+ # * preserve_job_time => Boolean
199
+ # Preserves any job time limits you previously set. As long as this flag
200
+ # is set, you can establish a per-job time limit once, then alter other
201
+ # limits in subsequent calls. This flag cannot be used with job_time.
202
+ #
203
+ # * priority_class => Numeric
204
+ # Causes all processes associated with the job to use the same priority
205
+ # class, e.g. ABOVE_NORMAL_PRIORITY_CLASS.
206
+ #
207
+ # * process_memory => Numeric
208
+ # Causes all processes associated with the job to limit their committed
209
+ # memory. When a process attempts to commit memory that would exceed
210
+ # the per-process limit, it fails. If the job object is associated with
211
+ # a completion port, a JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT message is
212
+ # sent to the completion port. If the job is nested, the effective
213
+ # memory limit is the most restrictive memory limit in the job chain.
214
+ #
215
+ # * process_time => Numeric
216
+ # Establishes a user-mode execution time limit for each currently
217
+ # active process and for all future processes associated with the job.
218
+ #
219
+ # * scheduling_class => Numeric
220
+ # Causes all processes in the job to use the same scheduling class. If
221
+ # the job is nested, the effective scheduling class is the lowest
222
+ # scheduling class in the job chain.
223
+ #
224
+ # * silent_breakaway_ok => Boolean
225
+ # Allows any process associated with the job to create child processes
226
+ # that are not associated with the job. If the job is nested and its
227
+ # immediate job object allows breakaway, the child process breaks away
228
+ # from the immediate job object and from each job in the parent job chain,
229
+ # moving up the hierarchy until it reaches a job that does not permit
230
+ # breakaway. If the immediate job object does not allow breakaway, the
231
+ # child process does not break away even if jobs in its parent job
232
+ # chain allow it.
233
+ #
234
+ # * subset_affinity => Numeric
235
+ # Allows processes to use a subset of the processor affinity for all
236
+ # processes associated with the job.
237
+ #--
238
+ # The options are based on the LimitFlags of the
239
+ # JOBOBJECT_BASIC_LIMIT_INFORMATION struct.
240
+ #
241
+ def configure_limit(options = {})
242
+ unless options.is_a?(Hash)
243
+ raise TypeError, "argument to configure must be a hash"
244
+ end
245
+
246
+ # Validate options
247
+ options.each{ |key,value|
248
+ unless VALID_OPTIONS.include?(key.to_s.downcase)
249
+ raise ArgumentError, "invalid option '#{key}'"
250
+ end
251
+ }
252
+
253
+ flags = 0
254
+ struct = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
255
+
256
+ if options[:active_process]
257
+ flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS
258
+ struct[:BasicInformatin][:ActiveProcessLimit] = options[:active_process]
259
+ end
260
+
261
+ if options[:affinity]
262
+ flags |= JOB_OBJECT_LIMIT_AFFINITY
263
+ struct[:BasicLimitInformation][:Affinity] = options[:affinity]
264
+ end
265
+
266
+ if options[:breakaway_ok]
267
+ flags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK
268
+ end
269
+
270
+ if options[:die_on_unhandled_exception]
271
+ flags |= JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
272
+ end
273
+
274
+ if options[:job_memory]
275
+ flags |= JOB_OBJECT_LIMIT_JOB_MEMORY
276
+ struct[:JobMemoryLimit] = options[:job_memory]
277
+ end
278
+
279
+ if options[:per_job_user_time_limit]
280
+ flags |= JOB_OBJECT_LIMIT_JOB_TIME
281
+ struct[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart] = options[:per_job_user_time_limit]
282
+ end
283
+
284
+ if options[:kill_on_job_close]
285
+ flags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
286
+ end
287
+
288
+ if options[:preserve_job_time]
289
+ flags |= JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME
290
+ end
291
+
292
+ if options[:priority_class]
293
+ flags |= JOB_OBJECT_LIMIT_PRIORITY_CLASS
294
+ struct[:BasicLimitInformation][:PriorityClass] = options[:priority_class]
295
+ end
296
+
297
+ if options[:process_memory]
298
+ flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
299
+ struct[:ProcessMemoryLimit] = options[:process_memory]
300
+ end
301
+
302
+ if options[:process_time]
303
+ flags |= JOB_OBJECT_LIMIT_PROCESS_TIME
304
+ struct[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = options[:process_time]
305
+ end
306
+
307
+ if options[:scheduling_class]
308
+ flags |= JOB_OBJECT_LIMIT_SCHEDULING_CLASS
309
+ struct[:BasicLimitInformation][:SchedulingClass] = options[:scheduling_class]
310
+ end
311
+
312
+ if options[:silent_breakaway_ok]
313
+ flags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
314
+ end
315
+
316
+ if options[:subset_affinity]
317
+ flags |= JOB_OBJECT_LIMIT_SUBSET_AFFINITY | JOB_OBJECT_LIMIT_AFFINITY
318
+ end
319
+
320
+ if options[:minimum_working_set_size]
321
+ flags |= JOB_OBJECT_LIMIT_WORKINGSET
322
+ struct[:BasicLimitInformation][:MinimumWorkingSetSize] = options[:minimum_working_set_size]
323
+ end
324
+
325
+ if options[:maximum_working_set_size]
326
+ flags |= JOB_OBJECT_LIMIT_WORKINGSET
327
+ struct[:BasicLimitInformation][:MaximumWorkingSetSize] = options[:maximum_working_set_size]
328
+ end
329
+
330
+ struct[:BasicLimitInformation][:LimitFlags] = flags
331
+
332
+ bool = SetInformationJobObject(
333
+ @job_handle,
334
+ JobObjectExtendedLimitInformation,
335
+ struct,
336
+ struct.size
337
+ )
338
+
339
+ unless bool
340
+ FFI.raise_windows_error('SetInformationJobObject', FFI.errno)
341
+ end
342
+
343
+ options
344
+ end
345
+
346
+ # Return a list of process ids that are part of the job.
347
+ #
348
+ def process_list
349
+ info = JOBOBJECT_BASIC_PROCESS_ID_LIST.new
350
+
351
+ bool = QueryInformationJobObject(
352
+ @job_handle,
353
+ JobObjectBasicProcessIdList,
354
+ info,
355
+ info.size,
356
+ nil
357
+ )
358
+
359
+ unless bool
360
+ FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
361
+ end
362
+
363
+ info[:ProcessIdList].to_a.select{ |n| n != 0 }
364
+ end
365
+
366
+ # Returns an AccountInfoStruct that shows various job accounting
367
+ # information, such as total user time, total kernel time, the
368
+ # total number of processes, and so on.
369
+ #
370
+ def account_info
371
+ info = JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION.new
372
+
373
+ bool = QueryInformationJobObject(
374
+ @job_handle,
375
+ JobObjectBasicAndIoAccountingInformation,
376
+ info,
377
+ info.size,
378
+ nil
379
+ )
380
+
381
+ unless bool
382
+ FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
383
+ end
384
+
385
+ struct = AccountInfo.new(
386
+ info[:BasicInfo][:TotalUserTime][:QuadPart],
387
+ info[:BasicInfo][:TotalKernelTime][:QuadPart],
388
+ info[:BasicInfo][:ThisPeriodTotalUserTime][:QuadPart],
389
+ info[:BasicInfo][:ThisPeriodTotalKernelTime][:QuadPart],
390
+ info[:BasicInfo][:TotalPageFaultCount],
391
+ info[:BasicInfo][:TotalProcesses],
392
+ info[:BasicInfo][:ActiveProcesses],
393
+ info[:BasicInfo][:TotalTerminatedProcesses],
394
+ info[:IoInfo][:ReadOperationCount],
395
+ info[:IoInfo][:WriteOperationCount],
396
+ info[:IoInfo][:OtherOperationCount],
397
+ info[:IoInfo][:ReadTransferCount],
398
+ info[:IoInfo][:WriteTransferCount],
399
+ info[:IoInfo][:OtherTransferCount]
400
+ )
401
+
402
+ struct
403
+ end
404
+
405
+ # Return limit information for the process group.
406
+ #
407
+ def limit_info
408
+ info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
409
+
410
+ bool = QueryInformationJobObject(
411
+ @job_handle,
412
+ JobObjectExtendedLimitInformation,
413
+ info,
414
+ info.size,
415
+ nil
416
+ )
417
+
418
+ unless bool
419
+ FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
420
+ end
421
+
422
+ struct = LimitInfo.new(
423
+ info[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart],
424
+ info[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart],
425
+ info[:BasicLimitInformation][:LimitFlags],
426
+ info[:BasicLimitInformation][:MinimumWorkingSetSize],
427
+ info[:BasicLimitInformation][:MaximumWorkingSetSize],
428
+ info[:BasicLimitInformation][:ActiveProcessLimit],
429
+ info[:BasicLimitInformation][:Affinity],
430
+ info[:BasicLimitInformation][:PriorityClass],
431
+ info[:BasicLimitInformation][:SchedulingClass],
432
+ info[:IoInfo][:ReadOperationCount],
433
+ info[:IoInfo][:WriteOperationCount],
434
+ info[:IoInfo][:OtherOperationCount],
435
+ info[:IoInfo][:ReadTransferCount],
436
+ info[:IoInfo][:WriteTransferCount],
437
+ info[:IoInfo][:OtherTransferCount],
438
+ info[:ProcessMemoryLimit],
439
+ info[:JobMemoryLimit],
440
+ info[:PeakProcessMemoryUsed],
441
+ info[:PeakJobMemoryUsed]
442
+ )
443
+
444
+ struct
445
+ end
446
+
447
+ # Waits for the processes in the job to terminate.
448
+ #--
449
+ # See http://blogs.msdn.com/b/oldnewthing/archive/2013/04/05/10407778.aspx
450
+ #
451
+ # TODO: Fix. I'm not sure this approach is feasible without the processes
452
+ # having been created in a suspended state.
453
+ #
454
+ =begin
455
+ def wait
456
+ io_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 1)
457
+
458
+ if io_port == 0
459
+ FFI.raise_windows_error('CreateIoCompletionPort', FFI.errno)
460
+ end
461
+
462
+ port = JOBOBJECT_ASSOCIATE_COMPLETION_PORT.new
463
+ port[:CompletionKey] = @job_handle
464
+ port[:CompletionPort] = io_port
465
+
466
+ bool = SetInformationJobObject(
467
+ @job_handle,
468
+ JobObjectAssociateCompletionPortInformation,
469
+ port,
470
+ port.size
471
+ )
472
+
473
+ FFI.raise_windows_error('SetInformationJobObject', FFI.errno) unless bool
474
+
475
+ @process_list.each{ |pid|
476
+ handle = OpenProcess(PROCESS_SET_QUOTA|PROCESS_TERMINATE, false, pid)
477
+
478
+ FFI.raise_windows_error('OpenProcess', FFI.errno) unless handle
479
+
480
+ # BUG: I get access denied errors here.
481
+ unless AssignProcessToJobObject(@job_handle, handle)
482
+ FFI.raise_windows_error('AssignProcessToJobObject', FFI.errno)
483
+ end
484
+
485
+ # TODO: Do I need to get the thread handle and explicitly close it?
486
+
487
+ CloseHandle(handle)
488
+
489
+ olap = OVERLAPPED.new
490
+ bytes = FFI::MemoryPointer.new(:ulong)
491
+ ckey = FFI::MemoryPointer.new(:uintptr_t)
492
+
493
+ while GetQueuedCompletionPort(io_port, bytes, ckey, olap, INFINITE) &&
494
+ !(ckey.read_pointer.to_i == @job_handle && ccode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
495
+ sleep 0.1
496
+ end
497
+ }
498
+ end
499
+ =end
500
+
501
+ private
502
+
503
+ # Automatically close job object when it goes out of scope.
504
+ #
505
+ def self.finalize(handle, closed)
506
+ proc{ CloseHandle(handle) unless closed }
507
+ end
508
+ end
509
+ end
@@ -0,0 +1,64 @@
1
+ require 'ffi'
2
+
3
+ module Windows
4
+ module Constants
5
+ private
6
+
7
+ JobObjectBasicAccountingInformation = 1
8
+ JobObjectBasicLimitInformation = 2
9
+ JobObjectBasicProcessIdList = 3
10
+ JobObjectBasicUIRestrictions = 4
11
+ JobObjectSecurityLimitInformation = 5
12
+ JobObjectEndOfJobTimeInformation = 6
13
+ JobObjectAssociateCompletionPortInformation = 7
14
+ JobObjectBasicAndIoAccountingInformation = 8
15
+ JobObjectExtendedLimitInformation = 9
16
+ JobObjectGroupInformation = 11
17
+ JobObjectNotificationLimitInformation = 12
18
+ JobObjectLimitViolationInformation = 13
19
+ JobObjectGroupInformationEx = 14
20
+ JobObjectCpuRateControlInformation = 15
21
+
22
+ PROCESS_ALL_ACCESS = 0x1F0FFF
23
+ PROCESS_SET_QUOTA = 0x0100
24
+ PROCESS_TERMINATE = 0x0001
25
+ SYNCHRONIZE = 0x00100000
26
+
27
+ INFINITE = 0xFFFFFFFF
28
+
29
+ INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address
30
+
31
+ JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008
32
+ JOB_OBJECT_LIMIT_AFFINITY = 0x00000010
33
+ JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
34
+ JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400
35
+ JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200
36
+ JOB_OBJECT_LIMIT_JOB_TIME = 0x00000004
37
+ JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
38
+ JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME = 0x00000040
39
+ JOB_OBJECT_LIMIT_PRIORITY_CLASS = 0x00000020
40
+ JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100
41
+ JOB_OBJECT_LIMIT_PROCESS_TIME = 0x00000002
42
+ JOB_OBJECT_LIMIT_SCHEDULING_CLASS = 0x00000080
43
+ JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000
44
+ JOB_OBJECT_LIMIT_SUBSET_AFFINITY = 0x00004000
45
+ JOB_OBJECT_LIMIT_WORKINGSET = 0x00000001
46
+
47
+ JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO = 4
48
+
49
+ WAIT_ABANDONED = 0x00000080
50
+ WAIT_IO_COMPLETION = 0x000000C0
51
+ WAIT_OBJECT_0 = 0x00000000
52
+ WAIT_TIMEOUT = 0x00000102
53
+ WAIT_FAILED = 0xFFFFFFFF
54
+
55
+ public
56
+
57
+ ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000
58
+ BELOW_NORMAL_PRIORITY_CLASS = 0x00004000
59
+ HIGH_PRIORITY_CLASS = 0x00000080
60
+ IDLE_PRIORITY_CLASS = 0x00000040
61
+ NORMAL_PRIORITY_CLASS = 0x00000020
62
+ REALTIME_PRIORITY_CLASS = 0x00000100
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ require 'ffi'
2
+
3
+ module Windows
4
+ module Functions
5
+ extend FFI::Library
6
+ ffi_lib :kernel32
7
+
8
+ typedef :uintptr_t, :handle
9
+ typedef :ulong, :dword
10
+
11
+ attach_function :AssignProcessToJobObject, [:handle, :handle], :bool
12
+ attach_function :CloseHandle, [:handle], :bool
13
+ attach_function :CreateJobObject, :CreateJobObjectA, [:pointer, :string], :handle
14
+ attach_function :CreateIoCompletionPort, [:handle, :handle, :uintptr_t, :dword], :handle
15
+ attach_function :GetQueuedCompletionStatus, [:handle, :pointer, :pointer, :pointer, :dword], :bool
16
+ attach_function :IsProcessInJob, [:handle, :handle, :pointer], :bool
17
+ attach_function :OpenProcess, [:dword, :bool, :dword], :handle
18
+ attach_function :OpenJobObject, :OpenJobObjectA, [:dword, :bool, :string], :handle
19
+ attach_function :QueryInformationJobObject, [:handle, :int, :pointer, :dword, :pointer], :bool
20
+ attach_function :ResumeThread, [:handle], :dword
21
+ attach_function :SetInformationJobObject, [:handle, :int, :pointer, :dword], :bool
22
+ attach_function :TerminateJobObject, [:handle, :uint], :bool
23
+ attach_function :WaitForSingleObjectEx, [:handle, :dword, :bool], :dword
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require 'ffi'
2
+
3
+ module FFI
4
+ extend FFI::Library
5
+
6
+ ffi_lib :kernel32
7
+
8
+ attach_function :FormatMessage, :FormatMessageA,
9
+ [:ulong, :pointer, :ulong, :ulong, :pointer, :ulong, :pointer], :ulong
10
+
11
+ def win_error(function, err=FFI.errno)
12
+ flags = 0x00001000 | 0x00000200
13
+ buf = FFI::MemoryPointer.new(:char, 1024)
14
+
15
+ FormatMessage(flags, nil, err , 0x0409, buf, 1024, nil)
16
+
17
+ function + ': ' + buf.read_string.strip
18
+ end
19
+
20
+ def raise_windows_error(function, err=FFI.errno)
21
+ raise SystemCallError.new(win_error(function, err), err)
22
+ end
23
+
24
+ module_function :win_error
25
+ module_function :raise_windows_error
26
+ end
@@ -0,0 +1,175 @@
1
+ require 'ffi'
2
+
3
+ module Windows
4
+ module Structs
5
+ extend FFI::Library
6
+
7
+ typedef :uintptr_t, :handle
8
+ typedef :ulong, :dword
9
+ typedef :ushort, :word
10
+
11
+ class LARGE_INTEGER < FFI::Union
12
+ layout(:QuadPart, :long_long)
13
+ end
14
+
15
+ class JOBOBJECT_BASIC_PROCESS_ID_LIST < FFI::Struct
16
+ layout(
17
+ :NumberOfAssignedProcesses, :ulong,
18
+ :NumberOfProcessIdsInList, :ulong,
19
+ :ProcessIdList, [:uintptr_t, 100] # Limit 100 processes per job (?)
20
+ )
21
+ end
22
+
23
+ class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION < FFI::Struct
24
+ layout(
25
+ :TotalUserTime, LARGE_INTEGER,
26
+ :TotalKernelTime, LARGE_INTEGER,
27
+ :ThisPeriodTotalUserTime, LARGE_INTEGER,
28
+ :ThisPeriodTotalKernelTime, LARGE_INTEGER,
29
+ :TotalPageFaultCount, :ulong,
30
+ :TotalProcesses, :ulong,
31
+ :ActiveProcesses, :ulong,
32
+ :TotalTerminatedProcesses, :ulong
33
+ )
34
+ end
35
+
36
+ class IO_COUNTERS < FFI::Struct
37
+ layout(
38
+ :ReadOperationCount, :ulong_long,
39
+ :WriteOperationCount, :ulong_long,
40
+ :OtherOperationCount, :ulong_long,
41
+ :ReadTransferCount, :ulong_long,
42
+ :WriteTransferCount, :ulong_long,
43
+ :OtherTransferCount, :ulong_long
44
+ )
45
+ end
46
+
47
+ class JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION < FFI::Struct
48
+ layout(
49
+ :BasicInfo, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,
50
+ :IoInfo, IO_COUNTERS
51
+ )
52
+ end
53
+
54
+ class JOBOBJECT_BASIC_LIMIT_INFORMATION < FFI::Struct
55
+ layout(
56
+ :PerProcessUserTimeLimit, LARGE_INTEGER,
57
+ :PerJobUserTimeLimit, LARGE_INTEGER,
58
+ :LimitFlags, :ulong,
59
+ :MinimumWorkingSetSize, :size_t,
60
+ :MaximumWorkingSetSize, :size_t,
61
+ :ActiveProcessLimit, :ulong,
62
+ :Affinity, :uintptr_t,
63
+ :PriorityClass, :ulong,
64
+ :SchedulingClass, :ulong
65
+ )
66
+ end
67
+
68
+ class JOBOBJECT_EXTENDED_LIMIT_INFORMATION < FFI::Struct
69
+ layout(
70
+ :BasicLimitInformation, JOBOBJECT_BASIC_LIMIT_INFORMATION,
71
+ :IoInfo, IO_COUNTERS,
72
+ :ProcessMemoryLimit, :size_t,
73
+ :JobMemoryLimit, :size_t,
74
+ :PeakProcessMemoryUsed, :size_t,
75
+ :PeakJobMemoryUsed, :size_t
76
+ )
77
+ end
78
+
79
+ class JOBOBJECT_ASSOCIATE_COMPLETION_PORT < FFI::Struct
80
+ layout(:CompletionKey, :uintptr_t, :CompletionPort, :handle)
81
+ end
82
+
83
+ class SECURITY_ATTRIBUTES < FFI::Struct
84
+ layout(
85
+ :nLength, :ulong,
86
+ :lpSecurityDescriptor, :pointer,
87
+ :bInheritHandle, :bool
88
+ )
89
+ end
90
+
91
+ class PROCESS_INFORMATION < FFI::Struct
92
+ layout(
93
+ :hProcess, :handle,
94
+ :hThread, :handle,
95
+ :dwProcessId, :dword,
96
+ :dwThreadId, :dword
97
+ )
98
+ end
99
+
100
+ class STARTUPINFO < FFI::Struct
101
+ layout(
102
+ :cb, :ulong,
103
+ :lpReserved, :string,
104
+ :lpDesktop, :string,
105
+ :lpTitle, :string,
106
+ :dwX, :dword,
107
+ :dwY, :dword,
108
+ :dwXSize, :dword,
109
+ :dwYSize, :dword,
110
+ :dwXCountChars, :dword,
111
+ :dwYCountChars, :dword,
112
+ :dwFillAttribute, :dword,
113
+ :dwFlags, :dword,
114
+ :wShowWindow, :word,
115
+ :cbReserved2, :word,
116
+ :lpReserved2, :pointer,
117
+ :hStdInput, :handle,
118
+ :hStdOutput, :handle,
119
+ :hStdError, :handle
120
+ )
121
+ end
122
+
123
+ # I'm assuming the anonymous struct for the internal union here.
124
+ class Overlapped < FFI::Struct
125
+ layout(
126
+ :Internal, :ulong,
127
+ :InternalHigh, :ulong,
128
+ :Offset, :ulong,
129
+ :OffsetHigh, :ulong,
130
+ :hEvent, :ulong
131
+ )
132
+ end
133
+
134
+ # Ruby Structs
135
+
136
+ AccountInfo = Struct.new('AccountInfo',
137
+ :total_user_time,
138
+ :total_kernel_time,
139
+ :this_period_total_user_time,
140
+ :this_period_total_kernel_time,
141
+ :total_page_fault_count,
142
+ :total_processes,
143
+ :active_processes,
144
+ :total_terminated_processes,
145
+ :read_operation_count,
146
+ :write_operation_count,
147
+ :other_operation_count,
148
+ :read_transfer_count,
149
+ :write_transfer_count,
150
+ :other_transfer_count
151
+ )
152
+
153
+ LimitInfo = Struct.new('LimitInfo',
154
+ :per_process_user_time_limit,
155
+ :per_job_user_time_limit,
156
+ :limit_flags,
157
+ :minimum_working_set_size,
158
+ :maximum_working_set_size,
159
+ :active_process_limit,
160
+ :affinity,
161
+ :priority_class,
162
+ :scheduling_class,
163
+ :read_operation_count,
164
+ :write_operation_count,
165
+ :other_operation_count,
166
+ :read_transfer_count,
167
+ :write_transfer_count,
168
+ :other_transfer_count,
169
+ :process_memory_limit,
170
+ :job_memory_limit,
171
+ :peak_process_memory_used,
172
+ :peek_job_memory_used
173
+ )
174
+ end
175
+ end
@@ -0,0 +1,126 @@
1
+ #######################################################################
2
+ # test_win32_job.rb
3
+ #
4
+ # Test suite for the win32-job library. You should run these tests
5
+ # via the "rake test" command.
6
+ #######################################################################
7
+ require 'test-unit'
8
+ require 'win32/job'
9
+
10
+ class TC_Win32_Job < Test::Unit::TestCase
11
+ def setup
12
+ @name = 'ruby_xxxxxx'
13
+ @job = Win32::Job.new(@name)
14
+ @pid = Process.spawn('notepad')
15
+ end
16
+
17
+ test "version number is what we expect" do
18
+ assert_equal('0.1.0', Win32::Job::VERSION)
19
+ end
20
+
21
+ test "constructor argument may be omitted" do
22
+ assert_nothing_raised{ Win32::Job.new }
23
+ end
24
+
25
+ test "constructor accepts a name for the job" do
26
+ assert_nothing_raised{ Win32::Job.new(@name) }
27
+ end
28
+
29
+ test "argument to constructor must be a string" do
30
+ assert_raise(TypeError){ Win32::Job.new(1) }
31
+ end
32
+
33
+ test "job_name basic functionality" do
34
+ assert_respond_to(@job, :job_name)
35
+ assert_nothing_raised{ @job.job_name }
36
+ assert_kind_of(String, @job.job_name)
37
+ end
38
+
39
+ test "job_name is read-only" do
40
+ assert_raise(NoMethodError){ @job.job_name = 'foo' }
41
+ end
42
+
43
+ test "name is an alias for job_name" do
44
+ assert_alias_method(@job, :name, :job_name)
45
+ end
46
+
47
+ test "close basic functionality" do
48
+ assert_respond_to(@job, :close)
49
+ assert_nothing_raised{ @job.close }
50
+ end
51
+
52
+ test "calling close multiple times has no effect" do
53
+ assert_nothing_raised{ @job.close }
54
+ assert_nothing_raised{ @job.close }
55
+ assert_nothing_raised{ @job.close }
56
+ end
57
+
58
+ test "close method does not accept any arguments" do
59
+ assert_raise(ArgumentError){ @job.close(1) }
60
+ end
61
+
62
+ test "add_process basic functionality" do
63
+ assert_respond_to(@job, :add_process)
64
+ end
65
+
66
+ test "add_process works as expected" do
67
+ assert_nothing_raised{ @job.add_process(@pid) }
68
+ end
69
+
70
+ test "add process requires a single argument" do
71
+ assert_raise(ArgumentError){ @job.add_process }
72
+ end
73
+
74
+ test "kill basic functionality" do
75
+ assert_respond_to(@job, :kill)
76
+ end
77
+
78
+ test "kill works as expected" do
79
+ @job.add_process(@pid)
80
+ assert_equal(@pid, @job.process_list.first)
81
+ assert_nothing_raised{ @job.kill }
82
+ assert_true(@job.process_list.empty?)
83
+ end
84
+
85
+ test "terminate is an alias for kill" do
86
+ assert_alias_method(@job, :kill, :terminate)
87
+ end
88
+
89
+ test "configure_limit basic functionality" do
90
+ assert_nothing_raised{ @job.configure_limit }
91
+ end
92
+
93
+ test "configure_limit works as expected" do
94
+ assert_nothing_raised{
95
+ @job.configure_limit(
96
+ :breakaway_ok => true,
97
+ :kill_on_job_close => true,
98
+ :process_memory => 1024 * 8,
99
+ :process_time => 1000
100
+ )
101
+ }
102
+ end
103
+
104
+ test "configure_limit raises an error if it detects an invalid option" do
105
+ assert_raise(ArgumentError){ @job.configure_limit(:bogus => 1) }
106
+ end
107
+
108
+ test "priority constants are defined" do
109
+ assert_not_nil(Win32::Job::ABOVE_NORMAL_PRIORITY_CLASS)
110
+ assert_not_nil(Win32::Job::BELOW_NORMAL_PRIORITY_CLASS)
111
+ assert_not_nil(Win32::Job::HIGH_PRIORITY_CLASS)
112
+ assert_not_nil(Win32::Job::IDLE_PRIORITY_CLASS)
113
+ assert_not_nil(Win32::Job::NORMAL_PRIORITY_CLASS)
114
+ assert_not_nil(Win32::Job::REALTIME_PRIORITY_CLASS)
115
+ end
116
+
117
+ def teardown
118
+ @name = nil
119
+
120
+ @job.close
121
+ @job = nil
122
+
123
+ Process.kill(9, @pid) rescue nil
124
+ @pid = nil
125
+ end
126
+ end
Binary file
data/win32-job.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'win32-job'
5
+ spec.version = '0.1.0'
6
+ spec.author = 'Daniel J. Berger'
7
+ spec.license = 'Artistic 2.0'
8
+ spec.email = 'djberg96@gmail.com'
9
+ spec.homepage = 'http://github.com/djberg96/win32-job'
10
+ spec.summary = 'Interface for Windows jobs (process groups)'
11
+ spec.test_file = 'test/test_win32_job.rb'
12
+ spec.files = Dir['**/*'].reject{ |f| f.include?('git') }
13
+
14
+ spec.extra_rdoc_files = ['README', 'CHANGES', 'MANIFEST']
15
+ spec.rubyforge_project = 'win32utils'
16
+ spec.required_ruby_version = '> 1.9.1'
17
+
18
+ spec.add_dependency('ffi')
19
+
20
+ spec.add_development_dependency('rake')
21
+ spec.add_development_dependency('test-unit')
22
+
23
+ spec.description = <<-EOF
24
+ The win32-job library provides an interface for jobs (process groups)
25
+ on MS Windows. This allows you to apply various limits and behavior to
26
+ groups of processes on Windows.
27
+ EOF
28
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: win32-job
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel J. Berger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: |2
56
+ The win32-job library provides an interface for jobs (process groups)
57
+ on MS Windows. This allows you to apply various limits and behavior to
58
+ groups of processes on Windows.
59
+ email: djberg96@gmail.com
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files:
63
+ - README
64
+ - CHANGES
65
+ - MANIFEST
66
+ files:
67
+ - CHANGES
68
+ - examples/example_job.rb
69
+ - lib/win32/job/constants.rb
70
+ - lib/win32/job/functions.rb
71
+ - lib/win32/job/helper.rb
72
+ - lib/win32/job/structs.rb
73
+ - lib/win32/job.rb
74
+ - MANIFEST
75
+ - Rakefile
76
+ - README
77
+ - test/test_win32_job.rb
78
+ - win32-job-0.1.0.gem
79
+ - win32-job.gemspec
80
+ homepage: http://github.com/djberg96/win32-job
81
+ licenses:
82
+ - Artistic 2.0
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - '>'
91
+ - !ruby/object:Gem::Version
92
+ version: 1.9.1
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project: win32utils
100
+ rubygems_version: 2.0.3
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: Interface for Windows jobs (process groups)
104
+ test_files:
105
+ - test/test_win32_job.rb