For many years I have used scripts of my own design to build workstations and to roll out software updates. At the time I created these I found that most of the tools which could accomplish these tasks were unwieldy. Group Policy software deployment in particular never really seemed fit for purpose since it extended login times so dramatically. My experience gained in a previous job spent packaging applications for deployment had taught me that all installed software populates consistent information in the Windows Registry, so in my current job I tended to audit this data directly via my scripts. This was saved into an SQL database from where it could be queried, or manipulated via a data source in Excel.
I’m working my notice period at the moment ready for a new job I’ll start in October, and so I’m going over the stuff I have created in the current job in order to prepare my handover documents. Mindful of the dependency my current employer has on these custom scripts I decided to get a quote for a Dell KACE solution, thinking that since it’s a Virtual Appliance, and since there are only 150 PCs here it shouldn’t be too expensive – after all it’s only really providing what my scripts already do (workstation builds, drivers, software deployment, and auditing). But here’s the thing – they wanted something like £13,000! (I can’t recall the precise figure). To put it in context this figure is around one third of the cost of replacing all the workstations with new ones, or say half the annual salary of an IT support technician – quite out of the question.
Unsurprisingly I have decided instead to simply tidy up my scripts to make them easier to use. Sure, you could accomplish these tasks with SCCM but that’s not free either. In an SME, why spend huge amounts of money on something that can be automated without much trouble using mechanisms that are built in. Heck, even the uninstall command line is stored in the registry for virtually all software – that’s how the Add/Remove Programs Control Panel works! And most software can be installed silently in the desired way provided you research the command line arguments to do so. It’s no accident that AppDeploy.com which was a great crowdsourced repository of this knowledge became KACE which was then acquired by Dell. It still exists, though the content doesn’t seem to be as well maintained as it was.
I have used a startup script written in VBScript to keep software up to date on workstations. A startup script runs as the SYSTEM account so permissions are not an issue. Since I also maintain an unattended installation I already have a package folder with all the scripts to install each package. All I needed to code was a way to audit the Registry for each package and add some logic around that. Up until now, I had tended to write sections of the script specifically tailored for each package, and from there it’s not much of a stretch to apply packages to a workstation based on its OS version, or Active Directory OU or group membership. For the script I have published below, I have recreated this logic as a single function which can be invoked with a one line entry for each package (see the highlighted part) – everything else is taken care of. I hope it helps someone to save £13,000 :)
Sample script output
Running software package check for Adobe Flash Player...
Registry data found at branch "Adobe Flash Player ActiveX"
Comparing detected version 11.3.300.271 against desired version 11.4.402.265
Removing old version 11.3.300.271
Killing iexplore.exe
Override detected, running "u:\packages\flash\uninstall_flash_player.exe -uninstall"
u:\packages\flash\uninstall_flash_player.exe -uninstall
Installing Adobe Flash Player 11.4.402.265
Running software package check for Paint.NET...
Registry data found at branch "{529125EF-E3AC-4B74-97E6-F688A7C0F1C0}"
Comparing detected version 3.60.0 against desired version 3.60.0
Paint.NET is already installed and up to date.
Running software package check for Adobe Reader...
Registry data found at branch "{AC76BA86-7AD7-1033-7B44-AA0000000001}"
Comparing detected version 10.0.0 against desired version 10.1.4
Removing old version 10.0.0
Using UninstallString from the Registry, plus "/qb-!"
MsiExec.exe /I{AC76BA86-7AD7-1033-7B44-AA0000000001} /qb-!
Installing Adobe Reader 10.1.4
Running software package check for Photo Gallery...
Registry data found at branch "{60A1253C-2D51-4166-95C2-52E9CF4F8D64}"
Comparing detected version 16.4.3503.0728 against desired version 16.4.3503.0728
Photo Gallery is already installed and up to date.
Running software package check for Mendeley Desktop...
Installing Mendeley Desktop 1.6
The script
'startup.vbs
'patters 2006-2012
Option Explicit
Dim objNetwork, objShell, objReg, strKey, colProcess, objProcess, arrSubKeys
Dim strFileServer
Const HKEY_CURRENT_USER = &H80000001
Const HKEY_LOCAL_MACHINE = &H80000002
'set up objects
Set objNetwork = CreateObject("WScript.Network")
Set objShell = CreateObject("WScript.Shell")
Set objReg = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
strFileServer = "YOURSERVERHERE"
MapNetworkDrive "U:","unattended"
Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"
Package "paintnet.cmd", "Paint.NET", "3.60.0", "/qb-!", False, False, ""
Package "adobe.cmd", "Adobe Reader", "10.1.4","/qb-!",False, False, array("outlook.exe","iexplore")
Package "photogal.cmd", "Photo Gallery", "16.4.3503.0728", "/qb-!", False, False, "iexplore.exe"
Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"
objNetwork.RemoveNetworkDrive "U:", True, True
WScript.Echo VbCrLf & "Finished software checks"
Function Package(strPackageName, strTargetDisplayName, strTargetVersion, strExtraUninstParams, boolExtraUninstQuotes, boolUninstForceOverride, ProcessToKill)
'=============================================================================
'To understand this function you need to know that installed software packages
'will populate keys below these branches of the Registry:
' HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
' HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
' (the latter for 32bit software on 64bit Windows)
'This is the data that is mined when you look at Add/Remove Programs
'in the Control Panel
'strPackageName is the package script on your package server (e.g. flash.cmd)
'strTargetDisplayName can be a full or partial match of the Registry key
'DisplayName (matches from the left)
' "Java(TM)" would match "Java(TM) 6 Update 5" and all other versions
'strTargetVersion is the full version number from DisplayVersion in the Registry
'Each decimal point of precision will be compared in turn.
'If the Registry key DisplayVersion is not used by a package, the same number
'of digits is parsed from the right hand side of the DisplayName string
'strExtraUninstParams is used when you want to override the command line
'specified by QuietUninstallString in the Registry, or for when that value is
'missing for example, sometimes InnoSetup packages will specify the switch
'/SILENT in QuietUninstallString, but you may need to override by appending
'/VERYSILENT to the command line in UninstallString
'If neither QuietUninstallString and UninstallString are present, the script
'will use strExtraUninstParams as the full uninstall command line
'Some packages define UninstallString as a long filename but forget to
'surround it with quotes. You can correct this by setting
'boolExtraUninstQuotes = True
' Package "mendeley.cmd", "Mendeley Desktop", "1.6", "/S", True, False, "winword.exe"
'In some cases you may want to ignore the value of both QuietUninstallString
'and UninstallString and override the command completely. To do this, set
'boolUninstForceOverride to True
' Package "flash.cmd", "Adobe Flash Player", "11.4.402.265", "u:\packages\flash\uninstall_flash_player.exe -uninstall", False, True, "iexplore.exe"
'Finally, ProcessToKill is a string or array containing the name(s) of any
'running process(es) you need to kill, if plugins are being installed for Word
'or Internet Explorer for instance.
'=============================================================================
Dim arrBranches, strBranch, boolRemoval, strActualDisplayName, strActualVersion
Dim strQuietUninstall, strUninstall
WScript.Echo VbCrLf & "Running software package check for " & strTargetDisplayName & "..."
'we need to iterate through both the 32 and 64bit uninstall branches of the Registry
arrBranches = Array("SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\", "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\")
For Each strBranch In arrBranches
'firstly, remove old version of package if it's present
objReg.EnumKey HKEY_LOCAL_MACHINE, strBranch, arrSubKeys
If IsArray(arrSubkeys) Then
For Each strKey in arrSubkeys
objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayName", strActualDisplayName
If Left(strActualDisplayName, Len(strTargetDisplayName)) = strTargetDisplayName Then
'we've found the target software package
WScript.Echo " Registry data found at branch """ & strKey & """"
'is there a version string (not all software will have one)?
objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "DisplayVersion", strActualVersion
If Not IsNull(strActualVersion) Then
Else
'if there's no version string we'll try to grab the same number of chars from the right hand side of the DisplayName string
strActualVersion = Right(strActualDisplayName, Len(strTargetVersion))
End If
If (IsUpgradeNeeded (strActualVersion,strTargetVersion)) = True Then
strQuietUninstall = ""
WScript.Echo " Removing old version " & strActualVersion
KillProcess ProcessToKill
'check the package's registry settings
objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "UninstallString", strUninstall
objReg.GetStringValue HKEY_LOCAL_MACHINE, strBranch & strKey, "QuietUninstallString", strQuietUninstall
If Not strExtraUninstParams = "" Then
'Extra parameters were sent to the function
If boolUninstForceOverride = True Then
'Entire uninstall command line was forced so use strExtraUninstParams, regardless of what's in the Registry
WScript.Echo " Override detected, running """ & strExtraUninstParams & """"
WScript.Echo " " & strExtraUninstParams
WinExec strExtraUninstParams
ElseIf Not IsNull(strUninstall) Then
'use the basic UninstallString plus the additional parameters
If boolExtraUninstQuotes = True Then
strUninstall = """" & strUninstall & """"
End If
strUninstall = strUninstall & " " & strExtraUninstParams
WScript.Echo " Using UninstallString from the Registry, plus """ & strExtraUninstParams & """"
WScript.Echo " " & strUninstall
WinExec strUninstall
Else
'no UninstallString was found in the Registry, so assume that strExtraUninstParams is the full removal command line
WScript.Echo " No UninstallString found, running """ & strExtraUninstParams & """"
WScript.Echo " " & strExtraUninstParams
WinExec strExtraUninstParams
End If
Else
'No extra parameters were sent to the function
'if there's already a value for QuietUninstallString then use that command line
If Not IsNull(strQuietUninstall) Then
WScript.Echo " Using QuietUninstallString directly from the Registry"
WScript.Echo " " & strQuietUninstall
WinExec strQuietUninstall
ElseIf Not IsNull(strUninstall) Then
'no QuietUninstallString was found, fall back to UninstallString
If boolExtraUninstQuotes = True Then
strUninstall = """" & strUninstall & """"
End If
WScript.Echo " Using UninstallString directly from the Registry"
WScript.Echo " " & strUninstall
WinExec strUninstall
Else
WScript.Echo " ERROR - this package doesn't seem to have any UninstallString defined - you'll need to send one to the Package function (see script source for details)"
Exit Function
End If
End If
Else
'IsUpgradeNeeded (strActualVersion,strTargetVersion) is False
'package was detected, but version is >= than the one specified
WScript.Echo " " & strTargetDisplayName & " is already installed and up to date."
Exit Function
End If
End If
Next
End If
Next
'install package
WScript.Echo " Installing " & strTargetDisplayName & " " & strTargetVersion
KillProcess ProcessToKill
WinExec "U:\packages\" & strPackageName
End Function
Function IsUpgradeNeeded(strVerActual,strVerDesired)
Dim arrActualVersion, arrDesiredVersion, i
'Break software version down on decimal points
arrActualVersion = split(strVerActual,".")
arrDesiredVersion = split(strVerDesired,".")
WScript.Echo " Comparing detected version " & strVerActual & " against desired version " & strVerDesired
'iterate, comparing each sub-version number starting from left
For i = 0 To UBound(arrActualVersion)
'WScript.Echo " comparing digit... is " & arrActualVersion(i) & " less than " & arrDesiredVersion(i)
If arrActualVersion(i) < arrDesiredVersion(i) Then
'installed version is out of date
IsUpgradeNeeded = True
Exit Function
ElseIf arrActualVersion(i) > arrDesiredVersion(i) Then
'installed version is newer
IsUpgradeNeeded = False
Exit Function
End If
Next
'thus far the version numbers are the same, but there may be additional
'decimal points of precision in the desired version
' e.g. Adobe Reader 10.1.4 is newer than 10.1
If UBound(arrDesiredVersion) > UBound(arrActualVersion) Then
IsUpgradeNeeded = True
Else
IsUpgradeNeeded = False
End If
End Function
Function MapNetworkDrive(strDriveLetter, strSharePath)
On Error Resume Next
'if the share name is not a UNC path, assume it's on the normal fileserver
If Not Left(strSharePath,2) = "\\" Then
strSharePath = "\\" & strFileServer & "\" & strSharePath
End If
If objFSO.DriveExists(strDriveLetter) Then
objNetwork.RemoveNetworkDrive strDriveLetter, True, True
End If
objNetwork.MapNetworkDrive strDriveLetter, strSharePath
If Err.Number <> 0 Then
WScript.Echo "Error - " & Err.Description
Err.Clear
End If
On Error Goto 0
End Function
Function WinExec(strExec)
Dim objExec, eTime
WinExec = True
Set objExec = objShell.Exec(strExec)
eTime = DateAdd("s", 120, Now)
Do While objExec.Status = 0
WScript.Sleep 1000
Loop
End Function
Function KillProcess(Process)
Dim strProcessElement
If IsArray(Process) Then
For Each strProcessElement in Process
KillIndividualProcess(strProcessElement)
Next
ElseIf Not Process = "" Then
KillIndividualProcess(Process)
End If
End Function
Function KillIndividualProcess(strProcess)
Dim colProcess, objProcess
Set colProcess = objWMI.ExecQuery("Select * from Win32_Process")
For Each objProcess in colProcess
If LCase(objProcess.Name) = LCase(strProcess) Then
WScript.Echo " Killing " & strProcess
'occasionally one parent process may kill all children leading to an object error
'so disable error handling temporarily
On Error Resume Next
objProcess.Terminate()
On Error Goto 0
End If
Next
End Function