win32-process 0.5.1
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.
- data/CHANGES +98 -0
- data/MANIFEST +14 -0
- data/README +118 -0
- data/lib/win32/process.rb +563 -0
- data/test/tc_process.rb +134 -0
- metadata +61 -0
data/CHANGES
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
== 0.5.1 - 24-Aug-2006
|
2
|
+
* Fixed a bug in the Process.create method where the return value for
|
3
|
+
CreateProcess() was being evaluated incorrectly. Thanks go to David Haney
|
4
|
+
for the spot.
|
5
|
+
* Added a slightly nicer error message if an invalid value is passed to the
|
6
|
+
Process.create method.
|
7
|
+
* Removed an extraneous '%' character from an error message.
|
8
|
+
|
9
|
+
== 0.5.0 - 29-Jul-2006
|
10
|
+
* The Process.create method now returns a ProcessInfo struct instead of the
|
11
|
+
pid. Note that you can still grab the pid out of the struct if needed.
|
12
|
+
* The Process.create method now allows the process_inherit and
|
13
|
+
thread_inherit options which determine whether a process or thread
|
14
|
+
object's handles are inheritable, respectively.
|
15
|
+
* The wait and wait2 methods will now work if GetProcessId() isn't defined
|
16
|
+
on your system.
|
17
|
+
* The 'inherit?' hash option was changed to just 'inherit' (no question mark).
|
18
|
+
* Minor doc correction - the 'inherit' option defaults to false, not true.
|
19
|
+
|
20
|
+
== 0.4.2 - 29-May-2006
|
21
|
+
* Fixed a typo/bug in Process.kill for signal 3, where I had accidentally
|
22
|
+
used CTRL_BRK_EVENT instead of the correct CTRL_BREAK_EVENT. Thanks go
|
23
|
+
to Luis Lavena for the spot.
|
24
|
+
|
25
|
+
== 0.4.1 - 13-May-2006
|
26
|
+
* Fixed a bug where spaces in $LOAD_PATH would cause Process.fork to fail.
|
27
|
+
Thanks go to Justin Bailey for the spot and patch.
|
28
|
+
* Added a short synopsis to the README file.
|
29
|
+
|
30
|
+
== 0.4.0 - 7-May-2006
|
31
|
+
* Now pure Ruby, courtesy of the Win32API package.
|
32
|
+
* Now comes with a gem.
|
33
|
+
* Modified Process.kill to send a signal to the current process if pid 0
|
34
|
+
is specified, as per the current 1.8 behavior.
|
35
|
+
* Process.create now accepts the 'environment' key/value, where you can
|
36
|
+
pass a semicolon-separated string as the environment for the new process.
|
37
|
+
* Moved the GUI related options of Process.create to subkeys of the
|
38
|
+
'startup_info' key. See documentation for details.
|
39
|
+
* Replaced Win32::ProcessError with just ProcessError.
|
40
|
+
|
41
|
+
== 0.3.3 - 16-Apr-2006
|
42
|
+
* Fixed a bug in Process.create with regards to creation_flags. Thanks go
|
43
|
+
to Tophe Vigny for the spot.
|
44
|
+
|
45
|
+
== 0.3.2 - 12-Aug-2005
|
46
|
+
* Fixed a bug in Process.kill where a segfault could occur. Thanks go to
|
47
|
+
Bill Atkins for the spot.
|
48
|
+
* Changed VERSION to WIN32_PROCESS_VERSION, because it's a module.
|
49
|
+
* Made the CHANGES, README and doc/process.txt documents rdoc friendly.
|
50
|
+
* Removed the process.rd file.
|
51
|
+
|
52
|
+
== 0.3.1 - 9-Dec-2004
|
53
|
+
* Modified Process.fork to return an actual PID instead of a handle. This
|
54
|
+
means that it should work with Process.kill and other methods that expect
|
55
|
+
an actual PID.
|
56
|
+
* Modified Process.kill to understand the strings "SIGINT", "INT", "SIGBRK",
|
57
|
+
"BRK", "SIGKILL" and "KILL". These correspond to signals 2, 3 and 9,
|
58
|
+
respectively.
|
59
|
+
* Added better $LOAD_PATH handling for Process.fork. Thanks go to Aslak
|
60
|
+
Hellesoy for the spot and the patch.
|
61
|
+
* Replaced all instances of rb_sys_fail(0) with rb_raise(). This is because
|
62
|
+
of a strange bug in the Windows Installer that hasn't been nailed down yet.
|
63
|
+
This means that you can't rescue Errno::ENOENT any more, but will have to
|
64
|
+
rescue StandardError. This only affects Process.kill.
|
65
|
+
* The signals that were formerly 1 and 2 and now 2 and 3. I did this because
|
66
|
+
I wanted the same signal number for SIGINT as it is on *nix.
|
67
|
+
* Added a test_kill.rb script under the examples directory.
|
68
|
+
* Other minor cleanup and corrections.
|
69
|
+
|
70
|
+
== 0.3.0 - 25-Jul-2004
|
71
|
+
* Added the create() method.
|
72
|
+
* Moved the example programs to doc/examples.
|
73
|
+
* Updated the docs, and toned down claims of fork's similarity to the Unix
|
74
|
+
version.
|
75
|
+
* Minor updates to the test suite.
|
76
|
+
|
77
|
+
== 0.2.1 - 17-May-2004
|
78
|
+
* Made all methods module functions, except fork, rather than singleton
|
79
|
+
methods.
|
80
|
+
* Minor doc changes.
|
81
|
+
|
82
|
+
== 0.2.0 - 11-May-2004
|
83
|
+
* Removed the Win32 module/namespace. You no longer 'include Win32' and you
|
84
|
+
no longer need to prefix Process with 'Win32::'.
|
85
|
+
* The fork() method is now a global function as well as a method of the
|
86
|
+
Process module. That means you can call 'fork' instead of 'Process.fork'
|
87
|
+
if you like.
|
88
|
+
* Doc updates to reflect the above changes.
|
89
|
+
|
90
|
+
== 0.1.1 - 6-Mar-2004
|
91
|
+
* Fixed bug where spaces in the directory name caused the fork() method to
|
92
|
+
fail (Park).
|
93
|
+
* Normalized tc_process.rb somewhat to make it easier to run outside of the
|
94
|
+
test directory if desired.
|
95
|
+
* Fixed up tc_process.rb a bit.
|
96
|
+
|
97
|
+
== 0.1.0 - 19-Feb-2004
|
98
|
+
* Initial release
|
data/MANIFEST
ADDED
data/README
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
= Description
|
2
|
+
This package provides the fork, wait, wait2, waitpid, and waitpid2 methods
|
3
|
+
for MS Windows. In addition, it provides a different implementation of the
|
4
|
+
kill method.
|
5
|
+
|
6
|
+
= Prerequisites
|
7
|
+
Ruby 1.8.2 or later.
|
8
|
+
The 'windows-pr' package 0.5.2 or later.
|
9
|
+
The 'sys-proctable' package 0.7.0 or later (test suite only).
|
10
|
+
|
11
|
+
= Supported Platforms
|
12
|
+
This code is supported on Windows NT, Windows 2000 or Windows XP Pro only.
|
13
|
+
It may work on Windows XP Home, but it is not officially supported for that
|
14
|
+
platform.
|
15
|
+
|
16
|
+
= Installation Instructions
|
17
|
+
== Manual Installation
|
18
|
+
ruby test/tc_process.rb (optional)
|
19
|
+
ruby install.rb
|
20
|
+
== Gem Installation
|
21
|
+
ruby win32-process.gemspec
|
22
|
+
win32-process-X.Y.Z-mswin32.gem
|
23
|
+
|
24
|
+
= Synopsis
|
25
|
+
require 'win32/process'
|
26
|
+
|
27
|
+
Process.fork{
|
28
|
+
3.times{
|
29
|
+
puts 'In the child'
|
30
|
+
sleep 1
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
Process.wait
|
35
|
+
puts 'Done'
|
36
|
+
|
37
|
+
= Developer's Notes
|
38
|
+
== The fork and wait methods
|
39
|
+
The fork method is emulated on Win32 by spawning a another Ruby process
|
40
|
+
against $PROGRAM_NAME via the CreateProcess() Win32 API function. It will
|
41
|
+
use its parent's environment and starting directory.
|
42
|
+
|
43
|
+
The various wait methods are a wrapper for the WaitForSingleObject() or
|
44
|
+
WaitForMultipleObjects() Win32 API functions, for the wait* and waitpid*
|
45
|
+
methods, respectively. In the case of wait2 and waitpid2, the exit value
|
46
|
+
is returned via the GetExitCodeProcess() Win32API function.
|
47
|
+
|
48
|
+
For now the waitpid and waitpid2 calls do not accept a second argument.
|
49
|
+
That's because we haven't yet determined if there's a signal we should
|
50
|
+
allow to be sent.
|
51
|
+
|
52
|
+
IMPORTANT!
|
53
|
+
Note that because fork is calling CreateProcess($PROGRAM_NAME), it will
|
54
|
+
start again from the top of the script instead of from the point of the
|
55
|
+
call. We will try to address this in a future release, if possible.
|
56
|
+
|
57
|
+
== The kill method
|
58
|
+
Initially, the kill method will try to get a HANDLE on the PID using the
|
59
|
+
OpenProcess() function. If that succeeds, we know the process is running.
|
60
|
+
|
61
|
+
In the event of signal 2 or signal 3, the GenerateConsoleCtrlEvent()
|
62
|
+
function is used to send a signal to that process. These will not kill
|
63
|
+
GUI processes. It will not (currently) send a signal to remote
|
64
|
+
processes.
|
65
|
+
|
66
|
+
In the event of signal 1 or 4-8, the CreateRemoteThread() function is used
|
67
|
+
after the HANDLE's process has been identified to create a thread
|
68
|
+
within that process. The ExitProcess() function is then sent to that
|
69
|
+
thread.
|
70
|
+
|
71
|
+
In the event of signal 9, the TerminateProcess() function is called. This
|
72
|
+
will almost certainly kill the process, but doesn't give the process a
|
73
|
+
chance to necessarily do any cleanup it might otherwise do.
|
74
|
+
|
75
|
+
== Differences between Ruby's kill and the Win32 Utils kill
|
76
|
+
Ruby does not currently use the CreateRemoteThread() + ExitProcess()
|
77
|
+
approach which is, according to everything I've read, a cleaner approach.
|
78
|
+
This includes not only online research but also Jeffrey Richter's
|
79
|
+
"Advanced Windows Programming, 3rd Edition."
|
80
|
+
|
81
|
+
Also, the way kill handles multiple pids works slightly differently (and
|
82
|
+
better IMHO) in the Win32 Utils version than the way Ruby currently
|
83
|
+
provides.
|
84
|
+
|
85
|
+
The reason Process.kill was originally added, in case anyone cares for
|
86
|
+
historical trivia, is that Ruby 1.6.x did not support Process.kill on
|
87
|
+
Windows at all.
|
88
|
+
|
89
|
+
== Notes
|
90
|
+
It is unlikely you will be able to kill system processes with this module.
|
91
|
+
It's probably better that you don't.
|
92
|
+
|
93
|
+
== Known Bugs
|
94
|
+
None known (though please see the +Details+ section for quirks). Any
|
95
|
+
bugs should be reported on the project page at
|
96
|
+
http://rubyforge.org/projects/win32utils.
|
97
|
+
|
98
|
+
== Future Plans
|
99
|
+
Train Process.fork to execute from the point of the call rather than the
|
100
|
+
top of the script (if possible).
|
101
|
+
|
102
|
+
Other suggestions welcome.
|
103
|
+
|
104
|
+
== License
|
105
|
+
Ruby's
|
106
|
+
|
107
|
+
== Copyright
|
108
|
+
(C) 2003-2006 Daniel J. Berger
|
109
|
+
All Rights Reserved
|
110
|
+
|
111
|
+
== Warranty
|
112
|
+
This package is provided "as is" and without any express or
|
113
|
+
implied warranties, including, without limitation, the implied
|
114
|
+
warranties of merchantability and fitness for a particular purpose.
|
115
|
+
|
116
|
+
== Author(s)
|
117
|
+
Park Heesob
|
118
|
+
Daniel J. Berger
|
@@ -0,0 +1,563 @@
|
|
1
|
+
require 'windows/error'
|
2
|
+
require 'windows/process'
|
3
|
+
require 'windows/synchronize'
|
4
|
+
require 'windows/handle'
|
5
|
+
require 'windows/library'
|
6
|
+
require 'windows/console'
|
7
|
+
require 'windows/window'
|
8
|
+
|
9
|
+
class ProcessError < RuntimeError; end
|
10
|
+
|
11
|
+
module Process
|
12
|
+
WIN32_PROCESS_VERSION = '0.5.1'
|
13
|
+
|
14
|
+
include Windows::Process
|
15
|
+
include Windows::Error
|
16
|
+
include Windows::Library
|
17
|
+
include Windows::Console
|
18
|
+
include Windows::Handle
|
19
|
+
include Windows::Synchronize
|
20
|
+
include Windows::Window
|
21
|
+
extend Windows::Error
|
22
|
+
extend Windows::Process
|
23
|
+
extend Windows::Synchronize
|
24
|
+
extend Windows::Handle
|
25
|
+
extend Windows::Library
|
26
|
+
extend Windows::Console
|
27
|
+
|
28
|
+
# Used by Process.create
|
29
|
+
ProcessInfo = Struct.new("ProcessInfo",
|
30
|
+
:process_handle,
|
31
|
+
:thread_handle,
|
32
|
+
:process_id,
|
33
|
+
:thread_id
|
34
|
+
)
|
35
|
+
|
36
|
+
@child_pids = [] # Static variable used for Process.fork
|
37
|
+
@i = -1 # Static variable used for Process.fork
|
38
|
+
|
39
|
+
# Waits for the given child process to exit and returns that pid.
|
40
|
+
#
|
41
|
+
# Note that the $? (Process::Status) global variable is NOT set. This
|
42
|
+
# may be addressed in a future release.
|
43
|
+
#
|
44
|
+
def waitpid(pid)
|
45
|
+
exit_code = [0].pack('L')
|
46
|
+
handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
|
47
|
+
|
48
|
+
if handle == INVALID_HANDLE_VALUE
|
49
|
+
raise ProcessError, get_last_error
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO: update the $? global variable (if/when possible)
|
53
|
+
status = WaitForSingleObject(handle, INFINITE)
|
54
|
+
|
55
|
+
unless GetExitCodeProcess(handle, exit_code)
|
56
|
+
raise ProcessError, get_last_error
|
57
|
+
end
|
58
|
+
|
59
|
+
CloseHandle(handle)
|
60
|
+
@child_pids.delete(pid)
|
61
|
+
|
62
|
+
# TODO: update the $? global variable (if/when possible)
|
63
|
+
exit_code = exit_code.unpack('L').first
|
64
|
+
|
65
|
+
pid
|
66
|
+
end
|
67
|
+
|
68
|
+
# Waits for the given child process to exit and returns an array containing
|
69
|
+
# the process id and the exit status.
|
70
|
+
#
|
71
|
+
# Note that the $? (Process::Status) global variable is NOT set. This
|
72
|
+
# may be addressed in a future release.
|
73
|
+
#
|
74
|
+
def waitpid2(pid)
|
75
|
+
exit_code = [0].pack('L')
|
76
|
+
handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
|
77
|
+
|
78
|
+
if handle == INVALID_HANDLE_VALUE
|
79
|
+
raise ProcessError, get_last_error
|
80
|
+
end
|
81
|
+
|
82
|
+
# TODO: update the $? global variable (if/when possible)
|
83
|
+
status = WaitForSingleObject(handle, INFINITE)
|
84
|
+
|
85
|
+
unless GetExitCodeProcess(handle, exit_code)
|
86
|
+
raise ProcessError, get_last_error
|
87
|
+
end
|
88
|
+
|
89
|
+
CloseHandle(handle)
|
90
|
+
@child_pids.delete(pid)
|
91
|
+
|
92
|
+
# TODO: update the $? global variable (if/when possible)
|
93
|
+
exit_code = exit_code.unpack('L').first
|
94
|
+
|
95
|
+
[pid, status]
|
96
|
+
end
|
97
|
+
|
98
|
+
# Sends the given +signal+ to an array of process id's. The +signal+ may
|
99
|
+
# be any value from 0 to 9, or the special strings 'SIGINT' (or 'INT'),
|
100
|
+
# 'SIGBRK' (or 'BRK') and 'SIGKILL' (or 'KILL'). An array of successfully
|
101
|
+
# killed pids is returned.
|
102
|
+
#
|
103
|
+
# Signal 0 merely tests if the process is running without killing it.
|
104
|
+
# Signal 2 sends a CTRL_C_EVENT to the process.
|
105
|
+
# Signal 3 sends a CTRL_BRK_EVENT to the process.
|
106
|
+
# Signal 9 kills the process in a harsh manner.
|
107
|
+
# Signals 1 and 4-8 kill the process in a nice manner.
|
108
|
+
#
|
109
|
+
# SIGINT/INT corresponds to signal 2
|
110
|
+
# SIGBRK/BRK corresponds to signal 3
|
111
|
+
# SIGKILL/KILL corresponds to signal 9
|
112
|
+
#
|
113
|
+
# Signals 2 and 3 only affect console processes, and then only if the
|
114
|
+
# process was created with the CREATE_NEW_PROCESS_GROUP flag.
|
115
|
+
#
|
116
|
+
def kill(signal, *pids)
|
117
|
+
case signal
|
118
|
+
when 'SIGINT', 'INT'
|
119
|
+
signal = 2
|
120
|
+
when 'SIGBRK', 'BRK'
|
121
|
+
signal = 3
|
122
|
+
when 'SIGKILL', 'KILL'
|
123
|
+
signal = 9
|
124
|
+
when 0..9
|
125
|
+
# Do nothing
|
126
|
+
else
|
127
|
+
raise ProcessError, "Invalid signal '#{signal}'"
|
128
|
+
end
|
129
|
+
|
130
|
+
killed_pids = []
|
131
|
+
|
132
|
+
pids.each{ |pid|
|
133
|
+
# Send the signal to the current process if the pid is zero
|
134
|
+
if pid == 0
|
135
|
+
pid = Process.pid
|
136
|
+
end
|
137
|
+
|
138
|
+
# No need for full access if the signal is zero
|
139
|
+
if signal == 0
|
140
|
+
access = PROCESS_QUERY_INFORMATION|PROCESS_VM_READ
|
141
|
+
handle = OpenProcess(access, 0 , pid)
|
142
|
+
else
|
143
|
+
handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
|
144
|
+
end
|
145
|
+
|
146
|
+
case signal
|
147
|
+
when 0
|
148
|
+
if handle != 0
|
149
|
+
killed_pids.push(pid)
|
150
|
+
CloseHandle(handle)
|
151
|
+
else
|
152
|
+
# If ERROR_ACCESS_DENIED is returned, we know it's running
|
153
|
+
if GetLastError() == ERROR_ACCESS_DENIED
|
154
|
+
killed_pids.push(pid)
|
155
|
+
else
|
156
|
+
raise ProcessError, get_last_error
|
157
|
+
end
|
158
|
+
end
|
159
|
+
when 2
|
160
|
+
if GenerateConsoleCtrlEvent(CTRL_C_EVENT, pid)
|
161
|
+
killed_pids.push(pid)
|
162
|
+
end
|
163
|
+
when 3
|
164
|
+
if GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid)
|
165
|
+
killed_pids.push(pid)
|
166
|
+
end
|
167
|
+
when 9
|
168
|
+
if TerminateProcess(handle, pid)
|
169
|
+
CloseHandle(handle)
|
170
|
+
killed_pids.push(pid)
|
171
|
+
@child_pids.delete(pid)
|
172
|
+
else
|
173
|
+
raise ProcessError, get_last_error
|
174
|
+
end
|
175
|
+
else
|
176
|
+
if handle != 0
|
177
|
+
thread_id = [0].pack('L')
|
178
|
+
dll = 'kernel32'
|
179
|
+
proc = 'ExitProcess'
|
180
|
+
|
181
|
+
thread = CreateRemoteThread(
|
182
|
+
handle,
|
183
|
+
0,
|
184
|
+
0,
|
185
|
+
GetProcAddress(GetModuleHandle(dll), proc),
|
186
|
+
0,
|
187
|
+
0,
|
188
|
+
thread_id
|
189
|
+
)
|
190
|
+
|
191
|
+
if thread
|
192
|
+
WaitForSingleObject(thread, 5)
|
193
|
+
CloseHandle(handle)
|
194
|
+
killed_pids.push(pid)
|
195
|
+
@child_pids.delete(pid)
|
196
|
+
else
|
197
|
+
CloseHandle(handle)
|
198
|
+
raise ProcessError, get_last_error
|
199
|
+
end
|
200
|
+
else
|
201
|
+
raise ProcessError, get_last_error
|
202
|
+
end
|
203
|
+
@child_pids.delete(pid)
|
204
|
+
end
|
205
|
+
}
|
206
|
+
|
207
|
+
killed_pids
|
208
|
+
end
|
209
|
+
|
210
|
+
# Process.create(key => value, ...) => ProcessInfo
|
211
|
+
#
|
212
|
+
# This is a wrapper for the CreateProcess() function. It executes a process,
|
213
|
+
# returning a ProcessInfo struct. It accepts a hash as an argument.
|
214
|
+
# There are six primary keys:
|
215
|
+
#
|
216
|
+
# * app_name (mandatory)
|
217
|
+
# * inherit (default: false)
|
218
|
+
# * process_inherit (default: false)
|
219
|
+
# * thread_inherit (default: false)
|
220
|
+
# * creation_flags (default: 0)
|
221
|
+
# * cwd (default: Dir.pwd)
|
222
|
+
# * startup_info (default: nil)
|
223
|
+
# * environment (default: nil)
|
224
|
+
#
|
225
|
+
# Of these, the 'app_name' must be specified or an error is raised.
|
226
|
+
#
|
227
|
+
# The startup_info key takes a hash. Its keys are attributes that are
|
228
|
+
# part of the StartupInfo struct, and are generally only meaningful for
|
229
|
+
# GUI or console processes. See the documentation on CreateProcess()
|
230
|
+
# and the StartupInfo struct on MSDN for more information.
|
231
|
+
#
|
232
|
+
# * desktop
|
233
|
+
# * title
|
234
|
+
# * x
|
235
|
+
# * y
|
236
|
+
# * x_size
|
237
|
+
# * y_size
|
238
|
+
# * x_count_chars
|
239
|
+
# * y_count_chars
|
240
|
+
# * fill_attribute
|
241
|
+
# * sw_flags
|
242
|
+
# * startf_flags
|
243
|
+
#
|
244
|
+
# The relevant constants for 'creation_flags', 'sw_flags' and 'startf_flags'
|
245
|
+
# are included in the Windows::Process, Windows::Console and Windows::Window
|
246
|
+
# modules. These come with the windows-pr package, a prerequisite of this
|
247
|
+
# package.
|
248
|
+
#
|
249
|
+
# The ProcessInfo struct contains the following members:
|
250
|
+
#
|
251
|
+
# * process_handle - The handle to the newly created process
|
252
|
+
# * thread_handle - The handle to the primary thread of the newly created
|
253
|
+
# process.
|
254
|
+
# * process_id - Process ID.
|
255
|
+
# * thread_id - Thread ID.
|
256
|
+
#
|
257
|
+
def create(args)
|
258
|
+
unless args.kind_of?(Hash)
|
259
|
+
raise TypeError, 'Expecting hash-style keyword arguments'
|
260
|
+
end
|
261
|
+
|
262
|
+
valid_keys = %w/
|
263
|
+
app_name inherit creation_flags cwd environment startup_info
|
264
|
+
thread_inherit process_inherit
|
265
|
+
/
|
266
|
+
|
267
|
+
valid_si_keys = %/
|
268
|
+
startf_flags desktop title x y x_size y_size x_count_chars
|
269
|
+
y_count_chars fill_attribute sw_flags stdin stdout stderr
|
270
|
+
/
|
271
|
+
|
272
|
+
# Set some default values
|
273
|
+
hash = {
|
274
|
+
'inherit' => 0,
|
275
|
+
'process_inherit' => 0,
|
276
|
+
'thread_inherit' => 0,
|
277
|
+
'creation_flags' => 0,
|
278
|
+
'cwd' => 0
|
279
|
+
}
|
280
|
+
env = 0
|
281
|
+
|
282
|
+
# Validate the keys, and convert symbols and case to lowercase strings.
|
283
|
+
args.each{ |key, val|
|
284
|
+
key = key.to_s.downcase
|
285
|
+
unless valid_keys.include?(key)
|
286
|
+
raise ProcessError, "invalid key '#{key}'"
|
287
|
+
end
|
288
|
+
|
289
|
+
# Convert true to 1 and nil/false to 0.
|
290
|
+
case val
|
291
|
+
when true, false
|
292
|
+
hash[key] = val == false ? 0 : 1
|
293
|
+
when nil
|
294
|
+
hash[key] = 0 # Win32API no likey nil
|
295
|
+
else
|
296
|
+
hash[key] = val
|
297
|
+
end
|
298
|
+
}
|
299
|
+
|
300
|
+
si_hash = {}
|
301
|
+
|
302
|
+
# If the startup_info key is present, validate its subkeys
|
303
|
+
if hash['startup_info']
|
304
|
+
hash['startup_info'].each{ |key, val|
|
305
|
+
key = key.to_s.downcase
|
306
|
+
unless valid_si_keys.include?(key)
|
307
|
+
raise ProcessError, "invalid startup_info key '#{key}'"
|
308
|
+
end
|
309
|
+
si_hash[key] = val
|
310
|
+
}
|
311
|
+
end
|
312
|
+
|
313
|
+
# The +app_name+ key is mandatory
|
314
|
+
unless hash['app_name']
|
315
|
+
raise ProcessError, 'app_name must be specified'
|
316
|
+
end
|
317
|
+
|
318
|
+
# The environment string should be passed as a string of ';' separated
|
319
|
+
# paths.
|
320
|
+
if hash['environment']
|
321
|
+
env = hash['environment'].split(File::PATH_SEPARATOR) << 0.chr
|
322
|
+
env = [env.join("\0")].pack('p*').unpack('L').first
|
323
|
+
end
|
324
|
+
|
325
|
+
startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
326
|
+
startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
|
327
|
+
procinfo = [0,0,0,0].pack('LLLL')
|
328
|
+
|
329
|
+
# Process SECURITY_ATTRIBUTE structure
|
330
|
+
process_security = 0
|
331
|
+
if hash['process_inherit?']
|
332
|
+
process_security = [0,0,0].pack('LLL')
|
333
|
+
process_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
|
334
|
+
process_security[8,4] = [hash['process_inherit?']].pack('L')
|
335
|
+
end
|
336
|
+
|
337
|
+
# Thread SECURITY_ATTRIBUTE structure
|
338
|
+
thread_security = 0
|
339
|
+
if hash['thread_security?']
|
340
|
+
thread_security = [0,0,0].pack('LLL')
|
341
|
+
thread_security[0,4] = [12].pack('L') # sizeof(SECURITY_ATTRIBUTE)
|
342
|
+
thread_security[8,4] = [hash['thread_inherit?']].pack('L')
|
343
|
+
end
|
344
|
+
|
345
|
+
# The bytes not covered here are reserved (null)
|
346
|
+
unless si_hash.empty?
|
347
|
+
startinfo[0,4] = [startinfo.size].pack('L')
|
348
|
+
startinfo[8,4] = [si_hash['desktop']].pack('p*') if si_hash['desktop']
|
349
|
+
startinfo[12,4] = [si_hash['title']].pack('p*') if si_hash['title']
|
350
|
+
startinfo[16,4] = [si_hash['x']].pack('L') if si_hash['x']
|
351
|
+
startinfo[20,4] = [si_hash['y']].pack('L') if si_hash['y']
|
352
|
+
startinfo[24,4] = [si_hash['x_size']].pack('L') if si_hash['x_size']
|
353
|
+
startinfo[28,4] = [si_hash['y_size']].pack('L') if si_hash['y_size']
|
354
|
+
startinfo[32,4] = [si_hash['x_count_chars']].pack('L') if si_hash['x_count_chars']
|
355
|
+
startinfo[36,4] = [si_hash['y_count_chars']].pack('L') if si_hash['y_count_chars']
|
356
|
+
startinfo[40,4] = [si_hash['fill_attribute']].pack('L') if si_hash['fill_attribute']
|
357
|
+
startinfo[44,4] = [si_hash['startf_flags']].pack('L') if si_hash['startf_flags']
|
358
|
+
startinfo[48,2] = [si_hash['sw_flags']].pack('S') if si_hash['sw_flags']
|
359
|
+
startinfo[56,4] = [si_hash['stdin']].pack('L') if si_hash['stdin']
|
360
|
+
startinfo[60,4] = [si_hash['stdout']].pack('L') if si_hash['stdout']
|
361
|
+
startinfo[64,4] = [si_hash['stderr']].pack('L') if si_hash['stderr']
|
362
|
+
end
|
363
|
+
|
364
|
+
bool = CreateProcess(
|
365
|
+
0, # App name
|
366
|
+
hash['app_name'], # Command line
|
367
|
+
process_security, # Process attributes
|
368
|
+
thread_security, # Thread attributes
|
369
|
+
hash['inherit'], # Inherit handles?
|
370
|
+
hash['creation_flags'], # Creation flags
|
371
|
+
env, # Environment
|
372
|
+
hash['cwd'], # Working directory
|
373
|
+
startinfo, # Startup Info
|
374
|
+
procinfo # Process Info
|
375
|
+
)
|
376
|
+
|
377
|
+
unless bool
|
378
|
+
raise ProcessError, "CreateProcess() failed: ", get_last_error
|
379
|
+
end
|
380
|
+
|
381
|
+
ProcessInfo.new(
|
382
|
+
procinfo[0,4].unpack('L').first, # hProcess
|
383
|
+
procinfo[4,4].unpack('L').first, # hThread
|
384
|
+
procinfo[8,4].unpack('L').first, # hProcessId
|
385
|
+
procinfo[12,4].unpack('L').first # hThreadId
|
386
|
+
)
|
387
|
+
end
|
388
|
+
|
389
|
+
# Waits for any child process to exit and returns the process id of that
|
390
|
+
# child.
|
391
|
+
#
|
392
|
+
# Note that the $? (Process::Status) global variable is NOT set. This
|
393
|
+
# may be addressed in a future release.
|
394
|
+
#--
|
395
|
+
# The GetProcessId() function is not defined in Windows 2000 or earlier
|
396
|
+
# so we have to do some extra work for those platforms.
|
397
|
+
#
|
398
|
+
def wait
|
399
|
+
handles = []
|
400
|
+
|
401
|
+
# Windows 2000 or earlier
|
402
|
+
unless defined? GetProcessId
|
403
|
+
pids = []
|
404
|
+
end
|
405
|
+
|
406
|
+
@child_pids.each_with_index{ |pid, i|
|
407
|
+
handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
|
408
|
+
|
409
|
+
if handles[i] == INVALID_HANDLE_VALUE
|
410
|
+
err = "unable to get HANDLE on process associated with pid #{pid}"
|
411
|
+
raise ProcessError, err
|
412
|
+
end
|
413
|
+
|
414
|
+
unless defined? GetProcessId
|
415
|
+
pids[i] = pid
|
416
|
+
end
|
417
|
+
}
|
418
|
+
|
419
|
+
wait = WaitForMultipleObjects(
|
420
|
+
handles.size,
|
421
|
+
handles.pack('L*'),
|
422
|
+
0,
|
423
|
+
INFINITE
|
424
|
+
)
|
425
|
+
|
426
|
+
if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
|
427
|
+
index = wait - WAIT_OBJECT_0
|
428
|
+
handle = handles[index]
|
429
|
+
|
430
|
+
if defined? GetProcessId
|
431
|
+
pid = GetProcessId(handle)
|
432
|
+
else
|
433
|
+
pid = pids[index]
|
434
|
+
end
|
435
|
+
|
436
|
+
@child_pids.delete(pid)
|
437
|
+
handles.each{ |handle| CloseHandle(handle) }
|
438
|
+
return pid
|
439
|
+
end
|
440
|
+
|
441
|
+
nil
|
442
|
+
end
|
443
|
+
|
444
|
+
# Waits for any child process to exit and returns an array containing the
|
445
|
+
# process id and the exit status of that child.
|
446
|
+
#
|
447
|
+
# Note that the $? (Process::Status) global variable is NOT set. This
|
448
|
+
# may be addressed in a future release.
|
449
|
+
#--
|
450
|
+
# The GetProcessId() function is not defined in Windows 2000 or earlier
|
451
|
+
# so we have to do some extra work for those platforms.
|
452
|
+
#
|
453
|
+
def wait2
|
454
|
+
handles = []
|
455
|
+
|
456
|
+
# Windows 2000 or earlier
|
457
|
+
unless defined? GetProcessId
|
458
|
+
pids = []
|
459
|
+
end
|
460
|
+
|
461
|
+
@child_pids.each_with_index{ |pid, i|
|
462
|
+
handles[i] = OpenProcess(PROCESS_ALL_ACCESS, 0, pid)
|
463
|
+
|
464
|
+
if handles[i] == INVALID_HANDLE_VALUE
|
465
|
+
err = "unable to get HANDLE on process associated with pid #{pid}"
|
466
|
+
raise ProcessError, err
|
467
|
+
end
|
468
|
+
|
469
|
+
unless defined? GetProcessId
|
470
|
+
pids[i] = pid
|
471
|
+
end
|
472
|
+
}
|
473
|
+
|
474
|
+
wait = WaitForMultipleObjects(
|
475
|
+
handles.size,
|
476
|
+
handles.pack('L*'),
|
477
|
+
0,
|
478
|
+
INFINITE
|
479
|
+
)
|
480
|
+
|
481
|
+
if wait >= WAIT_OBJECT_0 && wait <= WAIT_OBJECT_0 + @child_pids.size - 1
|
482
|
+
index = wait - WAIT_OBJECT_0
|
483
|
+
handle = handles[index]
|
484
|
+
|
485
|
+
if defined? GetProcessId
|
486
|
+
pid = GetProcessId(handle)
|
487
|
+
else
|
488
|
+
pid = pids[index]
|
489
|
+
end
|
490
|
+
|
491
|
+
exit_code = [0].pack('l')
|
492
|
+
unless GetExitCodeProcess(handle, exit_code)
|
493
|
+
raise get_last_error
|
494
|
+
end
|
495
|
+
|
496
|
+
@child_pids.delete(pid)
|
497
|
+
|
498
|
+
handles.each{ |handle| CloseHandle(handle) }
|
499
|
+
return [pid, exit_code.unpack('l').first]
|
500
|
+
end
|
501
|
+
|
502
|
+
nil
|
503
|
+
end
|
504
|
+
|
505
|
+
# Creates the equivalent of a subshell via the CreateProcess() function.
|
506
|
+
# This behaves in a manner that is similar, but not identical to, the
|
507
|
+
# Kernel.fork method for Unix.
|
508
|
+
#
|
509
|
+
def fork
|
510
|
+
last_arg = ARGV.last
|
511
|
+
|
512
|
+
# Look for the 'child#xxx' tag
|
513
|
+
if last_arg =~ /child#\d+/
|
514
|
+
@i += 1
|
515
|
+
num = last_arg.split('#').last.to_i
|
516
|
+
if num == @i
|
517
|
+
if block_given?
|
518
|
+
status = 0
|
519
|
+
begin
|
520
|
+
yield
|
521
|
+
rescue Exception
|
522
|
+
status = -1 # Any non-zero result is failure
|
523
|
+
ensure
|
524
|
+
return status
|
525
|
+
end
|
526
|
+
end
|
527
|
+
return nil
|
528
|
+
else
|
529
|
+
return false
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Tag the command with the word 'child#xxx' to distinguish it
|
534
|
+
# from the calling process.
|
535
|
+
cmd = 'ruby -I "' + $LOAD_PATH.join(File::PATH_SEPARATOR) << '" "'
|
536
|
+
cmd << File.expand_path($PROGRAM_NAME) << '" ' << ARGV.join(' ')
|
537
|
+
cmd << ' child#' << @child_pids.length.to_s
|
538
|
+
|
539
|
+
startinfo = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
540
|
+
startinfo = startinfo.pack('LLLLLLLLLLLLSSLLLL')
|
541
|
+
procinfo = [0,0,0,0].pack('LLLL')
|
542
|
+
|
543
|
+
rv = CreateProcess(0, cmd, 0, 0, 1, 0, 0, 0, startinfo, procinfo)
|
544
|
+
|
545
|
+
if rv == 0
|
546
|
+
raise ProcessError, get_last_error
|
547
|
+
end
|
548
|
+
|
549
|
+
pid = procinfo[8,4].unpack('L').first
|
550
|
+
@child_pids.push(pid)
|
551
|
+
|
552
|
+
pid
|
553
|
+
end
|
554
|
+
|
555
|
+
module_function :kill, :wait, :wait2, :waitpid, :waitpid2, :create, :fork
|
556
|
+
end
|
557
|
+
|
558
|
+
# Create a global fork method
|
559
|
+
module Kernel
|
560
|
+
def fork(&block)
|
561
|
+
Process.fork(&block)
|
562
|
+
end
|
563
|
+
end
|
data/test/tc_process.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
###############################################################################
|
2
|
+
# tc_process.rb
|
3
|
+
#
|
4
|
+
# Test suite for the win32-process package. This test suite will start
|
5
|
+
# at least two instances of Notepad on your system, which will then
|
6
|
+
# be killed. Requires the sys-proctable package.
|
7
|
+
#
|
8
|
+
# I haven't added a lot of test cases for fork/wait because it's difficult
|
9
|
+
# to run such tests without causing havoc with TestUnit itself. Ideas
|
10
|
+
# welcome.
|
11
|
+
###############################################################################
|
12
|
+
Dir.chdir('..') if File.basename(Dir.pwd) == 'test'
|
13
|
+
$LOAD_PATH.unshift Dir.pwd
|
14
|
+
$LOAD_PATH.unshift Dir.pwd + '/lib'
|
15
|
+
Dir.chdir('test') rescue nil
|
16
|
+
|
17
|
+
require 'test/unit'
|
18
|
+
require 'win32/process'
|
19
|
+
|
20
|
+
begin
|
21
|
+
require 'sys/proctable'
|
22
|
+
rescue LoadError => e
|
23
|
+
site = 'http://www.rubyforge.org/projects/sysutils'
|
24
|
+
STDERR.puts "Stopping!"
|
25
|
+
STDERR.puts "The sys/proctable module is required to run this test suite."
|
26
|
+
STDERR.puts "You can find it at #{site} or the RAA"
|
27
|
+
exit!
|
28
|
+
end
|
29
|
+
|
30
|
+
include Sys
|
31
|
+
|
32
|
+
IO.popen("notepad")
|
33
|
+
IO.popen("notepad")
|
34
|
+
sleep 1
|
35
|
+
|
36
|
+
$pids = []
|
37
|
+
ProcTable.ps{ |s|
|
38
|
+
next unless s.comm =~ /notepad/i
|
39
|
+
$pids.push(s.pid)
|
40
|
+
}
|
41
|
+
|
42
|
+
class TC_Win32Process < Test::Unit::TestCase
|
43
|
+
def setup
|
44
|
+
@pids = $pids
|
45
|
+
@pid = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_version
|
49
|
+
assert_equal('0.5.1', Process::WIN32_PROCESS_VERSION)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_kill
|
53
|
+
assert_respond_to(Process, :kill)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_kill_expected_errors
|
57
|
+
assert_raises(ArgumentError){ Process.kill }
|
58
|
+
assert_raises(ProcessError){ Process.kill('SIGBOGUS') }
|
59
|
+
assert_raises(ProcessError){ Process.kill(0,9999999) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_kill_signal_0
|
63
|
+
pid = @pids.first
|
64
|
+
assert_nothing_raised{ Process.kill(0, pid) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_kill_signal_1
|
68
|
+
pid = @pids.shift
|
69
|
+
assert_nothing_raised{ Process.kill(1, pid) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_kill_signal_9
|
73
|
+
pid = @pids.pop
|
74
|
+
msg = "Could not find pid #{pid}"
|
75
|
+
assert_nothing_raised(msg){ Process.kill(9,pid) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_fork
|
79
|
+
assert_respond_to(Process, :fork)
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_create
|
83
|
+
assert_respond_to(Process, :create)
|
84
|
+
assert_nothing_raised{
|
85
|
+
@pid = Process.create(
|
86
|
+
:app_name => "notepad.exe",
|
87
|
+
:creation_flags => Process::DETACHED_PROCESS,
|
88
|
+
:process_inherit => false,
|
89
|
+
:thread_inherit => true,
|
90
|
+
:cwd => "C:\\"
|
91
|
+
).process_id
|
92
|
+
}
|
93
|
+
assert_nothing_raised{ Process.kill(1, @pid) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_create_expected_errors
|
97
|
+
assert_raises(TypeError){ Process.create("bogusapp.exe") }
|
98
|
+
assert_raises(ProcessError){ Process.create(:app_name => "bogusapp.exe") }
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_wait
|
102
|
+
assert_respond_to(Process, :wait)
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_wait2
|
106
|
+
assert_respond_to(Process, :wait2)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_waitpid
|
110
|
+
assert_respond_to(Process, :waitpid)
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_waitpid2
|
114
|
+
assert_respond_to(Process, :waitpid2)
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_creation_constants
|
118
|
+
assert_not_nil(Process::CREATE_DEFAULT_ERROR_MODE)
|
119
|
+
assert_not_nil(Process::CREATE_NEW_CONSOLE)
|
120
|
+
assert_not_nil(Process::CREATE_NEW_PROCESS_GROUP)
|
121
|
+
assert_not_nil(Process::CREATE_NO_WINDOW)
|
122
|
+
assert_not_nil(Process::CREATE_SEPARATE_WOW_VDM)
|
123
|
+
assert_not_nil(Process::CREATE_SHARED_WOW_VDM)
|
124
|
+
assert_not_nil(Process::CREATE_SUSPENDED)
|
125
|
+
assert_not_nil(Process::CREATE_UNICODE_ENVIRONMENT)
|
126
|
+
assert_not_nil(Process::DEBUG_ONLY_THIS_PROCESS)
|
127
|
+
assert_not_nil(Process::DEBUG_PROCESS)
|
128
|
+
assert_not_nil(Process::DETACHED_PROCESS)
|
129
|
+
end
|
130
|
+
|
131
|
+
def teardown
|
132
|
+
@pids = []
|
133
|
+
end
|
134
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: win32-process
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.5.1
|
7
|
+
date: 2006-08-24 00:00:00 -06:00
|
8
|
+
summary: Adds fork, wait, wait2, waitpid, waitpid2 and a special kill method
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: djberg96@gmail.com
|
12
|
+
homepage: http://www.rubyforge.org/projects/win32utils
|
13
|
+
rubyforge_project:
|
14
|
+
description: Adds fork, wait, wait2, waitpid, waitpid2 and a special kill method
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Daniel J. Berger
|
31
|
+
files:
|
32
|
+
- lib/win32/process.rb
|
33
|
+
- test/CVS
|
34
|
+
- test/tc_process.rb
|
35
|
+
- CHANGES
|
36
|
+
- CVS
|
37
|
+
- MANIFEST
|
38
|
+
- README
|
39
|
+
test_files:
|
40
|
+
- test/tc_process.rb
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
extra_rdoc_files:
|
44
|
+
- README
|
45
|
+
- CHANGES
|
46
|
+
executables: []
|
47
|
+
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
dependencies:
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: windows-pr
|
55
|
+
version_requirement:
|
56
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.5.2
|
61
|
+
version:
|