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
good job and many thaks for sharing your experiences
Kamarade
Hi,
I’ve found this page while looking for the creation of Windows PE 4.0. I’ve read your script and, well, it is awesome.
Actually I deploy applications using virtual applications or unattended installations but I usually did something similar in the past so I know that do this in an automated way is complicated but you made it simply.
Congratulations and keep your good job!
I recommend you look at my winpe project. Its geared around puppet but it aims to ultimately do what you are trying to for with all free tools, powershell though.
If not a puppet user you can remove ruby puppet and facter pieces and have a really repeatable process with your home rolled solution.
https://github.com/rismoney/puppet-baremetal-windows