Enum login time of users on Windows machine to check for logged on user

In my previous posting ("Check for logged on user on Windows machine") I showed a WMI-based method to check, if a certain user was logged on. Andreas Häber then suggested in a comment, to use the Win32 API function NetUserEnum instead. Since WMI always sounds to me like a heavyweight technology, I liked the idea to resort to "simple" Win32 API calls to solve the problem. Below you find the result of my attempt wrapped in a VB.NET module.

Usage:

Call GetUsersLastLogOn() to retrieve an array of all users of the machine running the code. The array contains UserLastLogOn objects providing a username and the date/time of the user´s last logon (lastLoggedOn).

Dim users() As LogOnInfo.UserLastLogOn
users = LogOnInfo.GetUsersLastLogOn()
For Each user As LogOnInfo.UserLastLogOn In users
    Console.WriteLine(user.username & ", " & user.lastLoggedOn)
Next

To solve the problem of a Windows service checking for logged on users you need to compare the logon time of the users to a timestamp (e.g. the start time of the Windows service process (call Process.GetCurrentProcess.StartTime())).

However, although a user might have logged on after the Windows service started, you never know, if he/she still is logged on. The last_logoff information in the USER_INFO_2 structure is not used by Windows.

That´s the main reason why I find this solution inferior to the one using WMI. Plus, it cost much more effort to code it (compiling information, translating the USER_INFO_2 structure to VB.NET, finding a way to convert UTC seconds to DateTime (easy, but eluded me for quite some time)).

How does it work:

NetUserEnum returns a list of USER_INFO_2 structures containing information about users of a certain machine. Passing nothing as the first parameter to NetUserEnum requests users from the local machine, passing 2 as the second parameter requests the info to be returned as USER_INFO_2 structures.

GetUsersLastLogOn() iterates over all USER_INFO_2 structures, extracts the user name and the last_logon time expressed in UTC seconds since 1/1/1970. The time is converted to a DateTime structure with a simple calculation: add the seconds to midnight of 1/1/1970. The extracted information is wrapped in UserLastLogOn objects and returned as an array.

Limitation:

NetUserEnum returns several USER_INFO_2 structures with each call. Calling it once on my system thus is enough. On machines with far more user accounts, though, it might be necessary to call the function several times. However, I did not test that scenario.

Code:

Imports System.Management
Imports System.Runtime.InteropServices

Module LogOnInfo
    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)> _
    Friend Structure USER_INFO_2
        Public name As String
        Public password As String
        Public password_age As Integer
        Public priv As Integer
        Public home_dir As String
        Public comment As String
        Public flags As Integer
        Public script_path As String
        Public auth_flags As Integer
        Public full_name As String
        Public usr_comment As String
        Public param As String
        Public workstations As String
        Public last_logon As Integer
        Public last_logoff As Integer
        Public acct_expires As Integer
        Public max_storage As Integer
        Public units_per_week As Integer
        Public logon_hours As Byte
        Public bad_pw_count As Integer
        Public num_logons As Integer
        Public logon_server As String
        Public country_code As Integer
        Public code_page As Integer
    End Structure


    Public Declare Function NetUserEnum Lib "Netapi32.dll" ( _
        <MarshalAs(UnmanagedType.LPWStr)> ByVal servername As String, _
        ByVal level As Integer, _
        ByVal filter As Integer, _
        ByRef bufptr As IntPtr, _
        ByVal prefmaxlen As Integer, _
        ByRef entriesread As Integer, _
        ByRef totalentries As Integer, _
        ByRef resume_handle As Integer) As Integer

    Public Declare Function NetApiBufferFree Lib "Netapi32.dll" (ByVal buffer As IntPtr) As Integer


    Public Class UserLastLogOn
        Private _username As String
        Private _lastLoggedOn As DateTime

        Friend Sub New(ByVal ui As USER_INFO_2)
            _username = ui.name
            _lastLoggedOn = #1/1/1970#.AddSeconds(ui.last_logon)
        End Sub

        Public ReadOnly Property username() As String
            Get
                Return _username
            End Get
        End Property

        Public ReadOnly Property lastLoggedOn() As DateTime
            Get
                Return _lastLoggedOn
            End Get
        End Property
    End Class

 

    Public Function GetUsersLastLogOn() As UserLastLogOn()
        Dim users As New ArrayList

        Dim entriesRead, totalEntries, hResume As Integer
        Dim bufPtr As IntPtr

        NetUserEnum(Nothing, 2, 2, bufPtr, -1, entriesRead, totalEntries, hResume)
        If entriesRead > 0 Then
            Dim iter As IntPtr = bufPtr
            For i As Integer = 0 To entriesRead - 1
                users.Add(New UserLastLogOn(Marshal.PtrToStructure(iter, GetType(USER_INFO_2))))
                iter = New IntPtr(iter.ToInt32 + Marshal.SizeOf(GetType(USER_INFO_2)))
            Next
        End If
        NetApiBufferFree(bufPtr)

        Return users.ToArray(GetType(UserLastLogOn))
    End Function


    Public Function IsUserLoggedOn(ByVal userName As String) As Boolean
        Dim mc As New ManagementClass("Win32_Process")
        Dim moc As ManagementObjectCollection = mc.GetInstances
        Dim mo As ManagementObject
        For Each mo In moc
            Dim p As New ROOT.CIMV2.Process(mo)

            Dim processDomain, processUser As String
            p.GetOwner(processDomain, processUser)
            If processUser = userName Then
                Return True
            End If
        Next
    End Function
End Module

Resources:

-Documentation of NetUserEnum Win32 API function
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/netmgmt/netmgmt/netuserenum.asp

-Documentation of USER_INFO_2 structure:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/netmgmt/netmgmt/user_info_2_str.asp

4 Comments

  • You might consider adding &lt;InAttribute()&gt; and &lt;OutAttribute()&gt; to the relevant parameters when declaring the API functions - makes the interop a bit quicker.

  • thanks for this posting



    However I'm getting

    Dim p As New ROOT.CIMV2.Process(mo)

    is an undefined type



    any help would be appreciated

  • Interesting code, but when I run it the last logged on time and date are in the future...? My system clock is correct. I just ran it at 2/15 10:19pm and the response is 2/16 3:19am. Any ideas?

  • The time returned is UTC time (universal coordinated time) whereas your system time (also in UTC) is adjusted and displayed in the local time zone.

Comments have been disabled for this content.