Programmatically Determine a Duration of a Locked Workstation?

C#Windows

C# Problem Overview


How can one determine, in code, how long the machine is locked?

Other ideas outside of C# are also welcome.


I like the windows service idea (and have accepted it) for simplicity and cleanliness, but unfortunately I don't think it will work for me in this particular case. I wanted to run this on my workstation at work rather than home (or in addition to home, I suppose), but it's locked down pretty hard courtesy of the DoD. That's part of the reason I'm rolling my own, actually.

I'll write it up anyway and see if it works. Thanks everyone!

C# Solutions


Solution 1 - C#

I hadn't found this before, but from any application you can hookup a SessionSwitchEventHandler. Obviously your application will need to be running, but so long as it is:

Microsoft.Win32.SystemEvents.SessionSwitch += new Microsoft.Win32.SessionSwitchEventHandler(SystemEvents_SessionSwitch);

void SystemEvents_SessionSwitch(object sender, Microsoft.Win32.SessionSwitchEventArgs e)
{
    if (e.Reason == SessionSwitchReason.SessionLock)
    { 
        //I left my desk
    }
    else if (e.Reason == SessionSwitchReason.SessionUnlock)
    { 
        //I returned to my desk
    }
}

Solution 2 - C#

I would create a Windows Service (a visual studio 2005 project type) that handles the OnSessionChange event as shown below:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    if (changeDescription.Reason == SessionChangeReason.SessionLock)
    { 
        //I left my desk
    }
    else if (changeDescription.Reason == SessionChangeReason.SessionUnlock)
    { 
        //I returned to my desk
    }
}

What and how you log the activity at that point is up to you, but a Windows Service provides quick and easy access to windows events like startup, shutdown, login/out, along with the lock and unlock events.

Solution 3 - C#

The solution below uses the Win32 API. OnSessionLock is called when the workstation is locked, and OnSessionUnlock is called when it is unlocked.

[DllImport("wtsapi32.dll")]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd,
int dwFlags);

[DllImport("wtsapi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr
hWnd);

private const int NotifyForThisSession = 0; // This session only

private const int SessionChangeMessage = 0x02B1;
private const int SessionLockParam = 0x7;
private const int SessionUnlockParam = 0x8;

protected override void WndProc(ref Message m)
{
    // check for session change notifications
    if (m.Msg == SessionChangeMessage)
    {
        if (m.WParam.ToInt32() == SessionLockParam)
            OnSessionLock(); // Do something when locked
        else if (m.WParam.ToInt32() == SessionUnlockParam)
            OnSessionUnlock(); // Do something when unlocked
    }

    base.WndProc(ref m);
    return;
}

void OnSessionLock() 
{
    Debug.WriteLine("Locked...");
}

void OnSessionUnlock() 
{
    Debug.WriteLine("Unlocked...");
}

private void Form1Load(object sender, EventArgs e)
{
    WTSRegisterSessionNotification(this.Handle, NotifyForThisSession);
}

// and then when we are done, we should unregister for the notification
//  WTSUnRegisterSessionNotification(this.Handle);

Solution 4 - C#

I know this is an old question but i have found a method to get the Lock State for a given session.

I found my answer https://stackoverflow.com/a/32115698/199111">here</a> but it was in C++ so i translated as much as i can to C# to get the Lock State.

So here goes:

static class SessionInfo {
private const Int32 FALSE = 0;



private static readonly IntPtr WTS_CURRENT_SERVER = IntPtr.Zero;

private const Int32 WTS_SESSIONSTATE_LOCK = 0;
private const Int32 WTS_SESSIONSTATE_UNLOCK = 1;

private static bool _is_win7 = false;

static SessionInfo() {
    var os_version = Environment.OSVersion;
    _is_win7 = (os_version.Platform == PlatformID.Win32NT && os_version.Version.Major == 6 && os_version.Version.Minor == 1);
}

[DllImport("wtsapi32.dll")]
private static extern Int32 WTSQuerySessionInformation(
    IntPtr hServer,
    [MarshalAs(UnmanagedType.U4)] UInt32 SessionId,
    [MarshalAs(UnmanagedType.U4)] WTS_INFO_CLASS WTSInfoClass,
    out IntPtr ppBuffer,
    [MarshalAs(UnmanagedType.U4)] out UInt32 pBytesReturned
);

[DllImport("wtsapi32.dll")]
private static extern void WTSFreeMemoryEx(
    WTS_TYPE_CLASS WTSTypeClass,
    IntPtr pMemory,
    UInt32 NumberOfEntries
);

private enum WTS_INFO_CLASS {
    WTSInitialProgram = 0,
    WTSApplicationName = 1,
    WTSWorkingDirectory = 2,
    WTSOEMId = 3,
    WTSSessionId = 4,
    WTSUserName = 5,
    WTSWinStationName = 6,
    WTSDomainName = 7,
    WTSConnectState = 8,
    WTSClientBuildNumber = 9,
    WTSClientName = 10,
    WTSClientDirectory = 11,
    WTSClientProductId = 12,
    WTSClientHardwareId = 13,
    WTSClientAddress = 14,
    WTSClientDisplay = 15,
    WTSClientProtocolType = 16,
    WTSIdleTime = 17,
    WTSLogonTime = 18,
    WTSIncomingBytes = 19,
    WTSOutgoingBytes = 20,
    WTSIncomingFrames = 21,
    WTSOutgoingFrames = 22,
    WTSClientInfo = 23,
    WTSSessionInfo = 24,
    WTSSessionInfoEx = 25,
    WTSConfigInfo = 26,
    WTSValidationInfo = 27,
    WTSSessionAddressV4 = 28,
    WTSIsRemoteSession = 29
}

private enum WTS_TYPE_CLASS {
    WTSTypeProcessInfoLevel0,
    WTSTypeProcessInfoLevel1,
    WTSTypeSessionInfoLevel1
}

public enum WTS_CONNECTSTATE_CLASS {
    WTSActive,
    WTSConnected,
    WTSConnectQuery,
    WTSShadow,
    WTSDisconnected,
    WTSIdle,
    WTSListen,
    WTSReset,
    WTSDown,
    WTSInit
}

public enum LockState {
    Unknown,
    Locked,
    Unlocked
}

[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX {
    public UInt32 Level;
    public UInt32 Reserved; /* I have observed the Data field is pushed down by 4 bytes so i have added this field as padding. */
    public WTSINFOEX_LEVEL Data;
}

[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX_LEVEL {
    public WTSINFOEX_LEVEL1 WTSInfoExLevel1;
}

[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX_LEVEL1 {
    public UInt32 SessionId;
    public WTS_CONNECTSTATE_CLASS SessionState;
    public Int32 SessionFlags;

    /* I can't figure out what the rest of the struct should look like but as i don't need anything past the SessionFlags i'm not going to. */

}

public static LockState GetSessionLockState(UInt32 session_id) {
    IntPtr ppBuffer;
    UInt32 pBytesReturned;

    Int32 result = WTSQuerySessionInformation(
        WTS_CURRENT_SERVER,
        session_id,
        WTS_INFO_CLASS.WTSSessionInfoEx,
        out ppBuffer,
        out pBytesReturned
    );

    if (result == FALSE)
        return LockState.Unknown;
        
    var session_info_ex = Marshal.PtrToStructure&lt;WTSINFOEX&gt;(ppBuffer);

    if (session_info_ex.Level != 1)
        return LockState.Unknown;

    var lock_state = session_info_ex.Data.WTSInfoExLevel1.SessionFlags;
    WTSFreeMemoryEx(WTS_TYPE_CLASS.WTSTypeSessionInfoLevel1, ppBuffer, pBytesReturned);

    if (_is_win7) {
        /* Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/ee621019(v=vs.85).aspx
            * Windows Server 2008 R2 and Windows 7:  Due to a code defect, the usage of the WTS_SESSIONSTATE_LOCK
            * and WTS_SESSIONSTATE_UNLOCK flags is reversed. That is, WTS_SESSIONSTATE_LOCK indicates that the
            * session is unlocked, and WTS_SESSIONSTATE_UNLOCK indicates the session is locked.
            * */
        switch (lock_state) {
            case WTS_SESSIONSTATE_LOCK:
                return LockState.Unlocked;

            case WTS_SESSIONSTATE_UNLOCK:
                return LockState.Locked;

            default:
                return LockState.Unknown;
        }
    }
    else {
        switch (lock_state) {
            case WTS_SESSIONSTATE_LOCK:
                return LockState.Locked;

            case WTS_SESSIONSTATE_UNLOCK:
                return LockState.Unlocked;

            default:
                return LockState.Unknown;
        }
    }
}




}

}

Note: The above code was extracted from a much larger project so if i missed a bit sorry. I havn't got time to test the above code but plan to come back in a week or two to check everything. I only posted it now because i didn't want to forget to do it.

Solution 5 - C#

If you're interested in writing a windows-service to "find" these events, topshelf (the library/framework that makes writing windows services much easier) has a hook.

public interface IMyServiceContract
{
    void Start();

    void Stop();

    void SessionChanged(Topshelf.SessionChangedArguments args);
}



public class MyService : IMyServiceContract
{

    public void Start()
    {
    }

    public void Stop()
    {

    }

    public void SessionChanged(SessionChangedArguments e)
    {
        Console.WriteLine(e.ReasonCode);
    }	
}

and now the code to wire up the topshelf service to the interface/concrete above

Everything below is "typical" topshelf setup.... except for 2 lines which I marked as

/* THIS IS MAGIC LINE */

Those are what get the SessionChanged method to fire.

I tested this with windows 10 x64. I locked and unlocked my machine and I got the desired result.

            IMyServiceContract myServiceObject = new MyService(); /* container.Resolve<IMyServiceContract>(); */


            HostFactory.Run(x =>
            {
                x.Service<IMyServiceContract>(s =>
                {
                    s.ConstructUsing(name => myServiceObject);
                    s.WhenStarted(sw => sw.Start());
                    s.WhenStopped(sw => sw.Stop());
                    s.WhenSessionChanged((csm, hc, chg) => csm.SessionChanged(chg)); /* THIS IS MAGIC LINE */
                });

                x.EnableSessionChanged(); /* THIS IS MAGIC LINE */
				
                /* use command line variables for the below commented out properties */
                /*
                x.RunAsLocalService();
                x.SetDescription("My Description");
                x.SetDisplayName("My Display Name");
                x.SetServiceName("My Service Name");
                x.SetInstanceName("My Instance");
                */

                x.StartManually(); // Start the service manually.  This allows the identity to be tweaked before the service actually starts

                /* the below map to the "Recover" tab on the properties of the Windows Service in Control Panel */
                x.EnableServiceRecovery(r =>
                {
                    r.OnCrashOnly();
                    r.RestartService(1); ////first
                    r.RestartService(1); ////second
                    r.RestartService(1); ////subsequents
                    r.SetResetPeriod(0);
                });

                x.DependsOnEventLog(); // Windows Event Log
                x.UseLog4Net();

                x.EnableShutdown();

                x.OnException(ex =>
                {
                    /* Log the exception */
                    /* not seen, I have a log4net logger here */
                });
            });					

My packages.config to provide hints about versions:

  <package id="log4net" version="2.0.5" targetFramework="net45" />
  <package id="Topshelf" version="4.0.3" targetFramework="net461" />
  <package id="Topshelf.Log4Net" version="4.0.3" targetFramework="net461" />

Solution 6 - C#

NOTE: This is not an answer, but a (contribution) to Timothy Carter answer, because my reputation doesn't allow me to comment so far.

Just in case somebody tried the code from Timothy Carter's answer and did not get it to work right away in a Windows service, there's one property that need to be set to true in the constructor of the service. Just add the line in the constructor:

CanHandleSessionChangeEvent = true;

And be sure not to set this property after the service is started otherwise an InvalidOperationException will be thrown.

Solution 7 - C#

In Windows Task Scheduler, you could create tasks that trigger on workstation lock and on workstation unlock. Each task could write a flag and timestamp to a file to state if the workstation is locked or unlocked and when it happened.

I realize that this is not a programmatic way. It is simpler than writing a service. It won't miss an event because your program happens to not be running at the time of lock/unlock transition.

Solution 8 - C#

Below is the 100% working code to find if the PC is locked or not.

Before using this use the namespace System.Runtime.InteropServices.

[DllImport("user32", EntryPoint = "OpenDesktopA", CharSet = CharSet.Ansi,SetLastError = true, ExactSpelling = true)]
private static extern Int32 OpenDesktop(string lpszDesktop, Int32 dwFlags, bool fInherit, Int32 dwDesiredAccess);

[DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern Int32 CloseDesktop(Int32 hDesktop);

[DllImport("user32", CharSet = CharSet.Ansi,SetLastError = true,ExactSpelling = true)]
private static extern Int32 SwitchDesktop(Int32 hDesktop);

public static bool IsWorkstationLocked()
{
    const int DESKTOP_SWITCHDESKTOP = 256;
    int hwnd = -1;
    int rtn = -1;

    hwnd = OpenDesktop("Default", 0, false, DESKTOP_SWITCHDESKTOP);

    if (hwnd != 0)
    {
        rtn = SwitchDesktop(hwnd);
        if (rtn == 0)
        {
            // Locked
            CloseDesktop(hwnd);
            return true;
        }
        else
        {
            // Not locked
            CloseDesktop(hwnd);
        }
    }
    else
    {
        // Error: "Could not access the desktop..."
    }

    return false;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAgentConundrumView Question on Stackoverflow
Solution 1 - C#Timothy CarterView Answer on Stackoverflow
Solution 2 - C#Timothy CarterView Answer on Stackoverflow
Solution 3 - C#adeel825View Answer on Stackoverflow
Solution 4 - C#RobertView Answer on Stackoverflow
Solution 5 - C#granadaCoderView Answer on Stackoverflow
Solution 6 - C#Abdul Rahman KayaliView Answer on Stackoverflow
Solution 7 - C#NathanView Answer on Stackoverflow
Solution 8 - C#dan_gView Answer on Stackoverflow