win32-process 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/CHANGES +98 -0
  2. data/MANIFEST +14 -0
  3. data/README +118 -0
  4. data/lib/win32/process.rb +563 -0
  5. data/test/tc_process.rb +134 -0
  6. 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
@@ -0,0 +1,14 @@
1
+ CHANGES
2
+ README
3
+ MANIFEST
4
+ install.rb
5
+ win32-process.gemspec
6
+
7
+ examples/test_create.rb
8
+ examples/test_fork_wait.rb
9
+ examples/test_fork_waitpid.rb
10
+ examples/test_kill.rb
11
+
12
+ lib/win32/process.rb
13
+
14
+ test/tc_process.rb
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
@@ -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: