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 +7 -0
- data/CHANGES +2 -0
- data/MANIFEST +12 -0
- data/README +50 -0
- data/Rakefile +31 -0
- data/examples/example_job.rb +29 -0
- data/lib/win32/job.rb +509 -0
- data/lib/win32/job/constants.rb +64 -0
- data/lib/win32/job/functions.rb +25 -0
- data/lib/win32/job/helper.rb +26 -0
- data/lib/win32/job/structs.rb +175 -0
- data/test/test_win32_job.rb +126 -0
- data/win32-job-0.1.0.gem +0 -0
- data/win32-job.gemspec +28 -0
- metadata +105 -0
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
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
|
data/win32-job-0.1.0.gem
ADDED
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
|