VMware View – Implementing Idle User Auto-Logout

We are going live with out first public VMware View terminals this week (Wyse P25 “zero-clients”… nice units).  I had what I expected would be a easy list of “little jobs” to be completed before going live. Famous last words…

One item on the list was implementing an “idle user logout” process.  This process would detect when a View session had gone idle, and would disconnect the session automatically (preferably after prompting the user).  This disconnected session then would be logged out by View Manager after a fixed amount of time.

This proved rather more difficult than I had predicted.  I tried several solutions before arriving at one that worked.  Among the failed solutions:

  • Using Group Policy to configure Remote Desktop Session Manager idle session limits.  The View configuration documents imply that this should work, but it does not.  I expect that the policies would be effective if you were connecting to your View desktops using RDP, but PCoIP sessions just will not disconnect automatically (at least, they would not for me).
  • Using the Windows Task Scheduler to configure a disconnect script that will trigger on idle.  This did not work for two reasons.  First, the Task Scheduler only evaluates for idle conditions every 15 minutes.  Second, for the Task Scheduler, “idle” means not only that the user is not directing mouse and keyboard to the computer, but that the CPU also is not doing anything.  As a result, we could not get consistent auto-logout times.

The solution that we settled on involved the use of a custom screensaver developed by the “Grim Admin”.  “ScreenSaver Operations”:


This is a great little utility that accomplishes what the “WinExit” screensaver used to for Win XP.  (WinExit cannot easily be used on Win7, and is a bit hostile to 64-bit Windows).  Screensaver Operations has a well-written README describing the use of registry entries to control the screensaver globally (i.e. for all users on the computer).  I set these registry operations as Group Policy Preferences, and we are in business.

Two slight complications… since the screensaver is 32-bit, you need to use the “sysnative” filesystem redirector if your want the screensaver to trigger 64-bit executables.  In our case, I wanted the screensaver to launch “tsdiscon.exe” (to disconnect the View session), so I had to use the path:
Additionally, you will need to specify the full path to the screensaver in the Group Policy dialogs (i.e. %SystemRoot%\SysWOW64\Screensaver Operations.scr).  If you fail to do so, the screensaver will appear to be configured in the Control Panel, and you will be able to preview it by clicking the “preview” button, but the screensaver WILL NEVER START.

Ashamedly I will admit that this little challenge too much longer to accomplish than it should have.  No wonder lab managers burn out so easily.

Evaluating Windows 8 Tablets – Samsung ATIV SmartPC Pro (700T)

The journey continues…

The boss approved purchase of a Samsung ATIV SmartPC Pro (the “700T” model).  I wassoooexcited… this was the tablet PC I had been waiting for.  Thin, light, and fully convertible from Ultrabook to slate.  Stylus included, 1080 high-definition display, full Intel i5 processor.  So much to love…

First impressions were really positive.  The build quality seemed really high… solid magnesium case, good keyboard response, fast boot, very responsive Wacom digitizer stylus.  As a tablet, this thing is awesome. And while it is expensize compared to an iPad, it is very cheap compared to the Tablet PCs of yesteryear.

However, I quickly ran into trouble.  When typing with the SmartPC on my lap, the keyboard would frequently disconnect from the display.  It would not fall off, but the tablet component would lose electrical connection to the keyboard, causing typing input to stop.  Sometimes this would happen as often as five times in a single line of text.  Awful!

There were other problems as well.  Like the HP Envy X2, the screen does not tilt back far enough to allow comfortable use of the keyboard on a countertop.  The 1080p display, which is very crisp and bright, is inconvenient to use for remote desktop connections to Server 2008 R2 and earlier hosts (the fonts do not scale for remote desktop sessions, leading to comically tiny print size and rediculiously small buttons and window controls).  The system did not include a TPM chip (that is only available on the models that ship with Win8 Pro… something that was not clear when ordering the device).  And finally, Samsung does not bundle drivers for the SmartPC in any way that is convenient for business deployment.  Re-imaging the systems would be a pain.

It also is worth noting that Microsoft decided that in-place upgrades of retail versions of Win 8 to volume license editions woudl not be supported.  If you want simply to install Win 8 Enterprise over the factory-shipped consumer edition of Win8, you are out of luck.  I also experienced this problem with the HP Envy X2.  For corporate users, volume license installs are strictly a nuke-and-repave operation.  Booooooo!  This is not Samsung’s fault, but the lack of support for business deployment (i.e. driver bundles or driver repository building tools) is a killer for the SmartPC in the enterprise.

I really wanted to love this device, but I really just have to return it.  Consumers seeking a top-performance tablet may love it, but it does not work for this sysadmin.  I am hoping that the Lenovo ThinkPad Helix will work out better.

Evaluating Windows 8 Tablets in the Enterprise – HP Envy X2

In desperation over our inability to tell University employees what they should be looking for in a Windows 8 tablet, I asked the boss if we could get our hands on one of the new Intel “Atom” processor-based Windows 8 tablets (these are the “Clover Trail” Atom processors, designed to compete with ARM-based devices).  I had been wanting to eval a Samsung ATIV Smart PC 500T, but these have been hard to get locally.  Instead, I bought a hot-off-the-shelf HP Envy X2.  This device boasts a well-engineered all-metal shell, full size keyboard dock with full-sized HDMI and SD card readers, and an extra battery in the dock for a claimed 15-hour run life.  It also claims to support an optional digitizer stylus.

I only have just started putting the machine though its paces.  My first impression is that it performs surprisingly well as a standard notebook, but that there will be significant challenges in supporting these types of devices at the same level as our existing business-model Dell systems.  I am not going to bother “reviewing” this tablet… others in the trade can handle that.  Rather, this blog post is going to address the challenges of supporting a consumer tablet in a business environment.

  • Processor:  The new “Clover Trail” Atom processors are 32-bit only.  Surprise!  I though the industry was leaving 32-bt behind, but it appears to be alive and well.  We had made the initial decision to support only 64-bit Windows 8, and have developed only 64-bit baseline images.  I see that this choice will need to be reconsidered.
  • EFI/UEFI:  These new systems boot using EFI, with emulated BIOS, with the “SafeBoot” option enabled.  Out of the box, you cannot boot to USB because the SafeBoot prohibits this.  You need to load your OS to change EFI options.  EFI is not identical between systems, so navigating the process of booting to deployment/maintenance media will be a tough challenge for technicians to work through.  I actually was completely unable to boot the Envy X2 to an USB flash drive, running either WinPE (MDT boot media) or the FreeDOS-based(?) CloneZilla  live CD.  Bummer.
  • Drivers:  Most new tablets are aimed at the consumer market.  As a result, the vendors make little effort to package drivers in a way that is convenient for local IT staff to integrate into on-premise Windows deployment tools.  The Envy X2 is no exception.  A small handful of one-off driver installers are available, including a big bundle of Intel Chipset drivers.  The chipset drivers were critical in getting a freshly installed Windows 8 Enterprise OS working with the hardware.
  • Windows Editions:  This tablet shipped with “Windows 8″.  Not “Home”, not “Professional”, not “Ultimate”.  I tried performing an in-place SKU upgrade to Windows 8 Enterprise, but setup.exe said that this was not supported, so I needed to do a full OS install.  This process worked, but it was seriously aggravating to have to boot to the OEM OS, start the Enterprise OS install, re-install all of the required drivers, then clean up the original OS install.  Our users will not want to have to deal with this, and it will make our IT support staff very tired.
  • Hardware:  No Ethernet.  Unfortunately, our MDT/LTI deployment tools are designed to run over Ethernet, not Wi-Fi.  The LTI scripts actually will terminate if a Wi-Fi connection is detected.  Of course, application-only LTI task sequences really should run just fine over wireless, but the scripts still will not run over wireless.  We either will have to comment out the Wi-Fi checks, or require that the person launching LTI have a USB Ethernet dongle handy.

So… a lot of challenges.  More details as time permits.

Oh, one small “review style” note.  I decided to evaluate this tablet because HP claims that it supports an optional stylus.  However, the stylus for this device is not actually available for sale at this time.  Further, the device does not use the common Wacom or N-Trig digitizers, so buying a spare “Bamboo” stylus will not help you here.  HP has chosen to use the new “Atmel” integrated touch/pen sensors, and as such an Atmel-compatible stylus is required.  I cannot find these on the market anywhere.  As a result I cannot make any recommendation for or against the purchase of this device for Tablet PC enthusiasts.  I don’t even know if the stylus will be available for sale before the return period for this device expires.


I returned this tablet.  Why?  It was not the screen resolution, which I though would be a problem but was not.  There were three primary reasons:

  1. It was not possible to determine the quality of the digitizer within the return period of the tablet.  I was unwilling to accept the risk of having a low-quality stylus for note taking.
  2. Keyboard dock quality was low.  The keyboard itself was reasonably good, but the trackpad was very annoying.  The texture was awful, and it was overly sensitive to the slightest palm brushes.  Given the small size of the keyboard deck, it was impossible to avoid brushing the trackpad, too.  Also, the screen did not tip back far enough for comfort when used on a countertop or other waist-height surface.
  3. Business deployment essentially was unsupportable.  HP support could not assist me with initialization of the TPM chip for BitLocker.  It appeared that a TPM was present, but there was no option in BIOS to reset the TPM, and the OS could not get ownership of the chip.  Also, the total lack of driver bundles would make deployment using MDT very difficult.
  4. The graphics card could not drive my external display at native resolution.  It maxed out at 1080p.

I did quite like this tablet, though.  Consumers seeking an additional computer for the road may really enjoy using it.  For fussy power users like me, it was close butnot quite there.

Moving User Profiles with PowerShell

Something that comes up with some frequency on Terminal Servers (or “Remote Desktop Servers”), but perhaps sometimes in VDI, is “How to I move a user profile from one drive to another”. The traditional answers include the use of the user profile management GUI, or some expensive piece of software. But what if you need to automate the job? Or if you don’t have any money for the project?

Answer? PowerShell, of course… and robocopy.

Below is a code snippet that will set existing user profiles to load from “C:\Users” to “E:\Users”:

#Collect profile reg keys for regular users ("S-1-5-21" excludes local admin, network service, and system)
$profiles = gci -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList" `
	| ? {$_.name -match "S-1-5-21-"} 

foreach ($profile in $profiles) {
	#Set the registry path in a format that can be used by the annoyingly demanding "get-itemproperty" cmdlet:
	$regPath = $(
		$($profile.pspath.tostring().split("::") | Select-Object -Last 1).Replace("HKEY_LOCAL_MACHINE","HKLM:")
	#Get the current filesystem path for the user profile, using get-ItemProperty"
	$oldPath = $(
		Get-ItemProperty -LiteralPath $regPath -name ProfileImagePath
	#Set a varialble for the new profile filesystem path:
	$newPath = $oldPath.Replace("C:\","E:\")
	#Set the new profile path using "set-itemproperty"
	Set-ItemProperty -LiteralPath $regPath -Name ProfileImagePath -Value $newPath

#Now copy the profile filesystem directories using "robocopy".

But this code will not actually move the data. For that, we need robocopy. Make sure that your users are logged off before performing this operation, otherwise “NTUSER.DAT” will not get moved, and your users will get a new TEMP profile on next login:

robocopy /e /copyall /r:0 /mt:4 /b /nfl /xj /xjd /xjf C:\users e:\Users

Finally, be sure to set the default location for new profiles and the “Public” directory to your new drive as well. For that, run “Regedit”, then go to:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList
and set new paths for the registry strings “ProfilesDirectory” and “Public”. Moving the default user profile is optional.

Oh yeah… you might want to purge the old Recycle Bin cruft for your moved users as well:

rmdir /s /q C:\$Recycle.Bin

SharePoint 2010 – Email Alerts to Site Administrators

We are in the final stages of preparation for the long-overdue upgrade to SharePoint 2010.  I have set up a preview site with a copy of the production SharePoint content database, and I want to notify all site owners that they should check out their sites for major problems.  How to do?  PowerShell?  Absolutely!

Set-PSDebug -Strict
Add-PSSnapin -Name microsoft.SharePoint.PowerShell

[string] $waUrl = "https://sharepoint2010.uvm.edu"
[string] $SmtpServer = "smtp.uvm.edu"
[string] $From = "saa-ad@uvm.edu"

$allAdmins = @()

[string] $subjTemplate = 'Pending Upgrade for your site -siteURL-'
[string] $bodyTemplate = @"
Message Body Goes Here.
Use the string -siteURL- in the body where you want the user's site address to appear.

$wa = Get-SPWebApplication -Identity $waUrl

foreach ($site in $wa.sites) {
	#Write-Host "Working with site: " + $site.url
	$siteAdmins = @()
	$siteAdmins = $site.RootWeb.SiteAdministrators
	ForEach ($admin in $siteAdmins) {
		#Write-Host "Adding Admin: " + $admin.UserLogin
		[string]$a = $($admin.UserLogin).Replace("CAMPUS\","")
		[string]$a = $a.replace(".adm","")
		[string]$a = $a.replace("-admin","")
		[string]$a = $a.replace("admin-","")
		if ($a -notmatch "sa_|\\system") { $allAdmins += , @($a; [string]$site.Url) }

$allAdmins = $allAdmins | Sort-Object -Unique
#$allAdmins = $allAdmins | ? {$_[0] -match "jgm"} | Select-Object -Last 4

foreach ($admin in $allAdmins) {
	[string] $to = $admin[0] + "@uvm.edu"
	[string] $siteUrl = $admin[1]
	[string] $subj = $subjTemplate.Replace("-siteURL-",$siteUrl)
	[string] $body = $bodyTemplate.Replace("-siteURL-",$siteUrl)
	Send-MailMessage -To $to -From $From -SmtpServer $SmtpServer -Subject $subj -BodyAsHtml $body

How to disable Internet Explorer Automatic Proxy Configuration

You would think this one would be easy…

Some of our users were noticing that it was taking over 30 seconds to launch IE to a web site that was configured at the command line (i.e. we run “iexplore.exe https://mysite.com”).  While page loading was indeed slow, at least 20 seconds of the delay was seen before IE even stated to load content from the web site in question (determined by using WireShark).  Instead what we saw was a lot of CLDAP chatter and the ever-revealing DNS lookup attempt for hosts starting with the RDN “WPAD”.  Looks like Internet Explorer Web Proxy Auto Detection is wasting our time again.

Traditionally, the solution was to use the “Internet Explorer Maintenance” settings in Group Policy to disable auto proxy config.  However, it appears that with the release of Windows 8, this branch of Group Policy is being deprecated.  So what it the “right” way to set this policy now?  I am using GP Preferences.

Using SysInternals ProcMon, we are able to see that the following registry value is modified when we manually disable automatic proxy detection:
HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections\DefaultConnectionSettings
this is a RegBinary setting, which makes intuitive understanding of the meaning of the value impossible.

Though experimentation, I was able to determine that when the ninth pair of digits is altered from “09″ to “01″, proxy auto configuration is disabled.  The parallel “SavedLegacySettings” value also gets modified, but this value has no effect on actual IE settings.  The fifth pair of settings also gets modified each time you reconfigure the IE connection settings.  I expect these digits represent some sort of change sequence number, as the setting different values here does not seem to affect a behavior change in IE.

So whoopee… another GP Preference setting, and I have saved my colleagues 20 seconds of time on each startup of IE.  If they all use the affected systems once a day for the next 80 days, this whole activity will have been worthwhile.

vSphere 5.1 – Train Wreck in Slow Motion

vSphere 5.1 arrived this summer to no great fan-fare. We waited a few weeks, heard no sounds of howling pain (we did not listen very hard, I guess), and decided to proceed with upgrading vCenter.  I have been digging out of the wreckage ever since.

How do you know if upgrading to vSphere 5.1 is right for you?  Here are a few bullet points to help you decide:

  • Do you have CA-signed (externally trusted, or in-house Enterprise CA server) certificates in use in your current vSphere environment?
  • Are you using an external MS SQL Server to host your vCenter database?  Are you using mirrored SQL databases?
  • Is your environment currently stable and reliable?

Is you answered “yes” to any of these questions, do not upgrade to vSphere 5.1.  At least, not yet. Do deceive yourself that that the vSphere 5.1.0a release will be any help, either.

What is the big problem, you ask?  The major source of pain in this release is the new “Single Sign-On Service” that handles authentication and authorization for all of the other vSphere components.  This component of vSphere has twitchy SSL certificate requirements that are poorly documented by VMware.  The SSL requirements are so touchy that in our case, even the self-signed certs generated by the installer did not work.  Unlike all of the other current vSphere components, it does not support mirrored SQL databases.  It has new permissions requirements in AD that are not documented at all, and at the time of our installation, did not even have a KB entry.  The installer is very buggy, most notably in that it requests that you set and admin password for the SSO Service, and demands password complexity, but it does not inform you when your password is unacceptably long (i.e. longer than 32 characters) or when your password contains illegal characters (i.e. most regular expression special characters).

So, if you do upgrade, be prepared for an extended service outage.  Give yourself a long service window.  Have your VMware support contract numbers handy.  Familiarize yourself with the myriad of locations that are used to log vCenter data.  Learn to use PowerShell (get-childitem -recurse | select-string -pattern “configSettingThatThevCenterInstallerBorkedUp”) and keep this page bookmarked:


Here are UVM we are indebted to Derek Seaman for his thorough documentation of the vSphere 5.1 installation process and detailed SSL certificate generation instructions.

Following are some installation quirks that we encountered, presented mainly for my own reference, but maybe you will find them useful as well:

  1. “Performance Charts Experienced an Internal Error” seen in the vSphere client after the upgrade:
    This happened because vCenter Web Services did not read the database mirroring configuration from our defined ODBC data sources… it grabbed the primary database only, and not the mirror data.  The fix?  Edit:
    “%ProgramData%\VMware\VMware VirtualCenter\vcdb.properties”
    Find the “url=” line, and append:
    (Where [mirrorServer] the the actual DB mirror host name.  Don’t forget the “\” before the “=”.)
  2. Some users with permissions to vCenter 5.0 cannot log in after the upgrade.  In the vSphere web client, these users are marked as “disabled”:
    This occurred for use for two reasons:

    1. The SSO Service installer prompts us for a service account to use during install.  Following installation, the service is seen to be running as “SYSTEM”, and not the specified service account.  Change the Service to run with your planned service account using services.msc after the installation.  As an alternative, you could specify those credentials  in the vSphere Web Client -> Administration ->Sign-On and Discovery -> Configuration -> Identity Sources.  Edit your identity source, and under “Authentication Source” select “password”, then enter your service account credentials.
    2. The SSO Service needs to read account attributes that cannot be read by a standard user account (at least, not in an AD forest at a Server 2008 R2 functional level).  When we asked VMware support to define the required permissions, they replied: “an account has to have at least read-only permissions over the user and group Organization Units furthermore read permissions also on the properties of the users, such as UserAccessControl.”  After some experimentation, I just gave the SSO Service account “read all properties” rights to the account OU, and login abilities were restored.
  3. Our SSO Service broke when the mirrored database servers that we currently use for vCenter services had a failover event.  During install, I used the standard “failoverPartner=” JDBC connection string property to specify our failover database server.  Unfortunately, the SSO service ignores this property.  I could not identify an acceptable workaround for this problem. Ultimately, I installed a SQL Express instance on our vCenter server to house just the SSO database.  I tried:
    1. Using SQL Aliases, but this failed because the JDBC driver is not aware of SQL Aliases.
    2. Using a script that edits the local “hosts” file on a database failover event.  I then used this host name alias for the database connections.  This almost worked.  I edited the following files to use the host alias, instead of the actual database server host name:
      Upon restart, the SSO Service was able to connect to the database, but it did not survive a failover.  Apparently the old database connection information was still in use somewhere, and VMware support was not helpful in identifying all of the database configuration locations for SSO.
    3. While VMware does have command line configuration tools that could have been used to script reconfiguration of the database connection strings, I have deemed that they are too fragile for production use.
  4. The option to authenticate using Windows session credentials in the vSphere Client (traditional version) stopped working after the 5.1 upgrade.  This is a bug that is fixed with the 5.1.0a release.  Unfortunately, the SSO installer for 5.1.0a does not work in upgrade mode.  Aargh!  I had to uninstall the SSO service to get the updated files into place.  Guess what the uninstaller does?  That’s right… it erases the SSO Service database (drops all tables!  Gah!), and deletes all configuration files for the service.  Before you upgrade, make sure that you have an SSO Service backup bundle.  I did, but it was outdated.  I had to re-register all of the vCenter components with SSO manually, which was a pain in the butt.
  5. vSphere Update Manager registered with vCenter using the wrong DNS name.  We could not scan ESXi hosts for updates, because vCenter was telling them to connect to an invalid URL.  To fix, I needed to search the registry for the incorrect host name, and replace with the correct one:
    “HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\VMware, Inc.\VMware Update Manager\VUMServer”
    For good measure I also edited:
    %Program Files(x86)%\VMware\Infrastructure\Update Manager\extension.xml
    To contain the correct host name.  Then we restart the Update Manager services, and we are back in business.
  6. Other fun related to VMware Update Manager… the SQL Account used by Update Manager cannot have a password that exceeds 24 characters in length. Special characters in the SQL Account password also may cause problems.

So, VMware is not my favorite company this month.  On to solve more problems.  We still cannot add new permissions to vCenter, and Performance Charts are loading like a slug in winter.

Unattended Install and Upgrade of Adobe Reader

Previously we explored how to increase the success rate of unattended application upgrades using our handy “killAndExec” VBScript. This works well for about 80% of our applications. What about Adobe Reader? Well… not so much.

Thankfully, the stock Adobe Reader installer deals with open files quite nicely, and does not care if Reader is in use by a browser during silent installs (this is the main reason that we needed killAndExec.vbs in the first place). However, not all Reader install operations are full installs. Interestingly, Adobe is one of the few vendors that I deal with that actually uses MSI patch files (MSP). To install a patch release of Reader (i.e. 10.1.4), you need first to install the base version (10.1), then one or more patch MSP files. This is easy for new installs… just run “setup.exe” silently, then run “msiexec /p (patchFileName) /qn” to install the patch.

However, the situation gets more complicated for upgrades. If the base product already is installed, setup.exe will return an error code. So, for upgrade scenarios, I have put together another handy VBScript to handle base version detection. The script follows:

option explicit
' Install Adobe Reader Script:
' J. Greg Mackinnon, 2012-06-12
' Intended to perform unattended installations of Adobe Reader by MS SCCM 2012.
' Installs the version of Adobe Reader in the same directory as the script, if not already installed.
' Also installs the current Reader patch, if requested.
' Installer string is specified in "sInstall".
' Requires: 
'     Adobe Reader setup and patch files in the same directory as the script.
'     "setup" and "basever" arguments required.
'     "patch" argument optional.
' Returns:
'     - Code 100 - if required arguments are not provided to the script.
'     - Return code of setup program added to the return code of the patch program, if no other errors occur.

dim oExec, oFS, oLog, oShell
dim cScrArgs
dim iExit
dim sBaseVer, sInstall, sLog, sOut, sPatch, sPath, sPF, sScrArg, sTemp, sVer
dim bDoPatch

sLog = "installAdobeReader.log"
iExit = cLng(0)

' Instantiate objects:
Set oShell = CreateObject( "WScript.Shell" )
Set oFS = CreateObject("Scripting.FileSystemObject")
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)

' Define Functions
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

sub subHelp
	echoAndLog "installAdobeReader.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Runs Adobe Reader silent setup (if not already present), then applies "
	echoAndLog "any specified MSP patch files for Reader."
	echoAndLog "Logs output to 'installAdobeReader.log' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/setup:""[setupFile]"""
	echoAndLog "     The primary Adobe Reader installation program.  If switches "
	echoAndLog "    are required for setup to run silently, they must be provided."
	echoAndLog "/basever:"
	echoAndLog "     The base Adobe Reader product version for which to check (i.e. 10.1)"
	echoAndLog ""
	echoAndLog "Optional arguments and syntax:"
	echoAndLog "/patch:""[patchFile]"""
	echoAndLog "     MSP patch file to install after Adobe Reader setup completes."
end sub
' End Functions

' Parse Arguments
if WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
'	For Each sScrArg in cScrArgs
'		echoAndLog sScrArg 'Echo supplied arguments to console
'	Next

	for each sScrArg in cScrArgs
		select case LCase(sScrArg)
			Case "setup"
				sInstall = cScrArgs.Item(sScrArg)
			Case "patch"
				sPatch = cScrArgs.Item(sScrArg)
			Case "basever"
				sBaseVer = CStr(cScrArgs.Item(sScrArg))
			Case Else
				echoAndLog vbCrLf & "Unknown switch or argument: " & sScrArg & "."
				echoAndLog "**********************************" & vbCrLf
		end select
	if (IsNull(sInstall) or IsEmpty(sInstall)) then
		echoAndLog "Required argument 'setup' was not provided."
		echoAndLog "**********************************" & vbCrLf
	elseif (IsNull(sBaseVer) or IsEmpty(sBaseVer)) then
		echoAndLog "Required argument 'basever' was not provided."
		echoAndLog "**********************************" & vbCrLf
	elseif (IsNull(sPatch) or IsEmpty(sPatch)) then 
		bDoPatch = False
		bDoPatch = True
	end if
elseif WScript.Arguments.Named.Count = 0 then 'Detect if required args are not defined.
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
end if
' End Argument Parsing

' Begin Main

' Complete version and installation strings:
sInstall = ".\" & sInstall
sPatch = "msiexec.exe /p " & sPatch & " /qb /norestart"

' Build path to Adobe Reader executable:
sPF = oShell.ExpandEnvironmentStrings( "%ProgramFiles%" )
sPath = sPF & "\Adobe\Reader 10.0\Reader\AcroRd32.exe"
echoAndLog "Acrobat Reader Path: " & sPath

' Get the version string on the currently installed Reader executable:
on error resume next
sVer = oFS.GetFileVersion(sPath)
on error goto 0
echoAndLog "Version of currently installer Adober Reader: " & sVer

' See if we already have the base version installed:
if InStr(Left(cStr(sVer),4),sBaseVer) then
	'Reader 10.1.x is already installed skip base product installation.
	echoAndLog "Base product installed.  Skipping setup..."
	'Install the base product.
	echoAndLog "Installing base product with command: " & sInstall
	set oExec = oShell.Exec(sInstall)
	Do While Not oExec.Status  1
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode)
end if

'Now install the patch:
if bDoPatch = True then
	echoAndLog "Patch installation requested."
	echoAndLog "Installing patch with command: " & sPatch
	set oExec = oShell.Exec(sPatch)
	Do While Not oExec.Status  1
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from patch installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode) + iExit
	echoAndLog "Patch installation was not requested.  Exiting."
end if

' End Main

KillAndExec.vbs – Ensuring application installer success with VBScript

Today’s scripting challenge…

We are attempting to use SCCM 2012 as a patch management solution for our centrally supported third party applications.  Great new features in SCCM 2012 allow us to write detection rules for applications to determine if superseded versions are present on the client system, and to trigger an immediate upgrade.  Cool Beans.  Problem is, a lot of application installers that ran reliably in our MDT “LiteTouch” environment (which is used to deploy new operating systems with no previously installed software) will not run silently or successfully on systems where previous application versions were already installed, and may currently be running.

This is an old problem for client system management… how can you update in-use files?  In most cases I have seen, the admin will schedule the updates to run when no one is logged in.  Unfortunately, this is an edge case for us.  Most systems are off when no one is logged in.  Another system is to force logoff for application updates.  While this would work, it seems like a “heavy” solution… why force the user to log off to update one application that may or may not be running?  Why force all applications closed on the off chance that one application will need to be terminated.

Our solution?  Kill only the processes that need to be terminated to ensure application installation success.  See the VBScript solution below (I flirted with writing this one in PowerShell, but the code signing requirements still intimidate me, and I may have the odd-duck XP client that still does not have PowerShell).  I have tested the script on Firefox, Thunderbird, VLC, Notepad++, WinSCP, Filezilla, and KeePass.  Rock On!

UPDATE: Since initial publication, I have added some logic to handle execution from “wscript”. If the script is executed from wscript.exe, console output will be suppressed. Additionally, the log file now is named “killAndExec-(exeFileName).log”. (This prevents SCCM from overwriting the log file the next time a program installer runs that also uses this script).

'KillAndExec.vbs script, J. Greg Mackinnon, 2012-09-13
' Kills processes named in the "kill" argument (comma-delimited)
' Runs the executable named in the "exec" argument
' Appends the executable arguments specified in the "args" argument (comma-delimited)
'Requires: "kill" and "exec" arguments.  The executable named in the "exec" arg must be in the same directory as this script.
' RC=101 - Error terminating the requests processes
' RC=100 - Invalid input parameters
' Other return codes - Pass-though of return code from WShell.Exec.Run using the provided input parameters

Option Explicit

const quote = """"

'Declare Variables:
Dim aExeArgs, aKills
Dim bBadArg, bNoArgs, bNoExeArg, bNoExec, bNoKill, bNoKillArg 
Dim cScrArgs
Dim iReturn
Dim oShell, oFS, oLog
Dim sBadArg, sCmd, sExe, sExeArg, sKill, sLog, sScrArg, sTemp

'Set initial values:
bBadArg = false
bNoArgs = false
bNoExeArg = false
bNoExec = false
bNoKill = false
bNoKillArg = false
iReturn = 0

'Instantiate Global Objects:
Set oShell = CreateObject("WScript.Shell")
Set oFS  = CreateObject("Scripting.FileSystemObject")

' Define Functions
Sub subHelp
	echoAndLog "KillAndExec.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Kills named processes and runs the provided executable."
	echoAndLog "Logs output to 'KillAndExec.vbs' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/kill:""[process1];[process2]..."""
	echoAndLog "     Specify the image name of one or more processes to terminate."
	echoAndLog "/exe:""[ExecutableFile.exe]"""
	echoAndLog "     Specify the name of the executable to run."
	echoAndLog ""
	echoAndLog "Optional arguments:"
	echoAndLog "/args""[arg1];[arg2];[arg3]..."""
	echoAndLog "     Specify one or more arguments to pass to the executable."
	echoAndLog "/noKill"
	echoAndLog "     Switch to suppress default process termination.  Used for testing."
	echoAndLog "/noExec"
	echoAndLog "     Switch to suppress default program execution.  USed for testing."
End Sub

function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

function fKillProcs(aKills)
' Requires:
'     aKills - an array of strings, with each entry being the name of a running process.   
	Dim cProcs
	Dim sProc, sQuery
	Dim oWMISvc, oProc

	Set oWMISvc = GetObject("winmgmts:{impersonationLevel=impersonate, (Debug)}\\.\root\cimv2")
	sQuery = "Select Name from Win32_Process Where " 'Root query, will be expanded.	
	'Complete the query string using process names in "aKill"
	for each sProc in aKills
		sQuery = sQuery & "Name = '" & sProc & "' OR "
	'Remove the trailing " OR" from the query string
	sQuery = Left(sQuery,Len(sQuery)-3)

	'Create a collection of processes named in the constructed WQL query
	Set cProcs = oWMISvc.ExecQuery(sQuery, "WQL", 48)
	echoAndLog vbCrLf & "----------------------------------"
	echoAndLog "Checking for processes to terminate..."
	'Set this to look for errors that aren't fatal when killing processes.
	On Error Resume Next
	'Cycle through found problematic processes and kill them.
	For Each oProc in cProcs
	   echoAndLog "Found process " & oProc.Name & "."
	   Select Case Err.Number
		   Case 0
			   echoAndLog "Killed process " & oProc.Name & "."
		   Case -2147217406
			   echoAndLog "Process " & oProc.Name & " already closed."
		   Case Else
			   echoAndLog "Could not kill process " & oProc.Name & "! Aborting Script!"
			   echoAndLog "Error Number: " & Err.Number
			   echoAndLog "Error Description: " & Err.Description
			   echoAndLog "Finished process termination function with error."
			   echoAndLog "----------------------------------"
			   echoAndLog vbCrLf & "Kill and Exec script finished."
			   echoAndLog "**********************************" & vbCrLf
	   End Select
	'Resume normal error handling.
	On Error Goto 0
	echoAndLog "Finished process termination function."
	echoAndLog "----------------------------------"
end function

function fGetHlpMsg(sReturn)
' Gets known help message content for the return code provided in "sReturn".
' Requires:
'     Existing WScript.Shell object named "oShell"
	Dim sCmd, sLine, sOut
	Dim oExec
	sCmd = "net.exe helpmsg " & sReturn
	echoAndLog "Help Text for Return Code:"
	set oExec = oShell.Exec(sCmd)
	Do While oExec.StdOut.AtEndOfStream  True
		sLine = oExec.StdOut.ReadLine
		sOut = sOut & sLine
	fGetHlpMsg = sOut
end function
' End Define Functions

' Parse Arguments
If WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
	For Each sScrArg in cScrArgs
		Select Case LCase(sScrArg)
			Case "nokill"
				bNoKill = true
			Case "noexec"
				bNoExec = true
			Case "kill"
				aKills = Split(cScrArgs.Item(sScrArg), ";", -1, 1)
			Case "exe"
				sExe = cScrArgs.Item(sScrArg)
			Case "args"
				aExeArgs = Split(cScrArgs.Item(sScrArg), ";", -1 ,1)
			Case Else
				bBadArg = True
				sBadArg = sScrArg
		End Select
	If (IsNull(sExe) or IsEmpty(sExe)) Then
		bNoExeArg = True
	ElseIf (IsNull(aKills) or IsEmpty(aKills)) Then
		bNoKillArg = True
	End If
ElseIf WScript.Arguments.Named.Count = 0 Then 'Detect if required args are not defined.
	bNoArgs = True
End If 
' End Argument Parsing

' Initialize Logging
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
sLog = "killAndExec-" & sExe & ".log"
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)
' End Initialize Logging

' Process Arguments
if bBadArg then
	echoAndLog vbCrLf & "Unknown switch or argument: " & sBadArg & "."
	echoAndLog "**********************************" & vbCrLf
elseif bNoArgs then
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
elseif bNoExeArg then
	echoAndLog "Required argument 'exe' was not provided."
	echoAndLog "**********************************" & vbCrLf
elseif bNoKillArg then
	echoAndLog "Required argument 'kill' was not provided."
	echoAndLog "**********************************" & vbCrLf
end if
' Log processes to kill:
for each sKill in aKills
	echoAndLog "Process to kill: " & sKill
' Log executable arguments:
echoAndLog "Executable to run: " & sExe
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	for each sExeArg in aExeArgs
		echoAndLog "Executable argument: " & sExeArg
	echoAndLog "Executable has no provided arguments."	
end if
' End Process Arguments

'Begin Main
'Build full command string:
if inStr(sExe," ") then 'Spaces in the exe file
	sExe = quote & sExe & quote 'Add quotations around the executable.
end if
if not (IsNull(aExeArgs) or IsEmpty(aExeArgs)) then
	sCmd = sExe & " " 
	for each sExeArg in aExeArgs
		if inStr(sExeArg," ") then
			sExeArg = quote & sExeArg & quote 'Add quotations around the argument.
		end if
		sCmd = sCmd & sExeArg & " "
	sCmd = sExe
end if
echoAndLog "Command to execute:"
echoAndLog sCmd

'Kill requested processes:
if bNoKill = false then
	fKillProcs aKills
	echoAndLog "/noKill switch has been set.  Processes will not be terminated."
end if
'Run the requested command:
echoAndLog vbCrLf & "----------------------------------"
if bNoExec = false then
	echoAndLog "Running the command..."
	on error resume next 'Disable exit on error to allow capture of oShell.Run execution problems.
	iReturn = oShell.Run(sCmd,10,True)
	if err.number  0 then 'Gather error data if oShell.Run failed.
	    echoAndLog "Error: " & Err.Number
		echoAndLog "Error (Hex): " & Hex(Err.Number)
		echoAndLog "Source: " &  Err.Source
		echoAndLog "Description: " &  Err.Description
		iReturn = Err.Number
	end if
	on error goto 0
	echoAndLog "Return code from the command: " & iReturn
	if iReturn  0 then 'If the command returned a non-zero code, then get help for the code:
		fGetHlpMsg iReturn
	end if 
	echoAndLog "/noExec switch has been set.  Executable will not run."
end if
echoAndLog "----------------------------------"

' End Main

WiFi Profiles for Windows 8

So Windows 8 is here, to little fanfare at the University.  While I am always happy to have an updated version of Windows to work with, I see that I have yet to blog anything about it.  Perhaps that is because, unlike with the release of Windows 7, there was so little that was relatively “wrong” with the previous release.  I find myself with not much “to do” to get the enterprise ready for Windows 8.  Other reasons for the lack of hype… Windows 7 applications seem, for the most part, to “just work” on Windows 8, thus necessitating very little in the way of application compatibility planning.

Still, we have run into a few hiccups.  I spent most of the last two days updating the UVM WiFi Configuration Tool scripts and experimenting with Group Policy settings to make WPA2-protected wireless working consistently (Previously discussed here, way back in ought-eight.).  In the end, there was very little that I did to the WiFi policies that was Windows 8 specific.  The WiFi profile that we are using maintains backward compatibility with both Windows 7 and Windows Vista.

Here are the details:

  • The 802.1x settings in our WiFi profile was updated to use “user authentication” instead of “user or computer authentication”.  Under XP, this option was called “user reauthentication”.  “ReAuthentication” meant that the computer would attempt to log on as the computer account, but that if the connection was lost, it would re-authenticate as the logged on user.  Under XP, it was not possible to prevent computer authentication attempts.  However, under Win7/Win8, user authentication is just that… only user authentication is attempted, computer authentication is excluded.  We have verified this by looking at the RADIUS server logs.  Switching to “user authentication” will cut down on log errors on the RADIUS servers, and will result in fewer errors on client systems as well.
  • We have added a new trust anchor for our RADIUS server certificate in the WiFi profile.  This was necessitated by mergers and acquisitions on the CA business.  “Equifax” provided our original WPA2/PEAP certificate.  When we went to renew our certificate, we found that Equifax had been acquired by GeoTrust, and that new certificates would be issued from a GeoTrust intermediate CA.  However, this intermediate CA would be cross-signed using the Equifax root CA, so the Equifax trust anchor would still work.  The problem is that if a system has both the GeoTrustandEquifax certs present in the local trusted roots certificate store, it will validate the “radius.uvm.edu” up to the GeoTrust anchor, and will ignore the cross-signing with Equifax.  This results in WiFi connection errors.  When I add the GeoTrust cert as an additional trust anchor, the problem goes away.
  • The VBScript I use to install the WiFi profile is packaged inside a 7-Zip self extractor.  The use of this self-extractor triggers the Windows “Program Compatibility Assistant”, which in turn raises a “This program might not have installed correctly” error after the tool runs.  This problem is corrected by embedding a “manifest” file into the tool.  Typically, this is done using the “mt.exe” tool included in the Windows SDK.  Unfortunately, MT.exe corrupts self-extracting 7-Zip archives (this also is a known problem with WinRAR, and perhaps other similar tools).  Fortunately I was able to work around the problem using “Resource Tuner” from Heaventools.  I needed to add “trustInfo” and “compatibility” sections to the manifest.  My blog engine is really bad about posting XML content in a page, so I will forego posting the manifest here. You can find sample manifests pretty easily though Google.
  • When we run the packaged configuration tool, we get a warning that the application package is unsigned and may not be trustworthy.  I used “signtool.exe” from the Windows SDK to add a signature to the executable, so now it is considered somewhat more trustworthy.  Good instructions on the use of signtool.exe can be found here:
    I am using a code signing cert that we obtained from the InCommon.org certificate service, hosted by Comodo.  It works.
  • Finally, I updated the profile installer VBScript to make reconfiguration a bit easier (subroutines were converted to functions so that variables set at the start of the script can be passed down to the function.  We then can set things like the trust anchor name, WiFi network name, and log file name at the start of the script where they are more easily edited.  Also, I removed support for Windows XP… no more Service Pack detection, Hotfix installation, or third-party profile installation utilities are needed by the script.  I was able to hack the script down to about a quarter of its original size as a result.  The new script is included below, for those who like that sort of thing…


Option Explicit
'On Error Resume Next
'Install UVM WPA2-Enterprise wireless profile
' Version 1.3 by J. Greg Mackinnon, University of Vermont
' Supported platforms:  Windows Vista, 7, and 8
' Requires external tools:  "CertMgr.exe" (from the Windows Platform SDK)
' Requires external files:  Root CA certificate file, 
'                           WiFi XML configuration files for Vista+ Windows OS.
'                            (obtained by running "netsh wlan export profile UVM .\"
' NOTE: modify variables in the "Define variables" section to suit your environment.

' Version 1.0 - Supported UVM WiFi using WPA2, Equifax certs, Windows XP SP2+ and Vista OS
' Version 1.1 - Updated to support Windows 7
' Version 1.2 - Updated to support Windows 8.  Removed support for XP 
'             - Removed third-party "ZWlanCfg" utility and OS Hotfix installation functions (were only needed for XP support)
' Version 1.3 - Converted existing subroutines to functions to allow for easier switching of CAs and WiFi networks.
'             - Moved Global Variables to the top of the script for easier modification.
'             - Updated CA cert and WPA Profile supporting files to use "GeoTrust" instead of "Equifax".

' Create constants
Const cLogFile = "install_UVM_WiFi.log"

' Declare variables
Dim oShell, oUserEnv, oFSO, oFile, oRegExp
Dim iSPVer
Dim sTempEnv, strComputer, sOSTest, sOS, sCertName, sCertFile, sNetName, sProfileFile
Dim bReRun

' Define variables
bReRun = False
strComputer = "."
sOSTest = "Vista|Windows 7|Windows 8" 'Regular Expression for OS compatibility testing
sCertName = "GeoTrust Global CA"      'Friendly name of the trust anchor certificate
sCertFile = "GeoTrustGlobalCA.cer"    'Name of the trust anchor file
sNetName = "UVM"                      'Name of the WiFi Access Point
sProfileFile = ".\Wi-Fi-UVM.xml"      'Name of the Vista+ wlan profile file.

' Instantiate global objects
Set oShell = WScript.CreateObject("WScript.Shell")
Set oFSO = CreateObject("Scripting.FileSystemObject")
sTempEnv = oShell.ExpandEnvironmentStrings("%TEMP%") & "\"
Set oFile = oFSO.CreateTextFile(sTempEnv & cLogFile,True)
Set oRegExp = New RegExp
oRegExp.IgnoreCase = True
oRegExp.Global = True
oRegExp.Pattern = sOSTest

' Define Functions
Function fDetectOS(sOS, iSPVer)
'Detect OS Function - detects OS Caption string and Service Pack integer from WMI WIN32_OperatingSystem.
'Expects to varibles passed, returns the full OS Caption String, and SP Major Version intger
	'Declare variables
	Dim colItems
	Dim objWMIService, objItem
	'Instantiate local objects/collections
	Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\CIMV2") 
	Set colItems = objWMIService.ExecQuery("Select * from Win32_OperatingSystem")

	For Each objItem In colItems
	  sOS = objItem.Caption
	  oFile.WriteLine "Detected Operating System: " & sOS
	  iSPVer = CInt(objItem.ServicePackMajorVersion)
	  oFile.WriteLine "Detected Service Pack Version: " & iSPVer
	  oFile.WriteLine "Service Pack Minor Version: " & objItem.ServicePackMinorVersion
	'Clean local objects/variables
	Set objItem = Nothing
	Set colItems = Nothing
	Set objWMIService = Nothing
End Function

Function fInstCert(sCertName,sCertFile)
' Installs cert with sCertName root CA cert into machine "root" store.
' Requires:  certmgr.exe from the Windows Platform SDK (available with VS .NET or VS 2008 installations), 
'	sCertName variable - contains the friendly name of the root CA
'	sCertFile variable - contains the name of the root CA certificate file
' Requres:  Root CA cert file
' Notes:  We use the "root" argument to certmgr.exe to install into the "Trusted Root Certificate Authorities".  
'		We also could use "ca" to install Intermediate Certificate Authorities.
'		In a previous version of this script we used "oShell.Run", but his returned unexpected results on the
'		Windows 7 platform... using .Exec now.
	Dim bCertPresent, bInstSuccess
	Dim oExec
	Dim sOut

	bCertPresent = false
	bInstSuccess = false
	set oExec = oShell.Exec("certmgr.exe -c -s -r localMachine root")

	Do Until oExec.StdOut.AtEndOfStream
		sOut = oExec.StdOut.ReadLine()
		if InStr(sOut, sCertName) Then
			'oFile.WriteLine sOut
			'WScript.Echo sOut
			bCertPresent = true
		End If

	if bCertPresent = false then
		oFile.WriteLine "Root Certificate for """ & sCertName & """ needs to be installed.  Attempting install..."
		set oExec = oShell.Exec("certmgr.exe -add -c " & sCertFile & " -s -r localMachine root")
		Do Until oExec.StdOut.AtEndOfStream
			sOut = oExec.StdOut.ReadLine()
			if InStr(sOut, "Succeeded") Then
				'oFile.WriteLine sOut
				bInstSuccess = true
			End If
		if bInstSuccess = true then
			oFile.WriteLine "Certificate installed successfully"
			oFile.WriteLine "Certificate failed to install... You will need to install the " _
				& "certificate manually.  See the instructions at https://www.uvm.edu/ets/wireless " _
				& ", then run this script again to compelte installation of the UVM wireless profile."
			WScript.Quit -2
		end if
		oFile.WriteLine "Root Certificate for """ & sCertName & """ is already installed."
	End If
End Function

Function fImportProfile(sProfileFile,sNetName)
'Imports Vista+ Wireless Profile using NETSH command.  
'Requires: a Vista+ wifi profile file exported using NETSH, 
'	sProfileFile - string containing name of the wlan XML profile file to be imported
'	sNetName - string contining the name of the wlan profile name (WiFi Network Name)

	'On Error Resume Next
	Const cUserScope = "all"
	Dim iStrMatch
	Dim oExec, oStdOut
	Dim sStdOutLine
	oFile.WriteLine "Executing command: netsh wlan add profile filename=""" & sProfileFile & """ user=" & cUserScope & ""
	Set oExec = oShell.Exec("netsh wlan add profile filename=""" & sProfileFile & """ user=" & cUserScope & "")
	Set oStdOut = oExec.stdOut
	While Not oStdOut.AtEndOfStream
		sStdOutLine = oStdOut.ReadLine
		iStrMatch = CInt(InStr(sStdOutLine, "Profile " & sNetName & " is added on interface"))
		If iStrMatch > 0 Then
			WScript.Echo "The " & sNetName & " wireless profile was added successfully to your system"
		ElseIf iStrMatch = 0 Then
			WScript.Echo "The wireless profile failed to import.  Please see the manual profile " _
			& "configuration instructions available at http://www.uvm.edu/ets/wireless.  A " _
			& "log file named " & cLogFile & " which contains the full error message can be " _
			& "found in the " & sTempEnv & " directory."
			WScript.Quit -3
		End If
	Set oStdOut = Nothing
	Set oExec = Nothing
End Function
' End Functions

' Begin Main

fDetectOS sOS, iSPVer

If oRegExp.Test(sOS) = True Then
	fInstCert sCertName, sCertFile
	fImportProfile sProfileFile, sNetName
	oFile.WriteLine "Your operating system is not supported for use with this script."
	WScript.Quit -4
End If


' Environment cleanup 
Set oFile = Nothing
Set oFSO = Nothing
Set oUserEnv = Nothing
Set oShell = Nothing
Set oRegExp = Nothing

' End Main