How to retrieve the username of the user who logged onto Windows from windows service
Recently, I have been tasked to write a .NET application that will prepare the operating system environment for a user who had logged on a Windows 7 machine. As a first step, the application should grab some settings from the network by providing the username of the user to a server application. The application will then prepare the operating system environment based on the settings received.
Writing the application as a windows service is a favorable option. This is because a windows service application can be configured to run automatically and will run before any user logs into the operating system. Furthermore, a windows service can run as Local System
, which mean that it can execute most operating system facilities without facing permission issues.
Detecting user login event
Detecting user login event from a windows service is easy. By overriding the OnSessionChange
method from the ServiceBase
class, we can detect when a user log onto windows.
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.ServiceProcess; using System.Text; namespace MyWindowsService { public partial class MyWindowsService : ServiceBase { public MyWindowsService() { // Generated by Visual Studios InitializeComponent(); // This is needed for this window service to detect session changes base.CanHandleSessionChangeEvent = true; } protected override void OnStart(string[] args) { // Code to execute when windows service starts } protected override void OnStop() { // Code to execute when windows service stops } protected override void OnSessionChange(SessionChangeDescription changeDescription) { // Use a switch block to segment the actions to perform for // different session changes switch (changeDescription.Reason) { case SessionChangeReason.SessionLogon: // Retrieve the username of the user who had just logged on string username = Machine.getInstance().getUsername(); break; case SessionChangeReason.SessionLogoff: // Could perform any restoration of the operating system // environment here. break; case SessionChangeReason.ConsoleLogon: // Triggered whenever a user logs in after // switching user accounts // Triggered once by the windows service // account whenever a user logs in break; } // Inform the base class of the change as well base.OnSessionChange(changeDescription); } } }
As long as the windows service is started automatically, the OnSessionChange
method block will be called whenever there are changes in the user session. This includes the case when a user logs on to windows, in which Machine.getInstance().getUsername()
will be called. Note that besides log on and log off events, there are several other events defined in the SessionChangeReason
enumeration that you can handle from your windows service.
Attempts made to retrieve the username
It makes sense for me to implement the Machine
class as a Singleton. Machine.getInstance()
will return me the Machine
Singleton. The getUsername
instance method will return the username of the user who is currently logged on to windows.
Environment.UserName and WindowsIdentity.GetCurrent().Name does not work
My first intuition to implementing the getUsername()
method is to utilitise facilities from the .NET framework, namely the UserName
property from the Environment
class or the Name
property of the WindowsIdentity
class. However, since the windows service is running as Local System
, which belongs to a separate environment from the logged on user, the string "SYSTEM" was returned instead.
Looking for the right username with WMI
Windows Management Instrumentation (WMI) was my next hope. A couple of google searches brought me two methods for retrieving the right username.
Using the Win32_Process WMI class
The first method utilises the Win32_Process WMI class to inspect the owners of the explorer.exe process. Although I could get the list of usernames who are logged onto the machine, I felt that too much work is required to get the user who is currently active.
ManagementScope ms = new ManagementScope("\\\\.\\root\\cimv2"); ObjectQuery query = new ObjectQuery ("select * from Win32_Process where name = 'explorer.exe'"); ManagementObjectSearcher searcher = new ManagementObjectSearcher(ms, query); ManagementObjectCollection returnCollection = searcher.Get(); foreach (ManagementObject mo in returnCollection) { string[] argList = new string[] { string.Empty }; int returnVal = Convert.ToInt32( mo.InvokeMethod("GetOwner", argList)); if (returnVal == 0) { Console.WriteLine("User name:"); Console.WriteLine(argList[0].ToString()); } }
Using the Win32_ComputerSystem WMI class
The second method utilises the Win32_ComputerSystem WMI class:
ManagementScope ms = new ManagementScope("\\\\.\\root\\cimv2"); ObjectQuery query = new ObjectQuery("SELECT * FROM Win32_ComputerSystem"); ManagementObjectSearcher searcher = new ManagementObjectSearcher(ms, query); foreach(ManagementObject mo in searcher.Get()) { Console.WriteLine(mo["UserName"].ToString()); }
This method is much better than the previous one in that there is only at most one instance of the Win32_ComputerSystem
WMI class. Whenever a user logged onto windows, the Username
attribute will contain the username of the user. In the case when there are no users in the windows system, there will be no instances of the Win32_ComputerSystem
class. These properties make Win32_ComputerSystem
my ideal choice in implementing the getUsername
method for my Machine
class.
Implementing the Machine class
public class Machine { private static Object _classLocker = new Object(); private static Machine _machine; private Machine() { } // end private Machine() public static Machine getInstance() { if (_machine == null) { lock (_classLocker) { if (_machine == null) { _machine = new Machine(); } } } return _machine; } // end public static Machine getInstance() public String getUsername() { string username = null; try { // Define WMI scope to look for the Win32_ComputerSystem object ManagementScope ms = new ManagementScope("\\\\.\\root\\cimv2"); ms.Connect(); ObjectQuery query = new ObjectQuery ("SELECT * FROM Win32_ComputerSystem"); ManagementObjectSearcher searcher = new ManagementObjectSearcher(ms, query); // This loop will only run at most once. foreach (ManagementObject mo in searcher.Get()) { // Extract the username username = mo["UserName"].ToString(); } // Remove the domain part from the username string[] usernameParts = username.Split('\\'); // The username is contained in the last string portion. username = usernameParts[usernameParts.Length - 1]; } catch (Exception) { // The system currently has no users who are logged on // Set the username to "SYSTEM" to denote that username = "SYSTEM"; } return username; } // end String getUsername() } // end class Machine
8 Comments
boss you done a great job 😀 live long
What about the case when multiple users are logged on? For example both via remote desktop on Windows Servers.
Hey, this is a good question.
When I was still with my previous company, I had tried to get username of a user logged in via remote desktop.
Although I was able to detect the login from windows service, I discovered that remote desktop accesses were not being recorded in the Win32_ComputerSystem WMI class. When I tried to read from the Win32_ComputerSystem instance, the username was empty.
I guess the username attribute Win32_ComputerSystem will be updated only for users who had physically logged into the computer.
Here are some options that I could think of to get the username of those remote users:
Reading the Success Audit entries in the Security log, I think you would need to enable log audit and if there is a domain level setting on log audits, you will not be able to get it from your physical machine.
Create a windows application and put it in the startup folder.
You could make your windows service serve HTTP requests.
When the application start, send the value of
Environment.UserName
that is retrieved via your windows application via HTTP get or post to your windows service. You could reference from this post to do HTTP get or post in C#.I am not sure how to detect switched logon from the windows application though, but I feel that it is possible.
Get those details from your active domain server. I think it is possible to get all the user login details happening in the domain from the active directory. But you will need to own the active directory server.
I am not able to try out these options though, since I no longer have all the equipment to try this out.
Perhaps you can try them out or think of more ways to achieve your objective? If you managed to get a solution, feel free to post it here as a comment so that others can benefit from it.
Your first method will return the usernames for those logged in via remote desktop. You get a list of users who own explorer.exe processes, that includes those logged in via remote desktop, but also those who connected via remote desktop, then just closed their RDC connection without using the Start>Disconnect route. Nothing perfect, but it works.
Hi William,
Thank you for coming by and your feedback. 🙂
this is not working when the wmi classes are not registered on a windows xp machine with 512 mb of ram
Hi Shashank,
Yes, you will need WMI classes to be registered for this solution to work
You could also use Cassia.dll – you will get a list of TS users, loop through them comparing sessionid will give you the username