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.
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: