Microsoft & .NETVisual C#Managed Extensions: Tracking User Idle Time Without Hooks

Managed Extensions: Tracking User Idle Time Without Hooks

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Welcome to this week’s installment of .NET Tips & Techniques! Each week, award-winning Architect and Lead Programmer Tom Archer demonstrates how to perform a practical .NET programming task using either C# or Managed C++ Extensions.

The goal of the .NET Programming Tips & Techniques series is to tackle tasks that have not-so-obvious solutions. One such task is that of monitoring a system for user activity. Many applications need to determine if a user has been idle for an extended period of time. One example is an application that logs out the current user if no user activity has taken place in a lengthy time—an indicator that the user has walked away from the computer and that the application is now sitting there unprotected with an active user login. Another example is a screen saver application. While writing a keyboard and mouse hook would seem to be the obvious choice (and one that is mentioned quite often as a means of tracking user idle time), the technique this article illustrates is using the GetLastInputInfo Win32 function from a Managed Extensions application.

Before delving into the details of this technique, let me first state a few words about hooks and why I wouldn’t use them in this particular situation. A few years ago, I wrote an article illustrating how to write keyboard hooks to monitor user keyboard activity. (In fact, this article lives on to this day in the DLL chapter of one of my books: Visual C++.NET Bible.) The point of that article/chapter was to illustrate an example of writing a Windows hook within a DLL. While my intention was to globally capture keystrokes regardless of the current application, many people use keyboard and mouse hooks to journal, or log, user activity. As a result of the continual increase in such applications—some of which are specifically meant to steal sensitive data such as passwords and credit card information—newer applications specifically try to block such hooks in order to “protect” the end-user. Another problem with using hooks to track idle time is that installing a system-wide hook is very invasive—the hook DLL has to be loaded into every desktop process. For these reasons, I would suggest using the GetLastInputInfo Win32 function in order to track user idle time.

The ModuleFunctionChecker and IdleTimer Classes

The GetLastInputInfo is applicable only if your code is running on Windows 2000 or greater. Therefore, the first thing I’ll present is a simple class with a single static function to test for the inclusion of a specified function within a specified module:

// You will need to include windows.h if not already done.
#include <windows.h>

class ModuleFunctionChecker
{
public:
  static bool DoesExist(LPCSTR moduleName, 
                        LPCSTR functionName)
  {
    bool success = false;

    HMODULE module = ::LoadLibrary(moduleName);
    if (module)
    {
      FARPROC function = ::GetProcAddress(module, 
                                          functionName);
      success = (NULL != function);
    }

    return !success;
  }
};

As you can see, the ModuleFunctionChecker::DoesExist function takes two parameters, a module name and a function name. The function first attempts to load the specified module via the LoadLibrary Win32 function. If that succeeds, the function’s address is located with a call to the GetProcAddress Win32 function. If that is successful, the function returns a value of true to indicate that the specified function does indeed exist in the module. If either function fails, the DoesExist returns a value of false.

Now let’s look at how to track idle time. Instead of breaking up the code with explanations (which I personally hate when I simply want to copy and paste something quickly into my application), I’ll first present the IdleTimer class and then explain some particulars:

// You will need the following define 
// and include of windows.h if not already done.
#define _WIN32_WINNT 0x0500
#include <windows.h>

using namespace System::Runtime::InteropServices;
using namespace System::Diagnostics;
using namespace System::Text;
using namespace System::Threading;

__gc class IdleTimer
{
public:
  __delegate void IdleTimerCallback();
private:
  IdleTimerCallback* callback;
  int maxIdleTime;

public:
  IdleTimer(int maxIdleTime, IdleTimerCallback* callback) 
    : maxIdleTime(maxIdleTime)
    , callback(callback) 
  {
    if (!ModuleFunctionChecker::DoesExist(_T("user32.dll"), 
                                          _T("GetLastInputInfo")))
      throw new System::Exception(S"Your version of Windows does not "
        S"support a needed function (GetLastInputInfo) for this class");

      Thread* t = new Thread(new ThreadStart(this,
                                             &IdleTimer::WatchIdleTime));
     t->IsBackground = true;
     t->Start();
  }

  void WatchIdleTime()
  {
    LASTINPUTINFO lii;
    memset(&lii, 0, sizeof(lii));

    lii.cbSize = sizeof(lii);
    for (;;)
    {
      ::GetLastInputInfo(&lii);

      long currTicks = System::Environment::TickCount;
      long lastInputTicks = lii.dwTime;
      long idleTicks = currTicks - lastInputTicks;

      StringBuilder* debug = new StringBuilder();
      debug->AppendFormat(S"Current tick = {0}, ", __box(currTicks));
      debug->AppendFormat(S"Last input tick = {0}, ", __box(lastInputTicks));
      debug->AppendFormat(S"Difference = {0}", __box(idleTicks));
      Debug::WriteLine(debug);

      if (idleTicks >= this->maxIdleTime)
        break;

      System::Threading::Thread::Sleep(1000);
    }
    this->callback();
  }
};

The first thing you’ll probably notice is the inclusion of the System::Threading namespace and the definition of the IdleTimerCallback delegate that is passed to the IdleTimer constructor. I used threading and delegates so that your application could use the IdleTimer class asynchronously, making it more practical.

The constructor takes only two parameters: a value indicating how long (in milliseconds) the system can be idle before the IdleTimer object alerts the caller and the callback function that is to be used to alert the caller. Within the constructor, the class first calls the aforementioned ModuleFunctionChecker::DoesExist function to confirm that the needed GetLastInputInfo function exists and then throws an exception if it returns false. If that check works, it spawns a thread that will track the idle time.

Note that the Thread::IsBackground property is set to true. This enables the CLR (Common Language Runtime) to kill the thread when the process dies. If you want the thread to continue checking for idle time even when the process ends, simply set this value to false.

Finally, the WatchIdleTime function (which is called when the thread starts) allocates a LASTINPUTINFO structure and then within an endless for loop repeatedly calls the GetLastInputInfo function, outputs some diagnostic information to the debug device, checks to see if the elapsed time has exceeded the client’s specified maximum idle time (in which case, the for loop is abandoned), and then sleeps for one second before doing it all over again. Once the for loop has been abandoned, the client’s callback function (specified in the IdleTimer constructor) is called.

The Client

Now let’s discuss the client, or user, of the IdleTimer class. As should almost always be the case, the client is pretty simple as most of the code is in the class. The following function is an example of instantiating the IdleTimer object and specifying a callback function (Form1::UserIdleTooLong) that adheres to the IdleTimer::IdleTimerCallback delegate syntax. (The value 5000 being passed to the IdleTimer constructor indicates that the max idle time should not exceed 5000 milliseconds, or 5 seconds.)

void button1_Click(System::Object *  sender, System::EventArgs *  e)
{
  try
  {
    IdleTimer* idle = 
      new IdleTimer(5000, new IdleTimer::IdleTimerCallback(this, 
                                                          &Form1::UserIdleTooLong));
  }
  catch(Exception* e)
  {
#pragma push_macro("MessageBox")
#undef MessageBox
    MessageBox::Show(e->Message, 
                     S"Error", 
                     MessageBoxButtons::OK, 
                     MessageBoxIcon::Error);
#pragma pop_macro("MessageBox")
  }
}

You can then code the Form1::UserIdleTooLong function to perform whatever application-specific logic you need:

void UserIdleTooLong()
{
#pragma push_macro("MessageBox")
#undef MessageBox
  MessageBox::Show(S"Hey! Wake up!", 
                   S"Idle Time Warning", 
                   MessageBoxButtons::OK, 
                   MessageBoxIcon::Warning);
#pragma pop_macro("MessageBox")
}

About the Author

The founder of the Archer Consulting Group (ACG), Tom Archer has been the project lead on three award-winning applications and is a best-selling author of 10 programming books as well as countless magazine and online articles.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories