Skip to the content Back to Top

Overview

Default IIS application pool settings allow for no more than 5 uncaught exceptions within 5 minutes, and when this magic number is reached, the application pool shuts itself down. Uncaught exceptions are somewhat rare for us in the web applications we write because we have frameworks that catch and log errors. Some of our older web applications suffer from uncaught exceptions however, and so does Inmagic Webpublisher on servers where we host clients that use that software.

It used to be that text alerts would wake us up in the middle of the night screaming that sites dependent on Webpublisher were down, and we would remote in to the server to restart the relevant application pool. Well, that was pretty much untenable, so I wrote a script to restart the application pool automatically that would trigger when the application pool's shutdown was recorded in the Windows Application Event Log. A caveat here is that application pools usually shut themselves down for good reason - you shouldn't apply this script as a bandaid if you can fix the underlying causes.

Prerequisites

  • PowerShell v2 (get current version with $PSVersionTable.PSVersion).
  • PowerShell execution policy must allow the script to run (i.e. Set-ExecutionPolicy RemoteSigned or Set-ExecutionPolicy Unsigned).

Install the Script

  1. Register a new Windows Application Event Log Source called 'AppPool Resurrector'. Do it manually or use my PowerShell script.
  2. Put the AppPoolResurrector.ps1 script somewhere on the server, and take note of the name of the application pool you want to monitor.
  3. Create a new task in Windows Task Scheduler once per application pool you want to monitor
    1. Trigger is 'On an Event' Event ID: 1000, Source: Application Error, Log: Application
    2. Action is 'Start a program', Program/script: PowerShell, Add arguments: -command &" 'c:\path\to\apppoolresurrector.ps1' 'name-of-app-pool' "

Note the script activates to check whether the named application pool is still running, and then proceeds to restart it if necessary. There will be times it is activated by a log event to find that the application pool is fine, probably because the log event was unrelated to the application pool in the first place.

Script Content

If you've read and followed up on my previous posts about handling errors, you might have found yourself pulling out your hair when you discovered that your error handling went south again when you changed your target ASP.NET framework from 2.0 to 3.5/4.0 (applies to WebForms only of course). So here I am to save your day again (and yes, I wasted an entire Friday evening figuring this one out - good times).

In a nutshell, upgrade to 3.5 and your awesomely handled 500 and 404 errors start going back to 302 and 200 redirects the ol' fashioned (and completely idiotic) ASP.NET way of doing things. Of course, if you are like me, you'd waste an entire evening on it. But fortunately, you're not so you won't.

There's two issues/solutions:

  1. As of ASP.NET 3.5 there's now a redirectMode="ResponseRewrite" setting for your customErrors setting in your Web.config (yeah!). Put it in and you're halfway there (the default is the aforementioned moronic "ResponseRedirect"). Now your:
  2.             Context.Response.Clear();
                Context.Response.TrySkipIisCustomErrors = true;
                Context.Response.StatusCode = statusCode;
                Context.Response.Status = status;
               
                Context.Server.Transfer(errorPage, false);
    is back firing on all cylinders again.
  3. Except that your 404s are fine now, but your 500 errors return your error page twice in the body of your error page! Genius that I am, I went back to my experiences with IIS 6 and wrapped error pages and figured out that IIS 7/7.5 was sending back "its own" error page and then my server transfer was getting tacked on as well. Again, this does not effect 404s - I'm guessing because we clear the error (see previous post). So remove the Server.Transfer for non-404s and we're golden again.

In summary, for IIS 7/7.5 and .NET 3.5/4.0, use the following as a starting point for your Global.asax.cs or, better yet, an error module (for good measure figure out how it could be all tested):

        private readonly static ILog Log = LogManager.GetLogger(typeof(Global));
        private string _errorPageLocation;
        private string _error404PageLocation;


        protected void Application_Start(object sender, EventArgs e)
        {
            // Wire up log4net database connection string if desired.


            if (Log.IsInfoEnabled)
                Log.Info("Log4Net initialized - Ignore.");
        }


        protected void Application_Error(object sender, EventArgs e)
        {
            InitConfigurationFields();


            Exception exception = Context.Server.GetLastError();

            if (exception is HttpUnhandledException)
                if (exception.InnerException != null)
                    exception = exception.InnerException;


            if (exception is HttpException)
            {
                var ex = (HttpException)exception;
                var statusCode = ex.GetHttpCode();

                if (statusCode == 404)
                {
                    ServerTransfer(_error404PageLocation,
"404 Not Found", statusCode);
                    return;
                }
            }


            Log.Error("Unhandled error caught in error module.", exception);

            //Todo: Any way to get the correct status statement for a specifc code?
//Hard-coding all to "500 Internal Server Error" here.
            ServerTransfer(_errorPageLocation, "500 Internal Server Error", 500);
        }


        /// <summary>
        /// Gets configuration fields for database connection (if applicable),
/// and customError redirects.
        /// </summary>
        private void InitConfigurationFields()
        {
            var section = WebConfigurationManager.GetSection("system.web/customErrors");

            if (section != null && section is CustomErrorsSection)
            {
                var customErrorsSection = (CustomErrorsSection)section;
                _errorPageLocation = customErrorsSection.DefaultRedirect;

                var error404 = customErrorsSection.Errors["404"];

                if (error404 != null)
                    _error404PageLocation = error404.Redirect;
            }


            if (string.IsNullOrEmpty(_errorPageLocation))
                throw new NullReferenceException(
"customErrors DefaultRedirect must be specified in Web.config.
...For i.e. ~/error.aspx"
);


            if (string.IsNullOrEmpty(_error404PageLocation))
                throw new NullReferenceException(
"customErrors 404 statusCode redirect must be specified
...in Web.config. For i.e. ~/page-not-found.aspx"
);
        }


        private void ServerTransfer(string errorPage, string status, int statusCode)
        {
            // If customErrors is off, just let ASP.NET default happen.
            if (Context == null || !Context.IsCustomErrorEnabled)
                return;


            // Want the error around so that we can provide a little more
// descriptive message to end user in error page.
            // However, for 404 errors (only!), not clearing the error causes IIS7 to
// still hijack the process and the custom 404 error page.
            if (statusCode == 404)
                Server.ClearError();

            Context.Response.Clear();
            Context.Response.TrySkipIisCustomErrors = true;
            Context.Response.StatusCode = statusCode;
            Context.Response.Status = status;

            // For .NET 3.5, doing a Server.Transfer combined with customErrors
// redirectMode="ResponseRewrite" returns the error page body *twice*.
            if (statusCode == 404)
                Context.Server.Transfer(errorPage, false);
        }

I had a problem: Kathy and Denise wanted Google Analytics configured for our demo Genie site that can be found at http://genie.andornot.com. However, Genie doesn't provide an easy way to centrally add the couple lines of Google Analytics code to every page served up by the application. For that matter, neither does any WebPublisher PRO site right out-of-the-box (easy-peasy though if we're using ASK and our WebPubResults control).

You might also have the same problem: you recognize that having multiple means of tracking and analyzing your site's traffic is no longer optional. However, while you've got server log file analysis handled (we use SmarterStats for all our hosted clients) your site doesn't have a single central place (such as site-wide central templates) to easily integrate a javascript-based page tagging solution such as Google Analytics.

So what do we do? Instead of updating 10s if not 100s or 1000s of files manually one-by-one (crossing my fingers that I could find them all) and hoping my code wouldn't need to change anytime soon, I came up with a modified version of the Web Analytics Tracking Module available on Microsoft's IIS.net site. Now, with a little elbow grease for initial server setup (seriously, not very much elbow grease at all), turning on site tracking for any site on our servers is simply a matter of placing a DLL in our application's bin directory, and adding a couple lines to our site's Web.config. Sweet.

Why not just use the original IIS.net Web Analytics Module?

Before jumping in and explaining how to set everything up, I should explain that I needed create my own custom build of the module because the module on IIS.net only works under IIS 7's integrated pipeline, not under IIS 7's classic pipeline (which Genie needs to run under) or IIS 6. The required changes ended up being relatively minor:

  1. Modified the ReadModuleConfiguration method in WebAnalyticsHttpModule.cs to the following:
  2. /// <summary>
    
    /// Reads the module specific configuration properties
    
    /// </summary>
    
    /// <param name="context"></param>
    
    /// <returns>Boolean indicating the success/failure</returns>
    
    private bool ReadModuleConfiguration(HttpContext context)
    
    {
    
         try
    
         {
    
             ConfigurationSection section = null;
    
             if (HttpRuntime.UsingIntegratedPipeline)
    
                section = WebConfigurationManager.GetSection(context, "system.webServer/webAnalytics", typeof(WebAnalyticsSection));
    
             else
    
                section = WebConfigurationManager.GetSection(context, "system.web/webAnalytics", typeof(WebAnalyticsSection));
    
              if (section != null)
    
                 _webAnalyticsModuleConfig = (WebAnalyticsSection)section;
    
         }
    
         catch (Exception)
    
         {
    
             return false;
    
         }
    
           return _webAnalyticsModuleConfig != null;
    
        }
    
    }
  3. Add the classic pipeline <system.web>/<webAnalytics> schema to WebAnalytics_schema.xml:
  4. <sectionSchema name="system.web/webAnalytics">
    
        <attribute name="trackingEnabled" type="bool" defautlValue="false"></attribute>
    
        <attribute name="trackingScript" type="string"  defautlValue="This is a default text"></attribute>
    
        <attribute name="insertionPoint" type="enum" defaultValue="body">
    
            <enum name="head" value="0" />
    
            <enum name="body" value="1" />
    
        </attribute>
    
    </sectionSchema>
    
  5. To make sure there's no versioning conflicts, I updated the Assembly's version to v1.1 in AssemblyInfo.cs (note the v1.1 references in the instructions below as opposed to v1.0):
  6. [assembly: AssemblyVersion("1.1.0.0")]
    
    [assembly: AssemblyFileVersion("1.1.0.0")]

 

Configure your server (one time only)

  1. Register the WebAnalyticsModule.dll in the GAC (for shiny stuff in the IIS 7 Manager GUI).

    gacutil -if WebAnalyticsModule.dll
  2. Copy the WebAnalytics_schema.xml to "%windir%\system32\inetsrv\config\schema" folder.
  3. Add the following section definition to the "%windir%\system32\inetsrv\config\applicationhost.config" file in the sectionGroup for "system.webServer"
  4. <section name="webAnalytics" overrideModeDefault="Allow" />
  5. Add the module to the IIS Manager configuration by adding to two collections in the "%windir%\system32\inetsrv\config\administration.config" file:
    • Add the following to the moduleProviders collection:
    • <add name="WebAnalytics" type="WebAnalyticsModule.WebAnalyticsProvider, WebAnalyticsModule, Version=1.1.0.0, Culture=neutral, PublicKeyToken=c6b7132bcfe43312" /> 
      
    • Add the following to the modules collection:
    • <add name="WebAnalytics" />


web-analytics-tracking-in-iis
Web Analytics Tracking now directly available in IIS 7 Manager (at least for sites running under Integrated Pipeline).

 

Configure your site/s

For each site you wish to have Google Analytics enabled on (or any other script/text/html you wish to have run on every page in the site), enable it according to the following instructions (slightly different for different flavours of IIS/pipeline).

IIS 7 Integrated Pipeline

  1. Place the same WebAnalyticsModule.dll in your application's bin directory or reference the GAC version via the Web.config's <system.web>/<compilation>/<assemblies> element.
  2. Add the module to the <modules> element in your Web.config. This can be done through the IIS Manager, but it's easier to simply add it directly in the Web.config:
  3. <modules>
    
      ...
    
      <add name="WebAnalytics" type="WebAnalyticsModule.WebAnalyticsHttpModule, WebAnalyticsModule, Version=1.1.0.0, Culture=neutral, PublicKeyToken=c6b7132bcfe43312" />
    
    </modules>
  4. Specify the script/text you wish to have inserted and the location to insert it at. Again, this can be added directly in the Web.config. However, because the actual script/text needs to be encoded in order to keep your config file from going boink, it's a lot easier to just use the GUI.

  5. web-analytics-tracking-in-iis-configuration
    Easy configuration within the IIS 7 Manager GUI, including properly encoding the script for the Web.config (safe for XML).

    Enabling the above adds a <webAnalytics> element to the <system.webServer> section, looking something like (note the encoding):
    <webAnalytics trackingEnabled="true" trackingScript="&lt;script type=&quot;text/javascript&quot;>&#xD;&#xA;  var _gaq = _gaq || [];&#xD;&#xA;  _gaq.push(['_setAccount', 'UA-494411-1']);&#xD;&#xA;  _gaq.push(['_setDomainName', '.andornot.com']);&#xD;&#xA;  _gaq.push(['_trackPageview']);&#xD;&#xA;  (function() {&#xD;&#xA;    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;&#xD;&#xA;    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';&#xD;&#xA;    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);&#xD;&#xA;  })();&#xD;&#xA;&lt;/script>" insertionPoint="head" />

 

IIS 7 Classic Pipeline

Exactly the same as the IIS 7 integrated pipeline instructions above, with the following changes:

  1. Add the module reference to the classic pipeline specific <system.web>/<httpModules> section instead of the <system.webServer>/<modules> section.
  2. Make it easy for yourself, and add the script-specific settings (as in step 3 above) via the GUI in order to make sure it's all encoded properly, but then move/copy the resulting <webAnalytics> element from <system.webServer> (IIS 7 integrated pipeline specific) to <system.web>.
  3. Add the following to the <configuration>/<configSections> element in your Web.config (there should be a better way to add another sub-section to the already defined <system.web> declaration in the <configSections> element, so let me know in the comments if you know what it is):
  4. <sectionGroup name="system.web" type="System.Web.Configuration.SystemWebSectionGroup, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> 
    
        <section name="webAnalytics" type="WebAnalyticsModule.WebAnalyticsSection, WebAnalyticsModule" /> 
    
    </sectionGroup> 
    

Caveat: as your app is running under classic pipeline, not all text/html resources are running through ASP.NET, and therefore, if you have for i.e. .html and/or .asp pages you want the script automatically added to, you will need to map those extensions to also route through the ASP.NET runtime. Pretty standard classic pipeline stuff: without doing so, only pages with the .aspx extension will have the script added.

 

IIS 6 (Classic Pipeline)

I haven't tested it, but I see no reason why it won't work with the same set up as IIS 7 Classic Pipeline, with the same caveats.

Notes:

  1. As you might have guessed, you can automatically insert *anything* using this module, as long as you don't mind it inserting just before the closing </head> tag or the closing </body> tag. Copyright statements, survey scripts, "Ted wuz here" alerts for every page...if you think of it, you can do it!
  2. Again, the fancy GUI stuff doesn't help you in classic pipeline scenarios (other than an XML-encoding aid as described above).
  3. Typically you want to register scripts as close to the end of your page as possible (various technical performance and usability reasons). However, Google Analytics' latest scripts have a "push" functionality making this a moot point: so register the latest scripts right before the closing </head> tag. See the "Asynchronous Tracking Usage Guide" for more information.

Different web analytics packages have different strengths and weaknesses, and it's only with multiple different perspectives on your site traffic that you even begin to get a clear picture of what is really happening with your site. Using this web analytics module makes it even easier to add and maintain a page tagging solution on your site.

 

Download

I'd like to just directly link to my compiled DLL and schema file of the module, but unfortunately, it's not at all clear what licensing model is associated with the original module (although it's clear from his article explaining the module, that Microsoft's Ruslan Yakushev intends for us to use it). Therefore, I'll be requesting clarification on this issue, but in the meantime, just download the source code from the original article and make the quick modifications as described above. If you have problems doing so, or you want help deploying it for your site, drop us a line.

“IE6, that shambling wreck that clings with nightmare strength to its living death.”
Peter Tyrrell, Literary genius nonpareil

When Internet Explorer 6 was first released way back in 2001 it quickly became the predominant browser. It was much better than everything else out there. But now NINE years later, working with IE6 is truly one of the most frustrating things a Web developer has to deal with. In 2009, it has countless issues: problems with transparent PNGs, flakey caching, esoteric rendering, lack of support for modern Web standards, and the list goes on and on.

robot-jonny-ie6
Image courtesy of RobotJonny

This post is about yet another IE6 hair-puller: gzip compression. Compressing HTML, CSS, javascript, etc. allows content to be scrunched up so that it’s served up way faster while reducing bandwidth costs. It’s a great way to increase the performance of your site…except when IE6 is involved that is: IE6 does not correctly handle data that has been compressed using the gzip algorithm. This would be frustrating to deal with, but nevertheless acceptable, except for the fact that IE6 problems with compressed content only pop up sometimes, on certain machines, and intermittently. There doesn’t seem to be any rhyme or reason to it. The worst kind of bugs are the ones you cannot faithfully replicate.

With IIS6 we use Port80 Software’s excellent httpZip module to compress site content (compression is available natively in IIS6, but it’s a pain to administrate, and doesn’t work well with older browsers). One of httpZip’s big selling features is that it accurately deals with browser idiosyncrasies; therefore, until now we haven’t had to worry about IE6 and compression.

However, we have recently started transitioning more sites from being hosted on Windows Server 2003 (with IIS6) over to Windows Server 2008 (IIS7) and Windows Server 2008 R2 (IIS7.5).  With IIS7, gzip compression can be easily and correctly implemented right out of the box. But we’ve discovered that in certain scenarios with static and dynamic compression enabled, IIS7 doesn’t safely handle IE6’s idiot tendencies concerning gzip compression. The immediate solution is to turn off dynamic compression altogether, and this seems to solve the problem, but that doesn’t sit right with me: do yet another regression for IE6 users that punishes non-IE6 users.

Therefore, on to saving the world from IE6, one workaround at a time! Seb Duggan presents a solution involving ISAPI_Rewrite which essentially sniffs out whether or not IE 5 or 6 is doing the request and turns off compression for CSS and JS files if that is the case. Sounds good (although he does a sniff for pre-SP2 IE6 only, which I believe is erroneous). However, we already use IIS7’s URL Rewrite Module and I would prefer to avoid bringing another cook into the kitchen. There’s a couple of obstacles to getting this working however:

  1. The current version of the URL Rewrite Module v1.1 does not support rewriting server variables.
  2. The IIS7 URL Rewrite Module has very different syntax.

So how did I do it?

  • Download and install the URL Rewrite Module 2.0 RC (Release Candidate) or wait until the RTM is released (don’t know when). This version supports rewriting request server variables so we can fake IIS7 into thinking the requesting browser does not support gzip encoding (because for all intents and purposes, it DOESN’T – at least for CSS and javascript files that is).
  • Brute force hacking and documentation reading resulted in the following (the handy “Import Rules”, which can be used to translate Apache mod_rewrite rules, wasn’t up to the task). The important thing to remember is that each dash is replaced with an underscore and each server variable is made all caps and prefixed with “HTTP_”. Oh, and looking at the RFC specs, “0” is valid for setting the encoding to nothing (“.*” didn’t work for some reason).
    <system.webServer>
        …
    <rewrite>
            <rules>
                <rule name="IE56 Do not gzip js and css" stopProcessing="false">
                    <match url="\.(css|js)" /> <!— Match all .css and .js requests -->
                    <conditions>
                        <add input="{HTTP_USER_AGENT}" pattern="MSIE\ [56]" />
    <!— Where the User Agent includes MSIE 5 or MSIE 6 -->
                    </conditions>
                    <action type="None" />
    <!— Don’t do any redirects, rewrites, etc. -->
                    <serverVariables>
                        <set name="HTTP_ACCEPT_ENCODING" value="0" />
    <!—Make it so the request’s Accept-Encoding variable is
    set to nothing instead of gzip,decompress -->
                    </serverVariables>
                </rule>
            </rules>
        </rewrite>

    </system.webServer>
  • Explicitly allow the Accept-Encoding server variable to be overwritten by specifying the following in the applicationHost.config file (required for security reasons). If you don’t do this part, you’ll get an "HTTP/1.1 500 URL Rewrite Module Error".

    <rewrite>
        <allowedServerVariables>
            <add name="HTTP_ACCEPT_ENCODING" />
        </allowedServerVariables>
         …
    </rewrite>
  • You can implement the above on a site-by-site basis or server-wide (rules in site’s Web.config or location specific in applicationHost.config or site-wide in applicationHost.config).
  • ie6-gzip
    Fiddler showing IE6 with compression off for .js and .css files (while still compressing all other applicable files such as .htm).

    ie8-gzip
    Fiddler showing IE8 with compression on for all applicable resource types.

    The usual caveats apply to implementing the above: we don’t guarantee any of it. If it kills your cat, I know nothing. If you lose your job because of it, it’s your fault, not mine. On the other hand, if it helps you out or if you figure out a refined implementation, please let us know.

This post's categories

Disclaimer: Not officially supported. Plunging heedlessly on...

1 - Back up INI files

First back up all your .ini files. Be sure you are getting the CORRECT copy of the ini file: the ini files in the Program Files directory are access-protected in Vista and Win7 because the Program Files area is a forbidden zone. You must open the ini file with elevated privileges, like with Notepad "run as administrator", and save it somewhere safe.

  • dbtwpub.ini
  • inmagic.ini
  • dbtext.ini

2 - Upgrade from previous version

If you want to upgrade from previous version of WPP instead installing a fresh copy, uninstall the previous version MANUALLY first, because the WPP 12 installer tries to uninstall without elevated permission, and thus fails.

3 - Run installer as admin

Run the WPP 12 installer with administrator privileges. If you have a setup.exe you can right-click to "run as administrator".

My preview version of the installer is an *.msi file only, which doesn't have a right-click "run as admin" option. Instead, I launch the msi with msiexec from an elevated command prompt:

  1. Search for "cmd.exe" from Windows Start Menu
  2. Right-click cmd.exe and "run as administrator"
  3. Change directory to location of the msi, e.g. cd c:\users\ptyrrell\downloads
  4. Run the msi with the msiexec /i option, e.g. msiexec /i "Inmagic DBText WebPublisher PRO.msi"

4 - Fulfill prerequisites

The installer is cleverer than previous versions when checking for prerequisites, so you'll probably have to go and install or enable various Windows features before continuing.

wpp12_prereqs 

IIS 6 Compatibility (IIS7 only) seems to be satisfied by enabling the "IIS Metabase and IIS 6 configuration compatibility" Windows feature.

wpp12_iis6

Be warned: if you don't run the installer with elevated privileges, you will continue to fail the prerequisite check even after installing/enabling the right prerequisites!

 

5 - Test the install

Restore your backed up ini files. Run a query on the sample cars textbase to ensure WPP is returning results as expected.

If you are installing on a 64 bit machine, you need to enable 32 bit applications on the relevant application pool as covered in this previous post called How to Install Webpublisher on 64-bit IIS 7.

6 - Be the star you know you are

You did it! Now cut out a cardboard star with safety scissors, pencil "STAR HACKER" on it, and get your mum to pin it to your chest, glitter optional. Salute yourself in the mirror. Star! Hacker!

Categories

Let Us Help You!

We're Librarians - We Love to Help People