Handling multiple processes on Windows

G

Gregory Brown

I started up a ruby group at my university and this weeks 'challenge'
was to take one of your old messy programs from another language and
port it to Ruby, cleaning it up along the way. So I chose a terrible
little Tk based wrapper around mpg123 as my challenge. However, now
I'm thinking I should have picked something that showed how much easier
Ruby is, rather than wander into the gray areas.

The problem with this is, I need to run multiple processes. If I
wanted it to just work on *nix I could just use fork, as I did in the
perl script but a lot of the people in this group are tied down to
Windows (much to my chagrin :-/) so I figured I'd be nice and make my
ported script Windows friendly.

So my question is, how exactly would you go about writing a program
that could fire a process to a command line application, let it run,
give control back to your main program, and kill the process if an
event occurs (Like the pressing of a 'stop' button or even command line
interaction).

I thought about threading but since the threads are done in process I'm
afraid of explosions and complications galore.

So for those of you Windows savvy Rubyist's, what is a good way to do
multi processing in such a scenario?

For the brave, I've included a link to the source I'm porting, which is
in obvious need of cleanup.

http://cvs.sourceforge.net/viewcvs.py/tkmp3/tk-mp3/src/tk-mp3.pl?rev=1.1.1.1&view=auto
 
X

x1

If you run into an issue spawning process and getting command prompts
that pop up, Simon Kroger gave me a very nice fix.. Here's the code
for future reference:
[...]
hmmm ... i will read through the mentioned section ....

I did.

Ok, here is the 'solution': one has to use CreateProcess to spawn the
child. Unfortunately this is a bit more complicated but thanks to
Win32API its all achievable in pure ruby.

The stuff in $0 =3D=3D __FILE__ builds a simple fox gui and starts cmd.exe
if a button is pressed. It uses a pipe to write to the stdin of the
child ('dir\n') and reads back the childs stdout via another pipe and
dumps it to a text control. (The stderror is also mapped but not used)

It's still some work to make it look as easy as popen, but definitaly
possible.

Here is the code:
(some lines are copied from this list and the net)

--------------------------------------------------------------------
require 'Win32API'

NORMAL_PRIORITY_CLASS =3D 0x00000020
STARTUP_INFO_SIZE =3D 68
PROCESS_INFO_SIZE =3D 16
SECURITY_ATTRIBUTES_SIZE =3D 12

ERROR_SUCCESS =3D 0x00
FORMAT_MESSAGE_FROM_SYSTEM =3D 0x1000
FORMAT_MESSAGE_ARGUMENT_ARRAY =3D 0x2000

HANDLE_FLAG_INHERIT =3D 1
HANDLE_FLAG_PROTECT_FROM_CLOSE =3D2

STARTF_USESHOWWINDOW =3D 0x00000001
STARTF_USESTDHANDLES =3D 0x00000100

def raise_last_win_32_error
errorCode =3D Win32API.new("kernel32", "GetLastError", [], 'L').call
if errorCode !=3D ERROR_SUCCESS
params =3D [
'L', # IN DWORD dwFlags,
'P', # IN LPCVOID lpSource,
'L', # IN DWORD dwMessageId,
'L', # IN DWORD dwLanguageId,
'P', # OUT LPSTR lpBuffer,
'L', # IN DWORD nSize,
'P', # IN va_list *Arguments
]

formatMessage =3D Win32API.new("kernel32", "FormatMessage", params, 'L'=
)
msg =3D ' ' * 255
msgLength =3D formatMessage.call(FORMAT_MESSAGE_FROM_SYSTEM +
FORMAT_MESSAGE_ARGUMENT_ARRAY, '', errorCode, 0, msg, 255, '')

msg.gsub!(/\000/, '')
msg.strip!
raise msg
else
raise 'GetLastError returned ERROR_SUCCESS'
end
end

def create_pipe # returns read and write handle
params =3D [
'P', # pointer to read handle
'P', # pointer to write handle
'P', # pointer to security attributes
'L'] # pipe size

createPipe =3D Win32API.new("kernel32", "CreatePipe", params, 'I')

read_handle, write_handle =3D [0].pack('I'), [0].pack('I')
sec_attrs =3D [SECURITY_ATTRIBUTES_SIZE, 0, 1].pack('III')

raise_last_win_32_error if createPipe.Call(read_handle,
write_handle, sec_attrs, 0).zero?

[read_handle.unpack('I')[0], write_handle.unpack('I')[0]]
end

def set_handle_information(handle, flags, value)
params =3D [
'L', # handle to an object
'L', # specifies flags to change
'L'] # specifies new values for flags

setHandleInformation =3D Win32API.new("kernel32",
"SetHandleInformation", params, 'I')
raise_last_win_32_error if setHandleInformation.Call(handle,
flags, value).zero?
nil
end

def close_handle(handle)
closeHandle =3D Win32API.new("kernel32", "CloseHandle", ['L'], 'I')
raise_last_win_32_error if closeHandle.call(handle).zero?
end

def create_process(command, stdin, stdout, stderror)
params =3D [
'L', # IN LPCSTR lpApplicationName
'P', # IN LPSTR lpCommandLine
'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes
'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes
'L', # IN BOOL bInheritHandles
'L', # IN DWORD dwCreationFlags
'L', # IN LPVOID lpEnvironment
'L', # IN LPCSTR lpCurrentDirectory
'P', # IN LPSTARTUPINFOA lpStartupInfo
'P'] # OUT LPPROCESS_INFORMATION lpProcessInformation

startupInfo =3D [STARTUP_INFO_SIZE, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW, 0,
0, 0, stdin, stdout, stderror].pack('IIIIIIIIIIIISSIIII')

processInfo =3D [0, 0, 0, 0].pack('IIII')
command << 0

createProcess =3D Win32API.new("kernel32", "CreateProcess", params, 'I')
raise_last_win_32_error if createProcess.call(0,
command, 0, 0, 1, 0, 0, 0, startupInfo, processInfo).zero?

hProcess, hThread, dwProcessId, dwThreadId =3D processInfo.unpack('LLLL')

close_handle(hProcess)
close_handle(hThread)

[dwProcessId, dwThreadId]
end

def write_file(hFile, buffer)
params =3D [
'L', # handle to file to write to
'P', # pointer to data to write to file
'L', # number of bytes to write
'P', # pointer to number of bytes written
'L'] # pointer to structure for overlapped I/O

written =3D [0].pack('I')
writeFile =3D Win32API.new("kernel32", "WriteFile", params, 'I')

raise_last_win_32_error if writeFile.call(hFile, buffer, buffer.size,
written, 0).zero?

written.unpack('I')
end

def read_file(hFile)
params =3D [
'L', # handle of file to read
'P', # pointer to buffer that receives data
'L', # number of bytes to read
'P', # pointer to number of bytes read
'L'] #pointer to structure for data

number =3D [0].pack('I')
buffer =3D ' ' * 255

readFile =3D Win32API.new("kernel32", "ReadFile", params, 'I')

return '' if readFile.call(hFile, buffer, 255, number, 0).zero?

buffer[0...number.unpack('I')[0]]
end


if $0 =3D=3D __FILE__
require 'fox12'

include Fox

application =3D FXApp.new("popen", "popen")

main =3D FXMainWindow.new(application, "popen", nil, nil, DECOR_ALL,
0, 0, 500, 500, 10, 10, 10, 10, 10, 10)

button =3D FXButton.new(main, "&Do it!", nil, nil, 0,
FRAME_THICK|FRAME_RAISED|LAYOUT_FILL_X|LAYOUT_TOP|LAYOUT_LEFT,
0, 0, 0, 0, 10, 10, 5, 5)

frame =3D FXHorizontalFrame.new(main,
FRAME_THICK|FRAME_SUNKEN|LAYOUT_FILL_X|LAYOUT_FILL_Y,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

edit =3D FXText.new(frame, nil, 0,
LAYOUT_FILL_X|LAYOUT_FILL_Y|TEXT_READONLY, 0, 0, 0, 0)

edit.textColor, edit.backColor =3D 0xFFFFFF, 0

button.connect(SEL_COMMAND) do
cmd =3D 'cmd.exe'
input =3D "dir\nexit\n"

# create 3 pipes
child_in_r, child_in_w =3D create_pipe
child_out_r, child_out_w =3D create_pipe
child_error_r, child_error_w =3D create_pipe

# Ensure the write handle to the pipe for STDIN is not inherited.
set_handle_information(child_in_w, HANDLE_FLAG_INHERIT, 0)
set_handle_information(child_out_r, HANDLE_FLAG_INHERIT, 0)
set_handle_information(child_error_r, HANDLE_FLAG_INHERIT, 0)

processId, threadId =3D create_process(cmd, child_in_r,
child_out_w, child_error_w)

# we have to close the handles, so the pipes terminate with the process
close_handle(child_out_w)
close_handle(child_error_w)

write_file(child_in_w, input)

while !(buffer =3D read_file(child_out_r)).empty?
edit.appendText(buffer.gsub("\r", ''))
end
end

application.create()
main.show(PLACEMENT_SCREEN)
application.run()
end
----------------------------------------------------------------------
 
J

Joel VanderWerf

Gregory said:
So my question is, how exactly would you go about writing a program
that could fire a process to a command line application, let it run,
give control back to your main program, and kill the process if an
event occurs (Like the pressing of a 'stop' button or even command line
interaction).

I thought about threading but since the threads are done in process I'm
afraid of explosions and complications galore.

Use a ruby thread to manage the external process. There are some libs
for this (session is one, I think it will give you better access to
in/out/err streams). But ruby itself has some basic support:

cmd_th = Thread.new do
system "something"
end

if user_presses_stop
cmd_th.kill
end
 
A

Ara.T.Howard

Use a ruby thread to manage the external process. There are some libs
for this (session is one, I think it will give you better access to
in/out/err streams). But ruby itself has some basic support:

cmd_th = Thread.new do
system "something"
end

if user_presses_stop
cmd_th.kill
end

session could be ported to windows using the win32-popen3 call. volunteers?

-a
--
===============================================================================
| email :: ara [dot] t [dot] howard [at] noaa [dot] gov
| phone :: 303.497.6469
| Your life dwells amoung the causes of death
| Like a lamp standing in a strong breeze. --Nagarjuna
===============================================================================
 
D

Daniel Berger

Ara.T.Howard said:
session could be ported to windows using the win32-popen3 call. volunteers?

-a

I think you just did. I'll setup your account. ;)

Dan
 
G

Gregory Brown

Joel said:
Use a ruby thread to manage the external process. There are some libs
for this (session is one, I think it will give you better access to
in/out/err streams). But ruby itself has some basic support:

cmd_th = Thread.new do
system "something"
end

if user_presses_stop
cmd_th.kill
end

This very well may be exactly what I had in mind. I haven't had a
chance to touch a Windows box but will be able to this afternoon so
I'll let you know how it turns out. Thanks!
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,997
Messages
2,570,239
Members
46,827
Latest member
DMUK_Beginner

Latest Threads

Top