Chapter 15: Debugging ATL Server Applications


THE MSDN DOCUMENTATION for ATL Server contains several articles on debugging ATL Server applications. They explain how to set a breakpoint in an ISAPI Web application or in a Web service, how to attach the debugger to the IIS process running a specific Web application, and how to trace debug information in a server-safe manner (i.e., using the WebDbg sample that comes with Visual Studio .NET to avoid blocking pop-up assertion dialog boxes).

This chapter assumes that you re already familiar with the debugging information presented in MSDN. Our purpose in this chapter is to enable a knowledgeable developer who has already mastered the Web debugging features offered by Visual Studio .NET to get as close as possible to the source of the problem he or she is tracking.

Server-Side Debugging

This section offers hints for tracking problems in server applications, based on where these problems seem to occur.

ISAPI Initialization

According to the ISAPI specification, each DLL has to expose three functions to be called by IIS. GetExtensionVersion and TerminateExtension are invoked by IIS when the ISAPI DLL is loaded and unloaded, respectively. Then HttpExtensionProc is invoked for processing each HTTP request. In this section we cover the initialization of an ISAPI application, so we focus on the GetExtensionVersion implementation.

The typical ATL Server ISAPI DLL instantiates a CIsapiExtension derivative, a class that provides implementations for the three methods described previously. The exported GetExtensionVersion function calls into the GetExtensionVersion method of the CIsapiExtension derivative. The code in this method is initializes the internal components of the ISAPI extension: the DLL cache, the stencil cache, and the thread pool.

An error in the initialization of these components is fatal for the ATL Server application (the GetExtensionVersion function will return a failure code to IIS, which will unload the ISAPI DLL and all subsequent requests will fail). Such an error is reported to the client just as an HTTP 500 error (Internal Server Error), with no details about what happened .

Furthermore, the errors in the GetExtensionVersion function are particularly difficult to debug. Whenever an error occurs in, say, a request handler, pressing F5 (or whatever the preferred keystroke is for launching the debugger) helps a lot in solving the problem. The usual F5 debugging doesn t work for this function because of the way F5 debugging works: Visual Studio sends a DEBUG request to the IIS server, which loads the ATL Server ISAPI DLL. The application is responsible for sending back to the client (Visual Studio, in this case) the debug session ID as well as the process ID for the process that loaded the ISAPI DLL. After receiving this information, Visual Studio attaches the debugger to that process, and the user can walk through the code. Now, if the initialization code fails (i.e., GetExtensionVersion returns FALSE ), the ISAPI DLL isn t loaded and it can t handle the DEBUG request, so the F5 debugging attempt will fail.

Note  

For complete details on F5 debugging support, please see the implementation of the ProcessDebug function in atlisapi.h.

So, you end up with an initialization function that, on failure, breaks not only the application but also the usual debugging methodology. We ll discuss now how you can handle such a failure.

First, let s see how you can detect such a failure. If you check the implementation of CIsapiExtension::GetExtensionVersion , you ll notice that any potential failure generates a call like this:

 return SetCriticalIsapiError(IDS_ATLSRV_CRITICAL_DLLCACHEFAILED); 

SetCriticalIsapiError has two implementations in atlisapi.h. Using #define s, only one of them will be compiled into the ISAPI DLL.

The default implementation will log an error description into the system s application event log. For instance, the preceding line will log an error like this: A critical error has occurred initializing the ISAPI extension: DLL cache initialization failed. So, if an ATL Server application fails to load, checking the application log will let you know whether the problem occurred in the initialization or elsewhere.

After that, the SetCriticalIsapiError function, according to the ISAPI specification, should return FALSE , so that the error is propagated to IIS. Oddly enough, the function returns TRUE (i.e., it doesn t signal the loading error to the IIS). It also sets an internal variable ( m_dwCriticalIsapiError ) to contain the error code just signaled. The reason for this behavior is that ATL Server wants to enable sending the error information to the client. By returning TRUE here, IIS doesn t unload the ISAPI DLL and continues with the request processing, and the HttpExtensionProc function will be invoked. The first lines of HttpExtensionProc will check for the value of the m_dwCriticalIsapiError internal variable and, if this contains an error, it will send the error message to the user (the same error message that got logged in the event handler). This way, a client will receive the information about the failing DLL cache initialization rather than a generic Internal Server Error message.

Sending the error information to the user can be controlled via conditional compiling. If the ATL_CRITICAL_ISAPI_ERROR_LOGONLY constant is defined at compile time, then the error will be logged into the event log, but the details will not be sent to the client. Instead, a generic A server error has occurred message will be sent.

Note  

The default wizard-generated application will send error information to the user in Debug mode and will only log the error in the event log for the Release build.

You can change the behavior described previously by defining ATL_NO_CRITICAL_ISAPI_ERROR at compile time. This way, the second implementation of SetCriticalIsapiError is compiled. The second implementation does nothing, but it returns FALSE , forcing IIS to unload the ISAPI DLL.

Assuming that you want to get the combined behavior of both implementations (logging the error in the event log, but also returning FALSE to IIS to force unloading the DLL), you would have to modify the exported GetExtensionVersion function s code as follows :

   BOOL bRet    = theExtension.GetExtensionVersion(pVer);     if(bRet && theExtension.GetCriticalIsapiError() != 0)     {         return FALSE;     } 

Debugging a problem based on the errors in the application error log is very helpful if you don t have direct access to the server where the product is deployed, or it isn t possible to debug the application there.

If you can install a debugger on the server, you can use a more effective approach. We should mention again that the main debugging issue is that F5 debugging won t work for the initialization code. If the debugger is already attached to the process that loads the ISAPI DLL, then you can investigate the cause of the failure.

For the wizard-generated ATL Server applications, the deployment tool sets the application protection to Low (IIS process). That means the application is loaded in IIS. The IIS process is usually well known (it s inetinfo.exe for IIS 5. x , Windows 2000, and Windows XP). So, all you have to do is attach the debugger to the inetinfo.exe process before your first attempt to launch the failing ISAPI extension. If you ve already made an attempt to launch the application, then you can reset IIS (from the command line by using either the iisreset command or the net stop w3svc / net start w3svc commands). Then, just attempting to load the ISAPI application in the browser will hit the breakpoint set in GetExtensionVersion .

For custom deployed applications (which don t necessarily run in the IIS process), you have to identify the loader process first. For Medium application protection, IIS will load all the ISAPI DLLs into a separate process, usually dllhost.exe. For Isolated mode, IIS will spawn a separate process for each ISAPI application.

In this case, the following code snippet will help you in identifying the process to attach the debugger to (the modified body of the GetExtensionVersion function is presented):

 extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer)  {      DWORD    dwProcId = ::GetCurrentProcessId();      TCHAR    szChar[MAX_PATH];      _stprintf(szChar, _T("Attach to: %d"), dwProcId);      ::MessageBox(NULL, szChar, _T("ATL Server Debugging"),  MB_OK  MB_SERVICE_NOTIFICATION);      return theExtension.GetExtensionVersion(pVer);  } 
Note  

You will need to enable the World Wide Web Publishing Service to interact with the desktop in order to see the message. To do this, open the properties of the service from the service administration snap-in and select the checkbox that enables this functionality under the Log On tab.

Once you launch the application in the browser, a message box will appear that includes text similar to the following:

 --------------------------- ATL Server Debugging  --------------------------- Attach to: 288  --------------------------- OK  --------------------------- 

You should attach the debugger to the specified process ID, set a breakpoint right after the MessageBox call in the preceding code snippet, and then click OK on the message box. The breakpoint should be hit immediately, allowing debugging of the initialization code.

Request Loading and Processing

We mentioned the HttpExtensionProc function, invoked for processing each request, in the previous section. In this section we explain how to track down problems that appear between the moment when the request is accepted by the ISAPI extension and the moment when the application DLL takes control over the request (i.e., when HandleRequest is invoked). During this time, the request is managed by the internals of ATL Server, and finding a bug requires a good understanding of what s happening.

The HttpExtensionProc implementation posts the request in the thread pool queue. Whenever a working thread is free, it picks the first work item in the queue and executes that work item. As shown in previous chapters, the execution is performed by the per-thread worker class. By default, this worker class is CIsapiWorker . Whenever a new work item is fetched from the queue, it s passed to CIsapiWorker s Execute method.

There the actual execution of the request starts. Also, it s very important to realize that it s there that any exception from the execution is caught.

The code for CIsapiWorker::Execute looks like this (error handling and ATL-specific macros aside):

 try  {      (static_cast<IIsapiExtension*>(pvParam))->DispatchStencilCall(pRequestInfo);  }  catch(...)  {      ATLASSERT(FALSE);  } 
Note  

While you develop an ATL Server request handler, it s possible that some requests pop up an assertion dialog box on the screen with a message like this: \inetinfo.exe: Assertion failed in atlisapi.h, line 663 . This problem (which we also describe in Chapter 27) means that an exception occurred in the handling of the request. To find the source of the exception, just set a breakpoint on the line below the one that invokes DispatchStencilCall . Then, in the Visual Studio Debug menu, select Exceptions and select Break into the debugger for all types of exceptions.

DispatchStencilCall parses the request s URL and determines the handler that has to be invoked for handling the request based on the extension of the URL object. Then, the actual handler interface is obtained by calling either LoadDispatchFile (for SRF files) or LoadDllHandler (for DLLs without stencil support).

Note  

If the URL s object (the object requested by the HTTP client) is an SRF file, that file is loaded and parsed until the {{handler ..}} replacement tag is encountered . This replacement tag describes the DLL containing the main request handler, as well as the name of that handler. For instance, SRF file content such as

  {{handler MyApp.dll/SomeHandler}}  

actually means

  "Load the MyApp.dll library and look for a handler called SomeHandler"  

The same request handler can be described in an URL like this:

  http://localhost/MyTestApp/MyApp.dll?Handler=SomeHandler  

Any ATL Server application DLL (i.e., a DLL that contains at least a request handler) exports a set of methods for handling those request handlers. To find those handlers, just go in a command prompt in the same directory where the DLL is stored and execute the following:

 C:\MyFolder>dumpbin /EXPORTS SomeApp.dll 

The output will contain something like this:

 3    5 00033374 _GetAtlHandlerByName@12    4    6 0003333D _InitializeAtlHandlers@8    5    7 00033C02 _UninitializeAtlHandlers@0 

which means that the respective methods are exported by the DLL.

The ordinals, as well as the addresses of the exported functions, may differ between applications, but you can t use a DLL as a request handler if these methods (at least _GetAtlHandlerByName ) aren t exported.

The actual implementation of these exported functions is usually hidden by either the macros or the attributes used in developing the request handler DLL:

 [request_handler("Default")] 

or

 HANDLE_ENTRY("Default", CSomeAppHandler) 
Note  

You can understand the actual implementation by using the wizard to generate an ATL Server project (without an ISAPI application), disabling the Attributed code option in the Developer support pane, and then inspecting the HANDLE_ENTRY macro s usage in the .cpp file generated by the wizard, as well as its implementation in atlisapi.h.

It s important to remember the following points:

  • InitializeAtlHandlers initializes all the handlers contained in the request handler.

  • UninitializeAtlHandlers uninitializes the handlers.

  • GetAtlHandlerByName loads a handler by its name.

  • InitializeAtlHandlers and _UnitializeAtlHandlers , if implemented (if found in the application DLL), are invoked when the DLL is loaded and unloaded, respectively.

  • GetAtlHandlerByName provides the ISAPI code with the actual request handler implementation that s supposed to handle the requests for that named handler. In the preceding example, _GetAtlHandlerByName returns the address of a CSomeAppHandler instance.

Once the handler has been identified, it s cast to an IRequestHandler pointer, the base class for all the request handlers supported by ATL Server. Among other functions, this interface provides the HandleRequest method, which transfers control from the ISAPI extension to the request handler. HandleRequest is a method you can override in your request handler class to gain more control over the behavior of the Web application.

The preceding details, although not directly related to the purpose of this chapter (to provide debugging hints for ATL Server applications), will hopefully simplify the process of finding the location of bugs while handling the request for you. DispatchStencilCall is responsible for caching the result of an invocation and also for deciding when a request has to be executed and when the cached result of a previous execution can be returned.

Finding Errors in SRF Files

In this section you ll focus on identifying problems with parsing SRF files. The most common errors in SRF files are generated by misspelled replacement tag names . We use a simple scenario to show how to find these kinds of problems.

You ll start by creating a wizard-generated ATL Server application that contains an ISAPI extension and a request handler. Then, in the wizard-generated SRF file, just modify the {{Hello}} replacement tag to be {{Helo}} (a misspelled version of the replacement tag name) and launch the application in the browser. The response looks like the following:

 Server Error  SRF file could not be loaded. 

This is hardly enough information to help you find the problem. The reason this information isn t more detailed is that the Web application s clients aren t supposed to accidentally get access to the internal structure of the application (information such as replacement handler names, request handler names, and so forth shouldn t be exposed to clients ”not even in case of an error).

As a developer, however, you have a way to get extended information. Just go into the stdafx.h file of the application DLL generated earlier and add the following line before #include <atlstencil.h>:

 #define ATL_DEBUG_STENCILS 

After you rebuild and launch the application, the browser returns the following:

 <html>  <head>  </head>  <body>  This is a test: {{Helo}}<br>  </body>  </html> 

When ATL_DEBUG_STENCILS is defined, ATL Server simply copies the request handlers it doesn t understand into the output buffer, so curly braces will describe the place where the error occurred (in this case, the misspelled {{Helo}} replacement tag, which should actually be {{Hello}} ).

Let s see how you can find this error without using the ATL_DEBUG_STENCILS symbol. Once the stencil file is loaded, it s preparsed (i.e., the replacement tags are identified and kept in an array of tokens, with each token containing the name of the replacement tag). After preparsing, the array of tokenized replacement tags is resolved ”that is, the method to be called is determined for each of the tags. This happens in the CStencil::FinishParseReplacements method, in atlstencil.h.

The code for FinishParseReplacements iterates through the vector of tokenized replacement tags and, whenever one of them can t be resolved, invokes either the AddError method (for syntax errors in the SRF) or the AddReplacementError method (for replacement handlers that can t be resolved). So, by setting a breakpoint in both functions, you ll catch the SRF file error.

Note  

To find AddError and AddReplacementError in atlstencil.h, just look for the strings AddError(UINT or AddReplacementError(LPCSTR . They occur less often than the actual call to functions.

After you set the breakpoints, if you load the application in the browser again the breakpoints are hit. You have just to go one step back in the call stack (in FinishParseReplacements ) and inspect the values of the following local variables in the debugger:

 +    token.szMethodName    0x01c3fea1 "Helo"  +    token.pStart        0x0163a78a "{{ Helo}}<br></body></html> 

This not only shows you the misspelled tag (the method name), but also gives you an idea of where in the SRF file the error occurred.

FinishParseReplacements will also generate an error whenever an {{if}} / {{else}} / {{while}} block isn t correctly closed, so understanding the implementation of this function will help with most of the SRF syntax problems that you may encounter.

So far, we ve discussed ways of finding syntactical errors in SRF files. Assuming that an SRF file is correct, its rendering may still generate an error if one of the replacement methods fails. In the next section we analyze how to identify a failing replacement method.

Identifying a Failing Replacement Method

First, let s see what kind of failures require special effort to locate. A failure that throws an exception needs no extra work: The debugger can catch an exception as it s thrown and break the code execution exactly at the point where the exception is thrown. We described in the Request Loading and Processing section how to catch an exception in the request handler.

A replacement method may also fail by returning an unsuccessful HTTP error code. You can t catch such a failure in the way we described earlier, as no exception is thrown. We discuss in this section a way of identifying this kind of failing replacement method.

To demonstrate , in the test application you built in the previous section, you ll change the implementation of the replacement method for {{Hello}} and make it return HTTP_FAIL instead of HTTP_SUCCESS , as generated by the wizard. The following response is generated when the page is loaded in the browser:

 <html><head><title>Server Error</title></head>  <body>Server Error</body></html><html>  <head>  </head>  <body>  This is a test: Hello World! 

The Server Error part of the response is generated by the failing method. Now you ll try to debug the Web application to find the failing invocation.

The replacement method is invoked during stencil processing. The method invocation is performed by a CStencil method called RenderReplacement . If you search for this method in atlstencil.h, you ll find the implementation. The implementation identifies the method to be invoked and then calls that method with the following code if the replacement tag has no parameters:

 if (pfn)  {      hcErr = (pT->*pfn)();  } 

It calls that method with the following code if the replacement tag has parameters:

 if (pfn)  {      hcErr = (pT->*pfn)(pvParam);  } 

In the preceding code snippets, pfn is a pointer to the replacement method. Assuming you can somehow break the execution of the program here when a method fails, stepping into the (pT- > *pfn) call in the preceding code will debug exactly the failing replacement method.

To break the execution when a replacement method fails, you can set a conditional breakpoint at the last line of the function. That line returns the result of the invocation:

 return hcErr; 

In the ATL Server version that comes with Visual Studio .NET 2003, that line is 379 in atlstencil.h.

The conditional breakpoint should get hit whenever hcErr represents an error. Now, an HTTP error code is built with a macro like this:

 #define HTTP_ERROR(err, sub) \     ((HTTP_CODE)(DWORD_PTR)MAKELONG((WORD)err, (WORD)sub)) 

Particularly, HTTP_SUCCESS is defined as HTTP_ERROR(0,0) , whereas HTTP_FAIL is defined as HTTP_ERROR(500, SUBERR_NONE) . The HTTP error codes are built using the error 500 and some suberror code. Therefore, you want the conditional breakpoint to get hit whenever the error code is 500 or more. The condition for the breakpoint could be described as have the LOWORD part greater or equal to 500, or

 (hcErr & 0xfffff) >= 500 
Note  

To set a conditional breakpoint, just set the breakpoint, then right-click the red button inserted in the source code and select Breakpoint Properties. In the Breakpoint Properties dialog box, select Condition, which will allow you to enter the condition.

Now, with the conditional breakpoint in place, you can start the application again in the debugger. Whenever the breakpoint is hit, you know that a replacement method just failed. To know exactly which method failed, you can reexecute the RenderReplacement function. To do so, just scroll up to the beginning of the function, right-click, and select Set Next Statement. When you execute the code into the debugger, you ll then get exactly the failing method to be called again when stepping into the (pT- > *pfn) invocation described previously.




ATL Server. High Performance C++ on. NET
Observing the User Experience: A Practitioners Guide to User Research
ISBN: B006Z372QQ
EAN: 2147483647
Year: 2002
Pages: 181

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net