Managed Extensions: Tracking User Idle Time Without Hooks

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 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: 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 (that 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 whether 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 because 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 then can 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

Tom Archer - MSFT

I am a Program Manager and Content Strategist for the Microsoft MSDN Online team managing the Windows Vista and Visual C++ developer centers. Before being employed at Microsoft, I was awarded MVP status for the Visual C++ product. A 20+ year veteran of programming with various languages - C++, C, Assembler, RPG III/400, PL/I, etc. - I've also written many technical books (Inside C#, Extending MFC Applications with the .NET Framework, Visual C++.NET Bible, etc.) and 100+ online articles.

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

  • The explosion in mobile devices and applications has generated a great deal of interest in APIs. Today's businesses are under increased pressure to make it easy to build apps, supply tools to help developers work more quickly, and deploy operational analytics so they can track users, developers, application performance, and more. Apigee Edge provides comprehensive API delivery tools and both operational and business-level analytics in an integrated platform. It is available as on-premise software or through …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds