win32-job 0.1.3 → 0.1.4
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 +4 -4
- checksums.yaml.gz.sig +1 -0
- data.tar.gz.sig +2 -0
- data/CHANGES +22 -16
- data/MANIFEST +2 -0
- data/README +55 -55
- data/Rakefile +3 -6
- data/appveyor.yml +46 -0
- data/certs/djberg96_pub.pem +21 -0
- data/lib/win32-job.rb +1 -0
- data/lib/win32/job.rb +505 -505
- data/lib/win32/job/functions.rb +25 -25
- data/test/test_win32_job.rb +139 -139
- data/win32-job.gemspec +28 -27
- metadata +39 -8
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30a60b887397b24322804e276cf42a9809f709e9
|
4
|
+
data.tar.gz: cad1ab937502a18749ebb235b229e80ec7cdc86b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a029f9e719ac44d0452169b189610173e9b92f0d99888687ad07019e529891eef3133be3133264ac6676d0b3af3c692bfef6aa3faf5ea27278718cec7b3300fc
|
7
|
+
data.tar.gz: b00089d6fcffd6019c30bd8ed21aaa233006f5dc61b5c2d8af85dd65585e56f4fef18ddced3e8ddaa95347f7923ed6b1aae5b888a7a22f994928043ba426eada
|
checksums.yaml.gz.sig
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
�P�5 �hђ��4,���|�.��& ��w�2(y¸$�����Yc���O�}V"��џ����{�f�p&>�z��m;�����Q{\{�baR�OЊ�i9�e1��׃����s����_.;��.K^�W��'FF���Z�_UY{P-�_��R�~uu�.���)}���VZ�E�B#v\dy���C�W�+�iɚ!;��RM�_�I0��x� wk6��}~���2��*7
|
data.tar.gz.sig
ADDED
data/CHANGES
CHANGED
@@ -1,16 +1,22 @@
|
|
1
|
-
= 0.1.
|
2
|
-
*
|
3
|
-
*
|
4
|
-
*
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
*
|
11
|
-
|
12
|
-
= 0.1.
|
13
|
-
*
|
14
|
-
|
15
|
-
|
16
|
-
*
|
1
|
+
= 0.1.4 - 10-Nov-2015
|
2
|
+
* This gem is now signed.
|
3
|
+
* The gem related tasks in the Rakefile now assume Rubygems 2.x.
|
4
|
+
* Added a win32-job.rb file for convenience.
|
5
|
+
* Added an appveyor.yml file for the MS continuous integration service.
|
6
|
+
|
7
|
+
= 0.1.3 - 20-Oct-2014
|
8
|
+
* Updates to the gem:create and gem:install tasks.
|
9
|
+
* Removed references to Rubyforge in the gemspec.
|
10
|
+
* Use relative_require where appropriate.
|
11
|
+
|
12
|
+
= 0.1.2 - 6-Feb-2014
|
13
|
+
* Altered the constructor. The second argument now controls whether or not
|
14
|
+
to raise an error if the job already exists. By default it will open the job
|
15
|
+
if it already exists.
|
16
|
+
* Fixed a bug in the process_list method where it could return incorrect values.
|
17
|
+
|
18
|
+
= 0.1.1 - 5-Feb-2014
|
19
|
+
* Added a wait method.
|
20
|
+
|
21
|
+
= 0.1.0 - 4-Feb-2014
|
22
|
+
* Initial release.
|
data/MANIFEST
CHANGED
data/README
CHANGED
@@ -1,55 +1,55 @@
|
|
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
|
-
* Ruby 1.9.3 or later
|
10
|
-
|
11
|
-
== Synopsis
|
12
|
-
require 'win32/job'
|
13
|
-
include Win32
|
14
|
-
|
15
|
-
job = Job.create('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
|
-
job.add_process(1234)
|
25
|
-
job.add_process(1235)
|
26
|
-
|
27
|
-
job.close
|
28
|
-
|
29
|
-
== Known Issues
|
30
|
-
None known.
|
31
|
-
|
32
|
-
Please file any bug reports on the project page at
|
33
|
-
http://github.com/djberg96/win32-job
|
34
|
-
|
35
|
-
== License
|
36
|
-
Artistic 2.0
|
37
|
-
|
38
|
-
== Contributions
|
39
|
-
Although this library is free, please consider having your company
|
40
|
-
setup a gittip if used by your company professionally.
|
41
|
-
|
42
|
-
http://www.gittip.com/djberg96/
|
43
|
-
|
44
|
-
== Copyright
|
45
|
-
(C) 2003-
|
46
|
-
All Rights Reserved
|
47
|
-
|
48
|
-
== Warranty
|
49
|
-
This package is provided "as is" and without any express or
|
50
|
-
implied warranties, including, without limitation, the implied
|
51
|
-
warranties of merchantability and fitness for a particular purpose.
|
52
|
-
|
53
|
-
== Authors
|
54
|
-
Daniel J. Berger
|
55
|
-
Park Heesob
|
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
|
+
* Ruby 1.9.3 or later
|
10
|
+
|
11
|
+
== Synopsis
|
12
|
+
require 'win32/job'
|
13
|
+
include Win32
|
14
|
+
|
15
|
+
job = Job.create('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
|
+
job.add_process(1234)
|
25
|
+
job.add_process(1235)
|
26
|
+
|
27
|
+
job.close
|
28
|
+
|
29
|
+
== Known Issues
|
30
|
+
None known.
|
31
|
+
|
32
|
+
Please file any bug reports on the project page at
|
33
|
+
http://github.com/djberg96/win32-job
|
34
|
+
|
35
|
+
== License
|
36
|
+
Artistic 2.0
|
37
|
+
|
38
|
+
== Contributions
|
39
|
+
Although this library is free, please consider having your company
|
40
|
+
setup a gittip if used by your company professionally.
|
41
|
+
|
42
|
+
http://www.gittip.com/djberg96/
|
43
|
+
|
44
|
+
== Copyright
|
45
|
+
(C) 2003-2015 Daniel J. Berger
|
46
|
+
All Rights Reserved
|
47
|
+
|
48
|
+
== Warranty
|
49
|
+
This package is provided "as is" and without any express or
|
50
|
+
implied warranties, including, without limitation, the implied
|
51
|
+
warranties of merchantability and fitness for a particular purpose.
|
52
|
+
|
53
|
+
== Authors
|
54
|
+
Daniel J. Berger
|
55
|
+
Park Heesob
|
data/Rakefile
CHANGED
@@ -7,13 +7,10 @@ CLEAN.include("**/*.gem", "**/*.rbc", "**/*.rbx")
|
|
7
7
|
namespace :gem do
|
8
8
|
desc 'Create the win32-job gem'
|
9
9
|
task :create => [:clean] do
|
10
|
+
require 'rubygems/package'
|
10
11
|
spec = eval(IO.read('win32-job.gemspec'))
|
11
|
-
|
12
|
-
|
13
|
-
else
|
14
|
-
require 'rubygems/package'
|
15
|
-
Gem::Package.build(spec)
|
16
|
-
end
|
12
|
+
spec.signing_key = File.join(Dir.home, '.ssh', 'gem-private_key.pem')
|
13
|
+
Gem::Package.build(spec, true)
|
17
14
|
end
|
18
15
|
|
19
16
|
desc 'Install the win32-job gem'
|
data/appveyor.yml
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
version: '{build}'
|
2
|
+
branches:
|
3
|
+
only:
|
4
|
+
- ffi
|
5
|
+
skip_tags: true
|
6
|
+
clone_depth: 10
|
7
|
+
environment:
|
8
|
+
matrix:
|
9
|
+
- ruby_version: 193
|
10
|
+
ruby_dir: 1.9.1
|
11
|
+
- ruby_version: 200
|
12
|
+
ruby_dir: 2.0.0
|
13
|
+
- ruby_version: 200-x64
|
14
|
+
ruby_dir: 2.0.0
|
15
|
+
- ruby_version: 21
|
16
|
+
ruby_dir: 2.1.0
|
17
|
+
- ruby_version: 21-x64
|
18
|
+
ruby_dir: 2.1.0
|
19
|
+
- ruby_version: 22
|
20
|
+
ruby_dir: 2.2.0
|
21
|
+
- ruby_version: 22-x64
|
22
|
+
ruby_dir: 2.2.0
|
23
|
+
install:
|
24
|
+
- ps: >-
|
25
|
+
$env:path = "C:\Ruby" + $env:ruby_version + "\bin;" + $env:path
|
26
|
+
|
27
|
+
$tpath = "C:\Ruby" + $env:ruby_version + "\lib\ruby\" + $env:ruby_dir + "\test"
|
28
|
+
|
29
|
+
if ((test-path $tpath) -eq $True){ rm -recurse -force $tpath }
|
30
|
+
|
31
|
+
gem update --system > $null
|
32
|
+
|
33
|
+
if ((gem query -i ffi) -eq $False){ gem install ffi --no-document }
|
34
|
+
|
35
|
+
if ((gem query -i test-unit -v ">= 3.0") -eq $False){ gem install test-unit --no-document }
|
36
|
+
cache:
|
37
|
+
- C:\Ruby193\lib\ruby\gems\1.9.1
|
38
|
+
- C:\Ruby200\lib\ruby\gems\2.0.0
|
39
|
+
- C:\Ruby200-x64\lib\ruby\gems\2.0.0
|
40
|
+
- C:\Ruby21\lib\ruby\gems\2.1.0
|
41
|
+
- C:\Ruby21-x64\lib\ruby\gems\2.1.0
|
42
|
+
- C:\Ruby22\lib\ruby\gems\2.2.0
|
43
|
+
- C:\Ruby22-x64\lib\ruby\gems\2.2.0
|
44
|
+
build: off
|
45
|
+
test_script:
|
46
|
+
- cmd: rake
|
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDcDCCAligAwIBAgIBATANBgkqhkiG9w0BAQUFADA/MREwDwYDVQQDDAhkamJl
|
3
|
+
cmc5NjEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPyLGQBGRYDY29t
|
4
|
+
MB4XDTE1MDkwMjIwNDkxOFoXDTE2MDkwMTIwNDkxOFowPzERMA8GA1UEAwwIZGpi
|
5
|
+
ZXJnOTYxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkWA2Nv
|
6
|
+
bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMyTkvXqRp6hLs9eoJOS
|
7
|
+
Hmi8kRYbq9Vkf15/hMxJpotYMgJVHHWrmDcC5Dye2PbnXjTkKf266Zw0PtT9h+lI
|
8
|
+
S3ts9HO+vaCFSMwFFZmnWJSpQ3CNw2RcHxjWkk9yF7imEM8Kz9ojhiDXzBetdV6M
|
9
|
+
gr0lV/alUr7TNVBDngbXEfTWscyXh1qd7xZ4EcOdsDktCe5G45N/o3662tPQvJsi
|
10
|
+
FOF0CM/KuBsa/HL1/eoEmF4B3EKIRfTHrQ3hu20Kv3RJ88QM4ec2+0dd97uX693O
|
11
|
+
zv6981fyEg+aXLkxrkViM/tz2qR2ZE0jPhHTREPYeMEgptRkTmWSKAuLVWrJEfgl
|
12
|
+
DtkCAwEAAaN3MHUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFEwe
|
13
|
+
nn6bfJADmuIDiMSOzedOrL+xMB0GA1UdEQQWMBSBEmRqYmVyZzk2QGdtYWlsLmNv
|
14
|
+
bTAdBgNVHRIEFjAUgRJkamJlcmc5NkBnbWFpbC5jb20wDQYJKoZIhvcNAQEFBQAD
|
15
|
+
ggEBAHmNOCWoDVD75zHFueY0viwGDVP1BNGFC+yXcb7u2GlK+nEMCORqzURbYPf7
|
16
|
+
tL+/hzmePIRz7i30UM//64GI1NLv9jl7nIwjhPpXpf7/lu2I9hOTsvwSumb5UiKC
|
17
|
+
/sqBxI3sfj9pr79Wpv4MuikX1XPik7Ncb7NPsJPw06Lvyc3Hkg5X2XpPtLtS+Gr2
|
18
|
+
wKJnmzb5rIPS1cmsqv0M9LPWflzfwoZ/SpnmhagP+g05p8bRNKjZSA2iImM/GyYZ
|
19
|
+
EJYzxdPOrx2n6NYR3Hk+vHP0U7UBSveI6+qx+ndQYaeyCn+GRX2PKS9h66YF/Q1V
|
20
|
+
tGSHgAmcLlkdGgan182qsE/4kKM=
|
21
|
+
-----END CERTIFICATE-----
|
data/lib/win32-job.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'win32/job'
|
data/lib/win32/job.rb
CHANGED
@@ -1,505 +1,505 @@
|
|
1
|
-
require_relative 'job/constants'
|
2
|
-
require_relative 'job/functions'
|
3
|
-
require_relative 'job/structs'
|
4
|
-
require_relative '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.
|
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
|
-
# The name of the job, if specified.
|
45
|
-
attr_reader :job_name
|
46
|
-
|
47
|
-
alias :name :job_name
|
48
|
-
|
49
|
-
# Create a new Job object identified by +name+. If no name is provided
|
50
|
-
# then an anonymous job is created.
|
51
|
-
#
|
52
|
-
# If the job already exists then the existing job is opened instead, unless
|
53
|
-
# the +open_existing+ method is false. In that case an error is
|
54
|
-
# raised.
|
55
|
-
#
|
56
|
-
# The +security+ argument accepts a raw SECURITY_ATTRIBUTES struct that is
|
57
|
-
# passed to the CreateJobObject function internally.
|
58
|
-
#
|
59
|
-
def initialize(name = nil, open_existing = true, security = nil)
|
60
|
-
raise TypeError unless name.is_a?(String) if name
|
61
|
-
|
62
|
-
@job_name = name
|
63
|
-
@process_list = []
|
64
|
-
@closed = false
|
65
|
-
|
66
|
-
@job_handle = CreateJobObject(security, name)
|
67
|
-
|
68
|
-
if @job_handle == 0
|
69
|
-
FFI.raise_windows_error('CreateJobObject', FFI.errno)
|
70
|
-
end
|
71
|
-
|
72
|
-
if FFI.errno == ERROR_ALREADY_EXISTS && !open_existing
|
73
|
-
raise ArgumentError, "job '#{name}' already exists"
|
74
|
-
end
|
75
|
-
|
76
|
-
if block_given?
|
77
|
-
begin
|
78
|
-
yield self
|
79
|
-
ensure
|
80
|
-
close
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
ObjectSpace.define_finalizer(self, self.class.finalize(@job_handle, @closed))
|
85
|
-
end
|
86
|
-
|
87
|
-
# Add process +pid+ to the job object. Process ID's added to the
|
88
|
-
# job are tracked via the Job#process_list accessor.
|
89
|
-
#
|
90
|
-
# Note that once a process is added to a job, the association cannot be
|
91
|
-
# broken. A process can be associated with more than one job in a
|
92
|
-
# hierarchy of nested jobs, however.
|
93
|
-
#
|
94
|
-
# You may add a maximum of 100 processes per job.
|
95
|
-
#
|
96
|
-
def add_process(pid)
|
97
|
-
if @process_list.size > 99
|
98
|
-
raise ArgumentError, "maximum number of processes reached"
|
99
|
-
end
|
100
|
-
|
101
|
-
phandle = OpenProcess(PROCESS_ALL_ACCESS, false, pid)
|
102
|
-
|
103
|
-
if phandle == 0
|
104
|
-
FFI.raise_windows_error('OpenProcess', FFI.errno)
|
105
|
-
end
|
106
|
-
|
107
|
-
pbool = FFI::MemoryPointer.new(:int)
|
108
|
-
|
109
|
-
IsProcessInJob(phandle, 0, pbool)
|
110
|
-
|
111
|
-
if pbool.read_int == 0
|
112
|
-
unless AssignProcessToJobObject(@job_handle, phandle)
|
113
|
-
FFI.raise_windows_error('AssignProcessToJobObject', FFI.errno)
|
114
|
-
end
|
115
|
-
@process_list << pid
|
116
|
-
else
|
117
|
-
raise ArgumentError, "pid #{pid} is already part of a job"
|
118
|
-
end
|
119
|
-
|
120
|
-
pid
|
121
|
-
end
|
122
|
-
|
123
|
-
# Close the job object.
|
124
|
-
#
|
125
|
-
def close
|
126
|
-
CloseHandle(@job_handle) if @job_handle
|
127
|
-
@closed = true
|
128
|
-
end
|
129
|
-
|
130
|
-
# Kill all processes associated with the job object that are
|
131
|
-
# associated with the current process.
|
132
|
-
#
|
133
|
-
# Note that killing a process does not dissociate it from the job.
|
134
|
-
#
|
135
|
-
def kill
|
136
|
-
unless TerminateJobObject(@job_handle, Process.pid)
|
137
|
-
FFI.raise_windows_error('TerminateJobObject', FFI.errno)
|
138
|
-
end
|
139
|
-
|
140
|
-
@process_list = []
|
141
|
-
end
|
142
|
-
|
143
|
-
alias terminate kill
|
144
|
-
|
145
|
-
# Set various job limits. Possible options are:
|
146
|
-
#
|
147
|
-
# * active_process => Numeric
|
148
|
-
# Establishes a maximum number of simultaneously active processes
|
149
|
-
# associated with the job.
|
150
|
-
#
|
151
|
-
# * affinity => Numeric
|
152
|
-
# Causes all processes associated with the job to use the same
|
153
|
-
# processor affinity.
|
154
|
-
#
|
155
|
-
# * breakaway_ok => Boolean
|
156
|
-
# If any process associated with the job creates a child process using
|
157
|
-
# the CREATE_BREAKAWAY_FROM_JOB flag while this limit is in effect, the
|
158
|
-
# child process is not associated with the job.
|
159
|
-
#
|
160
|
-
# * die_on_unhandled_exception => Boolean
|
161
|
-
# Forces a call to the SetErrorMode function with the SEM_NOGPFAULTERRORBOX
|
162
|
-
# flag for each process associated with the job. If an exception occurs
|
163
|
-
# and the system calls the UnhandledExceptionFilter function, the debugger
|
164
|
-
# will be given a chance to act. If there is no debugger, the functions
|
165
|
-
# returns EXCEPTION_EXECUTE_HANDLER. Normally, this will cause termination
|
166
|
-
# of the process with the exception code as the exit status.
|
167
|
-
#
|
168
|
-
# * job_memory => Numeric
|
169
|
-
# Causes all processes associated with the job to limit the job-wide
|
170
|
-
# sum of their committed memory. When a process attempts to commit
|
171
|
-
# memory that would exceed the job-wide limit, it fails. If the job
|
172
|
-
# object is associated with a completion port, a
|
173
|
-
# JOB_OBJECT_MSG_JOB_MEMORY_LIMIT message is sent to the completion
|
174
|
-
# port.
|
175
|
-
#
|
176
|
-
# * job_time => Numeric
|
177
|
-
# Establishes a user-mode execution time limit for the job.
|
178
|
-
#
|
179
|
-
# * kill_on_job_close => Boolean
|
180
|
-
# Causes all processes associated with the job to terminate when the
|
181
|
-
# last handle to the job is closed.
|
182
|
-
#
|
183
|
-
# * minimum_working_set_size => Numeric
|
184
|
-
# Causes all processes associated with the job to use the same minimum
|
185
|
-
# set size. If the job is nested, the effective working set size is the
|
186
|
-
# smallest working set size in the job chain.
|
187
|
-
#
|
188
|
-
# * maximum_working_set_size => Numeric
|
189
|
-
# Causes all processes associated with the job to use the same maximum
|
190
|
-
# set size. If the job is nested, the effective working set size is the
|
191
|
-
# smallest working set size in the job chain.
|
192
|
-
#
|
193
|
-
# * per_job_user_time_limit
|
194
|
-
# The per-job user-mode execution time limit, in 100-nanosecond ticks.
|
195
|
-
# The system adds the current time of the processes associated with the
|
196
|
-
# job to this limit.
|
197
|
-
#
|
198
|
-
# For example, if you set this limit to 1 minute, and the job has a
|
199
|
-
# process that has accumulated 5 minutes of user-mode time, the limit
|
200
|
-
# actually enforced is 6 minutes.
|
201
|
-
#
|
202
|
-
# The system periodically checks to determine whether the sum of the
|
203
|
-
# user-mode execution time for all processes is greater than this
|
204
|
-
# end-of-job limit. If so all processes are terminated.
|
205
|
-
#
|
206
|
-
# * per_process_user_time_limit
|
207
|
-
# The per-process user-mode execution time limit, in 100-nanosecond
|
208
|
-
# ticks. The system periodically checks to determine whether each
|
209
|
-
# process associated with the job has accumulated more user-mode time
|
210
|
-
# than the set limit. If it has, the process is terminated.
|
211
|
-
# If the job is nested, the effective limit is the most restrictive
|
212
|
-
# limit in the job chain.
|
213
|
-
#
|
214
|
-
# * preserve_job_time => Boolean
|
215
|
-
# Preserves any job time limits you previously set. As long as this flag
|
216
|
-
# is set, you can establish a per-job time limit once, then alter other
|
217
|
-
# limits in subsequent calls. This flag cannot be used with job_time.
|
218
|
-
#
|
219
|
-
# * priority_class => Numeric
|
220
|
-
# Causes all processes associated with the job to use the same priority
|
221
|
-
# class, e.g. ABOVE_NORMAL_PRIORITY_CLASS.
|
222
|
-
#
|
223
|
-
# * process_memory => Numeric
|
224
|
-
# Causes all processes associated with the job to limit their committed
|
225
|
-
# memory. When a process attempts to commit memory that would exceed
|
226
|
-
# the per-process limit, it fails. If the job object is associated with
|
227
|
-
# a completion port, a JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT message is
|
228
|
-
# sent to the completion port. If the job is nested, the effective
|
229
|
-
# memory limit is the most restrictive memory limit in the job chain.
|
230
|
-
#
|
231
|
-
# * process_time => Numeric
|
232
|
-
# Establishes a user-mode execution time limit for each currently
|
233
|
-
# active process and for all future processes associated with the job.
|
234
|
-
#
|
235
|
-
# * scheduling_class => Numeric
|
236
|
-
# Causes all processes in the job to use the same scheduling class. If
|
237
|
-
# the job is nested, the effective scheduling class is the lowest
|
238
|
-
# scheduling class in the job chain.
|
239
|
-
#
|
240
|
-
# * silent_breakaway_ok => Boolean
|
241
|
-
# Allows any process associated with the job to create child processes
|
242
|
-
# that are not associated with the job. If the job is nested and its
|
243
|
-
# immediate job object allows breakaway, the child process breaks away
|
244
|
-
# from the immediate job object and from each job in the parent job chain,
|
245
|
-
# moving up the hierarchy until it reaches a job that does not permit
|
246
|
-
# breakaway. If the immediate job object does not allow breakaway, the
|
247
|
-
# child process does not break away even if jobs in its parent job
|
248
|
-
# chain allow it.
|
249
|
-
#
|
250
|
-
# * subset_affinity => Numeric
|
251
|
-
# Allows processes to use a subset of the processor affinity for all
|
252
|
-
# processes associated with the job.
|
253
|
-
#--
|
254
|
-
# The options are based on the LimitFlags of the
|
255
|
-
# JOBOBJECT_BASIC_LIMIT_INFORMATION struct.
|
256
|
-
#
|
257
|
-
def configure_limit(options = {})
|
258
|
-
unless options.is_a?(Hash)
|
259
|
-
raise TypeError, "argument to configure must be a hash"
|
260
|
-
end
|
261
|
-
|
262
|
-
# Validate options
|
263
|
-
options.each{ |key,value|
|
264
|
-
unless VALID_OPTIONS.include?(key.to_s.downcase)
|
265
|
-
raise ArgumentError, "invalid option '#{key}'"
|
266
|
-
end
|
267
|
-
}
|
268
|
-
|
269
|
-
flags = 0
|
270
|
-
struct = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
|
271
|
-
|
272
|
-
if options[:active_process]
|
273
|
-
flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS
|
274
|
-
struct[:BasicInformatin][:ActiveProcessLimit] = options[:active_process]
|
275
|
-
end
|
276
|
-
|
277
|
-
if options[:affinity]
|
278
|
-
flags |= JOB_OBJECT_LIMIT_AFFINITY
|
279
|
-
struct[:BasicLimitInformation][:Affinity] = options[:affinity]
|
280
|
-
end
|
281
|
-
|
282
|
-
if options[:breakaway_ok]
|
283
|
-
flags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
284
|
-
end
|
285
|
-
|
286
|
-
if options[:die_on_unhandled_exception]
|
287
|
-
flags |= JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
|
288
|
-
end
|
289
|
-
|
290
|
-
if options[:job_memory]
|
291
|
-
flags |= JOB_OBJECT_LIMIT_JOB_MEMORY
|
292
|
-
struct[:JobMemoryLimit] = options[:job_memory]
|
293
|
-
end
|
294
|
-
|
295
|
-
if options[:per_job_user_time_limit]
|
296
|
-
flags |= JOB_OBJECT_LIMIT_JOB_TIME
|
297
|
-
struct[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart] = options[:per_job_user_time_limit]
|
298
|
-
end
|
299
|
-
|
300
|
-
if options[:kill_on_job_close]
|
301
|
-
flags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
302
|
-
end
|
303
|
-
|
304
|
-
if options[:preserve_job_time]
|
305
|
-
flags |= JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME
|
306
|
-
end
|
307
|
-
|
308
|
-
if options[:priority_class]
|
309
|
-
flags |= JOB_OBJECT_LIMIT_PRIORITY_CLASS
|
310
|
-
struct[:BasicLimitInformation][:PriorityClass] = options[:priority_class]
|
311
|
-
end
|
312
|
-
|
313
|
-
if options[:process_memory]
|
314
|
-
flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
|
315
|
-
struct[:ProcessMemoryLimit] = options[:process_memory]
|
316
|
-
end
|
317
|
-
|
318
|
-
if options[:process_time]
|
319
|
-
flags |= JOB_OBJECT_LIMIT_PROCESS_TIME
|
320
|
-
struct[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = options[:process_time]
|
321
|
-
end
|
322
|
-
|
323
|
-
if options[:scheduling_class]
|
324
|
-
flags |= JOB_OBJECT_LIMIT_SCHEDULING_CLASS
|
325
|
-
struct[:BasicLimitInformation][:SchedulingClass] = options[:scheduling_class]
|
326
|
-
end
|
327
|
-
|
328
|
-
if options[:silent_breakaway_ok]
|
329
|
-
flags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
|
330
|
-
end
|
331
|
-
|
332
|
-
if options[:subset_affinity]
|
333
|
-
flags |= JOB_OBJECT_LIMIT_SUBSET_AFFINITY | JOB_OBJECT_LIMIT_AFFINITY
|
334
|
-
end
|
335
|
-
|
336
|
-
if options[:minimum_working_set_size]
|
337
|
-
flags |= JOB_OBJECT_LIMIT_WORKINGSET
|
338
|
-
struct[:BasicLimitInformation][:MinimumWorkingSetSize] = options[:minimum_working_set_size]
|
339
|
-
end
|
340
|
-
|
341
|
-
if options[:maximum_working_set_size]
|
342
|
-
flags |= JOB_OBJECT_LIMIT_WORKINGSET
|
343
|
-
struct[:BasicLimitInformation][:MaximumWorkingSetSize] = options[:maximum_working_set_size]
|
344
|
-
end
|
345
|
-
|
346
|
-
struct[:BasicLimitInformation][:LimitFlags] = flags
|
347
|
-
|
348
|
-
bool = SetInformationJobObject(
|
349
|
-
@job_handle,
|
350
|
-
JobObjectExtendedLimitInformation,
|
351
|
-
struct,
|
352
|
-
struct.size
|
353
|
-
)
|
354
|
-
|
355
|
-
unless bool
|
356
|
-
FFI.raise_windows_error('SetInformationJobObject', FFI.errno)
|
357
|
-
end
|
358
|
-
|
359
|
-
options
|
360
|
-
end
|
361
|
-
|
362
|
-
# Return a list of process ids that are part of the job.
|
363
|
-
#
|
364
|
-
def process_list
|
365
|
-
info = JOBOBJECT_BASIC_PROCESS_ID_LIST.new
|
366
|
-
|
367
|
-
bool = QueryInformationJobObject(
|
368
|
-
@job_handle,
|
369
|
-
JobObjectBasicProcessIdList,
|
370
|
-
info,
|
371
|
-
info.size,
|
372
|
-
nil
|
373
|
-
)
|
374
|
-
|
375
|
-
unless bool
|
376
|
-
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
377
|
-
end
|
378
|
-
|
379
|
-
info[:ProcessIdList].to_a[0...info[:NumberOfProcessIdsInList]]
|
380
|
-
end
|
381
|
-
|
382
|
-
# Returns an AccountInfoStruct that shows various job accounting
|
383
|
-
# information, such as total user time, total kernel time, the
|
384
|
-
# total number of processes, and so on.
|
385
|
-
#
|
386
|
-
def account_info
|
387
|
-
info = JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION.new
|
388
|
-
|
389
|
-
bool = QueryInformationJobObject(
|
390
|
-
@job_handle,
|
391
|
-
JobObjectBasicAndIoAccountingInformation,
|
392
|
-
info,
|
393
|
-
info.size,
|
394
|
-
nil
|
395
|
-
)
|
396
|
-
|
397
|
-
unless bool
|
398
|
-
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
399
|
-
end
|
400
|
-
|
401
|
-
struct = AccountInfo.new(
|
402
|
-
info[:BasicInfo][:TotalUserTime][:QuadPart],
|
403
|
-
info[:BasicInfo][:TotalKernelTime][:QuadPart],
|
404
|
-
info[:BasicInfo][:ThisPeriodTotalUserTime][:QuadPart],
|
405
|
-
info[:BasicInfo][:ThisPeriodTotalKernelTime][:QuadPart],
|
406
|
-
info[:BasicInfo][:TotalPageFaultCount],
|
407
|
-
info[:BasicInfo][:TotalProcesses],
|
408
|
-
info[:BasicInfo][:ActiveProcesses],
|
409
|
-
info[:BasicInfo][:TotalTerminatedProcesses],
|
410
|
-
info[:IoInfo][:ReadOperationCount],
|
411
|
-
info[:IoInfo][:WriteOperationCount],
|
412
|
-
info[:IoInfo][:OtherOperationCount],
|
413
|
-
info[:IoInfo][:ReadTransferCount],
|
414
|
-
info[:IoInfo][:WriteTransferCount],
|
415
|
-
info[:IoInfo][:OtherTransferCount]
|
416
|
-
)
|
417
|
-
|
418
|
-
struct
|
419
|
-
end
|
420
|
-
|
421
|
-
# Return limit information for the process group.
|
422
|
-
#
|
423
|
-
def limit_info
|
424
|
-
info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
|
425
|
-
|
426
|
-
bool = QueryInformationJobObject(
|
427
|
-
@job_handle,
|
428
|
-
JobObjectExtendedLimitInformation,
|
429
|
-
info,
|
430
|
-
info.size,
|
431
|
-
nil
|
432
|
-
)
|
433
|
-
|
434
|
-
unless bool
|
435
|
-
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
436
|
-
end
|
437
|
-
|
438
|
-
struct = LimitInfo.new(
|
439
|
-
info[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart],
|
440
|
-
info[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart],
|
441
|
-
info[:BasicLimitInformation][:LimitFlags],
|
442
|
-
info[:BasicLimitInformation][:MinimumWorkingSetSize],
|
443
|
-
info[:BasicLimitInformation][:MaximumWorkingSetSize],
|
444
|
-
info[:BasicLimitInformation][:ActiveProcessLimit],
|
445
|
-
info[:BasicLimitInformation][:Affinity],
|
446
|
-
info[:BasicLimitInformation][:PriorityClass],
|
447
|
-
info[:BasicLimitInformation][:SchedulingClass],
|
448
|
-
info[:IoInfo][:ReadOperationCount],
|
449
|
-
info[:IoInfo][:WriteOperationCount],
|
450
|
-
info[:IoInfo][:OtherOperationCount],
|
451
|
-
info[:IoInfo][:ReadTransferCount],
|
452
|
-
info[:IoInfo][:WriteTransferCount],
|
453
|
-
info[:IoInfo][:OtherTransferCount],
|
454
|
-
info[:ProcessMemoryLimit],
|
455
|
-
info[:JobMemoryLimit],
|
456
|
-
info[:PeakProcessMemoryUsed],
|
457
|
-
info[:PeakJobMemoryUsed]
|
458
|
-
)
|
459
|
-
|
460
|
-
struct
|
461
|
-
end
|
462
|
-
|
463
|
-
# Waits for the processes in the job to terminate.
|
464
|
-
#--
|
465
|
-
# See http://blogs.msdn.com/b/oldnewthing/archive/2013/04/05/10407778.aspx
|
466
|
-
#
|
467
|
-
def wait
|
468
|
-
io_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 1)
|
469
|
-
|
470
|
-
if io_port == 0
|
471
|
-
FFI.raise_windows_error('CreateIoCompletionPort', FFI.errno)
|
472
|
-
end
|
473
|
-
|
474
|
-
port = JOBOBJECT_ASSOCIATE_COMPLETION_PORT.new
|
475
|
-
port[:CompletionKey] = @job_handle
|
476
|
-
port[:CompletionPort] = io_port
|
477
|
-
|
478
|
-
bool = SetInformationJobObject(
|
479
|
-
@job_handle,
|
480
|
-
JobObjectAssociateCompletionPortInformation,
|
481
|
-
port,
|
482
|
-
port.size
|
483
|
-
)
|
484
|
-
|
485
|
-
FFI.raise_windows_error('SetInformationJobObject', FFI.errno) unless bool
|
486
|
-
|
487
|
-
olap = FFI::MemoryPointer.new(Overlapped)
|
488
|
-
bytes = FFI::MemoryPointer.new(:ulong)
|
489
|
-
ckey = FFI::MemoryPointer.new(:uintptr_t)
|
490
|
-
|
491
|
-
while GetQueuedCompletionStatus(io_port, bytes, ckey, olap, INFINITE) &&
|
492
|
-
!(ckey.read_pointer.to_i == @job_handle && bytes.read_ulong == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
|
493
|
-
sleep 0.1
|
494
|
-
end
|
495
|
-
end
|
496
|
-
|
497
|
-
private
|
498
|
-
|
499
|
-
# Automatically close job object when it goes out of scope.
|
500
|
-
#
|
501
|
-
def self.finalize(handle, closed)
|
502
|
-
proc{ CloseHandle(handle) unless closed }
|
503
|
-
end
|
504
|
-
end
|
505
|
-
end
|
1
|
+
require_relative 'job/constants'
|
2
|
+
require_relative 'job/functions'
|
3
|
+
require_relative 'job/structs'
|
4
|
+
require_relative '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.4'
|
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
|
+
# The name of the job, if specified.
|
45
|
+
attr_reader :job_name
|
46
|
+
|
47
|
+
alias :name :job_name
|
48
|
+
|
49
|
+
# Create a new Job object identified by +name+. If no name is provided
|
50
|
+
# then an anonymous job is created.
|
51
|
+
#
|
52
|
+
# If the job already exists then the existing job is opened instead, unless
|
53
|
+
# the +open_existing+ method is false. In that case an error is
|
54
|
+
# raised.
|
55
|
+
#
|
56
|
+
# The +security+ argument accepts a raw SECURITY_ATTRIBUTES struct that is
|
57
|
+
# passed to the CreateJobObject function internally.
|
58
|
+
#
|
59
|
+
def initialize(name = nil, open_existing = true, security = nil)
|
60
|
+
raise TypeError unless name.is_a?(String) if name
|
61
|
+
|
62
|
+
@job_name = name
|
63
|
+
@process_list = []
|
64
|
+
@closed = false
|
65
|
+
|
66
|
+
@job_handle = CreateJobObject(security, name)
|
67
|
+
|
68
|
+
if @job_handle == 0
|
69
|
+
FFI.raise_windows_error('CreateJobObject', FFI.errno)
|
70
|
+
end
|
71
|
+
|
72
|
+
if FFI.errno == ERROR_ALREADY_EXISTS && !open_existing
|
73
|
+
raise ArgumentError, "job '#{name}' already exists"
|
74
|
+
end
|
75
|
+
|
76
|
+
if block_given?
|
77
|
+
begin
|
78
|
+
yield self
|
79
|
+
ensure
|
80
|
+
close
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
ObjectSpace.define_finalizer(self, self.class.finalize(@job_handle, @closed))
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add process +pid+ to the job object. Process ID's added to the
|
88
|
+
# job are tracked via the Job#process_list accessor.
|
89
|
+
#
|
90
|
+
# Note that once a process is added to a job, the association cannot be
|
91
|
+
# broken. A process can be associated with more than one job in a
|
92
|
+
# hierarchy of nested jobs, however.
|
93
|
+
#
|
94
|
+
# You may add a maximum of 100 processes per job.
|
95
|
+
#
|
96
|
+
def add_process(pid)
|
97
|
+
if @process_list.size > 99
|
98
|
+
raise ArgumentError, "maximum number of processes reached"
|
99
|
+
end
|
100
|
+
|
101
|
+
phandle = OpenProcess(PROCESS_ALL_ACCESS, false, pid)
|
102
|
+
|
103
|
+
if phandle == 0
|
104
|
+
FFI.raise_windows_error('OpenProcess', FFI.errno)
|
105
|
+
end
|
106
|
+
|
107
|
+
pbool = FFI::MemoryPointer.new(:int)
|
108
|
+
|
109
|
+
IsProcessInJob(phandle, 0, pbool)
|
110
|
+
|
111
|
+
if pbool.read_int == 0
|
112
|
+
unless AssignProcessToJobObject(@job_handle, phandle)
|
113
|
+
FFI.raise_windows_error('AssignProcessToJobObject', FFI.errno)
|
114
|
+
end
|
115
|
+
@process_list << pid
|
116
|
+
else
|
117
|
+
raise ArgumentError, "pid #{pid} is already part of a job"
|
118
|
+
end
|
119
|
+
|
120
|
+
pid
|
121
|
+
end
|
122
|
+
|
123
|
+
# Close the job object.
|
124
|
+
#
|
125
|
+
def close
|
126
|
+
CloseHandle(@job_handle) if @job_handle
|
127
|
+
@closed = true
|
128
|
+
end
|
129
|
+
|
130
|
+
# Kill all processes associated with the job object that are
|
131
|
+
# associated with the current process.
|
132
|
+
#
|
133
|
+
# Note that killing a process does not dissociate it from the job.
|
134
|
+
#
|
135
|
+
def kill
|
136
|
+
unless TerminateJobObject(@job_handle, Process.pid)
|
137
|
+
FFI.raise_windows_error('TerminateJobObject', FFI.errno)
|
138
|
+
end
|
139
|
+
|
140
|
+
@process_list = []
|
141
|
+
end
|
142
|
+
|
143
|
+
alias terminate kill
|
144
|
+
|
145
|
+
# Set various job limits. Possible options are:
|
146
|
+
#
|
147
|
+
# * active_process => Numeric
|
148
|
+
# Establishes a maximum number of simultaneously active processes
|
149
|
+
# associated with the job.
|
150
|
+
#
|
151
|
+
# * affinity => Numeric
|
152
|
+
# Causes all processes associated with the job to use the same
|
153
|
+
# processor affinity.
|
154
|
+
#
|
155
|
+
# * breakaway_ok => Boolean
|
156
|
+
# If any process associated with the job creates a child process using
|
157
|
+
# the CREATE_BREAKAWAY_FROM_JOB flag while this limit is in effect, the
|
158
|
+
# child process is not associated with the job.
|
159
|
+
#
|
160
|
+
# * die_on_unhandled_exception => Boolean
|
161
|
+
# Forces a call to the SetErrorMode function with the SEM_NOGPFAULTERRORBOX
|
162
|
+
# flag for each process associated with the job. If an exception occurs
|
163
|
+
# and the system calls the UnhandledExceptionFilter function, the debugger
|
164
|
+
# will be given a chance to act. If there is no debugger, the functions
|
165
|
+
# returns EXCEPTION_EXECUTE_HANDLER. Normally, this will cause termination
|
166
|
+
# of the process with the exception code as the exit status.
|
167
|
+
#
|
168
|
+
# * job_memory => Numeric
|
169
|
+
# Causes all processes associated with the job to limit the job-wide
|
170
|
+
# sum of their committed memory. When a process attempts to commit
|
171
|
+
# memory that would exceed the job-wide limit, it fails. If the job
|
172
|
+
# object is associated with a completion port, a
|
173
|
+
# JOB_OBJECT_MSG_JOB_MEMORY_LIMIT message is sent to the completion
|
174
|
+
# port.
|
175
|
+
#
|
176
|
+
# * job_time => Numeric
|
177
|
+
# Establishes a user-mode execution time limit for the job.
|
178
|
+
#
|
179
|
+
# * kill_on_job_close => Boolean
|
180
|
+
# Causes all processes associated with the job to terminate when the
|
181
|
+
# last handle to the job is closed.
|
182
|
+
#
|
183
|
+
# * minimum_working_set_size => Numeric
|
184
|
+
# Causes all processes associated with the job to use the same minimum
|
185
|
+
# set size. If the job is nested, the effective working set size is the
|
186
|
+
# smallest working set size in the job chain.
|
187
|
+
#
|
188
|
+
# * maximum_working_set_size => Numeric
|
189
|
+
# Causes all processes associated with the job to use the same maximum
|
190
|
+
# set size. If the job is nested, the effective working set size is the
|
191
|
+
# smallest working set size in the job chain.
|
192
|
+
#
|
193
|
+
# * per_job_user_time_limit
|
194
|
+
# The per-job user-mode execution time limit, in 100-nanosecond ticks.
|
195
|
+
# The system adds the current time of the processes associated with the
|
196
|
+
# job to this limit.
|
197
|
+
#
|
198
|
+
# For example, if you set this limit to 1 minute, and the job has a
|
199
|
+
# process that has accumulated 5 minutes of user-mode time, the limit
|
200
|
+
# actually enforced is 6 minutes.
|
201
|
+
#
|
202
|
+
# The system periodically checks to determine whether the sum of the
|
203
|
+
# user-mode execution time for all processes is greater than this
|
204
|
+
# end-of-job limit. If so all processes are terminated.
|
205
|
+
#
|
206
|
+
# * per_process_user_time_limit
|
207
|
+
# The per-process user-mode execution time limit, in 100-nanosecond
|
208
|
+
# ticks. The system periodically checks to determine whether each
|
209
|
+
# process associated with the job has accumulated more user-mode time
|
210
|
+
# than the set limit. If it has, the process is terminated.
|
211
|
+
# If the job is nested, the effective limit is the most restrictive
|
212
|
+
# limit in the job chain.
|
213
|
+
#
|
214
|
+
# * preserve_job_time => Boolean
|
215
|
+
# Preserves any job time limits you previously set. As long as this flag
|
216
|
+
# is set, you can establish a per-job time limit once, then alter other
|
217
|
+
# limits in subsequent calls. This flag cannot be used with job_time.
|
218
|
+
#
|
219
|
+
# * priority_class => Numeric
|
220
|
+
# Causes all processes associated with the job to use the same priority
|
221
|
+
# class, e.g. ABOVE_NORMAL_PRIORITY_CLASS.
|
222
|
+
#
|
223
|
+
# * process_memory => Numeric
|
224
|
+
# Causes all processes associated with the job to limit their committed
|
225
|
+
# memory. When a process attempts to commit memory that would exceed
|
226
|
+
# the per-process limit, it fails. If the job object is associated with
|
227
|
+
# a completion port, a JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT message is
|
228
|
+
# sent to the completion port. If the job is nested, the effective
|
229
|
+
# memory limit is the most restrictive memory limit in the job chain.
|
230
|
+
#
|
231
|
+
# * process_time => Numeric
|
232
|
+
# Establishes a user-mode execution time limit for each currently
|
233
|
+
# active process and for all future processes associated with the job.
|
234
|
+
#
|
235
|
+
# * scheduling_class => Numeric
|
236
|
+
# Causes all processes in the job to use the same scheduling class. If
|
237
|
+
# the job is nested, the effective scheduling class is the lowest
|
238
|
+
# scheduling class in the job chain.
|
239
|
+
#
|
240
|
+
# * silent_breakaway_ok => Boolean
|
241
|
+
# Allows any process associated with the job to create child processes
|
242
|
+
# that are not associated with the job. If the job is nested and its
|
243
|
+
# immediate job object allows breakaway, the child process breaks away
|
244
|
+
# from the immediate job object and from each job in the parent job chain,
|
245
|
+
# moving up the hierarchy until it reaches a job that does not permit
|
246
|
+
# breakaway. If the immediate job object does not allow breakaway, the
|
247
|
+
# child process does not break away even if jobs in its parent job
|
248
|
+
# chain allow it.
|
249
|
+
#
|
250
|
+
# * subset_affinity => Numeric
|
251
|
+
# Allows processes to use a subset of the processor affinity for all
|
252
|
+
# processes associated with the job.
|
253
|
+
#--
|
254
|
+
# The options are based on the LimitFlags of the
|
255
|
+
# JOBOBJECT_BASIC_LIMIT_INFORMATION struct.
|
256
|
+
#
|
257
|
+
def configure_limit(options = {})
|
258
|
+
unless options.is_a?(Hash)
|
259
|
+
raise TypeError, "argument to configure must be a hash"
|
260
|
+
end
|
261
|
+
|
262
|
+
# Validate options
|
263
|
+
options.each{ |key,value|
|
264
|
+
unless VALID_OPTIONS.include?(key.to_s.downcase)
|
265
|
+
raise ArgumentError, "invalid option '#{key}'"
|
266
|
+
end
|
267
|
+
}
|
268
|
+
|
269
|
+
flags = 0
|
270
|
+
struct = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
|
271
|
+
|
272
|
+
if options[:active_process]
|
273
|
+
flags |= JOB_OBJECT_LIMIT_ACTIVE_PROCESS
|
274
|
+
struct[:BasicInformatin][:ActiveProcessLimit] = options[:active_process]
|
275
|
+
end
|
276
|
+
|
277
|
+
if options[:affinity]
|
278
|
+
flags |= JOB_OBJECT_LIMIT_AFFINITY
|
279
|
+
struct[:BasicLimitInformation][:Affinity] = options[:affinity]
|
280
|
+
end
|
281
|
+
|
282
|
+
if options[:breakaway_ok]
|
283
|
+
flags |= JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
284
|
+
end
|
285
|
+
|
286
|
+
if options[:die_on_unhandled_exception]
|
287
|
+
flags |= JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
|
288
|
+
end
|
289
|
+
|
290
|
+
if options[:job_memory]
|
291
|
+
flags |= JOB_OBJECT_LIMIT_JOB_MEMORY
|
292
|
+
struct[:JobMemoryLimit] = options[:job_memory]
|
293
|
+
end
|
294
|
+
|
295
|
+
if options[:per_job_user_time_limit]
|
296
|
+
flags |= JOB_OBJECT_LIMIT_JOB_TIME
|
297
|
+
struct[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart] = options[:per_job_user_time_limit]
|
298
|
+
end
|
299
|
+
|
300
|
+
if options[:kill_on_job_close]
|
301
|
+
flags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
302
|
+
end
|
303
|
+
|
304
|
+
if options[:preserve_job_time]
|
305
|
+
flags |= JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME
|
306
|
+
end
|
307
|
+
|
308
|
+
if options[:priority_class]
|
309
|
+
flags |= JOB_OBJECT_LIMIT_PRIORITY_CLASS
|
310
|
+
struct[:BasicLimitInformation][:PriorityClass] = options[:priority_class]
|
311
|
+
end
|
312
|
+
|
313
|
+
if options[:process_memory]
|
314
|
+
flags |= JOB_OBJECT_LIMIT_PROCESS_MEMORY
|
315
|
+
struct[:ProcessMemoryLimit] = options[:process_memory]
|
316
|
+
end
|
317
|
+
|
318
|
+
if options[:process_time]
|
319
|
+
flags |= JOB_OBJECT_LIMIT_PROCESS_TIME
|
320
|
+
struct[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart] = options[:process_time]
|
321
|
+
end
|
322
|
+
|
323
|
+
if options[:scheduling_class]
|
324
|
+
flags |= JOB_OBJECT_LIMIT_SCHEDULING_CLASS
|
325
|
+
struct[:BasicLimitInformation][:SchedulingClass] = options[:scheduling_class]
|
326
|
+
end
|
327
|
+
|
328
|
+
if options[:silent_breakaway_ok]
|
329
|
+
flags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
|
330
|
+
end
|
331
|
+
|
332
|
+
if options[:subset_affinity]
|
333
|
+
flags |= JOB_OBJECT_LIMIT_SUBSET_AFFINITY | JOB_OBJECT_LIMIT_AFFINITY
|
334
|
+
end
|
335
|
+
|
336
|
+
if options[:minimum_working_set_size]
|
337
|
+
flags |= JOB_OBJECT_LIMIT_WORKINGSET
|
338
|
+
struct[:BasicLimitInformation][:MinimumWorkingSetSize] = options[:minimum_working_set_size]
|
339
|
+
end
|
340
|
+
|
341
|
+
if options[:maximum_working_set_size]
|
342
|
+
flags |= JOB_OBJECT_LIMIT_WORKINGSET
|
343
|
+
struct[:BasicLimitInformation][:MaximumWorkingSetSize] = options[:maximum_working_set_size]
|
344
|
+
end
|
345
|
+
|
346
|
+
struct[:BasicLimitInformation][:LimitFlags] = flags
|
347
|
+
|
348
|
+
bool = SetInformationJobObject(
|
349
|
+
@job_handle,
|
350
|
+
JobObjectExtendedLimitInformation,
|
351
|
+
struct,
|
352
|
+
struct.size
|
353
|
+
)
|
354
|
+
|
355
|
+
unless bool
|
356
|
+
FFI.raise_windows_error('SetInformationJobObject', FFI.errno)
|
357
|
+
end
|
358
|
+
|
359
|
+
options
|
360
|
+
end
|
361
|
+
|
362
|
+
# Return a list of process ids that are part of the job.
|
363
|
+
#
|
364
|
+
def process_list
|
365
|
+
info = JOBOBJECT_BASIC_PROCESS_ID_LIST.new
|
366
|
+
|
367
|
+
bool = QueryInformationJobObject(
|
368
|
+
@job_handle,
|
369
|
+
JobObjectBasicProcessIdList,
|
370
|
+
info,
|
371
|
+
info.size,
|
372
|
+
nil
|
373
|
+
)
|
374
|
+
|
375
|
+
unless bool
|
376
|
+
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
377
|
+
end
|
378
|
+
|
379
|
+
info[:ProcessIdList].to_a[0...info[:NumberOfProcessIdsInList]]
|
380
|
+
end
|
381
|
+
|
382
|
+
# Returns an AccountInfoStruct that shows various job accounting
|
383
|
+
# information, such as total user time, total kernel time, the
|
384
|
+
# total number of processes, and so on.
|
385
|
+
#
|
386
|
+
def account_info
|
387
|
+
info = JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION.new
|
388
|
+
|
389
|
+
bool = QueryInformationJobObject(
|
390
|
+
@job_handle,
|
391
|
+
JobObjectBasicAndIoAccountingInformation,
|
392
|
+
info,
|
393
|
+
info.size,
|
394
|
+
nil
|
395
|
+
)
|
396
|
+
|
397
|
+
unless bool
|
398
|
+
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
399
|
+
end
|
400
|
+
|
401
|
+
struct = AccountInfo.new(
|
402
|
+
info[:BasicInfo][:TotalUserTime][:QuadPart],
|
403
|
+
info[:BasicInfo][:TotalKernelTime][:QuadPart],
|
404
|
+
info[:BasicInfo][:ThisPeriodTotalUserTime][:QuadPart],
|
405
|
+
info[:BasicInfo][:ThisPeriodTotalKernelTime][:QuadPart],
|
406
|
+
info[:BasicInfo][:TotalPageFaultCount],
|
407
|
+
info[:BasicInfo][:TotalProcesses],
|
408
|
+
info[:BasicInfo][:ActiveProcesses],
|
409
|
+
info[:BasicInfo][:TotalTerminatedProcesses],
|
410
|
+
info[:IoInfo][:ReadOperationCount],
|
411
|
+
info[:IoInfo][:WriteOperationCount],
|
412
|
+
info[:IoInfo][:OtherOperationCount],
|
413
|
+
info[:IoInfo][:ReadTransferCount],
|
414
|
+
info[:IoInfo][:WriteTransferCount],
|
415
|
+
info[:IoInfo][:OtherTransferCount]
|
416
|
+
)
|
417
|
+
|
418
|
+
struct
|
419
|
+
end
|
420
|
+
|
421
|
+
# Return limit information for the process group.
|
422
|
+
#
|
423
|
+
def limit_info
|
424
|
+
info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION.new
|
425
|
+
|
426
|
+
bool = QueryInformationJobObject(
|
427
|
+
@job_handle,
|
428
|
+
JobObjectExtendedLimitInformation,
|
429
|
+
info,
|
430
|
+
info.size,
|
431
|
+
nil
|
432
|
+
)
|
433
|
+
|
434
|
+
unless bool
|
435
|
+
FFI.raise_windows_error('QueryInformationJobObject', FFI.errno)
|
436
|
+
end
|
437
|
+
|
438
|
+
struct = LimitInfo.new(
|
439
|
+
info[:BasicLimitInformation][:PerProcessUserTimeLimit][:QuadPart],
|
440
|
+
info[:BasicLimitInformation][:PerJobUserTimeLimit][:QuadPart],
|
441
|
+
info[:BasicLimitInformation][:LimitFlags],
|
442
|
+
info[:BasicLimitInformation][:MinimumWorkingSetSize],
|
443
|
+
info[:BasicLimitInformation][:MaximumWorkingSetSize],
|
444
|
+
info[:BasicLimitInformation][:ActiveProcessLimit],
|
445
|
+
info[:BasicLimitInformation][:Affinity],
|
446
|
+
info[:BasicLimitInformation][:PriorityClass],
|
447
|
+
info[:BasicLimitInformation][:SchedulingClass],
|
448
|
+
info[:IoInfo][:ReadOperationCount],
|
449
|
+
info[:IoInfo][:WriteOperationCount],
|
450
|
+
info[:IoInfo][:OtherOperationCount],
|
451
|
+
info[:IoInfo][:ReadTransferCount],
|
452
|
+
info[:IoInfo][:WriteTransferCount],
|
453
|
+
info[:IoInfo][:OtherTransferCount],
|
454
|
+
info[:ProcessMemoryLimit],
|
455
|
+
info[:JobMemoryLimit],
|
456
|
+
info[:PeakProcessMemoryUsed],
|
457
|
+
info[:PeakJobMemoryUsed]
|
458
|
+
)
|
459
|
+
|
460
|
+
struct
|
461
|
+
end
|
462
|
+
|
463
|
+
# Waits for the processes in the job to terminate.
|
464
|
+
#--
|
465
|
+
# See http://blogs.msdn.com/b/oldnewthing/archive/2013/04/05/10407778.aspx
|
466
|
+
#
|
467
|
+
def wait
|
468
|
+
io_port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 1)
|
469
|
+
|
470
|
+
if io_port == 0
|
471
|
+
FFI.raise_windows_error('CreateIoCompletionPort', FFI.errno)
|
472
|
+
end
|
473
|
+
|
474
|
+
port = JOBOBJECT_ASSOCIATE_COMPLETION_PORT.new
|
475
|
+
port[:CompletionKey] = @job_handle
|
476
|
+
port[:CompletionPort] = io_port
|
477
|
+
|
478
|
+
bool = SetInformationJobObject(
|
479
|
+
@job_handle,
|
480
|
+
JobObjectAssociateCompletionPortInformation,
|
481
|
+
port,
|
482
|
+
port.size
|
483
|
+
)
|
484
|
+
|
485
|
+
FFI.raise_windows_error('SetInformationJobObject', FFI.errno) unless bool
|
486
|
+
|
487
|
+
olap = FFI::MemoryPointer.new(Overlapped)
|
488
|
+
bytes = FFI::MemoryPointer.new(:ulong)
|
489
|
+
ckey = FFI::MemoryPointer.new(:uintptr_t)
|
490
|
+
|
491
|
+
while GetQueuedCompletionStatus(io_port, bytes, ckey, olap, INFINITE) &&
|
492
|
+
!(ckey.read_pointer.to_i == @job_handle && bytes.read_ulong == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO)
|
493
|
+
sleep 0.1
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
private
|
498
|
+
|
499
|
+
# Automatically close job object when it goes out of scope.
|
500
|
+
#
|
501
|
+
def self.finalize(handle, closed)
|
502
|
+
proc{ CloseHandle(handle) unless closed }
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|