Detecting idle users

Updated 2009-04Apr-10It's not too uncommon in business application development to face the requirement to log out users automatically after a period of time. Preventing access to the application by unauthorized users is frequently quoted as a reason for such a feature. This purpose, however, should better be handled on the corporate level. A policy that locks a computer after a certain period of time is more secure, more reliable and better way to prevent access.
Nonetheless, some users let applications run all night long. This is often facilitated by company policies that require the user to keep the computer running as backup and maintenance tasks are carried out during the night.

While an application that is connected to SQL server has little impact on SQL server's ability for maintenance issues, the same does not hold true for Visual FoxPro. Reindexing, packing, backing up, all requires exclusive access to the files.

Detecting activity used to be a tedious development issue. In most scenarios activity equals to moving the mouse and pressing keys. Up to VFP 8.0 the only solution was to insert code into the KeyPress and the MouseMove event of every single base class. Especially the KeyPress event does react differently when there's code in the event. Consequently, adding this feature at a later stage required a lot of testing. When third-party libraries or ActiveX controls had been used, this was even more difficult as they required special coding.

Visual FoxPro 9.0 introduced a great feature: BINDEVENT can bind to API messages. There are two ways to bind to messages. You can specify a single window and only receive messages that go to that specific window, or you can specify 0 as the window handle and automatically receive message for every FoxPro window.

The following program implements a timer that keeps track of the user activities. When the user has been idle for a specified number of minutes, the eventTimeout event is triggered. Here you can respond by logging the user out, shutting the application, or just giving a warning and increasing the timeout:


*============================================================
* Detects user activity and fires an event after the
* specified period of inactivity.
*============================================================
Define Class InactivityTimer as Timer

*----------------------------------------------------------
* API constants
*----------------------------------------------------------

define WM_KEYUP 0x0101

define WM_SYSKEYUP 0x0105

define WM_MOUSEMOVE 0x0200

define GWL_WNDPROC (-4)

*----------------------------------------------------------
* internal properties
*----------------------------------------------------------
nTimeOutInMinutes = 0
tLastActivity = {/:}
nOldProc = 0

*----------------------------------------------------------
* Timer configuration
*----------------------------------------------------------
Interval = 30000
Enabled = .T.

*------------------------------------------------------------
* Listen to API events when the form starts. You can pass
* the timeout as a parameter.
*------------------------------------------------------------
Procedure Init(tnTimeOutInMinutes)
DECLARE integer GetWindowLong IN WIN32API ;
integer hWnd, ;
integer nIndex
DECLARE integer CallWindowProc IN WIN32API ;
integer lpPrevWndFunc, ;
integer hWnd,integer Msg,;
integer wParam,;
integer lParam
THIS.nOldProc=GetWindowLong(_VFP.HWnd,GWL_WNDPROC)
If Vartype(m.tnTimeOutInMinutes) == "N"
This.nTimeOutInMinutes = m.tnTimeOutInMinutes
EndIf
This.tLastActivity = Datetime()
BindEvent(0,WM_KEYUP,This,"WndProc")
BindEvent(0,WM_MOUSEMOVE,This,"WndProc")
EndProc

*------------------------------------------------------------
* Stop listening
*------------------------------------------------------------
Procedure Unload
UnBindEvents(0,WM_KEYUP)
UnBindEvents(0,WM_MOUSEMOVE)
EndProc

*------------------------------------------------------------
* Every event counts as activity
*------------------------------------------------------------
Procedure WndProc( ;
hWnd as Long,Msg as Long,wParam as Long,lParam as Long )
This.tLastActivity = Datetime()
_screen.Caption = Str(Val(_Screen.Caption)+1)
Return CallWindowProc(this.noldproc,hWnd,msg,wParam,lParam)

*------------------------------------------------------------
* Check last activity against time out
*------------------------------------------------------------
Procedure Timer
Local ltFireEvent
ltFireEvent = This.tLastActivity + 60*This.nTimeOutInMinutes
If Datetime() > m.ltFireEvent
This.eventTimeout()
EndIf
EndProc

*------------------------------------------------------------
* Override this event or bind to it to respond to user
* inactivity. You can change the nTimeOutInMinutes to offer
* multiple stages of timeouts.
*------------------------------------------------------------
Procedure eventTimeout

EndDefine

To use the timer you can either create a sub class or bind to the eventTimeout using BINDEVENTS(). In any case, the instance must be available while the application is running, whether that's as a global variable, as a property in an application object, or by any other mean. For trying this class you can run the following sample code


Public goForm
goForm = CreateObject("InactivityDemo")

Define Class InactivityDemo as InactivityTimer
Procedure Init
DoDefault(1)
Procedure eventTimeout
MessageBox("Timeout!")
EndDefine

If you run this code and then stop working in Visual FoxPro for between one and one and a half minutes, you get a message box every 30 seconds until you stop the demo with CLEAR ALL.

There are a few situations where the inactivity timer is not updated. Long running statements like SELECT or COPY TO commands don't trigger any event handler. Moving the mouse only in the header area isn't captured by VFP. I also suspect that activity inside an ActiveX control is not reliable detected, either.