Exposing runtime data from IIS worker processes

Dynamic properties feature in IIS7 configurtion system lets you expose dynamic data as configuration system properties. RscaExt.xml which is part of IIS7 has all the RSCA (runtime status and control APIs) functionality exposed as dynamic properties through configuration system (read more here). One of the things RSCA let you do is expose custom runtime data from a worker process using GetCustomData method. You can call GetCustomData method for a worker process instance which sends a GL_RSCA_QUERY notification to the worker process. Once this notification is generated, IIS core engine then calls all the modules which subscribed to GL_RSCA_QUERY global notification. These modules can then return any runtime data they want to which is seen as the return value of GetCustomData method. RequestMonitorModule is one example of such module which subscribes to RQ_BEGIN_REQUEST, RQ_END_REQUEST to keep track of requests getting processed and also subscribes to GL_RSCA_QUERY to expose this requests in flight data through RSCA. CGlobalModule::OnGlobalRscaQuery method which gets invoked on GL_RSCA_QUERY event accepts IGlobalRSCAQueryProvider as an argument which can be used to get the function name (referred to as GuidOfFunctionCall in rscaext.xml), function parameters and also to set the return value of GetCustomData. For more information on IGlobalRSCAQueryProvider, go here.

Lets see a sample module which returns appPoolName when GL_RSCA_QUERY is issued with function name “AppPoolNameMethod”.

#include <windows.h>
#include <httpserv.h>

IHttpServer *  g_pGlobalInfo = NULL;

class SampleGlobalModule : public CGlobalModule
{
public:

   
GLOBAL_NOTIFICATION_STATUS
    OnGlobalRSCAQuery(
        IN IGlobalRSCAQueryProvider * pProvider
    )
    {
        UNREFERENCED_PARAMETER( pProvider );

       
HRESULT hr = S_OK;
        DWORD   dwLength = 0;

        //
        // Get function name
        //
        PCWSTR szFunctionName = pProvider->GetFunctionName();

       
//
        // If function name/guid is not as
        // expected, simply return
        //
        if(szFunctionName == NULL ||
           wcscmp(szFunctionName, L"AppPoolNameMethod") != 0)
        {
            goto finished;
        }

        //
        // Get function parameters. Not used in this sample.
        //
        PCWSTR szFunctionParameters = pProvider->GetFunctionParameters();

    
    //
        // In this sample, we are simply returning appPoolName
        // You can choose to return any runtime data like memory usage,
        // last requested file etc which can be possibly be collected
        // by subscribing to more global/request notifications
        //
        PCWSTR szwAppPoolName = g_pGlobalInfo->GetAppPoolName();
        dwLength = wcslen(szwAppPoolName);

       
//
        // Get output buffer
        //
        PBYTE pbBuffer = NULL;
        hr = pProvider->GetOutputBuffer(dwLength, &pbBuffer);
        if(FAILED(hr))
        {
            goto finished;
        }

        // Use to resize the buffer. Not used.
        //pProvider->ResizeOutputBuffer(2 * dwLength, dwLength, &pbBuffer);

        //
        // Write WCHAR* to byte array
        // Just truncate the high byte
        //
        for(DWORD i = 0; i < dwLength; ++i)
        {
            pbBuffer[i] = (byte)(szwAppPoolName[i] & 0x00FF);
        }

       
//
        // Set the result
        //
        pProvider->SetResult(dwLength, S_OK);

finished:

        if(SUCCEEDED(hr))
        {
            return GL_NOTIFICATION_HANDLED;
        }
        else
        {
            return GL_NOTIFICATION_CONTINUE;
        }
    }

   
VOID Terminate()
    {
        //
        // Remove the class from memory
        //

       
delete this;
    }
};

__declspec
(dllexport)
HRESULT
__stdcall
RegisterModule(
    DWORD dwServerVersion,
    IHttpModuleRegistrationInfo * pModuleInfo,
    IHttpServer * pGlobalInfo
)
{
    UNREFERENCED_PARAMETER( dwServerVersion );
    UNREFERENCED_PARAMETER( pGlobalInfo );

    //
    // Keep IHttpServer* for future use
    //
    g_pGlobalInfo = pGlobalInfo;

   
//
    // Create object
    //
    SampleGlobalModule * pGlobalModule = new SampleGlobalModule;
    if (NULL == pGlobalModule)
    {
        return HRESULT_FROM_WIN32(ERROR_NOT_ENOUGH_MEMORY);
    }

    //
    // Subscribe to GL_RSCA_QUERY event
    //
    return pModuleInfo->SetGlobalNotifications(pGlobalModule, GL_RSCA_QUERY);
}

Installing the module

For adding this module to the pipeline, add an entry like following to <system.webServer/globalModules> section.
<add name="SampleGlobalModule" image="c:\getcustomdatasample\sampleglobalmodule.dll" />

As the module is not subscribing to any request notifications, you don’t need to add this to <system.webServer/modules> section.

Javascript sample calling GetCustomData using AhAdmin APIs

Sample below shows how to make the call to GetCustomData method passing in the function name/parameters and how to retrieve output. IGlobalRSCAQueryProvider makes you return the output as a byte array but configuration system does base64 encoding of the byte array to return it as string. You have to do base64 decoding of the output you get from configuration system to get the actual byte array returned by OnGlobalRscaQuery method. You are free to interpret this byte array as you wish.

//
// Javascript code is making GetCustomData call which
// in turn end up sending a GL_RSCA_QUERY to the worker process
//
try
{
    var ahadmin = new ActiveXObject("Microsoft.ApplicationHost.AdminManager");

    //
    // Get DefaultAppPool element
    //
    var section = ahadmin.GetAdminSection(
        "system.applicationHost/applicationPools",
        "MACHINE/WEBROOT/APPHOST");

    //
    // Get First Application Pool (in my case DefaultAppPool)
    //
    if(section.Collection.Count == 0)
    {
        WScript.Echo("No application pool defined");
        WScript.Quit();
    }

    var defAppPool = section.Collection.Item(0);

    //
    // Get wps running for this apppool and pick one
    //
    var wps = defAppPool.GetElementByName("workerProcesses");
    if(wps.Collection.Count == 0)
    {
        WScript.Echo("No worker process running for " +
            "application pool " + defAppPool.GetPropertyByName("name").Value);
        WScript.Quit();
    }

    var wp = wps.Collection.Item(0);

   
//
    // Call GetCustomData method for this worker process
    //
    var method = wp.Methods.Item("GetCustomData");
    var methodInstance = method.CreateInstance();

    //
    // Set function name and parameters
    //
    var methodInput = methodInstance.Input;
    methodInput.GetPropertyByName("guidIdOfFunctionCall").Value = " AppPoolNameMethod";
    methodInput.GetPropertyByName("parametersOfFunctionCall").Value = "0";

    //
    // Execute method and get output
    //
    methodInstance.Execute();
    var methodOutput = methodInstance.Output;
    var data = methodOutput.GetPropertyByName("data").Value;

    //
    // Print the output which is base64 encoded
    // and also after doing the decoding
    //
    WScript.Echo("Base64 encoded output: " + data);
    WScript.Echo("Base64 decoded output: " + base64decode(data));
}
catch(e)
{
    WScript.Echo(e.number);
    WScript.Echo(e.description);
}

function
base64decode(input)
{
    var decodeStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    var ch1, ch2, ch3;
    var e1, e2, e3, e4;
    var output = "";

    input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
    for(var i = 0; i < input.length;)
    {
        e1 = decodeStr.indexOf(input.charAt(i++));
        e2 = decodeStr.indexOf(input.charAt(i++));
        e3 = decodeStr.indexOf(input.charAt(i++));
        e4 = decodeStr.indexOf(input.charAt(i++));

       
ch1 = (e1 << 2) | (e2 >> 4);
        ch2 = ((e2 & 15) << 4) | (e3 >> 2);
        ch3 = ((e3 & 3) << 6) | e4;

       
output = output + String.fromCharCode(ch1);
        if (e3 != 64)
        {
            output = output + String.fromCharCode(ch2);
        }
        if (e4 != 64)
        {
            output = output + String.fromCharCode(ch3);
        }
    }

   
return output;
}

This sample is not doing something very useful but few interesting things you can do using this functionality is expose list of items in file/token/memory cache (might not be straight forward to keep track of current list), calculate and expose some custom performance counters, trigger custom actions in the worker process like clearing cache entries for a user etc. 


Let me know if you have any questions or need help exposing some data.
Kanwal