Introduction
Microsoft recently released a tool called Hfnetchk that checks NT 4 and Windows 2000 systems and generates a
report of which service packs and hotfixes need to be installed. While this is a great idea, the tool is a console-mode
program and only prints out a list of Knowledge Base article numbers; it doesn't list URLs or take you to the Knowledge
Base articles about the hotfixes. Fortunately, Hfnetchk has a switch to produce tab-delimited output, which makes
it rather simple to parse the output and show it in a more friendly UI. This article describes WHotfixCheck, a
tool I wrote to provide just such a friendly UI, and touches on some interesting programming topics that I encountered
while writing it.
Updates:
- Sept. 23, 2001 - Added ability to scan remote computers. Added code to save and load last-used settings to/from
the registry. Thanks to Uwe Keim for adding these new features!
- Oct. 11, 2001 - Added Save Results button to save the scan results to an HTML file.
- Oct 18, 2001 - Added support for more Hfnetchk switches: show missing or installed hotfixes, skip registry
checks, verbose output. Made the HTML output a bit nicer-looking.
- Nov. 7, 2001 - Improved handling of errors and messages from Hfnetchk. If Hfnetchk outputs a message (such as "no hotfixes available"), WHotfixCheck now shows that in a message box. Added XP theme support.
Using WHotfixCheck
To use WHotfixCheck, you must first download and install Hfnetchk from Microsoft. Browse to the
Knowledge Base article on the tool and follow the download link in that article. Once it's installed, run WHotfixCheck
and enter the path to hfnetchk.exe
in the top edit box. If you have never run Hfnetchk before, leave
the other edit box empty and Hfnetchk will download the necessary data file. If you have run it, there will be
a file called mssecure.xml
in the same directory as hfnetchk.exe
; enter the path to that
file in the second edit box.
In the Hotfixes to show combo box, select which hotfixes you want to see. The default is Necessary
hotfixes, which shows hotfixes you have not installed. Select Installed hotfixes to see hotfixes you
have already installed. Missing hotfixes is similar to Necessary hotfixes but also shows hotfixes
that have been superseded by later hotfixes.
If you are viewing installed hotfixes, and WHotfixCheck reports that a hotfix that you have already installed
is missing, check the Skip registry checks checkbox. Not all hotfixes are recorded in the registry, so they
are always reported as not being installed. Checking Skip registry checks makes Hfnetchk ignore such hotfixes
and not report them.
Check Verbose output if you want to see additional details in the hotfix list. This will show the status
for each hotfix (found, not found, etc.) and an explanation of how Hfnetchk determined the status.
In the What to Scan combo box is a list of options relating to what computers should be scanned. Hfnetchk
can remotely scan any computer on which you have administrator privileges. You can choose to scan just your local
computer, a remote computer (identified by its name or IP address), a range of IP addresses, or an entire domain.
Once you have provided the locations of the needed files, click Run to begin the scan. If everything
goes well, you will see a list of available hotfixes that are not installed on your computer. See the screen shot
above for an idea of what the list looks like. Each line of the table lists the computer name, product name, Microsoft
security bulletin number, and Knowledge Base article number. The latter two columns are linked to the corresponding
web pages at Microsoft's site, so you can use those links to quickly get to Microsoft's descriptions of the hotfixes.
After running a scan, you can save the results to an HTML file by clicking the Save Results button.
Implementation Details
In this section I'll describe some of the more interesting parts of the code. You can safely skip this section
if you aren't concerned with the internal workings of the program.
Running Hfnetchk and capturing its output
If WHotfixCheck were a console program, we could use the CRT function _popen()
to run Hfnetchk
and read its output. However, _popen()
doesn't work when called from a GUI program, so we must create
a pipe ourselves and attach it to Hfnetchk's standard output.
The first step is to set up security attributes for the pipe handles. The SECURITY_ATTRIBUTES
struct
contains a flag that determines whether the child process we'll launch can inherit handles from our process. This
flag must be set to TRUE
.
LRESULT CMainDlg::OnRun(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES) };
sa.bInheritHandle = TRUE;
Next, we create the pipe. We get back two handles, one to the read end and one to the write end.
HANDLE hStdoutRd, hStdoutWr;
if ( !CreatePipe ( &hStdoutRd, &hStdoutWr, &sa, 0 ))
return 0;
Next, we set up the structures used by CreateProcess()
. The important part here is that we set
the child process's standard output to be the write end of the pipe.
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdOutput = hStdoutWr;
Next, we launch Hfnetchk.
if ( !CreateProcess ( NULL, szCommandLine, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS,
NULL, NULL, &si, &pi ))
{
return 0;
}
The fifth parameter is a flag that indicates whether the child process inherits our handles. This must be TRUE
for Hfnetchk to be able to use the pipe handle we're passing it.
Once Hfnetchk has been launched, we can start reading its output. To make parsing easier, we dump the output
to a temp file first. The first step is to close our handle to the write end of the pipe. If we don't do this,
the write end will not close when Hfnetchk is done with it, because the pipe won't close when there are open handles
to it.
CloseHandle ( hStdoutWr );
Next, we create a temp file to store Hfnetchk's output.
HANDLE hTempFile;
hTempFile = CreateFile ( szTempFile, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL );
if ( INVALID_HANDLE_VALUE == hTempFile )
return 0;
Now we can enter a loop and read the data from the pipe.
BYTE buff[1024];
DWORD dwRead, dwWritten;
while ( ReadFile ( hStdoutRd, buff, countof(buff), &dwRead, NULL ) && dwRead != 0 )
{
WriteFile ( hTempFile, buff, dwRead, &dwWritten, NULL );
}
ReadFile()
will return FALSE
when Hfnetchk has written all its data into the pipe
and terminated, thus closing the write end of the pipe.
After that, it's a simple matter of parsing the lines with strtok()
. Each line contains details
about one hotfix. We pull out the computer name, product name, security bulletin number, and Knowledge Base article
number and store them in a struct:
struct CPatchInfo
{
CString sComputer, sProduct, sBulletin, sKBNumber;
};
OnRun()
builds an array of structs and passes it to ShowPatchList()
, which handles
the UI.
Displaying the results
WHotfixCheck hosts a WebBrowser control and uses the IE DHTML object model to display HTML in the browser. The
starting point it to get an IWebBrowser2
pointer on the control.
void CMainDlg::ShowPatchList ( CSimpleArray<CPatchInfo>& aPatchInfo )
{
CComPtr<IUnknown> punkIE;
CComQIPtr<IWebBrowser2> pWB2;
AtlAxGetControl ( GetDlgItem(IDC_IE), &punkIE );
pWB2 = punkIE;
Now, we can get a pointer to the HTML document, and then the <body>
element. Once we have
access to the body, we can insert any HTML we want. (There's actually a lot more code involved, but I've omitted
it here for clarity.)
CComPtr<IDispatch> pdispDoc;
CComQIPtr<IHTMLDocument2> pDoc;
CComPtr<IHTMLElement> peltBody;
pWB2->get_Document ( &pdispDoc );
pDoc = pdispDoc;
pDoc->get_body ( &peltBody );
Now we can insert HTML into the body. The first step is to create a <table>
with one row
that contains the column headings.
peltBody->put_innerHTML ( CComBSTR("<table width=100% id=\"patches\" cols=3><tr>...</tr></table>"));
The <table>
has an id
field so we can modify it with the DHTML object model.
The next step is to get a IHTMLTable
interface on the table. There are two ways to do this. To maintain
compatibility with IE 4, WHotfixCheck accesses the all
collection of the document and gets the table
from that collection. A simpler way is to use IHTMLDocument3::getElementById()
to get an interface
on the table by passing its id, but this method requires IE 5.
CComPtr<IHTMLElementCollection> pColl;
CComPtr<IDispatch> pdispTable;
CComQIPtr<IHTMLTable> pTable;
pDoc->get_all ( &pColl );
pColl->item ( CComVariant("patches"), CComVariant(0), &pdispTable );
pTable = pdispTable;
Now we can start adding rows to the table. We loop through the array of CPatchInfo
structs and
for each one, assemble either a string or an HTML fragment to go in the table. Adding to the table involves first
adding a row, then adding cells to the row. Here's how we add a new row:
for ( int i = 0; i < aPatchInfo.GetSize(); i++ )
{
CComPtr<IDispatch> pdispRow;
CComQIPtr<IHTMLTableRow> pRow;
pTable->insertRow ( -1, &pdispRow );
pRow = pdispRow;
Passing a row index of -1 puts the new row at the end of the table. With the IHTMLTableRow
interface,
we can add cells to the row. Here is the code to add column 1, a plain string listing the computer name:
CComPtr<IDispatch> pdispCell;
CComQIPtr<IHTMLElement> peltCell;
pRow->insertCell ( -1, &pdispCell );
peltCell = pdispCell;
peltCell->put_innerText ( CComBSTR(aPatchInfo[i].sComputer) );
pdispCell.Release();
peltCell.Release();
IHTMLElement::put_innerText()
sets the text of the element. The word "inner" indicates
that the text being changed is inside the HTML tags (<td>
and </td>
in this
case). Column 2 is created similarly, and shows the product name.
For column 3, we create an HTML fragment containing an <a>
tag, which makes a hyperlink.
CString sText;
pRow->insertCell ( -1, &pdispCell );
peltCell = pdispCell;
sText.Format ( _T("<a href=\"http://www.microsoft.com/technet/security/bulletin/%s.asp\" target=_blank>%s</a>"),
(LPCTSTR) aPatchInfo[i].sBulletin, (LPCTSTR) aPatchInfo[i].sBulletin );
peltCell->put_innerHTML ( CComBSTR(sText) );
pdispCell.Release();
peltCell.Release();
This time we use IHTMLElement::put_innerHTML()
to insert an HTML fragment into the cell. Column
4 is created similarly to column 3, and links to the Knowledge Base article.
As you can see, accessing the DHTML object model from C++ is a bit cumbersome, since many methods return IDispatch
interfaces, which you then have to query for the interface you really need. The documentation is also lacking in
some areas, most notably the fact that the Web Workshop pages that have the object model reference do not appear
in the Contents pane, so you can't use the Locate button in the help viewer. I would normally use the Locate button
to jump right to the list of methods in an interface.
Room for Improvement
The code in ShowPatchList()
that gets a pointer to the <body>
is rather ugly,
since it has to handle the special case of the first call, where there is no document or body in the WebBrowser
yet. I threw together something that creates a dummy body, since I found no IHTMLDocument
method that
looked like it could do the job.
Links
Find out more about Hfnetchk in these Knowledge Base articles:
WHotfixCheck and Hfnetchk require Microsoft's XML parser. Download
version 3 here.