以编程方式确定锁定工作站的持续时间?

115

如何在代码中确定计算机锁定的时间?

欢迎分享除C#之外的其他想法。


我喜欢Windows服务的想法(并已接受此建议), 因为它简单而干净,但不幸的是,在这种特殊情况下,我认为它对我没有用。 我想在工作站上运行它,而不是在家里(或者我想增加一下),但由于国防部的限制,我的工作站被严格控制。 这也是我自己写代码的原因之一。

我还是会把它写出来并查看是否有效。谢谢大家!

8个回答

155

我之前没有发现这个功能,但从任何应用程序中,你都可以连接一个SessionSwitchEventHandler处理程序。显然,你的应用程序需要在运行状态下,但只要它处于运行状态:

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
    }
}

3
已在 Windows 7 x64 和 Windows 10 x64 上进行了100%测试。 - Contango
哇,太棒了!没有错误,没有异常,流畅而干净! - Mayer Spitz
1
这是正确的做法。根据此Microsoft文章,“没有可以调用的函数来确定工作站是否已锁定。”必须使用SessionSwitchEventHandler进行监视。 - JonathanDavidArndt

35

我将创建一个Windows服务(Visual Studio 2005项目类型),该服务处理如下所示的OnSessionChange事件:

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
    }
}

如何记录活动以及如何记录是由你决定的,但Windows服务可以快速且轻松地访问Windows事件,例如启动、关闭、登录、注销以及锁定和解锁事件。


19

下面的解决方案使用Win32 API。当工作站被锁定时,调用OnSessionLock函数;当解锁时,调用OnSessionUnlock函数。

[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);

1
如果你发现SessionSwitch事件(来自其他答案)没有触发(例如,你的应用程序抑制了它),那么这是一个不错的选择。 - kad81
1
对于未来的读者...我认为这里的覆盖来自于System.Windows.Forms.Form,就像你可能会编写这样一个类:public class Form1 : System.Windows.Forms.Form - granadaCoder
SystemEvents.SessionSwitch 无法正常工作时,这对我很有效。 - DCOPTimDowd

6

我知道这是一个老问题,但我找到了一种方法来获取给定会话的锁定状态。

我在这里找到了答案,但它是用C++编写的,所以我尽可能将其翻译为C#以获取锁定状态。

下面是代码:

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<WTSINFOEX>(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;
            }
        }
    }
}

注意:上面的代码是从一个更大的项目中提取出来的,如果我漏掉了一些部分,请谅解。我没有时间测试上面的代码,但计划在一两周后回来检查一切。我现在发布它只是因为我不想忘记要做这件事。

这个可行(目前已在Windows 7上测试)。谢谢,我们已经寻找了几周,你的答案来得正是时候! - SteveP
1
代码中存在一些错误:
  1. if (session_info_ex.Level != 1) - 如果条件为真,内存将不会被释放。
  2. 如果 session_info_ex.Level 不等于 1,则不应执行此操作:Marshal.PtrToStructure<WTSINFOEX>(ppBuffer);,因为返回的缓冲区大小可能与 WTSINFOEX 的大小不同。
- SergeyT
  1. 不需要添加字段 UInt32 Reserved;,而应该完全定义结构体 WTSINFOEX_LEVEL1。在这种情况下,编译器将正确地填充(对齐)结构体内的字段。
  2. 此处误用了函数 WTSFreeMemoryEx。应该使用 WTSFreeMemoryWTSFreeMemoryEx 旨在释放 WTSEnumerateSessionsEx 后的内存。
- SergeyT
  1. 在所有属性中必须使用 CharSet = CharSet.Auto
- SergeyT

5

注意:这不是一个答案,而是对Timothy Carter的回答做出的贡献,因为我的声望还不允许我发表评论。

以防万一,如果有人尝试了Timothy Carter的答案,并且在Windows服务中无法立即正常工作,则需要在服务的构造函数中将一个属性设置为true。 只需在构造函数中添加以下行:

CanHandleSessionChangeEvent = true;

请确保在服务启动之前设置此属性,否则将抛出InvalidOperationException异常。


4
如果您有兴趣编写一个Windows服务来“查找”这些事件,Topshelf(使编写Windows服务更加容易的库/框架)有一个钩子。
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);
    }   
}

现在,我们需要编写代码将topshelf服务连接到上面的接口/实现。

以下内容是“典型”的topshelf设置……除了我标记为“这是魔法行”的两行代码之外。

那些代码将触发SessionChanged方法。

我在Windows 10 x64上进行了测试。我锁定和解锁我的计算机,获得了预期的结果。

            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 */
                });
            });                 

我的packages.config文件提供版本提示:

  <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" />

如果您已经实现了ServiceControl并且没有隐式创建服务类实例,那么可以使用x.EnableSessionChanged();ServiceSessionChange接口实现相结合。例如:x.Service<ServiceImpl>();。您必须在ServiceImpl类中实现ServiceSessionChangeclass ServiceImpl : ServiceControl, ServiceSessionChange - oleksa

0
在Windows任务计划程序中,您可以创建触发“工作站锁定”和“工作站解锁”的任务。每个任务都可以将标志和时间戳写入文件,以说明工作站何时被锁定或解锁。
我意识到这不是一种编程方式。 它比编写服务更简单。 它不会因为程序在锁定/解锁转换时恰好未运行而错过事件。

-4
以下是可用于查找PC是否已锁定的100%工作代码。
在使用此代码之前,请使用命名空间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;
}

4
请查看MSDN中的OpenInputDesktop和GetUserObjectInformation,以获取活动桌面的名称。上述代码对于在多个桌面工作、使用Microsoft的desktops.exe实用程序或其他情况下的用户来说并不安全/友好。或者更好的方法是尝试在活动桌面上创建一个窗口(SetThreadDesktop),如果成功,则在其上显示您的UI。如果不行,则它是受保护/特殊的桌面,因此不要这么做。 - eselk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接