Pythonic control of VMware -- ctypes does not provide access to 'menu' gui-control.

Z

zapazap

Dear Snake Charming Gurus,

(Was: http://mail.python.org/pipermail/python-list/2004-January/204454.html)

First, a thank you to Tim Golden, Thomas Heller, and Mark Hammond
for your earlier help with this problem. I am uncertain about what
etiquette calls for, but more on that later.

My Objective:
I am trying to control the _VMWare Desktop_ application
pythonically on WinXP Professional OS, with hopes of doing
the same later on Linux. (If it can be done easily on Linux
I might quit the WinXP work now, but I have never done any
automation like this on Linux before.)

My Progress:
I have been trying to extend Simon Brunning's _winGuiAuto_
module to deal with tabs listviews. This was the subject
of my previous thread. I am facing a more fundamental
problem. I am unable even to exercise the application's
main menu, which is something I thought winGuiAuto in its
present form should have been able to do.

My Test:
I have a unittest that successfully opens and closes both
_Notepad_ and _Freecell_, but fails to close _VMware_.

My Suspicion:
I suspect that there is a different "path" (so to speak) to
the menu control in the VMWare application than exists in
the other two applications.

Question #1 (My Etiquette):
Having said "Much Thanks!!" at the end of my first post, should
I have written back in thanks, privately or publicly? I greatly
appreciated the help (and helpful it was), but have long thought
"thanks in advance" covered the "friction" of followup thanks.
But I recently read ESR's "How To Ask Questions The Smart Way"
and am not so sure.

Question #2:
Is my suspicion correct? Might I have to go through several
nested controls before I get to the menu control of some apps?
If so, I think may need to search for some interactive browser
and/or debugger for windows app to reverse engineer the app's
structure. But I fear how much windows arcana I will need to
learn if I am to do this :-( -- advice of any sort would sure
be encouraging.

Question #3:
Perhaps life would be much easier for me if I do it in Linux?
(Since most of my work will eventually be done inside the VMs,
the host OS does not matter so greatly to me.)

Let me say that my goal is for a user to be able to see ONLY the
virtual machine, but be able to have snapshots taken and restored,
devices reassigned, etc, from within the VM without seeing the
host. I will just have the VM talk to the Host via sockets, that
is no problem to hack on. My only difficulty is having a process
on the host actually drive the VMWare application in a way that
does not require the VM to leave full-screen mode. Perhaps this
description might help someone in their helping me?

My failing unittest follows. It was run under Python 2.3.2.

Thank you again!
- Bryan Hann (zapazap AT yahoo DOT com)

---------------snip-------------------------------------------------
# Warning: close all instances of NOTEPAD, FREECELL
# and VMWARE before running this test

import os
import sys
import time
import unittest
import ctypes
import win32con
import win32gui

##########################################
# begin user configurable values
##########################################

# path to vmware folder
VMWARE_HOME = 'D:\\Progra~1\\VMware\\VMware~1'
# number of seconds to wait for the OS to respond to a change request.
DELAY = 2.0
# command prefix
CMD = 'start cmd /c '

##########################################
# end user configurable values
##########################################


class Test_CloseViaMenu(unittest.TestCase):

def test_freecell(self):

# I document this method; the others are similar.

# text to be found in the title of a freecell application
appname = 'FreeCell'
command = CMD + appname
# ensure no currently running instance of the application
assert not get_hwnds(appname)
# launch the application
os.system(command)
# give it time
time.sleep(DELAY)
# get the (unique) hwnd for the application's main window
[hwnd] = get_hwnds(appname)

# find the id for the Game|Exit menu entry
hmenu = ctypes.windll.user32.GetMenu(hwnd)
assert hmenu, 'Application %s has no menu!' % appname
assert menu_name(hmenu,0)=='&Game'
hmenu = ctypes.windll.user32.GetSubMenu(hmenu, 0)
assert menu_name(hmenu,9)=='E&xit'
ExitID = ctypes.windll.user32.GetMenuItemID(hmenu, 9)

# try to close the application
win32gui.PostMessage(hwnd, win32con.WM_COMMAND, ExitID, 0)

# give it time
time.sleep(DELAY)
# it should be gone now
assert not get_hwnds(appname)

def test_notepad(self):

appname = 'Notepad'
command = CMD + appname
assert not get_hwnds(appname)
os.system(command)
time.sleep(DELAY)
[hwnd] = get_hwnds(appname)

# find the id for the File|Exit menu entry
hmenu = ctypes.windll.user32.GetMenu(hwnd)
assert hmenu, 'Application %s has no menu!' % appname
assert menu_name(hmenu,0) == '&File'
hmenu = ctypes.windll.user32.GetSubMenu(hmenu, 0)
assert menu_name(hmenu,8) == 'E&xit'
ExitID = ctypes.windll.user32.GetMenuItemID(hmenu, 8)

win32gui.PostMessage(hwnd, win32con.WM_COMMAND, ExitID, 0)

time.sleep(DELAY)
assert not get_hwnds(appname)

def test_vm(self):

appname ='VMware'
command = CMD + appname
assert not get_hwnds(appname)
os.system(command)
time.sleep(DELAY)
[hwnd] = get_hwnds(appname)

# find the id for the File|Exit menu entry
hmenu = ctypes.windll.user32.GetMenu(hwnd)
assert hmenu, 'Application %s has no menu!' % appname
assert menu_name(hmenu,0)=='&File'
hmenu = ctypes.windll.user32.GetSubMenu(hmenu, 0)
assert menu_name(hmenu,12)=='E&xit'
ExitID = ctypes.windll.user32.GetMenuItemID(hmenu, 8)

win32gui.PostMessage(hwnd, win32con.WM_COMMAND, ExitID, 0)

time.sleep(DELAY)
assert not get_hwnds(self.appname)

def menu_name(hMenu,nn):
"""
Given an hMeny and index nn, return the name of
the nn-th item in the menu.
"""
dummy = ctypes.c_buffer("\000" * 32)
ctypes.windll.user32.GetMenuStringA(
ctypes.c_int(hMenu),
ctypes.c_int(nn),
dummy,
ctypes.c_int(len(dummy)),
win32con.MF_BYPOSITION
)
return dummy.value

def get_hwnds(text):
"""
Return list of all top level hwnds with specified
text in the title.
"""
fn = win32gui.GetWindowText
list = []
win32gui.EnumWindows( (lambda hwnd,acc: acc.append(hwnd)), list)
return [ hwnd for hwnd in list if text in fn(hwnd) ]

if __name__=='__main__':
fn = sys.getwindowsversion
win_ver = {4: "NT", 5: "2K", 6: "XP"}[fn()[0]]
assert win_ver in ["2K","XP"]
if not win_ver == "2K":
print >> sys.stderr, 'warning: tested only on windows 2K'
os.environ['PATH'] = os.environ['PATH'] + ';' + VMWARE_HOME
unittest.main()


##########################################
# The following is the output I got
##########################################

"""
...F
======================================================================
FAIL: test_vm (__main__.Test_CloseViaMenu)
 
M

Michael Geary

zapazap said:
My Test:
I have a unittest that successfully opens and closes both
_Notepad_ and _Freecell_, but fails to close _VMware_.

My Suspicion:
I suspect that there is a different "path" (so to speak) to
the menu control in the VMWare application than exists in
the other two applications.

If you are trying to close an application, I wouldn't access its menu at
all. The normal way to close a Windows app is to send or post a WM_CLOSE
message to its main window.
Question #2:
Is my suspicion correct? Might I have to go through several
nested controls before I get to the menu control of some apps?
If so, I think may need to search for some interactive browser
and/or debugger for windows app to reverse engineer the app's
structure. But I fear how much windows arcana I will need to
learn if I am to do this :-( -- advice of any sort would sure
be encouraging.

If you need to find out more about how an application is structured,
Winspector Spy may be useful:

http://www.windows-spy.com/
Let me say that my goal is for a user to be able to see ONLY the
virtual machine, but be able to have snapshots taken and restored,
devices reassigned, etc, from within the VM without seeing the
host. I will just have the VM talk to the Host via sockets, that
is no problem to hack on. My only difficulty is having a process
on the host actually drive the VMWare application in a way that
does not require the VM to leave full-screen mode. Perhaps this
description might help someone in their helping me?

Won't the user still be able to press Ctrl+Alt (or whatever you set the
hotkey to) to break out of full screen mode and get back to the host system?
# path to vmware folder
VMWARE_HOME = 'D:\\Progra~1\\VMware\\VMware~1'

You shouldn't need to hard code this path. Windows has an AppPath to
VMware.exe, so it should be able to run it with just the filename.
# number of seconds to wait for the OS to respond to a change request.
DELAY = 2.0
# command prefix
CMD = 'start cmd /c '
...
# text to be found in the title of a freecell application
appname = 'FreeCell'
command = CMD + appname
# ensure no currently running instance of the application
assert not get_hwnds(appname)
# launch the application
os.system(command)

Don't use os.system to run a program in Windows. It launches a new instance
of CMD.EXE. You won't notice this if you're running in a console window
already, but if you're not in a console window, os.system will open one
needlessly.

You can use os.startfile to run a program using the ShellExecute function in
Windows, which is much better than using os.system.

Or use win32api.ShellExecute, which gives you more control because it
supports all of the arguments to the ShellExecute function.
# give it time
time.sleep(DELAY)

If you were coding in C, I would recommend using ShellExecuteEx instead of
ShellExecute. ShellExecuteEx gives you the process and thread handles to the
application, so you can use the WaitForInputIdle() function instead of an
unreliable time delay.

I don't see a wrapper for ShellExecuteEx in win32api, but you could probably
do it with ctypes.
# get the (unique) hwnd for the application's main window
[hwnd] = get_hwnds(appname)

# find the id for the Game|Exit menu entry
hmenu = ctypes.windll.user32.GetMenu(hwnd)
assert hmenu, 'Application %s has no menu!' % appname
assert menu_name(hmenu,0)=='&Game'
hmenu = ctypes.windll.user32.GetSubMenu(hmenu, 0)
assert menu_name(hmenu,9)=='E&xit'
ExitID = ctypes.windll.user32.GetMenuItemID(hmenu, 9)

# try to close the application
win32gui.PostMessage(hwnd, win32con.WM_COMMAND, ExitID, 0)

Yeah, definitely skip all the menu stuff and use WM_CLOSE. Untested:

win32gui.PostMessage( hwnd, win32con.WM_CLOSE, 0, 0 )
# give it time
time.sleep(DELAY)
# it should be gone now
assert not get_hwnds(appname)

Better yet, use SendMessage instead of PostMessage:

win32gui.SendMessage( hwnd, win32con.WM_CLOSE, 0, 0 )

The difference between PostMessage and SendMessage is that SendMessage waits
until the target window function returns, so you wouldn't need the delay.
def get_hwnds(text):
"""
Return list of all top level hwnds with specified
text in the title.
"""
fn = win32gui.GetWindowText
list = []
win32gui.EnumWindows( (lambda hwnd,acc: acc.append(hwnd)), list)
return [ hwnd for hwnd in list if text in fn(hwnd) ]

This doesn't seem like a good idea. Some unrelated window could have the
same text in its title.

Instead of GetWindowText, I'd use win32gui.GetClassName and look for the
window class name you want. You can use Winspector Spy to find the class
name for any window.

There's no guarantee that class names will be unique either, but it's a lot
better than using window titles. You could check both.

A general comment about closing applications: As you know, many apps will
put up a confirmation dialog box when you close them. In your Notepad test,
what if the user had edited the text? Notepad would put up a "do you want to
save" dialog and your code would take the assert. VMware always puts up a
confirmation message box if the VM is running. SendMessage(WM_CLOSE) would
avoid this problem by waiting for the WM_CLOSE message to return.

-Mike
 
Z

zapazap

Thanks Mike.

Michael Geary said:
If you are trying to close an application, I wouldn't access its menu at
all. The normal way to close a Windows app is to send or post a WM_CLOSE
message to its main window.

Noted, and my intent is to bypass the menu as much as possible when
driving the app. But without documentation of the app's API, I have
no obvious (to me) recourse but using the menu. In the unittest, I
closed the app with the menu in order to exercise the menu. The app
closing is just a convenient side effect :)
If you need to find out more about how an application is structured,
Winspector Spy may be useful: http://www.windows-spy.com/

And this may be just the thing I am looking for, maybe even to
bypass the menu altogether. You are keeping my hope alive, thanks!
Won't the user still be able to press Ctrl+Alt (or whatever you set the
hotkey to) to break out of full screen mode and get back to the host system?

Probably. My concern is less over determined crakers than over teachers
freaking out when the VM leaves full screen mode -every time- the host
does something to it. (I have hopes of deploying this in a school setting,
with teachers modifying and snapshoting the students' VM, without the
teacher having to know that it is a VM at all.) Disabling the Ctrl+Alt
would indeed be nice, but bulletproofing is not a priority at this stage.

Indeed, at this stage, I want this to be automated for my own purposes,
because I am both lazy and careless in routine tasks on my own machine!
Here I do not mind the VM leaving full screen mode. I am disciplined
enough to "sit on my hands" while it does it's thing. Students may not
though!
Don't use os.system to run a program in Windows....
You can use os.startfile to run a program using the ShellExecute function...
Or use win32api.ShellExecute...
Noted.

If you were coding in C, I would recommend using ShellExecuteEx instead of
ShellExecute. ShellExecuteEx gives you the process and thread handles to the
application, so you can use the WaitForInputIdle() function instead of an
unreliable time delay.

I don't see a wrapper for ShellExecuteEx in win32api, but you could probably
do it with ctypes.

That is new to me, and does sound interesting.
The difference between PostMessage and SendMessage is that SendMessage waits
until the target window function returns, so you wouldn't need the delay.
Ditto.
def get_hwnds(text):
"""
Return list of all top level hwnds with specified
text in the title.
"""
fn = win32gui.GetWindowText
list = []
win32gui.EnumWindows( (lambda hwnd,acc: acc.append(hwnd)), list)
return [ hwnd for hwnd in list if text in fn(hwnd) ]

This doesn't seem like a good idea. Some unrelated window could have the
same text in its title.

Instead of GetWindowText, I'd use win32gui.GetClassName and look for the
window class name you want. You can use Winspector Spy to find the class
name for any window.
...

There's no guarantee that class names will be unique either, but it's a lot
better than using window titles. You could check both.

A general comment about closing applications: As you know, many apps will
put up a confirmation dialog box when you close them. In your Notepad test,
what if the user had edited the text? Notepad would put up a "do you want to
save" dialog and your code would take the assert. VMware always puts up a
confirmation message box if the VM is running. SendMessage(WM_CLOSE) would
avoid this problem by waiting for the WM_CLOSE message to return.

Yup, I agree - for my project, but not this particular unittest. Which
is also why I do not bother to deal with closing dialog boxes. My test
should never cause a dialog to appear, and I wanted to trim it to the
bare minimum that shows the problem. (Perhaps I need not have had the
test exercise both Notepad and Freecell? One working example may have
been enough?)

I will check windows-spy and hope that I do not face too steep a learning
curve regarding windows innards (which the menu route would have avoided!)

Thank you Mike. Your comments are appreciated, and
 

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

No members online now.

Forum statistics

Threads
473,969
Messages
2,570,161
Members
46,710
Latest member
bernietqt

Latest Threads

Top