win32-job 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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