26

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.

  1. 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 query

    ASSOCIATORS 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?

  2. 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 query Win32_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.

sikender
  • 5,883
  • 7
  • 42
  • 80
Andre Hofmeister
  • 3,185
  • 11
  • 51
  • 74

4 Answers4

19

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.

Add reference to the COM component named, COM+ 1.0 Admin Type Library aka COMAdmin.

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).

class Program
{
    static SensEvents SensEvents { get; set; }

    static void Main(string[] args)
    {
        SensEvents = new SensEvents();
        SensEvents.LogonEvent += OnSensLogonEvent;
        Console.WriteLine("Waiting for events. Press [ENTER] to stop.");
        Console.ReadLine();
    }

    static void OnSensLogonEvent(object sender, SensLogonEventArgs e)
    {
        Console.WriteLine("Type:" + e.Type + ", UserName:" + e.UserName + ", SessionId:" + e.SessionId);
    }
}

public sealed class SensEvents
{
    private static readonly Guid SENSGUID_EVENTCLASS_LOGON2 = new Guid("d5978650-5b9f-11d1-8dd2-00aa004abd5e");
    private Sink _sink;

    public event EventHandler<SensLogonEventArgs> LogonEvent;

    public SensEvents()
    {
        _sink = new Sink(this);
        COMAdminCatalogClass catalog = new COMAdminCatalogClass(); // need a reference to COMAdmin

        // we just need a transient subscription, for the lifetime of our application
        ICatalogCollection subscriptions = (ICatalogCollection)catalog.GetCollection("TransientSubscriptions");

        ICatalogObject subscription = (ICatalogObject)subscriptions.Add();
        subscription.set_Value("EventCLSID", SENSGUID_EVENTCLASS_LOGON2.ToString("B"));
        subscription.set_Value("SubscriberInterface", _sink);
        // NOTE: we don't specify a method name, so all methods may be called
        subscriptions.SaveChanges();
    }

    private void OnLogonEvent(SensLogonEventType type, string bstrUserName, uint dwSessionId)
    {
        EventHandler<SensLogonEventArgs> handler = LogonEvent;
        if (handler != null)
        {
            handler(this, new SensLogonEventArgs(type, bstrUserName, dwSessionId));
        }
    }

    private class Sink : ISensLogon2
    {
        private SensEvents _events;

        public Sink(SensEvents events)
        {
            _events = events;
        }

        public void Logon(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.Logon, bstrUserName, dwSessionId);
        }

        public void Logoff(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.Logoff, bstrUserName, dwSessionId);
        }

        public void SessionDisconnect(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.SessionDisconnect, bstrUserName, dwSessionId);
        }

        public void SessionReconnect(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.SessionReconnect, bstrUserName, dwSessionId);
        }

        public void PostShell(string bstrUserName, uint dwSessionId)
        {
            _events.OnLogonEvent(SensLogonEventType.PostShell, bstrUserName, dwSessionId);
        }
    }

    [ComImport, Guid("D597BAB4-5B9F-11D1-8DD2-00AA004ABD5E")]
    private interface ISensLogon2
    {
        void Logon([MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void Logoff([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionDisconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionReconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void PostShell([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
    }
}

public class SensLogonEventArgs : EventArgs
{
    public SensLogonEventArgs(SensLogonEventType type, string userName, uint sessionId)
    {
        Type = type;
        UserName = userName;
        SessionId = sessionId;
    }

    public string UserName { get; private set; }
    public uint SessionId { get; private set; }
    public SensLogonEventType Type { get; private set; }
}

public enum SensLogonEventType
{
    Logon,
    Logoff,
    SessionDisconnect,
    SessionReconnect,
    PostShell
}

Note: Ensure that Visual Studio is running with administrator priviledges by right-clicking your Visual Studio shortcut and clicking run as administrator, otherwise an System.UnauthorizedAccessException will be thrown when the program is run.

WonderWorker
  • 8,539
  • 4
  • 63
  • 74
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • 1
    Thanks, that’s pretty nice and works very well! For .Net 4 and the Interop embedded problem please see this [link](http://stackoverflow.com/questions/2483659/interop-type-cannot-be-embedded). – Andre Hofmeister May 10 '13 at 09:41
  • I am developing a project that capture administrators' operations when they login computer by RDP or Console.But i do not know how to start a process to capture screen when detecting a user login ? Can you give me help? Thanks! – zhoulin Wang Aug 07 '13 at 03:52
  • @zhoulinWang - please ask a question and someone may help. – Simon Mourier Aug 07 '13 at 06:15
  • I must make a project that start a process when detecting users login and stop the process when they logout. But now I don't know how to detect when they login or logout. Please give me C# code example.Thanks! – zhoulin Wang Aug 08 '13 at 03:12
  • I run the above code example in windows 2008 service with visual studio 2010. When a administrator user logon the windows 2008 from local pc by remote desktop, the result didn't show the userName and sessionId except 'Waiting for events. Press [ENTER] to stop.'. – zhoulin Wang Aug 08 '13 at 03:22
  • About Sens.dll: You can look up the DLL in Registry. `HKEY_CLASSES_ROOT\CLSID\{D5978650-5B9F-11D1-8DD2-00AA004ABD5E}\InprocServer32` says the DLL is now called ES.dll (Windows 10 21H2) – Thomas Weller Jan 13 '22 at 09:57
19

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 the WTS_SESSION_LOGON and WTS_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 the CanHandleSessionChangeEvent 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

 public class SimpleService : ServiceBase {
    ...
    public SimpleService()
    {
        CanPauseAndContinue = true;
        CanHandleSessionChangeEvent = true;
        ServiceName = "SimpleService";
    }

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    {
        EventLog.WriteEntry("SimpleService.OnSessionChange", DateTime.Now.ToLongTimeString() +
            " - Session change notice received: " +
            changeDescription.Reason.ToString() + "  Session ID: " + 
            changeDescription.SessionId.ToString());


        switch (changeDescription.Reason)
        {
            case SessionChangeReason.SessionLogon:
                EventLog.WriteEntry("SimpleService.OnSessionChange: Logon");
                break;

            case SessionChangeReason.SessionLogoff:       
                EventLog.WriteEntry("SimpleService.OnSessionChange Logoff"); 
                break;
           ...
        }
Community
  • 1
  • 1
Lorenzo Dematté
  • 7,638
  • 3
  • 37
  • 77
  • This is about WTSRegisterSessionNotification? Which window handler should I use? Or does I misunderstood your description? – Andre Hofmeister May 10 '13 at 09:37
  • @hofmeister, no you do not need WTSRegisterSessionNotification if you are in a service: Services already receive session notifications posted to their service control routine. Since you are using C#, I suppose you are implementing your service by deriving ServiceBase. Am I right? – Lorenzo Dematté May 10 '13 at 09:42
  • Yes I do, in short form. While I deriving from `ServiceBase` class, I could just override the `OnSessionChange` method? – Andre Hofmeister May 10 '13 at 09:50
  • Yes, exactly! Do not forget to set the CanHandleSessionChangeEvent property to true, or your OnSessionChange override will not get called; then every time a user logs on/off (but also with other events, like lock/unlock..) your method will be called and you will be notified – Lorenzo Dematté May 10 '13 at 09:54
  • I added some sample code, adapted from MSDN, to make it clearer – Lorenzo Dematté May 10 '13 at 10:08
  • Thanks, the event notification works fine but how do I get the username for the logon and logoff. I tried to query wmi, but this just works for the logon. Any idea? – Andre Hofmeister May 10 '13 at 15:56
  • Follow the linked answer.. :) – Lorenzo Dematté May 13 '13 at 07:36
  • [Here](http://www.pinvoke.net/default.aspx/wtsapi32.wtsquerysessioninformation) are additional information’s how to get the username with WTSQuerySessionInformation from a session id in C#. To get the username from the logoff event you could store the session id and username in a dictionary or map and access this when the logoff event occurs. Thanks everyone! – Andre Hofmeister May 24 '13 at 08:17
  • @hofmeister you are welcome! Feel free to edit my answer to include your comment and the link, if you wish – Lorenzo Dematté May 24 '13 at 08:42
  • +1 for mention about `CanHandleSessionChangeEvent` as this is not automatically "scaffolded" nor mentioned on Microsoft's [Walkthrough: Creating a Windows Service Application](https://msdn.microsoft.com/en-us/library/zt39148a(v=vs.110).aspx?cs-save-lang=1&cs-lang=csharp#code-snippet-14). Here is the "service" I came up with: [NetworkAdapterHelpers.cs](https://gist.github.com/rdev5/fb5aacc9e5f5a89fe21f9bed9d7f3a29) :) – Matt Borja Oct 06 '16 at 21:07
2

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.

    [DllImport("Wtsapi32.dll")]
    private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned);
    [DllImport("Wtsapi32.dll")]
    private static extern void WTSFreeMemory(IntPtr pointer);

    private enum WtsInfoClass
    {
        WTSUserName = 5, 
        WTSDomainName = 7,
    }

    private static string GetUsername(int sessionId, bool prependDomain = true)
    {
        IntPtr buffer;
        int strLen;
        string username = "SYSTEM";
        if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSUserName, out buffer, out strLen) && strLen > 1)
        {
            username = Marshal.PtrToStringAnsi(buffer);
            WTSFreeMemory(buffer);
            if (prependDomain)
            {
                if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
                {
                    username = Marshal.PtrToStringAnsi(buffer) + "\\" + username;
                    WTSFreeMemory(buffer);
                }
            }
        }
        return username;
    }

With the above code in your class, you can simply get the username in the method you're overriding like this:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    string username = GetUsername(changeDescription.SessionId);
    //continue with any other thing you wish to do
}

NB: Remember to add CanHandleSessionChangeEvent = true; In the constructor of the class inheriting from ServiceBase

Soma Mbadiwe
  • 1,594
  • 16
  • 15
1

I use ServiceBase.OnSessionChange to catch the different user events and load the necessary information afterwards.

protected override void OnSessionChange(SessionChangeDescription desc)
{
    var user = Session.Get(desc.SessionId);
}

To load the session information I use the WTS_INFO_CLASS. See my example below:

internal static class NativeMethods
{
    public enum WTS_INFO_CLASS
    {
        WTSInitialProgram,
        WTSApplicationName,
        WTSWorkingDirectory,
        WTSOEMId,
        WTSSessionId,
        WTSUserName,
        WTSWinStationName,
        WTSDomainName,
        WTSConnectState,
        WTSClientBuildNumber,
        WTSClientName,
        WTSClientDirectory,
        WTSClientProductId,
        WTSClientHardwareId,
        WTSClientAddress,
        WTSClientDisplay,
        WTSClientProtocolType,
        WTSIdleTime,
        WTSLogonTime,
        WTSIncomingBytes,
        WTSOutgoingBytes,
        WTSIncomingFrames,
        WTSOutgoingFrames,
        WTSClientInfo,
        WTSSessionInfo
    }

    [DllImport("Kernel32.dll")]
    public static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    public static extern bool WTSQuerySessionInformation(IntPtr hServer, Int32 sessionId, WTS_INFO_CLASS wtsInfoClass, out IntPtr ppBuffer, out Int32 pBytesReturned);

    [DllImport("Wtsapi32.dll")]
    public static extern void WTSFreeMemory(IntPtr pointer);
}

public static class Status
{
    public static Byte Online
    {
        get { return 0x0; }
    }

    public static Byte Offline
    {
        get { return 0x1; }
    }

    public static Byte SignedIn
    {
        get { return 0x2; }
    }

    public static Byte SignedOff
    {
        get { return 0x3; }
    }
}

public static class Session
{
    private static readonly Dictionary<Int32, User> User = new Dictionary<Int32, User>();

    public static bool Add(Int32 sessionId)
    {
        IntPtr buffer;
        int length;

        var name = String.Empty;
        var domain = String.Empty;

        if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSUserName, out buffer, out length) && length > 1)
        {
            name = Marshal.PtrToStringAnsi(buffer);
            NativeMethods.WTSFreeMemory(buffer);
            if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSDomainName, out buffer, out length) && length > 1)
            {
                domain = Marshal.PtrToStringAnsi(buffer);
                NativeMethods.WTSFreeMemory(buffer);
            }
        }

        if (name == null || name.Length <= 0)
        {
            return false;
        }

        User.Add(sessionId, new User(name, domain));

        return true;
    }

    public static bool Remove(Int32 sessionId)
    {
        return User.Remove(sessionId);
    }

    public static User Get(Int32 sessionId)
    {
        if (User.ContainsKey(sessionId))
        {
            return User[sessionId];
        }

        return Add(sessionId) ? Get(sessionId) : null;
    }

    public static UInt32 GetActiveConsoleSessionId()
    {
        return NativeMethods.WTSGetActiveConsoleSessionId();
    }
}

public class AvailabilityChangedEventArgs : EventArgs
{
    public bool Available { get; set; }

    public AvailabilityChangedEventArgs(bool isAvailable)
    {
        Available = isAvailable;
    }
}

public class User
{
    private readonly String _name;

    private readonly String _domain;

    private readonly bool _isDomainUser;

    private bool _signedIn;

    public static EventHandler<AvailabilityChangedEventArgs> AvailabilityChanged;

    public User(String name, String domain)
    {
        _name = name;
        _domain = domain;

        if (domain.Equals("EXAMPLE.COM"))
        {
            _isDomainUser = true;
        }
        else
        {
            _isDomainUser = false;
        }
    }

    public String Name
    {
        get { return _name; }
    }

    public String Domain
    {
        get { return _domain; }
    }

    public bool IsDomainUser
    {
        get { return _isDomainUser; }
    }

    public bool IsSignedIn
    {
        get { return _signedIn; }
        set
        {
            if (_signedIn == value) return;

            _signedIn = value;

            OnAvailabilityChanged(this, new AvailabilityChangedEventArgs(IsSignedIn));
        }
    }

    protected void OnAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)
    {
        if (AvailabilityChanged != null)
        {
            AvailabilityChanged(this, e);
        }
    }
}

The following code use the static AvailabilityChanged event from User, which gets fired as soon as the session state changes. The arg e contains the specific user.

public Main()
{
  User.AvailabilityChanged += UserAvailabilityChanged;
}

private static void UserAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)
{
  var user = sender as User;

  if (user == null) return;

  System.Diagnostics.Debug.WriteLine(user.IsSignedIn);
}
Andre Hofmeister
  • 3,185
  • 11
  • 51
  • 74
  • Thanks, I see login event got created. But signed in property always returns as "False". Second, EventID always shows as 0 in the event viewer. – kudlatiger Jul 11 '16 at 11:03
  • Listen to the `AvailabilityChanged` event for the user. As soon as the user state change, the event get fired. – Andre Hofmeister Jul 11 '16 at 11:37
  • I got you but EventID comes as 0, can we set custom event ID? I thought 70001 is event ID for login and 70002 is for logout. But it is not recording – kudlatiger Jul 11 '16 at 11:45
  • @codetoshare I'm sorry - what do you mean by the event id? Which source? For Microsoft Windows Security Auditing the event id for login is 4648. Please have a look at my last change in the answer. I attached an example, how you can catch the session change. – Andre Hofmeister Jul 11 '16 at 11:57
  • Thanks. got you. I am using windows server 2012 R2. This is showing 4624 for logon and 4634 for logout. I have 200+ VMs. so manually finding who logged in to these machines is time consuming. so I am looking for solution – kudlatiger Jul 11 '16 at 12:20