I hope most of you already know what an assertion is, because it's the most important proactive programming tool in your debugging arsenal. For those who are unfamiliar with the
Even if you've
If used sufficiently, assertions will tell you most of the information you need to diagnose a problem at the first sign of trouble. Without assertions, you'll
A side benefit of using plenty of assertions is that they serve as outstanding additional documentation in your code. What assertions capture is your intent. I'm sure you go well out of your way to keep your design documents
My stock answer when asked what to assert is to assert everything. I would love to say that for every line of code you should have an assertion, but it's an
Assertions should never change any
In this section, I want to concentrate on how to use assertions and what to assert. I'll do this by showing code examples. I need to mention that in these examples, Debug.Assert is the .NET assertion from the System.Diagnostic namespace, and ASSERT is the native C++ method, which I'll introduce later in this chapter.
|
|
The Battle
A long, long time ago, I worked at a company whose software product had serious stability problems. As the senior
After sending out the memo, I
BOOL DoSomeWork ( HMODULE * pModArray , int iCount , LPCTSTR szBuff ) { ASSERT ( if ( ( pModArray == NULL ) && ( IsBadWritePtr ( pModArray , ( sizeof ( HMODULE ) * iCount ) ) && ( iCount != 0 ) && ( szBuff != NULL ) ) ) { return ( FALSE ) ; } ) ; for ( int i = 0 ; i < iCount ; i++ ) { pModArray[ i ] = m_pDataMods[ i ] ; }
}
The Outcome
I should also mention here that my boss and I
By this point, I was livid and screamed at the top of my lungs, "Whoever wrote this needs to be
The Lesson
I can't stress this enough: use assertions
in addition
to normal error handling, never as a replacement for it. If you have an assertion, you need to have some
|
|
The first rule when using assertions is to check a single item at a time. If you check multiple conditions with just one assertion, you have no way of knowing which condition caused the failure. In the following example, I show the same function with two assertion checks. Although the assertion in the first function will catch a bad parameter, the assertion won't tell you which condition failed or even which of the three parameters is the offending one.
// The wrong way to write an assertion. Which parameter was bad? BOOL GetPathItem ( int i , LPTSTR szItem , int iLen ) { ASSERT ( ( i > 0 ) && ( NULL != szItem ) && ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) && ( FALSE == IsBadStringPtr ( szItem , iLen ) ) ) ;
} // The proper way. Each parameter is checked individually so that you // can see which one failed. BOOL GetPathItem ( int i , LPTSTR szItem , int iLen ) { ASSERT ( i > 0 ) ; ASSERT ( NULL != szItem ) ; ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ; ASSERT ( FALSE == IsBadStringPtr ( szItem , iLen ) ) ;
}
When you assert a condition, you need to strive to check the condition completely. For example, if your .NET method takes a string as a parameter and you expect the string to have something in it, checking against null checks only part of the error condition.
// An example of checking only a part of the error condition bool LookupCustomerName ( string CustomerName ) { Debug.Assert ( null != CustomerName , "null != CustomerName" ) ;
}
You can check the full condition by also checking to see whether the string is empty.
// An example of completely checking the error condition bool LookupCustomerName ( string CustomerName ) { Debug.Assert ( null != CustomerName , "null != CustomerName" ) ; Debug.Assert ( 0 != CustomerName.Length ,"\"\" != CustomerName.Length" ) ;
Another step I always take is to ensure that I'm
// Example of a poorly written assertion: nCount should be positive, // but the assertion doesn't fail when nCount is negative. void UpdateListEntries ( int nCount ) { ASSERT ( nCount ) ;
} // A proper assertion that explicitly checks against what the value // is supposed to be void UpdateListEntries ( int nCount ) { ASSERT ( nCount > 0 ) ;
}
The incorrect sample
Now that you're armed with an idea of how to assert, we can
As you move inside your module, the parameters of the module's private methods might not require as much checking, depending
I sit right in the middle of the two extremes. Deciding how many parameter assertions are right for you just takes some experience. As you get a feel for where you typically encounter problems in your code, you'll figure out where and when you need to assert parameters internal to your module. One safeguard I've learned to use is to add parameter assertions whenever a bad parameter blows up my code. That way, the mistake won't get repeated because the assertion will catch it.
Another area that's mandatory for assertions is method return values because the return values tell you whether methods succeeded or failed. One of the biggest problems I see in debugging other developers' code is that they simply call methods without ever checking the return value. I have seen so many cases in which I've looked for a bug, only to find out that some method early on in the code failed but no one bothered to check its return value. Of course, by the time you realize the culprit, the bug is manifested, so the program dies or corrupts data some 20 minutes later. By asserting return values appropriately, you at least know about a problem when it happens.
Keep in mind that I'm not advocating asserting on every single possible failure. Some failures are expected in code, and you should handle them appropriately. Having an assertion fire each time a lookup in the database fails will likely drive everyone to disabling assertions in the project. Be smart about it, and assert on return values when it's something serious. Handling good data throughout your program should never cause an assertion to trigger.
Finally, I recommend that you use assertions when you need to check an assumption. For example, if the specifications for a class require 3 MB of disk space, you should check this assumption with a conditional compilation assertion inside that class to ensure the
In both of the
Listing 3-1: AssertTableExists checks whether a table exists
|
|
[Conditional("DEBUG")] static public void AssertTableExists ( string ConnStr , string TableName ) { SqlConnection Conn = new SqlConnection ( ConnStr ) ; StringBuilder sBuildCmd = new StringBuilder ( ) ; sBuildCmd.Append ( "select * from dbo.sysobjects where " ) ; sBuildCmd.Append ( "id = object_id('" ) ; sBuildCmd.Append ( TableName ) ; sBuildCmd.Append ( "')" ) ; // Make the command. SqlCommand Cmd = new SqlCommand ( sBuildCmd.ToString ( ) , Conn ) ; try { // Open the database. Conn.
Open
( ) ; // Create a dataset to fill. DataSet TableSet = new DataSet ( ) ; // Create the data adapter. SqlDataAdapter TableDataAdapter = new SqlDataAdapter ( ) ; // Set the command to do the select. TableDataAdapter.SelectCommand = Cmd ; // Fill the dataset from the adapter. TableDataAdapter.Fill ( TableSet ) ; // If anything showed up, the table exists. if ( 0 == TableSet.Tables[0].Rows.Count ) { String sMsg = "Table : '" + TableName + "' does not exist!\r\n" ; Debug.Assert ( false , sMsg ) ; } } catch ( Exception e ) { Debug.Assert ( false , e.Message ) ; } finally { Conn.Close ( ) ; } }
|
|
Before I describe issues unique to the various assertions for .NET and native code, I want to show an example of how I handle assertions. Listing 3-2 shows the StartDebugging function from the native code debugger in Chapter 4. This code is an entry point from one module to another, so it shows all the appropriate assertions I covered in this section. I chose a C++ method because so many more problems can surface in native C++ and thus there are more conditions to assert. I'll go over some of the issues you'll see in this example later in the section "Assertions in Native C++ Applications."
Listing 3-2: Full assertion example
|
|
HANDLE DEBUGINTERFACE_DLLINTERFACE __stdcall StartDebugging ( LPCTSTR szDebuggee , LPCTSTR szCmdLine , LPDWORD lpPID , CDebugBaseUser * pUserClass , LPHANDLE lpDebugSyncEvents ) { // Assert the parameters. ASSERT ( FALSE == IsBadStringPtr ( szDebuggee , MAX_PATH ) ) ; ASSERT ( FALSE == IsBadStringPtr ( szCmdLine , MAX_PATH ) ) ; ASSERT ( FALSE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) ) ; ASSERT ( FALSE == IsBadReadPtr ( pUserClass , sizeof ( CDebugBaseUser * ) ) ) ; ASSERT ( FALSE == IsBadWritePtr ( lpDebugSyncEvents , sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) ) ; // Check them all for real. if ( ( TRUE == IsBadStringPtr ( szDebuggee , MAX_PATH ) ) ( TRUE == IsBadStringPtr ( szCmdLine , MAX_PATH ) ) ( TRUE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) ) ( TRUE == IsBadReadPtr ( pUserClass , sizeof ( CDebugBaseUser * ) ) ) ( TRUE == IsBadWritePtr ( lpDebugSyncEvents , sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) ) ) { SetLastError ( ERROR_INVALID_PARAMETER ) ; return ( INVALID_HANDLE_VALUE ) ; } // The string used for the startup acknowledgment event TCHAR szStartAck [ MAX_PATH ] = _T ( "\0" ) ; // Load up the string for startup acknowledgment. if ( 0 == LoadString ( GetDllHandle ( ) , IDS_DBGEVENTINIT , szStartAck , MAX_PATH ) ) { ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!" ) ; return ( INVALID_HANDLE_VALUE ) ; } // The handle of the startup acknowledgment that this function // will wait on until the debug thread gets started HANDLE hStartAck = NULL ; // Create the startup acknowledgment event. hStartAck = CreateEvent ( NULL , // Default security TRUE , // Manual-reset event FALSE , // Initial state=Not signaled szStartAck ) ; // Event
name
ASSERT ( NULL != hStartAck ) ; if ( NULL == hStartAck ) { return ( INVALID_HANDLE_VALUE ) ; } // Bundle up the parameters. THREADPARAMS stParams ; stParams.lpPID = lpPID ; stParams.pUserClass = pUserClass ; stParams.szDebuggee = szDebuggee ; stParams.szCmdLine = szCmdLine ; // The handle to the debug thread HANDLE hDbgThread = INVALID_HANDLE_VALUE ; // Try to create the thread. UINT dwTID = 0 ; hDbgThread = (HANDLE)_beginthreadex ( NULL , 0 , DebugThread , &stParams , 0 , &dwTID ) ; ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ; if (INVALID_HANDLE_VALUE == hDbgThread ) { VERIFY ( CloseHandle ( hStartAck ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Wait until the debug thread gets good and cranking. DWORD dwRet = ::WaitForSingleObject ( hStartAck , INFINITE ) ; ASSERT (WAIT_OBJECT_0 == dwRet ) ; if (WAIT_OBJECT_0 != dwRet ) { VERIFY ( CloseHandle ( hStartAck ) ) ; VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Get rid of the acknowledgment handle. VERIFY ( CloseHandle ( hStartAck ) ) ; // Check that the debug thread is still running. If it isn't, // the debuggee probably couldn't get started. DWORD dwExitCode = ~STILL_ACTIVE ; if ( FALSE == GetExitCodeThread ( hDbgThread , &dwExitCode ) ) { ASSERT ( !"GetExitCodeThread failed!" ) ; VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ; } ASSERT ( STILL_ACTIVE == dwExitCode ) ; if ( STILL_ACTIVE != dwExitCode ) { VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Create the synchronization events so that the main thread can // tell the debug loop what to do. BOOL bCreateDbgSyncEvts = CreateDebugSyncEvents ( lpDebugSyncEvents , *lpPID ) ; ASSERT ( TRUE == bCreateDbgSyncEvts ) ; if ( FALSE == bCreateDbgSyncEvts ) { // This is a serious problem. I got the debug thread going, but // I was unable to create the synchronization events that the //
user
interface thread needs to control the debug thread. My // only option here is to punt. I'll kill the // debug thread and just return. I can't do much else. TRACE ( "StartDebugging : CreateDebugSyncEvents failed\n" ) ; VERIFY ( TerminateThread ( hDbgThread , (DWORD)-1 ) ) ; VERIFY ( CloseHandle ( hDbgThread ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Just in case someone modifies the function and fails to properly // initialize the returned value. ASSERT ( INVALID_HANDLE_VALUE != hDbgThread ) ; // Life is good! return ( hDbgThread ) ; }
|
|
Before I get into the gritty details of .NET assertions, I want to mention one key mistake I've seen in almost all .NET code written, especially in many of the samples from which developers are
void DoSomeWork ( string TheName ) { Debug.Assert ( TheName.Length > 0 ) ;
Instead of triggering the assertion, if
TheName
is
null
, calling the
Length
property causes a
System.NullReferenceException
exception in your application, effectively crashing it. This is the horrible case where the assertion is
The fact that .NET applications don't have to worry about pointers and memory blocks means that at least 60 percent of the assertions we were used to handling in the C++ days just went away. On the assertion front, the .NET team added as part of the
System.Diagnostic
namespace two objects,
Debug
and
Trace
, which are active only if you defined
DEBUG
or
TRACE
, respectively, when compiling your application. Both of these defines can be specified as part of the project Property Pages dialog box. As you've seen, the
Assert
method is the method handling assertions in .NET. Interestingly enough, both
Debug
and
Trace
have identical methods, including an
Assert
method. I find it a little confusing to have two possible assertions that are conditionally compiled differently. Consequently, since assertions should be active only in debug builds, I use only
Debug.Assert
for assertions. Doing so avoids
There are three overloaded
Assert
methods. All three take a Boolean value as their first or only parameter, and if the value is
false
, the assertion is triggered. As shown in the preceding examples in which I used
Debug.Assert
, one of the methods takes a second parameter of type
string
, which is shown as a message in the output. The final overloaded
Assert
method takes a third parameter of type
string
, which provides even more information when the assertion triggers. In my experience, the two-parameter approach is the
Debug.Assert ( i > 3 ) Debug.Assert ( i > 3 , "i > 3" ) Debug.Assert ( i > 3 , "i > 3" , "This means I got a bad parameter")
The .NET
Debug
object is intriguing because you can see the output in multiple ways. The output for the
Debug
object—and the
Trace
object for that matter—goes through another object, named a
TraceListener
. Classes derived from
TraceListener
are added to the
Debug
object's
Listener
collection property. The beauty of the
TraceListener
approach is that each time an assertion fails, the
Debug
object runs through the
Listener
collection and calls each
TraceListener
object in turn. This
The default TraceListener object, appropriately named DefaultTraceListener , sends the output to two different places, the most visible of which is the assertion message box shown in Figure 3-1. As you can see in the figure, the bulk of the message box is taken up with the stack walk and parameter types as well as the source and line for each item. The top lines of the message box report the string values you passed to Debug.Assert . In the case of Figure 3-1, I just passed "Debug.Assert assertion" as the second parameter to Debug.Assert .
Figure 3-1:
The
DefaultTraceListener
message box
The result of pressing each button is described in the title bar for the message box. The only interesting button is Retry. If you're running under a debugger, you simply drop into the debugger at the line directly after the assertion. If you're not running under a debugger, clicking Retry triggers a special exception and then launches the Just In Time debugger selector to allow you to pick which registered debugger you'd like to use to debug the assertion.
In addition to the message box output,
Debug.Assert
also sends all the output through
OutputDebugString
so the attached debugger will get the output. The output has a nearly identical format, shown in the following code. Since the
DefaultTraceListener
does the
OutputDebugString
output, you can always use Mark Russinovich's
---- DEBUG ASSERTION FAILED ---- ---- Assert Short Message ---- Debug.Assert assertion ---- Assert Long Message ---- at HappyAppy.Fum() d:\asserterexample\asserter.cs(15) at HappyAppy.Fo(StringBuilder sb) d:\asserterexample\asserter.cs(20) at HappyAppy.Fi(IntPtr p) d:\asserterexample\asserter.cs(24) at HappyAppy.Fee(String Blah) d:\asserterexample\asserter.cs(29) at HappyAppy.Baz(Double d) d:\asserterexample\asserter.cs(34) at HappyAppy.Bar(Object o) d:\asserterexample\asserter.cs(39) at HappyAppy.Foo(Int32 i) d:\asserterexample\asserter.cs(46) at HappyAppy.Main() d:\\asserterexample\asserter.cs(76)
Armed with the information supplied by
Debug.Assert
, you should never again have to
BugslayerUtil.NET.DLL
, I went ahead and wrote the correct versions for
TextWriterTraceListener
and
EventLogTraceListener
for you:
BugslayerTextWriterTraceListener
and
BugslayerEventLogTraceListener
, respectively.
Neither BugslayerTextWriterTraceListener nor BugslayerEventLogTraceListener are very exciting classes. BugslayerTextWriterTraceListener is derived directly from TextWriterTraceListener , so all it does is override the Fail method, which is what Debug.Assert calls to do the output. Keep in mind that when using BugslayerTextWriterTraceListener or TextWriterTraceListener , the associated text file for the output isn't flushed unless you set the trace element autoflush attribute to true in the application configuration file, explicitly call Close on the stream or file, or set Debug.AutoFlush to true so that each write causes a flush to disk. For some bizarre reason, the EventLogTraceListener class is sealed, so I couldn't derive directly from it and had to derive from the abstract TraceListener class directly. However, I did retrieve the stack trace in an interesting way. The default StackTrace class provided by .NET makes it easy to get the current stack trace at any point, as the following snippet shows:
StackTrace StkTrc = new StackTrace ( ) ;
Compared with the gyrations you have to go through with native code to get a stack trace, the .NET way is a fine example of how .NET can make your life easier.
StackTrace
returns the collection of
StackFrame
objects that comprise the stack. Looking through the documentation for
StackFrame
, you'll see that it has all sorts of interesting methods for getting the source line and number. The
StackTrace
object has a
ToString
method that I thought for sure would have some sort of option for adding the source and line to the resulting stack trace. Alas, I was wrong. Therefore, I had to spend 30 minutes coding up and testing a class that
BugslayerStackTrace
derived from
StackTrace
, which
Listing 3-3: BugslayerStackTrace building a full stack trace with source and line information
|
|
/// <summary> /// Builds a readable representation of the stack trace /// </summary> /// <returns> /// A readable representation of the stack trace /// </returns> public override string ToString ( ) { // New up the StringBuilder to hold all the stuff. StringBuilder StrBld = new StringBuilder ( ) ; // First thing on is a line feed. StrBld.Append ( DefaultLineEnd ) ; // Loop'em and do'em! You can't use foreach here as StackTrace // is not derived from IEnumerable. for ( int i = 0 ; i < FrameCount ; i++ ) { StackFrame StkFrame = GetFrame ( i ) ; if ( null != StkFrame ) { BuildFrameInfo ( StrBld , StkFrame ) ; } } return ( StrBld.ToString ( ) ) ; } /*///////////////////////////////////////////////////////////////// // Private methods. /////////////////////////////////////////////////////////////////*/ /// <summary> /// Takes care of the scut work to convert a frame into a string /// and to plop it into a string builder /// </summary> /// <param name="StrBld"> /// The StringBuilder to append the results to /// </param> /// <param name="StkFrame"> /// The stack frame to convert /// </param> private void BuildFrameInfo ( StringBuilder StrBld , StackFrame StkFrame ) { // Get the method from all the cool reflection stuff. MethodBase Meth = StkFrame.GetMethod ( ) ; // If nothing is returned, get out now. if ( null == Meth ) { return ; } // Grab the method. String StrMethName = Meth.ReflectedType.Name ; // Slap in the function indent if one is there. if ( null != FunctionIndent ) { StrBld.Append ( FunctionIndent ) ; } // Get the class type and name on there. StrBld.Append ( StrMethName ) ; StrBld.Append ( "." ) ; StrBld.Append ( Meth.Name ) ; StrBld.Append ( "(" ) ; // Slap the parameters on, including all param
names
. ParameterInfo[] Params = Meth.GetParameters ( ) ; for ( int i = 0 ; i < Params.Length ; i++ ) { ParameterInfo CurrParam = Params[ i ] ; StrBld.Append ( CurrParam.ParameterType.Name ) ; StrBld.Append ( " " ) ; StrBld.Append ( CurrParam.Name ) ; if ( i != ( Params.Length - 1 ) ) { StrBld.Append ( ", " ) ; } } // Close the param list. StrBld.Append ( ")" ) ; // Get the source and line on only if there is one. if ( null != StkFrame.GetFileName ( ) ) { // Am I supposed to indent the source? If so, I need to put // a line break on the end followed by the indent. if ( null != SourceIndentString ) { StrBld.Append ( LineEnd ) ; StrBld.Append ( SourceIndentString ) ; } else { // Just add a space. StrBld.Append ( ' ' ) ; } // Get the file and line of the problem on here. StrBld.Append ( StkFrame.GetFileName ( ) ) ; StrBld.Append ( "(" ) ; StrBld.Append ( StkFrame.GetFileLineNumber().ToString()); StrBld.Append ( ")" ) ; } // Always stick a line feed on. StrBld.Append ( LineEnd ) ; }
|
|
Now that you have other TraceListener classes that are worth adding to the Listeners collection, we can add and remove TraceListener objects in code. As with any .NET collection, call the Add method to add an object and the Remove method to get rid of one. Note that the default trace listener is named "Default." The following code example shows adding BugslayerTextWriterTraceListener and removing DefaultTraceListener :
Stream AssertFile = File.Create ( "BSUNBTWTLTest.txt" ) ; BugslayerTextWriterTraceListener tListener = new BugslayerTextWriterTraceListener ( AssertFile ) ; Debug.Listeners.Add ( tListener ) ; Debug.Listeners.Remove ( "Default" ) ;
If you develop console and Windows Forms applications, for the most part, DefaultTraceListener should serve most of your needs. However, having a message box that pops up every once in a while can wreak havoc on any automated test scripts you might have. Alternatively, you use a third-party component in a Win32 service and the debug build of that component is properly using Debug.Assert . In both of these cases, you want to be able to shut off the message box generated by DefaultTraceListener . You could add code to remove the DefaultTraceListener object, but you can also remove it without touching the code.
Any .NET binary can have an external XML configuration file associated with it. This file resides in the same directory as the binary and is the name of the binary with .CONFIG appended to the end. For example, the configuration file for FOO.EXE is FOO.EXE.CONFIG. You can easily add a configuration file to your project by adding a new XML file named
APP.CONFIG
. That file will automatically be
In the XML configuration file, the
assert
element under
system.diagnostics
has two attributes. If you set the first attribute,
assertuienabled
, to
false
, .NET doesn't display message boxes and the output still goes through
OutputDebugString
. The second attribute,
logfilename
, allows you to specify a file you want any assertion output written to. Interestingly, when you specify a file in the
logfilename
attribute, any trace statements, which I'll discuss later in the chapter, will also appear in the file. A minimal configuration file is shown in the next code snippet, and you can see how easy it is to shut off the assertion message boxes. Don't forget that the master configuration file MACHINE.CONFIG has the same settings as the EXE configuration file, so you can
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <system.diagnostics> <assert assertuienabled="false" logfilename="tracelog.txt" /> </system.diagnostics> </configuration>
As I mentioned earlier, you can add and remove listeners without touching the code, and as you probably guessed, the configuration file has something to do with it. This file looks straightforward in the documentation, but the documentation at the time I am writing this book is not correct. After a little experimentation, I figured out all the gyrations necessary to control your listeners correctly without changing the code.
All the action happens under the trace element of the configuration. The trace element happens to have one very important optional attribute you should always set to true in your configuration files: autoflush . By setting autoflush to true , you force the output buffer to be flushed each time a write operation occurs. If you don't set autoflush , you'll have to add calls to your code to get the output.
Underneath trace is the listener element, where TraceListener objects are added or removed. Removing a TraceListener object is very simple. Specify the remove element, and set the name attribute to the string name of the desired TraceListener object. Below is the complete configuration file necessary to remove DefaultTraceListener .
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <system.diagnostics> <trace autoflush="true" indentsize="0"> <listeners> <remove name="Default" /> </listeners> </trace> </system.diagnostics> </configuration>
The add element has two required attributes. The name attribute is a string that specifies the name of the TraceListener object as it is placed into the TraceListener.Name property. The second attribute, type , is the one that's confusing, and I'll explain why. The documentation shows only adding a type that is in the global assembly cache (GAC) and hints that adding your own trace listeners is much harder than it needs to be. The one optional attribute, initializeData , is the string passed to the constructor of the TraceListener object.
To add a TraceListener object that's in the GAC, the type element specifies only the complete class of the TraceListener object. The documentation indicates that to add a TraceListener object that's not in the GAC, you'll have to plug in a whole bunch of stuff like culture and public key tokens. Fortunately, all you need to do is simply specify the complete class, a comma, and the name of the assembly. That's what causes the System.Configuration. ConfigurationException to be thrown, so don't include the comma and class name. The following shows the proper way of adding the global TextWriterTraceListener class:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <system.diagnostics> <trace autoflush="true" indentsize="0"> <listeners> <add name="CorrectWay" type="System.Diagnostics.TextWriterTraceListener" initializeData="TextLog.log"/> </listeners> </trace> </system.diagnostics> </configuration>
To add those
TraceListener
objects that don't reside in the GAC, the assembly containing the
TraceListener
derived class must reside in the same directory as the binary. I tried every possible
BugslayerUtil.NET.DLL
:
<?xml version="1.0" encoding="UTF-8" ?> <configuration> <system.diagnostics> <trace autoflush="true" indentsize="0"> <listeners> <add name="AGoodListener" type= "Wintellect.BugslayerTextWriterTraceListener,BugslayerUtil.NET" initializeData="BSUTWTL.log"/> </listeners> </trace> </system.diagnostics> </configuration>
I'm really glad to see a development platform that has ideas for handling assertions built in. We have a whole namespace in
System.Diagnostics
that contains all these helpful classes, culminating in the
Debug
object. Like most of you, I started learning .NET by creating console and Windows Forms applications because they were a lot easier to fit in my head at the time. When I turned to ASP.NET, I was already using
Debug.Assert
, and I figured that Microsoft had done the right thing by getting rid of the message box automatically. Surely they realized that when running under ASP.NET, I'd be able to break into the debugger when encountering an assertion. Imagine my surprise when I triggered an assertion and nothing
Without assertions, I felt like I was programming naked, and I knew I had to do something about it. After thinking about whether to introduce a new assertion object, I decided the best solution was to stick with
Debug.Assert
as the one and only way of handling assertions. Doing so enabled me to deal with several key issues. The first was having one consistent way of doing assertions across all of .NET—I didn't ever want to wonder whether code would run in Windows Forms or ASP.NET and possibly use the wrong assertion. The second issue
A third issue was to make using the assertion library as painless as possible. After writing lots of utility code, I realized the importance of integrating the assertion library easily into an application. The final issue I wanted to deal with was having a server-side control that enabled you to see the assertions easily on the page. All the code is in
BugslayerUtil.NET.DLL
, so you might want to open that project as well as the test harness application, BSUNAssertTest, which is located in the Test directory below BugslayerUtil.NET. Make sure you create a virtual directory in Microsoft Internet Information Services (IIS) that points to the BSUNAssertTest directory before you open the project.
The issues I wanted to address made it blindingly obvious that I was looking at creating a special class derived from
TraceListener
. I'll talk about that code in a moment, but no matter how cool I made
TraceListener
, I had to find a way to hook up my
TraceListener
object as well as remove
DefaultTraceListener
. No matter what, that was going to require a code change on your part because I had to get some code executed. To make using assertions simple and to ensure the assertion library would be called as early as possible, I used a class derived from
System.Web.HttpApplication
because the constructor and Init method are the very first things called in an ASP.NET application. The first step to assertion nirvana is to derive your
Global
class in
Global.ASAX
.cs (or
Global.ASAX
.vb) by using my
AssertHttpApplication
class. That will get my
ASPTraceListener
properly hooked up as well as put a reference to it in the application state bag under "ASPTraceListener" so that you can change output options on the fly. If all you want in your application is the option to stop when an assertion triggers, this is all you have to do.
To see assertions on the page, I wrote a very simple control named, appropriately enough, AssertControl. You can add AssertControl to your Toolbox by right-clicking on the Web Forms tab and selecting Add/Remove Items from the shortcut menu. In the Customize Toolbox dialog box, select the .NET tab, click the Browse button, and browse over to
BugslayerUtil.NET.DLL
in the File Open dialog box. Now you can simply drag AssertControl to any page for which you need assertions. You don't need to touch the control in your code because the
ASPTraceListener
class will hunt it down on the page and produce the appropriate output. Even if AssertControl is nested in another control, it will still be found. If no assertions occur when processing the page on the server, AssertControl produces no output at all. If you do have an assertion, the same assertion messages and stack trace displayed in a Windows-based or console application are displayed by AssertControl. Since multiple assertions can appear on the page, AssertControl shows all of them. Figure 3-2 shows the BSUNAssertTest page after an assertion is triggered. The text at the bottom of the page is the AssertControl output.
Figure 3-2:
An
ASP.NET
application displaying an assertion using AssertControl
All the real work takes place in the
ASPTraceListener
class, the bulk of which is shown in Listing 3-4. To be your
|
Property |
Default Value |
Description |
|---|---|---|
|
ShowDebugLog |
true |
Shows output in any attached debugger |
|
ShowOutputDebugString |
false |
Shows output through OutputDebugString |
|
EventSource |
null/Nothing |
The name of the event source for writing output to the event log. No permissions or security checking for access to the event log is done inside
|
|
Writer |
null/Nothing |
The TextWriter object for writing output to a file |
|
LaunchDebuggerOnAssert |
true |
If a debugger is attached, the debugger stops immediately when an assertion triggers. |
All the main work for doing the assertion output, which includes finding the assertion controls on the page, is done by the
ASPTraceListener.HandleOutput
method, shown in Listing 3-4. My first attempt at writing the
HandleOutput
method was much more involved. Although I could get the current
IHttpHandler
for the current HTTP request from the static
HttpContext.Current.Handler
property, I couldn't find a way to determine whether the handler was an actual
System.Web.UI.Page
. If I could figure out that it was a page, I could easily grind through and find any assertion controls on the page. My original attempt was to write quite a bit of code by using the very cool reflection interfaces so that I could walk the derivation chains
Once I had the Page object, I started looking for an AssertControl on the page. I knew it could be embedded inside another control, so I used a little recursion to walk through everything. Of course, when doing recursion, I needed to ensure I had a degenerative case or I could have easily ended up in an infinite recursion case. For ASPTraceListener.FindAssertControl , I chose to take advantage of the very interesting out keyword, which allows you to pass a method parameter by reference but doesn't require you to initialize it. It's more logical to treat the condition not found as null , and the out keyword lets me do that.
The final work I do with an assertion in the ASPTraceListener.HandleOutput method is determine whether I'm supposed to pop into the debugger when an assertion is triggered. The wonderful System.Diagnostics.Debugger object allows you to communicate with a debugger from inside your code. If a debugger is currently debugging the code, the Debugger.IsAttached property will be true and you can simply call Debugger.Break to force a breakpoint stop in the debugger. This solution assumes, of course, that you're actively debugging that particular Web site. I still need to handle the case of getting a debugger cranked up when you're not debugging.
If you look at the
Debugger
class, you'll see it has a very cool method named
Launch
that allows you to start a debugger to attach to your process. However, if the user account the process is running under isn't in the Debugger Users
On Windows 2000 and Windows XP, ASP.NET runs under the ASPNET account, so that's what you'll add to the Debugger Users group. Once you've added the account, you'll need to restart IIS so that Debugger.Launch will bring up the Just-In-Time (JIT) Debugging dialog. For Windows Server 2003, ASP.NET runs under the NETWORK SERVICE account. After you add NETWORK SERVICE to Debugger Users, you'll need to restart the machine.
Once I got Debugger.Launch working by getting the security correct, I had to ensure that I wasn't going to be calling Debugger.Launch only under the right conditions. If I called Debugger.Launch when no one was logged in on the server, I was going to cause lots of problems because the JIT debugger could be waiting for a key press on a window station no one could get to! In the ASPTraceListener class, I need to make sure the HTTP request is from the local machine because that indicates someone is logged in and there to debug the assertion. The ASPTraceListener.IsRequestFromLocalMachine method checks to see whether either the host address is 127.0.0.1 or the LOCAL_ADDR server variable is equal to the user's host address.
One final comment I have to make about bringing up the debugger involves Terminal Services. If you have a Remote Desktop Connection window open to a server, the Web address for any
While I expected to see either the security warning message box or the JIT Debugger dialog box, all I saw was a hung browser. I was quite perplexed until I walked over to the server and moved the mouse. There on the server's logon screen was my security message box! It dawned on me that although it looked like a bug, it was explainable. As it is the ASPNET/NETWORK SERVICE account that
The good news is that if you have a debugger attached, either inside the Remote Desktop Connection window or on another machine, the call to Debugger.Launch works exactly how you'd expect and stops in the debugger. Additionally, if you make the call into the server from a browser on another machine, the call to Debugger.Launch will not stop. The moral of the story is that if you're going to use Remote Desktop Connection to connect to the server and if you are going to run a browser inside that Remote Desktop Connection window (for example, on the server), you need to have a debugger attached to that server's ASP.NET process.
Although it's inexcusable that Microsoft made no provisions for assertions inside ASP.NET, at least armed with AssertControl, you can start programming. If you're looking for a control to learn how to extend, AssertControl is pretty bare bones. An interesting extension to AssertControl would be to use JavaScript in the code to bring up a better UI, like a Web dialog box, to tell the user there was a problem.
Listing 3-4: Important methods of ASPTraceListener
|
|
public class ASPTraceListener : TraceListener {
/* CODE REMOVED FOR CLARITY * /
// The method that's called when an assertion failed. public override void Fail ( String Message , String DetailMessage ) { // For reasons beyond me, it's nearly
impossible
to // consistently be able to get the number of items on the // stack up to the Debug.Assert. Sometimes it's 4 other // times it's 5. Unfortunately, the only way I can see // to handle this is to manually figure it out. Bummer. StackTrace StkSheez = new StackTrace ( ) ; int i = 0 ; for ( ; i < StkSheez.FrameCount ; i++ ) { MethodBase Meth = StkSheez.GetFrame(i).GetMethod ( ) ; // If nothing is returned, get out now. if ( null != Meth ) { if ( "Debug" == Meth.ReflectedType.Name ) { i++ ; break ; } } } BugslayerStackTrace Stk = new BugslayerStackTrace ( i ) ; HandleOutput ( Message , DetailMessage , Stk ) ; }
/* CODE REMOVED FOR CLARITY * /
/// <summary> /// Private assertion title message. /// </summary> private const String AssertionMsg = "ASSERTION FAILURE!\r\n" ; /// <summary> /// Private hard coded
carriage
return line feed string. /// </summary> private const String CrLf = "\r\n" ; /// <summary> /// The private assertion string boarder. /// </summary> private const String Border = "----------------------------------------\r\n" ; /// <summary> /// Output the assertion or trace message. /// </summary> /// <remarks> /// Takes care of all the output for the trace or assertion. /// </remarks> /// <param name="Message"> /// The message to display. /// </param> /// <param name="DetailMessage"> /// The detailed message to display. /// </param> /// <param name="Stk"> /// The value /// containing stack walk information for the assertion. If this is /// not null, this function is called from an assertion. Trace /// output sets this to null. /// </param> protected void HandleOutput ( String Message , String DetailMessage , BugslayerStackTrace Stk ) { // Create the StringBuilder to help me build the text // string for the output here. StringBuilder StrOut = new StringBuilder ( ) ; // If the StackArray is not null, it's an assertion. if ( null != Stk ) { StrOut.Append ( Border ) ; StrOut.Append ( AssertionMsg ) ; StrOut.Append ( Border ) ; } // Pop on the message. StrOut.Append ( Message ) ; StrOut.Append ( CrLf ) ; // Poke on the detail message if it's there. if ( null != DetailMessage ) { StrOut.Append ( DetailMessage ) ; StrOut.Append ( CrLf ) ; } // If an assertion, show the stack below a border. if ( null != Stk ) { StrOut.Append ( Border ) ; } // Go through and poke on all the stack information // if it's present. if ( null != Stk ) { Stk.SourceIndentString = " " ; Stk.FunctionIndent = " " ; StrOut.Append ( Stk.ToString ( ) ) ; } // Since I use the string multiple places, get it once here. String FinalString = StrOut.ToString ( ) ; if ( ( true == m_ShowDebugLog ) && ( true == Debugger.IsLogging ( ) ) ) { Debugger.Log ( 0 , null , FinalString ) ; } if ( true == m_ShowOutputDebugString ) { OutputDebugStringA ( FinalString ) ; } if ( null != m_EvtLog ) { m_EvtLog.WriteEntry ( FinalString , System.Diagnostics.EventLogEntryType.Error ) ; } if ( null != m_Writer ) { m_Writer.WriteLine ( FinalString ) ; // Add a CRLF just in case. m_Writer.WriteLine ( "" ) ; m_Writer.Flush ( ) ; } // Always do the page level output! if ( null != Stk ) { // Do the warning output to the current TraceContext. HttpContext.Current.Trace.Warn ( FinalString ) ; // Hunt down the AssertionControl on the page. // First, make sure the handler is a page! if ( HttpContext.Current.Handler is System.Web.UI.Page ) { System.Web.UI.Page CurrPage = (System.Web.UI.Page)HttpContext.Current.Handler ; // Take the easy way out if there are no // controls (which I doubt!) if ( true == CurrPage.HasControls( ) ) { // Hunt down the control. AssertControl AssertCtl = null ; FindAssertControl ( CurrPage.Controls , out AssertCtl ) ; // If there was one, add the happy assertion! if ( null != AssertCtl ) { AssertCtl.AddAssertion ( Message , DetailMessage , Stk ) ; } } } // Finally, launch the debugger if I'm supposed to. if ( true == m_LaunchDebuggerOnAssert ) { // If a debugger is already attached, I can just use // Debugger.Break on it. It doesn't matter where the // debugger is running, as long as it's running on this // process. if ( true == Debugger.IsAttached ) { Debugger.Break ( ) ; } else { // With the changes to the security model for the // RTM release of .NET, the ASPNET account that // ASPNET_WP.EXE uses is set to User instead of // running as the System account. In order to // allow Debugger.Launch to work, you need to add // ASPNET to the Debugger Users group. While this // is safe for development systems, you may want // to be careful on production systems. bool bRet = IsRequestFromLocalMachine ( ) ; if ( true == bRet ) { Debugger.Launch ( ) ; } } } } else { // The TraceContext is accessible right off the // HttpContext. HttpContext.Current.Trace.Write ( FinalString ) ; } } /// <summary> /// Determines if the request came from a local machine. /// </summary> /// <remarks> /// Checks if the IP address is 127.0.0.1 or the server variable /// LOCAL_ADDR matches the current machine. /// </remarks> /// <returns> /// Returns true if the request came from the local machine, /// false
otherwise
. /// </returns> private bool IsRequestFromLocalMachine ( ) { // Get the request object. HttpRequest Req = HttpContext.Current.Request ; // Is the user sitting on the loopback node? bool bRet = Req.UserHostAddress.Equals ( "127.0.0.1" ) ; if ( false == bRet ) { // Get the local IP address out of the server // variables. String LocalStr = Req.ServerVariables.Get ( "LOCAL_ADDR" ) ; // Compare the local IP with the IP address that // accompanied the request. bRet = Req.UserHostAddress.Equals ( LocalStr ) ; } return ( bRet ) ; } /// <summary> /// Finds any assertion controls on the page. /// </summary> /// <remarks> /// All assertion controls have the name "AssertControl" so this /// method simply
loops
through the page's control collection /// looking for them. It also looks through children of children /// recursively. /// </remarks> /// <param name="CtlCol"> /// The collection control to look through. /// </param> /// <param name="AssertCtrl"> /// The output parameter that contains the assertion control found. /// </param> private void FindAssertControl ( ControlCollection CtlCol , out AssertControl AssertCtrl ) { // Loop through all the controls in the control array. foreach ( Control Ctl in CtlCol ) { // Is this one the assertion control? if ( "AssertControl" == Ctl.GetType().Name ) { // Yep! Stop now. AssertCtrl = (AssertControl)Ctl ; return ; } else { // If this control has children do them too. if ( true == Ctl.HasControls ( ) ) { FindAssertControl ( Ctl.Controls , out AssertCtrl ) ; // If one of the children had the assertion, // I can stop now. if ( null != AssertCtrl ) { return ; } } } } // Didn't find it in this chain. AssertCtrl = null ; return ; } }
|
|
For
C and C++ also have all sorts of functions that can help make your assertions as descriptive as possible. Table 3-2 shows the helper functions you can use to check which condition you need.
|
Function |
Description |
|---|---|
|
GetObjectType |
A graphics device interface (GDI) subsystem function that returns the type for a GDI handle |
|
IsBadCodePtr |
Checks that the memory pointer can be executed |
|
IsBadReadPtr |
Checks that the memory pointer is readable for the specified number of bytes |
|
IsBadStringPtr |
Checks that the string pointer is readable up to the string's NULL terminator or the maximum number of
|
|
IsBadWritePtr |
Checks that the memory pointer is writable for the specified number of bytes |
|
IsWindow |
Checks whether the HWND parameter is a valid window |
The
IsBad*
functions are not thread-safe. Whereas one thread calls
IsBadWritePtr
to check the access permissions on a piece of memory, another thread could be changing the memory the pointer points to. What these functions give you is a snapshot of a moment in time. Some readers of this book's first edition argued that since the
IsBad*
functions aren't multithread-safe, you should never use them because they can lead you into a false sense of security. I couldn't
The following code shows one of the mistakes I used to make with my C++ assertions:
// Poor assertion usage BOOL CheckDriveFreeSpace ( LPCTSTR szDrive ) { ULARGE_INTEGER ulgAvail ; ULARGE_INTEGER ulgNumBytes ; ULARGE_INTEGER ulgFree ; if ( FALSE == GetDiskFreeSpaceEx ( szDrive , &ulgAvail , &ulgNumBytes , &ulgFree ) ) { ASSERT ( FALSE ) ; return ( FALSE ) ; }
}
Although I was using ASSERT , which is good, I wasn't showing the condition that failed. The assertion message box showed only the expression "FALSE," which isn't that helpful. When using an assertion, you want to try to get as much information about the assertion failure in the message box as possible.
My friend Dave Angel pointed out to me that in C and C++, you can just use the logical NOT operator ( ! ) and use a string as its operand. This combination gives you a much better expression in the assertion message box so that you at least have an idea of what failed without looking at the source code. The following example shows the proper way to assert a false condition:
// Proper assertion usage BOOL CheckDriveFreeSpace ( LPCTSTR szDrive ) { ULARGE_INTEGER ulgAvail ; ULARGE_INTEGER ulgNumBytes ; ULARGE_INTEGER ulgFree ; if ( FALSE == GetDiskFreeSpaceEx ( szDrive , &ulgAvail , &ulgNumBytes , &ulgFree ) ) { ASSERT ( !"GetDiskFreeSpaceEx failed!" ) ; return ( FALSE ) ; }
}
You can also extend Dave's assertion trick by using the logical AND conditional operator (&&) to perform a normal assertion and still get the message text. The following example shows how. Note that when using the logical AND trick, you do not use the "!" in front of the string.
BOOL AddToDataTree ( PTREENODE pNode ) { ASSERT ( ( FALSE == IsBadReadPtr ( pNode , sizeof ( TREENODE) ) ) && "Invalid parameter!" ) ;
}
Before we get into the various assertion macros and functions you'll encounter in Windows development as well as some of the problems with them, I want to talk about the
VERIFY
macro that's used quite a bit in Microsoft Foundation Class (MFC) library development. In a debug build, the
VERIFY
macro behaves the same way as a normal assertion because it's defined to be
ASSERT
. If the condition
In essence, the VERIFY macro allows you to have normal assertions with side effects, and those side effects stay in release builds. Ideally, you should never use conditions for any type of assertion that causes side effects. However, in one situation the VERIFY macro is useful—when you have a function that returns an error value that you wouldn't check otherwise. For example, when you call ResetEvent to clear a signaled event handle and the call fails, there's not much you can do other than terminate the application, which is why most engineers call ResetEvent and never check the return value in either debug or release builds. If you wrap the call with the VERIFY macro, at least you'll be notified in your debug builds that something went wrong. Of course, I could achieve the same results by using ASSERT , but VERIFY saves me the trouble of creating a new variable just to store and verify the return value of the ResetEvent call—a variable that would probably be used only in debug builds anyway.
I think most MFC programmers use the
VERIFY
macro for convenience, but you should try to break yourself of the habit. In most cases, when programmers use the
VERIFY
macro, they should be checking the return value instead. A good example of where everyone seems to use
VERIFY
is around the
CString::LoadString
member function, which loads resource strings. Using
VERIFY
this way is fine in a debug build because if
LoadString
fails, the
VERIFY
macro warns you. In a release build, however, if
LoadString
fails, you end up using an
|
|
The Battle
While working on a version of NuMega's BoundsChecker, we had incredible difficulty with random crashes that were almost impossible to duplicate. The only clues we had were that file handles and thread handles occasionally became invalid, which meant that files were
The Outcome
The team nearly tarred and feathered me because the problem turned out to be my fault. I was responsible for the debug loop in BoundsChecker. In the debug loop, you use the Windows debugging API to start and control another process, the debuggee, and to respond to debug events the debugger generates. Being a
Because I'm so careful, I knew that if an API gave you a handle to some object and you no longer needed the object, you called CloseHandle to free the underlying memory for that object. Therefore, whenever the debugging API gave me a handle, I closed that handle as soon as I finished using it. That seemed like the reasonable thing to do.
However, much to my chagrin, I hadn't read the fine print in the debugging API documentation, which says that the debugging API itself
To understand how this situation led to our problem, you need to know that when you close a handle, the operating system marks that handle value as available. Microsoft Windows NT 4, the operating system we were using at the time, is particularly aggressive about recycling handle values. (Microsoft Windows 2000 and Microsoft Windows XP exhibit the same
I was
The Lesson
I could have avoided this problem if I'd read the fine print in the debugging API documentation. Additionally—and this is the big lesson—I learned that you always check the return values to CloseHandle . Although you can't do much when you close an invalid handle, the operating system does tell you when you're doing something wrong, and you should pay attention.
As a side note, I want to mention that if you attempt to double-close a handle or pass a bad value to CloseHandle and you're running under a debugger, Windows operating systems will report an "Invalid Handle" exception (0xC0000008). When you see that exception value, you can stop and explore why it occurred.
I also learned that it really helps to be able to out-sprint your coworkers when they're chasing you with a pot of tar and bags of feathers.
|
|
Even though I define all my C++ assertion macros and functions to just plain
ASSERT
, which I'll talk about in a moment, I want to quickly go over the different types of assertions available in Visual C++ and provide a little information about their implementation. That way, if you see one of them in someone else's code, you can recognize it. I also want to alert you to the problems with some of the
The first type of assertion is from the C run-time library, the ANSI C standard assert macro. This version is portable across all C compilers and platforms and is defined by including ASSERT.H. In the Windows world, if you're working with a console application and it fails an assertion, assert will send the output to stderr . If your application is a Windows graphical user interface (GUI) application, assert will show the assertion failure as a message box.
The second type of assertion in the C run-time library is specific to Windows. These assertions are _ASSERT and _ASSERTE , which are defined in CRTDBG.H. The only difference between the two is that the _ASSERTE version also prints the expression passed as its parameter. Because the expression is so important to have, especially when your test engineers are testing, if you're using the C run-time library, you should always use _ASSERTE . Both macros are part of the extremely useful debug run-time library code, and the assertions are only one of its many features.
Although
assert
,
_ASSERT
, and
_ASSERTE
are convenient to use and free of charge, they do have a few drawbacks. The
assert
macro has two problems that can cause you some
The C run-time implementation macros, on the other hand, address the issue with defaulting to a message box by allowing you to redirect the assertion to a file or to the
OutputDebugString
API function by calling the
_CrtSetReportMode
function. All the Microsoft-supplied assertions suffer from one fatal flaw, however: they change the state of the system, which is the cardinal rule assertions can't break. Having your assertion calls suffer from side effects is almost
// Send the message over to the window. If it times out, the other // thread is hung, so I need to abort the thread. As a reminder, the // only way to check whether SendMessageTimeout failed is to check // GetLastError. If the function returned 0 and the last error is // 0, SendMessageTimeout timed out. _ASSERTE ( NULL != pDataPacket ) ; if ( NULL == pDataPacket ) { return ( ERR_INVALID_DATA ) ; } LRESULT lRes = SendMessageTimeout ( hUIWnd , WM_USER_NEEDNEXTPACKET , 0 , (LPARAM)pDataPacket , SMTO_BLOCK , 10000 , &pdwRes ) ; _ASSERTE ( FALSE != lRes ) ; if ( FALSE == lRes ) { // Get the last error value. DWORD dwLastErr = GetLastError ( ) ; if ( 0 == dwLastErr ) { // The UI is hung or not processing data fast enough. return ( ERR_UI_IS_HUNG ) ; } // If the error is anything else, there was a problem // with the data sent as a parameter. return ( ERR_INVALID_DATA ) ; } return ( ERR_SUCCESS ) ;
The problem, which is insidious, is that the supplied assertions destroy the last error value. In the preceding case, the " _ASSERTE ( FALSE != lRes ) " executes, shows the message box, and changes the last error value to 0. Thus in debug builds, the UI thread always appears to hang, whereas in the release build, you see the cases in which the parameters passed to SendMessageTimeout are bad.
The fact that the last error value is
If you're programming with MFC, you'll run into two additional assertion macros that are specific to MFC and are fantastic examples of proactive debugging. If you've declared your classes with DECLARE_DYNAMIC or DECLARE_SERIAL , you can use the ASSERT_KINDOF macro to check whether a pointer to a CObject-derived class is a specific class or is derived from a specific class. The ASSERT_KINDOF assertion is just a wrapper around the CObject::IsKindOf method. The following code snippet first checks the parameter in the ASSERT_KINDOF assertion and then does the real parameter error checking.
BOOL DoSomeMFCStuffToAFrame ( CWnd * pWnd ) { ASSERT ( NULL != pWnd ) ; ASSERT_KINDOF ( CFrameWnd , pWnd ) ; if ( ( NULL == pWnd ) ( FALSE == pWnd->IsKindOf ( RUNTIME_CLASS ( CFrameWnd ) ) ) ) { return ( FALSE ) ; }
// Do some MFC stuff; pWnd is
guaranteed
to be a CFrameWnd or // to be derived from a CFrameWnd.
}
The second MFC-specific assertion macro is
ASSERT_VALID
. This assertion resolves down to
AfxAssertValidObject
, which completely
Having told you what the problems are with the supplied assertions, now I want to show you how I was able to fix and extend the assertions to really make them tell you how and why you had a problem, plus much more. Figures 3-3 and 3-4 show examples of a
SUPERASSERT
error dialog box. In the first edition of this book, the
SUPERASSERT
output was a message box that showed the location of the failed assertion, the last error value translated into text, and the call stack. As you can see in Figures 3-3 and 3-4,
SUPERASSERT
has
Figure 3-3:
Example of a folded
SUPERASSERT
dialog box
Figure 3-4:
Example of an unfolded
SUPERASSERT
dialog box
The most amazing part about writing books and articles is the incredible conversations I've had with readers through e-mail and in person. I feel so lucky to learn from such amazingly smart folks! Soon after the first edition came out, Scott Bilas and I had a great e-mail exchange about his
Figure 3-3 shows the
The Ignore Once button, located below the failure edit control, simply continues execution. It is the default button, so you can press Enter or the spacebar to immediately move past an assertion after analyzing its cause. Abort Program calls ExitProcess to attempt to do a clean shutdown of the application. The Break Into Debugger button causes a DebugBreak call so that you can start debugging the failure by either popping into the debugger or starting the JIT debugger. The Copy To Clipboard button on the second row copies to the clipboard all the text from the failure edit control as well as the information from all threads you have done stack walks for. The last button, More>> or Less<<, toggles the dialog box folding.
The Create Mini Dump and Email Assertion
DBGHELP.DLL
loaded in the process space has the minidump functions exported, the Create Mini Dump button is enabled. If the minidump functions are not accessible, the button is disabled. To best preserve the state of the application,
SUPERASSERT
suspends all other threads in the application. This means that
SUPERASSERT
can't use the common file dialog because that dialog cranks up some background threads that stick around after it goes away. When
SUPERASSERT
suspends all the threads, the common file dialog code
The Email Assertion button is active only if you've put a special define in your source file that indicates the e-mail address you want the assertion information mailed to. This is a fantastic feature for testers to use to send the appropriate assertion to the correct developer. All the information, including all stack walks, is part of the e-mail message, so developers should be able to get exactly why the assertion triggered. At the top of each source file, you'll want to include code like the following to automatically get the e-mail capabilities. It's not required to use SUPERASSERT to define SUPERASSERT_EMAIL , but I strongly suggest you do.
#ifdef SUPERASSERT_EMAIL #undef SUPERASSERT_EMAIL // Please put your own email address in! #define SUPERASSERT_EMAIL "john@wintellect.com" #endif
The unfolded
SUPERASSERT
dialog box in Figure 3-4 has all sorts of helpful options in it. The Ignore section allows you to control how you want to ignore the assertion. The first button, Ignore Assertion Always, marks the particular assertion as ignored for the life of the program. You can also specify a specific ignore count by typing it into the edit box. To set the ignore specific count to the desired assertion, click This Assertion Only. To ignore
The final part of the dialog box is dedicated to the call stack. Even though I had the call stack in the first version of
SUPERASSERT
, if you look closely at the call stack in the edit box, you'll see all the local variables and their current values under each function! The SymbolEngine library, which is part of
BugslayerUtil.DLL
, can decode all basic types, structures, classes, and arrays. It's also smart enough to decode important values such as character pointers and character arrays. In Figure 3-4 you can see that it's showing both ANSI and Unicode strings as well as decoding a
RECT
structure. Building the proper symbol decoding was one of the toughest pieces of code I've written! If you're interested in the dirty details, you can read more about the SymbolEngine library in Chapter 4. The good news for you is that I did all the hard work, so you don't even have to think about it if you don't want to!
The first button in the Stack section, Walk Stack, allows you to walk the stack. Walk Stack is disabled in Figure 3-4 because the stack has already been walked. The Thread ID drop-down list allows you to pick the thread you want to take a look at. If the application has only a single thread, the Thread ID drop-down list doesn't appear in the dialog box. The Locals Depth drop-down list allows you choose how deep local variables will be expanded. This drop-down list is akin to the plus arrows next to expandable items in the debugger watch window. The higher the number, the more will be displayed about appropriate local variables. For example, if you had a local of type
int**
, you'd have to set the locals depth to 3 to see the value of the integer pointed to by that variable. The Expand Arrays check box tells
SUPERASSERT
to expand all array types
SUPERASSERT has some global options you can change on the fly as well. The Global SUPERASSERT Options dialog box is shown in Figure 3-5 and can be opened by selecting Options from the system menu.
Figure 3-5:
The Global SUPERASSERT Options dialog box
The Stack Walking section determines how much stack walking will be done when the
SUPERASSERT
dialog box appears. The default is to walk only the thread that had the assertion, but if you want to have the
I want to mention a few other details about SUPERASSERT . The first is that, as you see in Figures 3-3 and 3-4, SUPERASSERT has a grippy area in the lower right corner so that you can resize the dialog box. SUPERASSERT is also multiple-monitor-aware, so on the system menu is an option allowing you to center the dialog box on the current monitor so that you can get SUPERASSERT back to a desired location. What you can't see from the figures is that you don't have to display SUPERASSERT at all. At first, you might think that option is counterproductive, but I assure you it isn't! If you followed my recommendations in Chapter 2 and started testing your debug builds with a regression-testing tool, you know that handling random assertion messages is almost impossible. Because of the problems in handling assertion messages, your test engineers are much less likely to use the debug build. With my assertion code, you can specify that you want the output to go to OutputDebugString , a file handle, the event log, or any combination of the three. This flexibility allows you to run the code and get all the great assertion information but still automate your debug builds. Finally, SUPERASSERT is super smart about when it pops up. It always checks whether an interactive user is logged into the process windows station. If no one is interactively logged into that window station, SUPERASSERT won't pop up and hang your application.
Because of all the information SUPERASSERT gives me, I'm using the debugger less than ever before, which is a huge win for debugging speed. When I hit an assertion, I position the SUPERASSERT dialog box to pop up on my second monitor. I look through the local variable information and start reading source code on my primary monitor. I've found that I'm able to solve about 20 percent more bugs without starting the debugger. Although the first edition was very helpful, the second edition really rocks!
Integrating
SUPERASSERT
into your applications is quite easy. You simply need to include
BUGSLAYERUTIL.H
, which is probably best included in your precompiled header, and link against
BUGSLAYERUTIL.LIB
so that you bring
BUGSLAYERUTIL.DLL
into the address space. That gives you the ASSERT macro and automatically redirects any existing
ASSERT
and assert calls to my functions. My code does not redirect the
_ASSERT
and
_ASSERTE
macros because you might be doing some advanced work or specific output with the debug run-time library and I don't want to break your existing solutions. My code
If you'd like to change where output goes, such as to the event log or a text file, use the
SETDIAGASSERTOPTIONS
macro, which takes several self-explanatory bit field macros that determine the location of the output. These bit field macros are all defined in
DIAGASSERT.H
.
It's always a bad moment when another developer or tester
The Ignore button, if you haven't already guessed, is potentially a very dangerous option because people are so tempted to press it! Although it might have been a little draconian, I seriously
If you're using
SUPERASSERT
, and you want to see how many assertions have been triggered overall, you can look at the global variable,
g_iTotalAssertions
, in
SUPERASSERT.CPP
. Of course, this presumes that either you have a debugger attached to the crashed program or you have a memory dump of the crashed program. If you'd like to get the total assertion counts programmatically, call
GetSuperAssertionCount
, which is exported from
BUGSLAYERUTIL.DLL
.
What you might want to consider adding to SUPERASSERT is complete logging of all assertions that are triggered. That way you'd automatically have a running total of the number of Ignore values clicked by users, allowing you to validate what user actions led to the crash. Some companies automatically log assertions to a central database so that they can keep track of assertion frequencies and determine whether developers and testers are improperly using the Ignore button.
Since I've talked about protecting yourself against the user's
If you start looking at the code for
SUPERASSERT
, you might think it looks a little convoluted because it's spread across two separate sets of assertion code,
SUPERASSERT
.CPP and
DIAGASSERT.CPP
. The
DIAGASSERT.CPP
portion is actually the first version of
SUPERASSERT
. I left that code in place because quite a bit of UI code was involved with building the new
SUPERASSERT
, and since I couldn't use
SUPERASSERT
on itself, I needed a second set of assertions to make the
SUPERASSERT
development cleaner and more robust. Lastly, because
SUPERASSERT
requires more supporting code, the old assertion code is what you'll need to use in the one case in which
SUPERASSERT
can't be used, that is, when doing assertions in
RawDllMain
before anything in your module has been
The first interesting part of SUPERASSERT is the macro that eventually resolves down to the call to the SuperAssertion function, as shown in Listing 3-5. Since SUPERASSERT needs to keep track of local assertion ignore counts, the macro creates a new scope each time it's used. Inside the scope, it declares the two static integers to keep track of the number of times the particular assertion failed as well as the number of times the user wants to ignore the assertion. After checking the result of the expression, the rest of the macro is getting the current stack pointer and the frame pointer, and it is calling the actual SuperAssertion function.
Listing 3-5: The main ASSERT macro
|
|
#ifdef _M_IX86 #define NEWASSERT_REALMACRO( exp , type ) \ { \ /* The local instance of the ignore count and the total hits. */ \ static int sIgnoreCount = 0 ; \ static int sFailCount = 0 ; \ /* The local stack and frame at the assertion's location. */ \ DWORD dwStack ; \ DWORD dwStackFrame ; \ /* Check the expression. */ \ if ( ! ( exp ) ) \ { \ /* Houston, we have a problem. */ \ _asm { MOV dwStack , ESP } \ _asm { MOV dwStackFrame , EBP } \ if ( TRUE == SuperAssertion ( TEXT ( type ) , \ TEXT ( #exp ) , \ TEXT ( __FUNCTION__ ) , \ TEXT ( __FILE__ ) , \ __LINE__ , \ SUPERASSERT_EMAIL , \ (DWORD64)dwStack , \ (DWORD64)dwStackFrame , \ &sFailCount , \ &sIgnoreCount ) ) \ { \ __asm INT 3 \ } \ } \ } #endif // _M_IX86
|
|
Most processing of the assertion is handled in
SUPERASSERT.CPP
, shown in Listing 3-6. The majority of the work is done in two functions,
RealSuperAssertion
and
PopTheFancyAssertion
.
RealSuperAssertion
determines whether the assertion is ignored, builds the actual assertion message, and figures out where the output needs to go.
PopTheFancyAssertion
has more interesting functionality. To minimize the impact on the application, I suspend all the other threads in the application when the
SUPERASSERT
dialog box is up. That way I can cleanly get the call stacks as well. To suspend all the threads and stop everything, I boost the asserting thread to time-critical priority. I found that some tricky issues come up when you suspend all other threads in an application! The biggest problem was not being able to allocate memory right before I was going to suspend the threads. It's entirely possible for another thread to be holding onto the C run-time heap critical section, and if I suspend it and then need memory, I can't acquire the critical section. Although I can run through and count the threads before
SUPERASSERT.CPP
.
Listing 3-6: SUPERASSERT.CPP
|
|
/*---------------------------------------------------------------------- Debugging Applications for Microsoft .NET and Microsoft Windows Copyright (c) 1997-2003 John Robbins -- All rights reserved. ----------------------------------------------------------------------*/ #include "PCH.h" #include "BugslayerUtil.h" #include "SuperAssert.h" #include "AssertDlg.h" #include "CriticalSection.h" #include "resource.h" #include "Internal.h" /*////////////////////////////////////////////////////////////////////// // File Scope Typedefs, Constants, & Defines //////////////////////////////////////////////////////////////////////*/ // The maximum number of threads I can handle at once. const int k_MAXTHREADS = 100 ; // The GetProcessHandleCount typedef. typedef BOOL (__stdcall *GETPROCESSHANDLECOUNT)(HANDLE , PDWORD) ; /*////////////////////////////////////////////////////////////////////// // File Scope Prototypes //////////////////////////////////////////////////////////////////////*/ // Does the actual work to pop the assertion dialog. static INT_PTR PopTheFancyAssertion ( TCHAR * szBuffer , LPCSTR szEmail , DWORD64 dwStack , DWORD64 dwStackFrame , DWORD64 dwIP , int * piIgnoreCount ) ; //Tries to get the module causing the assertion. static SIZE_T GetModuleWithAssert ( DWORD64 dwIP , TCHAR * szMod , DWORD dwSize ) ; // Yes, this is the compiler intrinsic, but you have to prototype it in // order to use it. extern "C" void * _ReturnAddress ( void ) ; #pragma intrinsic ( _ReturnAddress ) // A function to hide the machinations to get the open handles in the // process. static BOOL SafelyGetProcessHandleCount ( PDWORD pdwHandleCount ) ; /*////////////////////////////////////////////////////////////////////// // File Scope Globals //////////////////////////////////////////////////////////////////////*/ // The number of assertions to ignore on a global basis. int g_iGlobalIgnoreCount = 0 ; // The total number of assertions. static int g_iTotalAssertions = 0 ; // The critical section that protects everything. static CCriticalSection g_cCS ; // The pointer to the GetProcessHandleCount function. static GETPROCESSHANDLECOUNT g_pfnGPH = NULL ; /*////////////////////////////////////////////////////////////////////// // Implementation! //////////////////////////////////////////////////////////////////////*/ // Turn off "unreachable code" error from this function calling // ExitProcess. #pragma warning ( disable : 4702 ) BOOL RealSuperAssertion ( LPCWSTR szType , LPCWSTR szExpression , LPCWSTR szFunction , LPCWSTR szFile , int iLine , LPCSTR szEmail , DWORD64 dwStack , DWORD64 dwStackFrame , DWORD64 dwIP , int * piFailCount , int * piIgnoreCount ) { // Always start bybumping up the total number of assertions seen // so far. g_iTotalAssertions++ ; // Bump up this particular instance failure count. if ( NULL != piFailCount ) { *piFailCount = *piFailCount + 1 ; } // See if there is any way to short circuit doing the whole dialog. // A "-1" means ignore everything. if ( ( g_iGlobalIgnoreCount < 0 ) ( ( NULL != piIgnoreCount ) && *piIgnoreCount < 0 ) ) { return ( FALSE ) ; } // If I am in the middle of ignoring all assertions for a bit, I can // skip out early! if ( g_iGlobalIgnoreCount > 0 ) { g_iGlobalIgnoreCount-- ; return ( FALSE ) ; } // Am I supposed to skip this local assertion? if ( ( NULL != piIgnoreCount ) && ( *piIgnoreCount > 0 ) ) { *piIgnoreCount = *piIgnoreCount - 1 ; return ( FALSE ) ; } // Holds the return value of the string (STRSAFE) manipulation // functions. HRESULT hr = S_OK ; // Save off the last error value so I don't whack it doing the // assertion dialog. DWORD dwLastError = GetLastError ( ) ; TCHAR szFmtMsg[ MAX_PATH ] ; DWORD dwMsgRes = ConvertErrorToMessage ( dwLastError , szFmtMsg , sizeof ( szFmtMsg ) / sizeof ( TCHAR ) ) ; if ( 0 == dwMsgRes ) { hr = StringCchCopy ( szFmtMsg , sizeof ( szFmtMsg ) / sizeof ( TCHAR ) , _T ( "Last error message text not available\r\n" ) ) ; ASSERT ( SUCCEEDED ( hr ) ) ; } // Get the module information. TCHAR szModuleName[ MAX_PATH ] ; if ( 0 == GetModuleWithAssert ( dwIP , szModuleName , MAX_PATH )) { hr = StringCchCopy ( szModuleName , sizeof ( szModuleName ) / sizeof (TCHAR) , _T ( "<unknown application>" ) ); ASSERT ( SUCCEEDED ( hr ) ) ; } // Grab the synchronization object to block other threads from // getting to this point. EnterCriticalSection ( &g_cCS.m_CritSec ) ; // The buffer to hold the expression message. TCHAR szBuffer[ 2048 ] ; #define BUFF_CHAR_SIZE ( sizeof ( szBuffer ) / sizeof ( TCHAR ) ) if ( ( NULL != szFile ) && ( NULL != szFunction ) ) { // Split out the base name from the whole filename. TCHAR szTempName[ MAX_PATH ] ; LPTSTR szFileName ; LPTSTR szDir = szTempName ; hr = StringCchCopy ( szDir , sizeof ( szTempName ) / sizeof ( TCHAR ) , szFile ); ASSERT ( SUCCEEDED ( hr ) ) ; szFileName = _tcsrchr ( szDir , _T ( '\\' ) ) ; if ( NULL == szFileName ) { szFileName = szTempName ; szDir = _T ( "" ) ; } else { *szFileName = _T ( '\0' ) ; szFileName++ ; } DWORD dwHandleCount = 0 ; if ( TRUE == SafelyGetProcessHandleCount ( &dwHandleCount ) ) { // Use the new STRSAFE functions to ensure I don't run off // the end of the buffer. hr = StringCchPrintf ( szBuffer , BUFF_CHAR_SIZE , _T ( "Type : %s\r\n" )\ _T ( "Expression : %s\r\n" )\ _T ( "Module : %s\r\n" )\ _T ( "Location : %s, Line %d in %s (%s)\r\n")\ _T ( "LastError : 0x%08X (%d)\r\n" )\ _T ( " %s" )\ _T ( "Fail count : %d\r\n" )\ _T ( "Handle count : %d" ), szType , szExpression , szModuleName , szFunction , iLine , szFileName , szDir , dwLastError , dwLastError , szFmtMsg , *piFailCount , dwHandleCount ); ASSERT ( SUCCEEDED ( hr ) ) ; } else { hr = StringCchPrintf ( szBuffer , BUFF_CHAR_SIZE , _T ( "Type : %s\r\n" ) \ _T ( "Expression : %s\r\n" ) \ _T ( "Module : %s\r\n" ) \ _T ( "Location : %s, Line %d in %s (%s)\r\n")\ _T ( "LastError : 0x%08X (%d)\r\n" ) \ _T ( " %s" ) \ _T ( "Fail count : %d\r\n" ) , szType , szExpression , szModuleName , szFunction , iLine , szFileName , szDir , dwLastError , dwLastError , szFmtMsg , *piFailCount ); ASSERT ( SUCCEEDED ( hr ) ) ; } } else { if ( NULL == szFunction ) { szFunction = _T ( "Unknown function" ) ; } hr = StringCchPrintf ( szBuffer , BUFF_CHAR_SIZE , _T ( "Type : %s\r\n" ) \ _T ( "Expression : %s\r\n" ) \ _T ( "Function : %s\r\n" ) \ _T ( "Module : %s\r\n" ) \ _T ( "LastError : 0x%08X (%d)\r\n" ) _T ( " %s" ) , szType , szExpression , szFunction , szModuleName , dwLastError , dwLastError , szFmtMsg ) ; ASSERT ( SUCCEEDED ( hr ) ) ; } if ( DA_SHOWODS == ( DA_SHOWODS & GetDiagAssertOptions ( ) ) ) { OutputDebugString ( szBuffer ) ; OutputDebugString ( _T ( "\n" ) ) ; } if ( DA_SHOWEVENTLOG == ( DA_SHOWEVENTLOG & GetDiagAssertOptions ( ) ) ) { // Only write to the event log if everything is really kosher. static BOOL bEventSuccessful = TRUE ; if ( TRUE == bEventSuccessful ) { bEventSuccessful = OutputToEventLog ( szBuffer ) ; } } if ( INVALID_HANDLE_VALUE != GetDiagAssertFile ( ) ) { static BOOL bWriteSuccessful = TRUE ; if ( TRUE == bWriteSuccessful ) { DWORD dwWritten ; int iLen =lstrlen ( szBuffer ) ; char * pToWrite = NULL ; #ifdef UNICODE pToWrite = (char*)_alloca ( iLen + 1 ) ; BSUWide2Ansi ( szBuffer , pToWrite , iLen + 1 ) ; #else pToWrite = szBuffer ; #endif bWriteSuccessful = WriteFile ( GetDiagAssertFile ( ) , pToWrite , iLen , &dwWritten , NULL ) ; if ( FALSE == bWriteSuccessful ) { OutputDebugString ( _T ( "\n\nWriting assertion to file failed.\n\n" ) ) ; } } } // By default, treat the return as an IGNORE. This works best in // the case the user does not want the MessageBox. INT_PTR iRet = IDIGNORE ; // Only show the dialog if the process is running interactively and // the user wants to see it. if ( ( DA_SHOWMSGBOX == ( DA_SHOWMSGBOX & GetDiagAssertOptions()))&& ( TRUE == BSUIsInteractiveUser ( ) ) ) { iRet = PopTheFancyAssertion ( szBuffer , szEmail , dwStack , dwStackFrame , dwIP , piIgnoreCount ) ; } // I'm done with the critical section! LeaveCriticalSection ( &g_cCS.m_CritSec ) ; SetLastError ( dwLastError ) ; // Does the user want to break into the debugger? if ( IDRETRY == iRet ) { return ( TRUE ) ; } // Does the user want to abort the program? if ( IDABORT == iRet ) { ExitProcess ( (UINT)-1 ) ; return ( TRUE ) ; } // The only option left is to ignore the assertion. return ( FALSE ) ; } // Takes care of the grunge to get the assertion dialog shown. static INT_PTR PopTheFancyAssertion ( TCHAR * szBuffer , LPCSTR szEmail , DWORD64 dwStack , DWORD64 dwStackFrame , DWORD64 dwIP , int * piIgnoreCount ) { // I don't do any memory allocation in this routine because I can // get into some weird problems. I am about to boost this threads // priority pretty high, in an attempt to starve the other // threads so I can suspend them. If I try to allocate memory at // that point, I can end up in a situation where a lower priority // thread has the CRT or OS heap synch object and this thread needs // it. Consequently, you are looking at one fat, happy deadlock. // (Yes, I originally did this to myself, that's how I know about // it!) THREADINFO aThreadInfo [ k_MAXTHREADS ] ; DWORD aThreadIds [ k_MAXTHREADS ] ; // The first thread in the threadinfo array is ALWAYS the current // thread. It's a zero based array, so the dialog code can treat // all threads as equals. However, for this function, the array // is treated as a one-based array so I don't suspend the current // thread and such. UINT uiThreadHandleCount = 1 ; aThreadInfo[ 0 ].dwTID = GetCurrentThreadId ( ) ; aThreadInfo[ 0 ].hThread = GetCurrentThread ( ) ; aThreadInfo[ 0 ].szStackWalk = NULL ; // The first thing is to blast the priority for this thread up to // real time. I don't want to have a thread created while I'm // preparing to suspend them. int iOldPriority = GetThreadPriority ( GetCurrentThread ( ) ) ; VERIFY ( SetThreadPriority ( GetCurrentThread ( ) , THREAD_PRIORITY_TIME_CRITICAL ) ) ; DWORD dwPID = GetCurrentProcessId ( ) ; DWORD dwIDCount = 0 ; if ( TRUE == GetProcessThreadIds ( dwPID , k_MAXTHREADS , (LPDWORD)&aThreadIds , &dwIDCount ) ) { // There has to be at least one thread!! ASSERT ( 0 != dwIDCount ) ; ASSERT ( dwIDCount < k_MAXTHREADS ) ; // Calculate the number of handles. uiThreadHandleCount = dwIDCount ; // If the number of handles is 1, it's a single threaded app, // and I don't need to do anything! if ( ( uiThreadHandleCount > 1 ) && ( uiThreadHandleCount < k_MAXTHREADS ) ) { // Open each handle, suspend it, and store the // handle so I can resume them later. int iCurrHandle = 1 ; for ( DWORD i = 0 ; i < dwIDCount ; i++ ) { // Of course, don't suspend this thread!! if ( GetCurrentThreadId ( ) != aThreadIds[ i ] ) { HANDLE hThread = OpenThread ( THREAD_ALL_ACCESS , FALSE , aThreadIds [ i ] ) ; if ( ( NULL != hThread ) && ( INVALID_HANDLE_VALUE != hThread ) ) { // If SuspendThread returns -1, there no point // and keeping that thread value around. if ( (DWORD)-1 != SuspendThread ( hThread ) ) { aThreadInfo[iCurrHandle].hThread = hThread ; aThreadInfo[iCurrHandle].dwTID = aThreadIds[ i ] ; aThreadInfo[iCurrHandle].szStackWalk = NULL; iCurrHandle++ ; } else { VERIFY ( CloseHandle ( hThread ) ) ; uiThreadHandleCount-- ; } } else { // Either this thread has some security set on // it or ithappened to end right after I // collected the threads. Consequently, I need // to decrement the total thread handles or I // will be one off. TRACE( "Can't open thread: %08X\n" , aThreadIds [ i ] ) ; uiThreadHandleCount-- ; } } } } } // Drop the thread priority back down! SetThreadPriority ( GetCurrentThread ( ) , iOldPriority ) ; // Ensure the application resources are set up. JfxGetApp()->m_hInstResources = GetBSUInstanceHandle ( ) ; // The assertion dialog its self. JAssertionDlg cAssertDlg ( szBuffer , szEmail , dwStack , dwStackFrame , dwIP , piIgnoreCount , (LPTHREADINFO)&aThreadInfo , uiThreadHandleCount ) ; INT_PTR iRet = cAssertDlg.DoModal ( ) ; if ( ( 1 != uiThreadHandleCount ) && ( uiThreadHandleCount < k_MAXTHREADS ) ) { // Crank up the thread priority again! int iOldPriority = GetThreadPriority ( GetCurrentThread ( ) ) ; VERIFY ( SetThreadPriority ( GetCurrentThread ( ) , THREAD_PRIORITY_TIME_CRITICAL ) ); // If I've suspended the other threads in the process, I need to // resume them, close the handles and delete the array. for ( UINT i = 1 ; i < uiThreadHandleCount ; i++ ) { VERIFY ( (DWORD)-1 != ResumeThread ( aThreadInfo[ i ].hThread ) ) ; VERIFY ( CloseHandle ( aThreadInfo[ i ].hThread ) ) ; } // Drop the thread priority back to what it was. VERIFY ( SetThreadPriority ( GetCurrentThread ( ) , iOldPriority ) ) ; } return ( iRet ) ; } BOOL BUGSUTIL_DLLINTERFACE SuperAssertionA ( LPCSTR szType , LPCSTR szExpression , LPCSTR szFunction , LPCSTR szFile , int iLine , LPCSTR szEmail , DWORD64 dwStack , DWORD64 dwStackFrame , int * piFailCount , int * piIgnoreCount ) { int iLenType = lstrlenA ( szType ) ; int iLenExp = lstrlenA ( szExpression ) ; int iLenFile = lstrlenA ( szFile ) ; int iLenFunc = lstrlenA ( szFunction ) ; wchar_t * pWideType = (wchar_t*) HeapAlloc ( GetProcessHeap ( ) , HEAP_GENERATE_EXCEPTIONS , ( iLenType + 1 ) * sizeof ( wchar_t ) ) ; wchar_t * pWideExp = (wchar_t*) HeapAlloc ( GetProcessHeap ( ) , HEAP_GENERATE_EXCEPTIONS , ( iLenExp + 1 ) * sizeof ( wchar_t ) ) ; wchar_t * pWideFile = (wchar_t*) HeapAlloc ( GetProcessHeap ( ) , HEAP_GENERATE_EXCEPTIONS , ( iLenFile + 1 ) * sizeof ( wchar_t ) ); wchar_t * pWideFunc = (wchar_t*) HeapAlloc ( GetProcessHeap ( ) , HEAP_GENERATE_EXCEPTIONS , ( iLenFunc + 1 ) * sizeof ( wchar_t ) ) ; BSUAnsi2Wide ( szType , pWideType , iLenType + 1 ) ; BSUAnsi2Wide ( szExpression , pWideExp , iLenExp + 1 ) ; BSUAnsi2Wide ( szFile , pWideFile , iLenFile + 1 ) ; BSUAnsi2Wide ( szFunction , pWideFunc , iLenFunc + 1 ) ; BOOL bRet ; bRet = RealSuperAssertion ( pWideType , pWideExp , pWideFunc , pWideFile , iLine , szEmail , dwStack , dwStackFrame , (DWORD64)_ReturnAddress ( ) , piFailCount , piIgnoreCount ) ; VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideType ) ) ; VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideExp ) ) ; VERIFY ( HeapFree ( GetProcessHeap ( ) , 0 , pWideFile ) ) ; return ( bRet ) ; } BOOL BUGSUTIL_DLLINTERFACE SuperAssertionW ( LPCWSTR szType , LPCWSTR szExpression , LPCWSTR szFunction , LPCWSTR szFile , int iLine , LPCSTR szEmail , DWORD64 dwStack , DWORD64 dwStackFrame , int * piFailCount , int * piIgnoreCount ) { return ( RealSuperAssertion ( szType , szExpression , szFunction , szFile , iLine , szEmail , dwStack , dwStackFrame , (DWORD64)_ReturnAddress ( ) , piFailCount , piIgnoreCount ) ) ; } // Returns the number of times an assertion has been triggered in an // application. This number takes into account any way the assertion // was ignored. int BUGSUTIL_DLLINTERFACE GetSuperAssertionCount ( void ) { return ( g_iTotalAssertions ) ; } static BOOL SafelyGetProcessHandleCount ( PDWORD pdwHandleCount ) { static BOOL bAlreadyLooked = FALSE ; if ( FALSE == bAlreadyLooked ) { HMODULE hKernel32 = ::LoadLibrary ( _T ( "kernel32.dll" ) ) ; g_pfnGPH = (GETPROCESSHANDLECOUNT) ::GetProcAddress ( hKernel32 , "GetProcessHandleCount" ) ; FreeLibrary ( hKernel32 ) ; bAlreadyLooked = TRUE ; } if ( NULL != g_pfnGPH ) { return ( g_pfnGPH ( GetCurrentProcess ( ) , pdwHandleCount ) ); } else { return ( FALSE ) ; } } static SIZE_T GetModuleWithAssert ( DWORD64 dwIP , TCHAR * szMod , DWORD dwSize ) { // Attempt to get the memory base address for the value on the // stack. From the base address, I'll try to get the module. MEMORY_BASIC_INFORMATION stMBI ; ZeroMemory ( &stMBI , sizeof ( MEMORY_BASIC_INFORMATION ) ) ; SIZE_T dwRet = VirtualQuery ( (LPCVOID)dwIP , &stMBI , sizeof ( MEMORY_BASIC_INFORMATION ) ); if ( 0 != dwRet ) { dwRet = GetModuleFileName ( (HMODULE)stMBI.AllocationBase , szMod , dwSize ) ; if ( 0 == dwRet ) { // Punt and simply return the EXE. dwRet = GetModuleFileName ( NULL , szMod , dwSize ) ; } } return ( dwRet ) ; }
|
|
The actual dialog code in
ASSERTDLG.CPP
is pretty uneventful, so it's not worth printing in the book. When Scott Bilas and I discussed what the dialog box should be written in, we realized it needed to be written in a lightweight language that didn't require any extra binaries other than the DLL containing the dialog box—pretty much
BugslayerUtil.DLL
, which does a whole lot more than just
SUPERASSERT
, is less than 70 KB.
|
|
As you look through my code, you'll notice that I always use statements such as "if (INVALID_HANDLE_VALUE == hFile)" instead of "if (hFile == INVALID_HANDLE_VALUE)." The reason I use this style is to avoid bugs. You can easily forget one of the equal signs, and using the former version will yield a compiler error if you do forget. The latter version might not issue a warning (whether or not it does depends on the warning level), and you'll change the variable's value. In compilers, trying to assign a value to a constant will produce a compiler error. If you've ever had to track down a bug involving an accidental assignment, you know just how difficult this type of bug is to find.
If you pay close attention, you'll notice that I also use constant variables on the left side of the equalities. As with the constant values case, the compilers will report errors when you try to assign a value to a constant variable. I've found that it's a lot easier to fix compiler errors than to fix bugs in the debugger.
Some developers have complained, sometimes vociferously, that the way I write conditional statements makes the code more confusing to read. I don't agree. My conditional statements take only a second longer to read and translate. I'm willing to give up that second to avoid wasting huge amounts of time later.
|
|