To GUI or not to GUI…
May 26, 2011 Leave a comment
That really is the question in my environment. First, a little background, I serve an environment with a bunch of knowledge workers. Unfortunately these knowledge workers cover different areas in their field and more unfortunately because of this diversity it’s hard to break most of them down into nice tidy groups for software distribution. Some may work in areas A, B and C, while others work in areas C, D, and E, and still others work in A, D, and F. That is to say while we have about 5 apps that run 80% of the business functions, another 50 or so fill niche roles depending on what type of work a user is doing, and those don’t always overlap cleanly.
To make matters worse these folks like to move around so we also have a very “self service” culture. For areas where licensing permits we allow users to determine what software load they really need and self install from Run Advertised Programs. However, many of the calculations done with the niche software change on a periodic basis and so that software must be regularly updated for those who have self installed.
This arrangement presents some unique challenges, not the least of which is that when a user self installs from RAP we want them to see the installation progressing and at the end be notified of the installation’s success or failure. However, when updating, where possible we don’t want to bother the user and just want the update to install silently. Normally this would dictate doubling up programs for each piece of software to be deployed, one for the self service install with UI and one for the silent updates, but not being a fan of doing everything twice I’ve come up with what I think is a rather novel solution.
It all starts with a way to determine if the installation in question has been started via Run Advertised Programs or via a mandatory assignment. The former we assume to be a user directed self service installation for which we should show a GUI, and the latter an update which should run silently. So the question becomes how to make that determination.
Enter C++. Ok, quit groaning, I realize that the SCCM crowd is more relegated to the likes of VBScript, PowerShell, and .NET, but in this case I had my reasons. You can’t go mucking around with window handles and the like in VBScript and I use this tool in builds where .NET may not be installed, which excludes .NET or PowerShell. So for the tool to work for me it needed to have no dependencies (I’ve even statically linked the C Runtime in this case). Fortunately if C++ is not your native tongue, I’ve provide all the bits for you and you can just skip ahead to how to use the tool. For those that are interested I’ve included the source so feel free to have a look and/or modify at will, but if you make modifications please respect the GPL and post your source (a link back would be appreciated too but certainly isn’t required).
So how does the ShowGUI.exe tool make the determination that it’s running from Run Advertised Programs? Through some careful examination with one of my favorite Sysinternals tools, Process Explorer, I’ve found that Run Advertised Programs and its related processes always load the module “ccmcore.dll”. So basically the process goes something like:
- Get a window handle to the foreground window
- Find the process ID that belongs to that window handle
- Enumerate the loaded modules to see if one of them is ccmcore.dll
Now, some of you may be asking “If you’ve got the handle for the foreground window why not just make a call to GetWindowText and see if the title is ‘Run Advertised Programs’?”. and this is in fact what the first iteration of ShowGUI did. I happen to be in an organization that is English only, and before starting this blog I used ShowGUI operating that way for quite some time without issue. However as I got ready to release this little utility out into the wild it dawned on me that what this first iteration failed to account for was localization. If you’ve installed any of the Client operating system localization ICPs Run Advertised Programs may not be titled as such. So I thought I would do you my dear reader a favor and retool things to work regardless of the window title.
So that’s all great theory, but how do we put things into practice? Well first another bit of background is in order. The people I work for generally shun all out repackaging. Sure an MST here and there are fine, but a full blown repackage is usually out of the question. Why? Well over the years we’ve been burned by people who didn’t do it well, sure, but more explicitly when you tell a vendor’s tech support that you repackaged their app for installation generally all bets are off. Also, given that most of the people who I work with are by no means coders but most can hack out a little VBScript here and there, most of our installations are wrapped up in a nice bit of script. Further, not being a fan of doing things twice you’ll find that I’ve developed a pretty extensive VBScript snippet library (much will land on this blog throughout future posts so stay tuned) which really reduces most of the scripting to “coding by Lego” as I like to call it. In other words you’ve got all the building blocks, go snap them together to make something cool.
To that end you will also find attached to this blog the “standard script template” that makes using ShowGUI.exe easier. I won’t cover it all here, but we’ll look at a few of the bits relevant to this discussion. First we have a header that among other things includes a silent flag and defaults it to false.
Dim bSilent: bSilent = False ' Silent Flag
Enough for the warm up, moving on we have a call to a function ScriptInit. This is where the meat of the automatic GUI determination happens. ScriptInit looks like this:
Private Sub ScriptInit() ' Auto check for silent mode if helper exe exists Dim objFSOSilent: Set objFSOSilent = CreateObject("Scripting.FileSystemObject") Dim ScriptPathSilent: ScriptPathSilent = objFSOSilent.GetParentFolderName(WScript.ScriptFullName) If Right(ScriptPathSilent, 1) <> "\" Then ScriptPathSilent = ScriptPathSilent & "\" If objFSOSilent.FileExists(ScriptPathSilent & "ShowGUI.exe") Then Dim objShellExecuteSilent: Set objShellExecuteSilent = CreateObject("Wscript.Shell") Dim iFailSafeSilent For iFailSafeSilent = 1 To 5 Dim testResultSilent: testResultSilent = objShellExecuteSilent.Run("""" & ScriptPathSilent & "ShowGUI.exe" & """", 0, True) If testResultSilent <> 1 Then bSilent = Not cBool(testResultSilent) Exit For End If WScript.Sleep(500) Next If testResultSilent = 1 Then bSilent = True ' Fail silent. 1 means that GetForegroundWindow in ShowGUI.exe returned Null ' Usually this is because a window was deactivating or the desktop on which ' the app is running is not the active desktop (i.e. computer is locked etc.) Set objShellExecuteSilent = Nothing End If Set objFSOSilent = Nothing ' Force if flag is Set Dim strArg For Each strArg In WScript.Arguments strArg = UCASE(strArg) Select Case strArg Case "/S", "-S", "--SILENT", "/QN" bSilent = True Case "/QB", "/QB!" bSilent = False Case "/TS" bSilent = True bTaskSequence = True End Select Next End Sub
First ScriptInit checks to see if the ShowGUI helper EXE exists in the script path. If not we’re not doing any auto GUI mode determination and the script will proceed to check for any explicit flags, which we’ll cover that later. If the ShowGUI helper exists in the script path then ScriptInit will go into a loop and execute it up to 5 times at half second intervals. Why execute the helper EXE up to 5 times? Well if you read the documentation for the GetForgroundWindow API you’ll notice that it is possible for there to be no foreground window. If anything failed while executing, ShowGUI returns 1 and usually delaying a few milliseconds and trying again will get a valid result. I picked half a second and 5 tries because they’re nice round numbers, and at most it will delay the program start by 2.5 seconds which is tolerable. One instance where the script will not get a valid result even after waiting is when there is no user logged on or the console is locked. In this scenario ScriptInit defaults to silent mode under the assumption that if there is no foreground window after 5 attempts then there is probably no user logged in or the console is locked and it’s a safe bet the user didn’t initiate the program from RAP.
Once any automated GUI mode detection has been made ScriptInit enumerates all of the command line parameters passed to the script and if any of them are in a list of well known silent or basic GUI flags (i.e. /s, /qn, /qb, etc.) it will override the automatic determination. The assumption is that if you explicitly specify a GUI mode on the command line you probably know what you are doing and that should take precedence over anything the script decided on its own.
So, the bSilent flag is set appropriately, what now? Now the real work happens and this is generally left up to your imagination, but here’s a quick example. Let’s say we’re installing a simple MSI. No changes needed, nothing fancy, just “MSIEXEC.EXE /I MyInstaller.msi /QB” will give you your basic hands off install. Well, to get that wrapped in the standard script template would be:
Dim objShellExecute: Set objShellExecute = CreateObject("Wscript.Shell") iRetVal = objShellExecute.Run("MSIEXEC.EXE /I MyInstaller.msi /QB", 1, True)
However instead of relegating our installation to always use the /QB switch and thus always show a basic UI, what if we did something like this:
Dim objShellExecute: Set objShellExecute = CreateObject("Wscript.Shell") iRetVal = objShellExecute.Run("MSIEXEC.EXE /I MyInstaller.msi " & IIF(bSilent, "/QN", "/QB"), 1, True)
Now if bSilent is true the command will be run with the /QN switch and be silent, otherwise it will be run with the /QB switch and show our basic UI. Viola! One package, program, and script that executes silently when run from a mandatory assignment and with a basic GUI when run by the user from RAP.
To be fair this system isn’t perfect. In a download and install situation if the user checks the box to automatically run the program when the download completes and then closes RAP before the program runs, well, it’s going to run silent. Also, if your environment changes the foreground window all bets are off. In other words you can’t call ShowGUI.exe from a batch file or PowerShell script because they both create a new console window which steals the focus (but if you’re creative I’m sure you can figure out how to run ShowGUI.exe from a VBScript helper and then launch your batch file or PowerShell script from there). If you find a better way I’m all ears (I’ll even do the coding for you), but I posted this question in the MyITForum.com forums several months ago and got no replies, so this is what I’ve come up with.
Keep in mind, the “standard disclaimer” applies. This works for me, it probably will not work for you. In fact, you probably shouldn’t use it at all as it may do things in your environment that can’t be foreseen, and let’s face it I very well may not have any idea what I’m talking about. But, if you’re brave and decide you really really want to give it a try despite my warnings, then you should probably do some extensive testing in a non-production environment first. I take no responsibility if this code and/or information causes your garden to wither and die, your hair to fall out, and your car keys to be forever missing. In fact I take no responsibility for anything this code may or may not do. Use it at your own risk.