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();
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))
{
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)
{
}
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)
{
}
}
if (dwEventsFlag == WTS_EVENT_NONE)
{
return; }
}
else
{
if(GetLastError() == ERROR_OPERATION_ABORTED)
{
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)
{
}
for(SessionsSet::iterator it = lostSessionPool.begin();
it != lostSessionPool.end(); ++it)
{
}
}
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);
}
}
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");
}
}
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
- Boost 1.40.0 can be downloaded from http://www.boost.org/users/history/version_1_40_0.
- 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)
- 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
- MSDN Remote Desktop API documentation (http://msdn.microsoft.com/en-us/library/aa383459%28v=VS.85%29.aspx)
- Boost libraries (http://www.boost.org/doc/libs/1_44_0/doc/html/thread/thread_management.html)
- Microsoft systems journal (http://www.microsoft.com/msj/1099/terminal/terminal.aspx)