I have to develop a program which runs on a local pc as a service an deliver couple of user status to a server. At the beginning I have to detect the user logon and logoff.
My idea was to use the ManagementEventWatcher
class and to query the Win32_LogonSession
to be notified if something changed.
My first test works well, here is the code part (This would executed as a thread from a service):
private readonly static WqlEventQuery qLgi = new WqlEventQuery("__InstanceCreationEvent", new TimeSpan(0, 0, 1), "TargetInstance ISA \"Win32_LogonSession\"");
public EventWatcherUser() {
}
public void DoWork() {
ManagementEventWatcher eLgiWatcher = new ManagementEventWatcher(EventWatcherUser.qLgi);
eLgiWatcher.EventArrived += new EventArrivedEventHandler(HandleEvent);
eLgiWatcher.Start();
}
private void HandleEvent(object sender, EventArrivedEventArgs e)
{
ManagementBaseObject f = (ManagementBaseObject)e.NewEvent["TargetInstance"];
using (StreamWriter fs = new StreamWriter("C:\\status.log", true))
{
fs.WriteLine(f.Properties["LogonId"].Value);
}
}
But I have some understanding problems and I’m not sure if this is the common way to solve that task.
If I query
Win32_LogonSession
I get several records which are associated to the same user. For example I get this IDs 7580798 and 7580829 and if I queryASSOCIATORS OF {Win32_LogonSession.LogonId=X} WHERE ResultClass=Win32_UserAccount
I get the same record for different IDs. (Win32_UserAccount.Domain="PC-Name",Name="User1")
Why are there several logon session with the same user? What is the common way to get the current signed in user? Or better how to get notified correctly by the login of a user?
I thought I could use the same way with
__InstanceDeletionEvent
to determine if a user is log off. But I guess if the event is raised, I cant queryWin32_UserAccount
for the username after that. I’m right?
I’m at the right direction or are there better ways? It would be awesome if you could help me!
Edit Is the WTSRegisterSessionNotification class the correct way? I don't know if it's possible, because in a service I haven't a window handler.
Here's the code (all of them residing inside a class; in my case, the class inheriting
ServiceBase
). This is especially useful if you also want to get the logged-on user's username.With the above code in your class, you can simply get the username in the method you're overriding like this:
NB: Remember to add
CanHandleSessionChangeEvent = true;
In the constructor of the class inheriting fromServiceBase
Since you are on a service, you can get session change events directly.
You can register yourself to receive the
SERVICE_CONTROL_SESSIONCHANGE
event. In particular, you will want to look for theWTS_SESSION_LOGON
andWTS_SESSION_LOGOFF
reasons.For details and links to the relevant MSDN docs, check this answer I wrote just yesterday.
In C# it is even easier, as ServiceBase already wraps the service control routine and exposes the event as an overridable
OnSessionChange
method for you. See MSDN docs for ServiceBase, and do not forget to set theCanHandleSessionChangeEvent
property to true to enable the execution of this method.What you get back when the framework calls your
OnSessionChange
override is a SessionChangeDescription Structure with a reason (logoff, logon, ...) and a session ID you can use to obtain information, for example, on the user logging on/off (see the link to my prev answer for details)EDIT: sample code
I use ServiceBase.OnSessionChange to catch the different user events and load the necessary information afterwards.
To load the session information I use the WTS_INFO_CLASS. See my example below:
The following code use the static
AvailabilityChanged
event fromUser
, which gets fired as soon as the session state changes. The arge
contains the specific user.You could use the System Event Notification Service technology which is part of Windows. It has the ISensLogon2 interface that provides logon/logoff events (and other events such as remote session connections).
Here is a piece of code (a sample Console Application) that demonstrates how to do it. You can test it using a remote desktop session from another computer for example, this will trigger the SessionDisconnect, SessionReconnect events for example.
This code should support all versions of Windows from XP to Windows 8.
Note Be sure to set the Embed Interop Types to 'False', otherwise you will get the following error: "Interop type 'COMAdminCatalogClass' cannot be embedded. Use the applicable interface instead."
Contrary to other articles you will find on the Internet about using this technology in .NET, it does not references the Sens.dll because ... it does not seem to exist on Windows 8 (I don't know why). However the technology seems supported and the SENS service is indeed installed and runs fine on Windows 8, so you just to need to declare the interfaces and guids manually (like in this sample), or reference an interop assembly created on an earlier version of Windows (it should work fine as the guids and various interfaces have not changed).
Note: Ensure that Visual Studio is running with administrator priviledges by right-clicking your Visual Studio shortcut and clicking
run as administrator
, otherwise anSystem.UnauthorizedAccessException
will be thrown when the program is run.