Questions? Comments? Email articles-hotfixes-wsh [at] tucuxi [dot] org
Well, I've been working all morning on a VBScript to automatically install updates in people's
logon scripts at work. Unfortunately, this is not easily done without the help of VBScript and
the Windows Scripting Host (WSH). Please note that this procedure requires users to have
Local Administrator privileges on their workstations. If this is not the case, you are probably
better served by using one of the following technologies:
The first step is to setup a repository of hotfixes on the network, accessible to all users. All Windows Hotfixes are now distributed with a filename in the form:
WindowsVVVV-KB######-PPP-LLL.exe
VVVV: Windows version - XP, 2000 etc.
######: Knowledge Base Article Number
PPP: Platform/Architecture - Usually x86
LLL: Language - In my case, ENU
Once we have these hotfixes, they are to be placed into a directory tree, as follows:
- \\server\software\hotfixes
- win2000
- sp3
- sp4
- qchain.exe
- Windows2000-KB885836-x86-ENU.EXE
- winxp
- sp2
- win2003
- sp0 (gold)
- win2000
Once this is done, we can move onto working with the script. The first step is to check what OS we are running, and what Service Pack. This can be done through WMI, the Windows Management Instrumentation. Since this's being written for the WSH, all we need do is construct a GetObject call, to winmgmts:\\COMPUTERNAME\root\cimv2. Since this script will be executed on the local computer, we can replace COMPUTERNAME with a single period, giving us:
Set objWMI = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Now that we have our management object, we execute a query using a syntax that's remarkably SQL-like. I haven't looked into WMI enough to tell if it actually is a SQL variant, but it's close enough for my needs. The Win32_OperatingSystem WMI object tells us all we need to know about a Windows box, and can be constructed thusly:
Set colItems = objWMI.Execquery("SELECT * FROM Win32_OperatingSystem",, 48)
This returns a collection of items, which is rather annoying, given that we're looking at one machine, but easily coded around. Simply use a For each ... loop, and grab out what we need:
For Each objItem in colItems
verBig = Left(objItem.Version, 3)
ServPack = objItem.ServicePackMajorVersion
OS = objItem.OSType
Next
Now all that's left to do is grab out the OS Type and version, and return accordingly. The OS types for Win95 and Win98 are defined in the WMI documentation for Win32_OperatingSystem, and are 16 and 17 respectively. NT-based Windows are a little more difficult to find, but solved with a Select Case statement:
Select Case OS
Case WMI_OS_NT
Select Case verBig
Case "5.0"
OSType = osWin2000
Case "5.1"
OSType = osWinXP
Case "5.2"
OSType = osWin2003
Case "4.0"
OSType = osWinNT4
ServPack = 0
End Select
.......
You may note that the Service Pack is set to 0 for Windows NT4, 95 and 98. This is due to a limitation of the WMI objects, which return a NULL service pack for these OSes. And, hell, if you're using this script to manage NT4 systems, you should probably be running SP6a anyways. Just chuck the files under winnt4\sp0 in the directory hierarchy, if need be.
Once we have retrieved OS and Service Pack, we can then move onto the meat of the script.. installing hotfixes. The basic switches being used for hotfix installation are /z /q, and correspond to 'don't reboot', and 'quiet mode'. Unfortunately, Microsoft has not standardised the unattended installation switches for hotfixes, and these differ between 2000 and XP/2003. We use /z /q /m for Win2k; /z /q /u for WinXP/20003, and /z /q for all others.
We then look through the filesystem of the hotfixes for appropriate .exe files to install. If the filename matches the regular expression (from the function getHotfixID), then it is executed.
One item of note here is the use of Wscript.echo in various places for status updates - if using the 'wscript' interpreter, many a message box will be displayed. This is bad. Use the following commandline to execute the script:
cscript //Nologo \\dc\sysvol\domain.net.au\scripts\wu.vbs
This will use the 'cscript' interpreter, which displays all messages to stdout. A much better alternative to bombarding users with irrelevant messages.
Now that we have identified the updates to install, we check whether they are already installed, using the registry key
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\
Uninstall\KB######\UninstallString
Using the WScript.Shell.RegRead() method, we check for the presence of an Uninstall application. This will detect all hotfixes that have been installed normally, but will not detect slipstreamed installs. This is not important here, but it is one caveat to note. Assuming the hotfix has not been installed, we now install it, and increment the nInstalled counter.
wshShell.run Hotfix.path & " " & Switches, 7, True
The 7 corresponds to a 'minimise and keep current focus' setting in WScript.Shell.Run(), and ensures that users are not bothered by updates. The third parameter specifies whether we should wait for a hotfix to complete installation before proceeding with the rest of the script. It's definitely a good idea to wait.
Now that all updates have been installed, if there has been more than one update installed, we run 'qchain'. This ensures that updates are performed safely, without worry of older hotfixes breaking newer ones. You can get qchain from Microsoft; just look for KB296861. Place qchain in the appropriate spot as indicated above.
If you have any questions, feel free to email the author.
' *******************
' * Windows Updates *
' *******************
Option Explicit
Const osUnknown = 0
Const osWin2000 = 1
Const osWinXP = 2
Const osWin2003 = 3
Const osWinNT4 = 4
Const osWin95 = 5
Const osWin98 = 6
Const WMI_OS_95 = 16
Const WMI_OS_98 = 17
Const WMI_OS_NT = 18
Const TEMP_FOLDER = 2
Function getWinVer()
Dim objWMI, objItem, colItems
Dim strComputer, verBig, OS, OSType, ServPack
' Localhost.
strComputer = "."
Set objWMI = GetObject("winmgmts:\\" & strComputer &"\root\cimv2")
Set colItems = objWMI.Execquery("SELECT * FROM Win32_OperatingSystem",, 48)
Set objWMI = Nothing
For Each objItem in colItems
verBig = Left(objItem.Version, 3)
ServPack = objItem.ServicePackMajorVersion
OS = objItem.OSType
Next
Set colItems = Nothing
OSType = osUnknown
Select Case OS
Case WMI_OS_NT
Select Case verBig
Case "5.0"
OSType = osWin2000
Case "5.1"
OSType = osWinXP
Case "5.2"
OSType = osWin2003
Case "4.0"
OSType = osWinNT4
ServPack = 0
End Select
Case WMI_OS_95
OSType = osWin95
ServPack = 0
Case WMI_OS_98
OSType = osWin98
ServPack = 0
End Select
getWinVer = OSType
End Function
Function getServicePack()
Dim objWMI, objItem, colItems
Dim strComputer, verBig, OS, OSType, ServPack
' Localhost.
strComputer = "."
Set objWMI = GetObject("winmgmts:\\" & strComputer &"\root\cimv2")
Set colItems = objWMI.Execquery("SELECT * FROM Win32_OperatingSystem",, 48)
Set objWMI = Nothing
For Each objItem in colItems
verBig = Left(objItem.Version, 3)
ServPack = objItem.ServicePackMajorVersion
OS = objItem.OSType
Next
Set colItems = Nothing
OSType = osUnknown
Select Case OS
Case WMI_OS_NT
Select Case verBig
Case "5.0"
OSType = osWin2000
Case "5.1"
OSType = osWinXP
Case "5.2"
OSType = osWin2003
Case "4.0"
OSType = osWinNT4
ServPack = 0
End Select
Case WMI_OS_95
OSType = osWin95
ServPack = 0
Case WMI_OS_98
OSType = osWin98
ServPack = 0
End Select
getServicePack = ServPack
End Function
Function getHotfixID(HotfixFile)
Dim regex, matches, n, match
Set regex = new RegExp
regex.IgnoreCase = True
regex.Pattern = "^(|.+-)(KB|Q)([0-9]+)(-[^.]+|).exe$"
getHotfixID = ""
if regex.test(HotfixFile) then
Set matches = regex.Execute(HotfixFile)
Set match = matches(0)
getHotfixID = match.SubMatches(2)
end if
End Function
Function isHotfixInstalled(hotfixID, installed)
Dim x
isHotfixInstalled = false
for x = 0 to ubound(installed)
if installed(x) = hotfixID then
isHotfixInstalled = true
end if
next
End Function
Function getMachineName()
Dim wshNet
Set wshNet = CreateObject("WScript.Network")
getMachineName = wshNet.ComputerName
Set wshNet = Nothing
End Function
Function QFECheck(hotfixPath)
Dim fso, wshShell, temp, file, l, regex, matches, match
Dim installed, instOld, x
installed = Array()
Set fso = CreateObject("Scripting.FileSystemObject")
Set wshShell = CreateObject("WScript.Shell")
Set regex = new RegExp
regex.pattern = "^(Q|KB)([0-9]+).*Current"
regex.IgnoreCase = true
temp = fso.GetSpecialFolder(TEMP_FOLDER)
if fso.fileExists(hotfixPath & "\qfecheck.exe") then
wshShell.run """" & hotfixPath & "\qfecheck.exe"" ""/l:" & temp & """", 7, True
if fso.fileExists(temp & "\" & getmachinename() & ".log") then
Set file = fso.OpenTextFile(temp & "\" & getmachinename() & ".log", 1, False, 0)
While not file.AtEndOfStream
l = file.readline
if regex.test(l) then
Set matches = regex.execute(l)
Set match = matches(0)
WScript.echo "lb: " & lbound(installed) & " ub: " & ubound(installed) & " " & match.submatches(1)
instOld = installed
Redim installed(ubound(installed)+1)
for x = lbound(instOld) to ubound(instOld)
installed(x) = instOld(x)
next
installed(ubound(installed)) = match.submatches(1)
end if
wend
Set file = Nothing
fso.DeleteFile temp & "\" & getmachinename() & ".log"
else
WScript.echo "Could not load qfecheck log"
end if
else
WScript.echo "Cannot find qfecheck"
WScript.quit 0
end if
QFECheck = installed
End Function
Sub displayUpdates(inst)
Dim s, x
s = "Installed updates:"
for x = lbound(inst) to ubound(inst)
s = s & " " & inst(x)
next
Wscript.echo s
End Sub
' *********************************
' * Body of the code starts here. *
' *********************************
Dim Switches, HotfixFolder, InstalledUpdates
Dim fso, fixFolder, hotfix, wshShell, hfixID, nInstalled
' Objects used in the script
Set fso = CreateObject("Scripting.FileSystemObject")
Set wshShell = CreateObject("WScript.Shell")
' Base setup for hotfixes
Switches = "/z /q"
HotfixFolder = "\\server\Software\Microsoft\Hotfixes"
nInstalled = 0
' Which OS are we running?
Select Case getWinVer()
Case osWinNT4
HotfixFolder = HotfixFolder & "\winnt4"
Case osWin2000
Switches = Switches & " /m"
HotfixFolder = HotfixFolder & "\win2000"
Case osWinXP
Switches = Switches & " /u"
HotfixFolder = HotfixFolder & "\winxp"
Case osWin2003
Switches = Switches & " /u"
HotfixFolder = HotfixFolder & "\win2003"
End Select
HotfixFolder = HotfixFolder & "\sp" & getServicePack()
WScript.echo "We are installing hotfixes from " & HotfixFolder
' Run qfecheck
installedUpdates = QFECheck(hotfixFolder)
displayUpdates installedUpdates
if fso.FolderExists(HotfixFolder) then
WScript.echo "Parsing hotfixes... "
Set fixFolder = fso.GetFolder(HotfixFolder)
For each hotfix in fixFolder.Files
if (lcase(hotfix.name) <> "qchain.exe") then
if (lcase(hotfix.name) <> "qfecheck.exe") then
if lcase(right(hotfix.name, 4)) = ".exe" then
hfixID = gethotfixid(hotfix.name)
if hfixID <> "" then
if isHotfixInstalled(hfixID, installedUpdates) then
WScript.Echo "Not Installing " & hfixID & ": Already installed"
else
WScript.Echo "Installing " & hfixID & "..."
wshShell.run Hotfix.path & " " & Switches, 7, True
nInstalled = nInstalled + 1
end if
end if
end if
end if
end if
Next
Set fixFolder = Nothing
if nInstalled > 1 then
if fso.FileExists(HotfixFolder & "\qchain.exe") then
WScript.echo "Running qchain..."
wshShell.run HotfixFolder & "\qchain.exe", 7, True
end if
end if
else
WScript.echo "Sorry, no hotfixes today."
end if
Set wshShell = Nothing
Set fso = Nothing