Click here to Skip to main content
Click here to Skip to main content

Monitoring of Logon/Logout in Terminal and Client Sessions

By , , 6 Dec 2010
Rate this:
Please Sign up or sign in to vote.

Introduction

This article deals with the session logon/logout notifications. For this purpose, WtsApi functions are used in the code. Project, which is attached to this article, is a simple service implemented for demonstration that writes all changes of session state to the log file. It is recommended to set service to start automatically on system start so it would be able to register the first session logon. It is recommended also to set the service dependence of TermServ service, so the service will definitely get the first notification from WTS, otherwise if the function of WtsApi is called before the dependent services of Remote Desktop Services have started, the RPC_S_INVALID_BINDING error code may be returned.

This article has a connected article about creating process in logged session.

Sessions Overview

We start with the brief overview of sessions. Every interactive user is associated with a session object. Sessions are identified by the session ID (SID). Sessions can be either local or remote. The local session is associated with the interactive user physically logged on to the machine. Service processes are also assigned to the local session. This session is always assigned the ID of 0, and is also referred to as the console. Here, I mean the first physical session, so if you use Fast User Switching on Windows XP, the session ID will be incremented for each new session. This is correct for the Windows operating systems until Windows Vista.  For these Windows versions, a console session can be any session with non-zero ID. Shortly, it was done to isolate services from interactive applications and prevent services from being hacked by end users. Anyway, we won't go into details of this theme. I just want to mention one nuance concerning the sessions running in the Terminal Services environment – there can be more than one interactive user in such session.

There are at least two ways to retrieve logon/logout event notification: System Event Notification Service (SENS) and the Windows Terminal Service (WTS). The connectivity functions and notifications of SENS are useful for applications written for mobile computers or computers connected to high latency local area networks, as it is written in MSDN. We won't stay for long on SENS description, because in this article we will use WTS API.

The Remote Desktop Services API (formerly known as Terminal Services) enables you to enumerate and manage Remote Desktop Session Host (RD Session Host) servers, client sessions, and processes; retrieve information about sessions and processes.

Sessions Enumerating. WtsApi Usage

For retrieving running session list and information about these sessions, the CSessionOwner class is implemented:

class CSessionOwner
{
public:
    ~CSessionOwner();
    static void GetSessions(std::vector<CSessionOwner> & vecSessions);
    static void GetActiveSessions(std::set<CSessionOwner> & setSessions);
            bool operator < (const CSessionOwner & other) const
    {
        return m_sid < other.GetSid();
    }
    
    int GetSid() const;
    bool IsActive() const;
    double GetLogonTime() const;
    std::wstring GetSessionUserName() const;
    std::wstring GetDomainName() const;
    std::wstring GetFullUserName() const;
private:
    CSessionOwner(const int& sid);
    
    int m_sid;
};

The GetSessions function encapsulates use of the WTSEnumerateSessions function to obtain the list of running sessions in the system.

void CSessionOwner::GetSessions(std::vector<CSessionOwner> & vecSessions)
{
    PWTS_SESSION_INFO pSessionInfo(NULL);
    DWORD count(0);
    if(!WTSEnumerateSessions( WTS_CURRENT_SERVER_HANDLE, 0, 1, &pSessionInfo, &count))
    {
        DWORD err = GetLastError();
        if (err == RPC_S_INVALID_BINDING)
            return;
        else
        {
            throw std::runtime_error("GetSessions failed");
        }
    }
    wts_resource<WTS_SESSION_INFO> wtsSessionInfo(pSessionInfo);
    for (DWORD i = 0; i < count; ++i)
    {
        int sid = wtsSessionInfo.get()[i].SessionId;
        vecSessions.push_back(CSessionOwner(sid));
    }
}

The wts_resource class is used in the method code. It is my simple class-guard for work with WTS resource, which has a pointer to a resource as a class member and calls the WTSFreeMemory function in its destructor. It has to be called because the WTSEnumerateSessions function allocates memory for the buffer and the calling side has to free the returned buffer.

	template<class ResourceType>
	class wts_resource
	{
		ResourceType * m_pResource;
	public:
		wts_resource()
			: m_pResource(0)
		{
		}

		wts_resource(ResourceType * pResource)
			: m_pResource( pResource )
		{
		}

		~wts_resource()
		{
			if (m_pResource)
				WTSFreeMemory(m_pResource);
		}

		void reset(ResourceType * pResource = 0);
		const ResourceType * get() const { return m_pResource; }
		ResourceType * get() { return m_pResource; }

		ResourceType * release();
		// other methods

	private:
		wts_resource(wts_resource & otherResource);
	};

As it was described above, in case of the WTSEnumerateSessions function failure, RPC_S_INVALID_BINDING error code may be the indicator of the situation when one of the dependent TermServ service is not started, so it is not an exceptional situation, and the best we can do is to add a log entry in this case.

Other CSessionOwner class methods are used to get information about the session, such as session ID, user name, connection state of session, etc. by calling WTSQuerySessionInformation with the required flag specifying the type of information to retrieve.

Important information of session is its connection state, that is returned by the IsActive() function:

bool CSessionOwner::IsActive() const
{
    DWORD bytesReturned(0);
    LPTSTR pBuf(NULL);
    if(!WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, 
		m_sid, WTSConnectState, &pBuf, &bytesReturned))
    {
        // this is not exceptional situation, the session may be closed at the moment, 
        // or close operation is pending on the session
        return false;
    }
    wts_resource<wchar_t> wtsPBuf(pBuf);
    int connectState = *(reinterpret_cast<int*> (wtsPBuf.get()));
    return (connectState == WTSActive);
}

All possible connection states of session are described in the _WTS_CONNECTSTATE_CLASS enum. An active session, no matter local or remote one, has connection state WTSActive. When remote desktop connection is enabled on the server, one more session appears immediately besides the current local session with the WTSListen state. Remote sessions that are not logged off but only closed, and the local session that is switched will have the WTSDisconnected connection state. There are some states that can be interesting for process management in the session. Now, when we can get the list of active sessions, let’s describe how to monitor changes in it.

Monitoring of the User Logon/Logout, Session Connect/Disconnect

I use the WTSWaitSystemEvent function in the sample project to wait for an event, such as the creation of a client session or a user logging on to the RD session host server.

Let’s take a look at the following class for work with WtsApi:

	class CSessionManager
	{
		typedef std::set<Common::CSessionOwner> SessionsSet;
		SessionsSet m_SessionsPool;

	public:		
		void operator () ();

		void StopSessionManager()
		{
			DWORD dwEventsFlag(0);
			WTSWaitSystemEvent(WTS_CURRENT_SERVER_HANDLE, 
					WTS_EVENT_FLUSH, &dwEventsFlag);
		}

		void GotSessionsSetGhanges(SessionsSet & addedSessions, 
					SessionsSet & lostSessions);
		void UpdateSessionsState();
	}; 

Functional code for sessions state monitoring is implemented in operator (). It is an operator to use it with boost::thread instead of a function. The following code has to run in the separate thread to get the notification about the changes of the session state. Here is a simple illustration of the work with WTSWaitSystemEvent function:

void CSessionManager::operator () ()
{
	Common::CSessionOwner::GetActiveSessions(m_SessionsPool);
	for(SessionsSet::iterator it = m_SessionsPool.begin(); 
		it != m_SessionsPool.end(); ++it)
	{
		// Write the list of the current active sessions to the log
	}

    for(;;)
    {
        DWORD dwEventsFlag(0);
        if(WTSWaitSystemEvent(WTS_CURRENT_SERVER_HANDLE, 
                              WTS_EVENT_LOGOFF | WTS_EVENT_LOGON | WTS_EVENT_CONNECT | 
			   WTS_EVENT_DISCONNECT
                               , &dwEventsFlag))
        {
            if( (WTS_EVENT_LOGON & dwEventsFlag) == WTS_EVENT_LOGON || 
		(WTS_EVENT_LOGOFF & dwEventsFlag) == WTS_EVENT_LOGOFF ||
                (WTS_EVENT_CONNECT & dwEventsFlag) == WTS_EVENT_CONNECT || 
		(WTS_EVENT_DISCONNECT & dwEventsFlag) == WTS_EVENT_DISCONNECT)
            {
                try
                {
                    UpdateSessionsState();
                }
                catch(const std::exception & ex)
                {
                    // write to the log 
                }
            }
            if (dwEventsFlag == WTS_EVENT_NONE)
            {
                return; // exit
            }
        }
        else
        {
            if(GetLastError() == ERROR_OPERATION_ABORTED)
            {
                // write to the log 
                return;
            }
            else
            {
                Sleep(100);
            }
        }
    }
} 

There is a set of events to be processed: session logon/logoff, connect/disconnect. Obviously, you can set the flags to wait for any event you want, but in this example we suppose that we want to get notifications only about these four events.

Here I’ll wander a bit from the main point and say a few words about boost threads. Boost enables to use multiple threads of execution with shared data in portable C++ code. It provides the boost::thread class for managing the threads, which is very simple and convenient to use.

The class given below is a wrapper for work with the boost thread:

class CThreadManager
{
	sm::CSessionManager m_sessMgr;
	boost::thread m_thread;

public:
	CThreadManager()
		: m_thread(boost::ref(m_sessMgr))
	{
	}

	~CThreadManager()
	{
		m_sessMgr.StopSessionManager();
		m_thread.join();
	}
};

Now we return to the CSessionManager class and UpdateSessionState function. It writes the information about the session to the log file– user name, log on or log out event:

void CSessionManager::UpdateSessionsState()
{
    SessionsSet addedSessionsPool;
    SessionsSet lostSessionPool;
    GotSessionsSetGhanges(addedSessionsPool, lostSessionPool);
    for(SessionsSet::iterator it = addedSessionsPool.begin(); 
	it != addedSessionsPool.end(); ++it)
    {
        // write UserName, GetSid to the log 
    }
    for(SessionsSet::iterator it = lostSessionPool.begin(); 
	it != lostSessionPool.end(); ++it)
    {
        // write UserName, GetSid to the log 
    } 
} 

The GotSessionsSetGhanges function is designed to watch for changes of the session list:

void CSessionManager::GotSessionsSetGhanges
	(SessionsSet & addedSessions, SessionsSet & lostSessions)
{
    Common::CSessionOwner::GetActiveSessions(addedSessions);
    for(SessionsSet::iterator sessionIt= m_SessionsPool.begin(); 
	sessionIt != m_SessionsPool.end(); ++sessionIt)
    {
        SessionsSet::iterator currentSessionsIt = addedSessions.find(*sessionIt);
        if(currentSessionsIt == addedSessions.end())
        {
            std::pair<SessionsSet::iterator, bool> res = lostSessions.insert(*sessionIt);
            if(!res.second)
            {
                throw std::runtime_error("CSessionManager::GotSessionsSetGhanges error: 
			Cannot insert logged off session into pool");
            }
        }
        else
        {
            addedSessions.erase(*sessionIt);
        }
    }

    // add the new sessions to the session pool    
    for(SessionsSet::iterator it = addedSessions.begin(); it != addedSessions.end(); ++it)
    {
        std::pair<SessionsSet::iterator, bool> res = m_SessionsPool.insert(*it);
        if(!res.second)
        {
            throw std::runtime_error("CSessionManager::GotSessionsSetGhanges error: 
			Cannot insert logged off session into pool");
        }
    }
    
    // erase the lost sessions from the session pool
    for(SessionsSet::iterator it = lostSessions.begin(); it != lostSessions.end(); ++it)
    {
        m_SessionsPool.erase(*it);
    }
}

Conclusion

The resulting sample project represents a service that monitors logging on/out of the active user local/remote session and connect/disconnect (disconnect for the terminal sessions or fast user switching for the local ones) using WTS API functions and writes every event to the log file, located in the same directory that the executable file is. To build the sources, you have to set boost environment variable. The project supports x86 and x64 platforms. To build the solution, boost environment variable has to be set, and it has to be built for both platforms. In this article, boost 1.40.0 version is used.

How to Build

  1. Boost 1.40.0 can be downloaded from http://www.boost.org/users/history/version_1_40_0.
  2. Set the environment variable $(UTILS_BOOST) - it can be found in the project properties (Configuration Properties->C++-> General/Additional Include Directories and Configuration Properties-> Linker-> General/Additional Library Directories)
  3. Then start the solution. You can write the .bat file and put it to the solution directory:
    set UTILS_BOOST=<path to boost, e.g. D:\boost_1.40.0\>
    SessionsMon.sln

    It will start the solution with the correct environment variable and you will not have to restart the system. If you set the environment variable via Computer/Properties/Advanced System Settings/Environment Variables..., you will be asked to restart your PC.

References

  1. MSDN Remote Desktop API documentation (http://msdn.microsoft.com/en-us/library/aa383459%28v=VS.85%29.aspx)
  2. Boost libraries (http://www.boost.org/doc/libs/1_44_0/doc/html/thread/thread_management.html)
  3. Microsoft systems journal (http://www.microsoft.com/msj/1099/terminal/terminal.aspx)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Authors

Apriorit Inc
Apriorit Inc.
Ukraine Ukraine
ApriorIT is a Software Research and Development company that works in advanced knowledge-intensive scopes.
 
Company offers integrated research&development services for the software projects in such directions as Corporate Security, Remote Control, Mobile Development, Embedded Systems, Virtualization, Drivers and others.
 
Official site http://www.apriorit.com
Group type: Organisation

31 members

Follow on   LinkedIn

Elizaveta Golub

Ukraine Ukraine
No Biography provided

Comments and Discussions

 
Questionruntime error :( Pinmemberlironda23-Jul-12 22:20 
GeneralSERVICE_CONTROL_SESSIONCHANGE PinmemberiEndGame9-May-11 23:56 
GeneralRe: SERVICE_CONTROL_SESSIONCHANGE PinmemberElizaveta Golub10-May-11 1:17 
GeneralRe: SERVICE_CONTROL_SESSIONCHANGE [modified] PinmemberiEndGame10-May-11 5:03 
GeneralRe: SERVICE_CONTROL_SESSIONCHANGE PinmemberElizaveta Golub10-May-11 22:33 
GeneralRe: SERVICE_CONTROL_SESSIONCHANGE PinmemberiEndGame11-May-11 4:56 
GeneralRe: SERVICE_CONTROL_SESSIONCHANGE PinmemberElizaveta Golub11-May-11 5:52 
QuestionEnvironment Variables for a session PinmemberEJHCoder20-Jan-11 5:57 
GeneralMy vote of 5 Pinmemberytfrdfiw16-Jan-11 2:36 
GeneralMy vote of 5 PinmemberMember 43208446-Dec-10 18:08 
GeneralMy vote of 4 PinmemberxComaWhitex6-Dec-10 17:19 
GeneralRe: My vote of 4 PinmemberElizaveta Golub7-Dec-10 23:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140421.2 | Last Updated 6 Dec 2010
Article Copyright 2010 by Apriorit Inc, Elizaveta Golub
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid