问题 让用户在C#中空闲时间?


我找到了关于如何获得用户空闲时间的教程 空闲时间

问题是它只有在应用程序在用户上运行时才有效。

我的应用程序在SYSTEM上运行。

我怎样才能获得空闲时间?或者如果PC空闲?


12545
2018-06-27 14:28


起源

由于在SYSTEM下运行的服务种类繁多,实际上很少“空闲”,所以这个数字几乎肯定为零。你能告诉我们你想用这个号码做些什么吗?你的更大目标是什么? - Cos Callis
我想更大的目标是看到连接的鼠标和键盘设备的空闲状态。 - Akku
如果您想要为登录用户获取空闲时间,您应该知道在具有快速用户切换或终端服务的现代Windows中没有这样的东西(可以有多个登录用户)。 - Damien_The_Unbeliever


答案:


据我所知,你可以使用GetLastInputInfo函数的结果。因此,我建议如下。

现在,假设您有在本地系统帐户下运行的可执行文件A.创建可执行文件B,它将收集相关的系统信息(在GetLastInputInfo的帮助下)。接下来,使用此类使用CreateProcessAsUser函数和已记录的用户令牌从可执行文件A运行可执行文件B(遗憾的是我无法找到发布它的stackoverflow问题):

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Helpers
{
    [StructLayout(LayoutKind.Sequential)]
    internal struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct SECURITY_ATTRIBUTES
    {
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public uint cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

    internal enum SECURITY_IMPERSONATION_LEVEL
    {
        SecurityAnonymous,
        SecurityIdentification,
        SecurityImpersonation,
        SecurityDelegation
    }

    internal enum TOKEN_TYPE
    {
        TokenPrimary = 1,
        TokenImpersonation
    }

    public class ImpersonateProcessAsLoggedUser
    {
        private const short SW_SHOW = 5;
        private const uint TOKEN_QUERY = 0x0008;
        private const uint TOKEN_DUPLICATE = 0x0002;
        private const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
        private const int GENERIC_ALL_ACCESS = 0x10000000;
        private const int STARTF_USESHOWWINDOW = 0x00000001;
        private const int STARTF_FORCEONFEEDBACK = 0x00000040;
        private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;

        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool CreateProcessAsUser(
            IntPtr hToken,
            string lpApplicationName,
            string lpCommandLine,
            ref SECURITY_ATTRIBUTES lpProcessAttributes,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            bool bInheritHandles,
            uint dwCreationFlags,
            IntPtr lpEnvironment,
            string lpCurrentDirectory,
            ref STARTUPINFO lpStartupInfo,
            out PROCESS_INFORMATION lpProcessInformation);


        [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx", SetLastError = true)]
        private static extern bool DuplicateTokenEx(
            IntPtr hExistingToken,
            uint dwDesiredAccess,
            ref SECURITY_ATTRIBUTES lpThreadAttributes,
            Int32 ImpersonationLevel,
            Int32 dwTokenType,
            ref IntPtr phNewToken);


        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool OpenProcessToken(
            IntPtr ProcessHandle,
            UInt32 DesiredAccess,
            ref IntPtr TokenHandle);

        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool CreateEnvironmentBlock(
            ref IntPtr lpEnvironment,
            IntPtr hToken,
            bool bInherit);


        [DllImport("userenv.dll", SetLastError = true)]
        private static extern bool DestroyEnvironmentBlock(
            IntPtr lpEnvironment);

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(
            IntPtr hObject);


        private static bool LaunchProcessAsUser(string cmdLine, IntPtr token, IntPtr envBlock)
        {
            bool result = false;


            var pi = new PROCESS_INFORMATION();
            var saProcess = new SECURITY_ATTRIBUTES();
            var saThread = new SECURITY_ATTRIBUTES();
            saProcess.nLength = (uint) Marshal.SizeOf(saProcess);
            saThread.nLength = (uint) Marshal.SizeOf(saThread);

            var si = new STARTUPINFO();
            si.cb = (uint) Marshal.SizeOf(si);


            //if this member is NULL, the new process inherits the desktop
            //and window station of its parent process. If this member is
            //an empty string, the process does not inherit the desktop and
            //window station of its parent process; instead, the system
            //determines if a new desktop and window station need to be created.
            //If the impersonated user already has a desktop, the system uses the
            //existing desktop.

            si.lpDesktop = @"WinSta0\Default"; //Modify as needed
            si.dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEONFEEDBACK;
            si.wShowWindow = SW_SHOW;
            //Set other si properties as required.

            result = CreateProcessAsUser(
                token,
                null,
                cmdLine,
                ref saProcess,
                ref saThread,
                false,
                CREATE_UNICODE_ENVIRONMENT,
                envBlock,
                null,
                ref si,
                out pi);


            if (result == false)
            {
                int error = Marshal.GetLastWin32Error();
                string message = String.Format("CreateProcessAsUser Error: {0}", error);
                Debug.WriteLine(message);
            }

            return result;
        }


        private static IntPtr GetPrimaryToken(int processId)
        {
            IntPtr token = IntPtr.Zero;
            IntPtr primaryToken = IntPtr.Zero;
            bool retVal = false;
            Process p = null;

            try
            {
                p = Process.GetProcessById(processId);
            }

            catch (ArgumentException)
            {
                string details = String.Format("ProcessID {0} Not Available", processId);
                Debug.WriteLine(details);
                throw;
            }


            //Gets impersonation token
            retVal = OpenProcessToken(p.Handle, TOKEN_DUPLICATE, ref token);
            if (retVal)
            {
                var sa = new SECURITY_ATTRIBUTES();
                sa.nLength = (uint) Marshal.SizeOf(sa);

                //Convert the impersonation token into Primary token
                retVal = DuplicateTokenEx(
                    token,
                    TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY,
                    ref sa,
                    (int) SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
                    (int) TOKEN_TYPE.TokenPrimary,
                    ref primaryToken);

                //Close the Token that was previously opened.
                CloseHandle(token);
                if (retVal == false)
                {
                    string message = String.Format("DuplicateTokenEx Error: {0}", Marshal.GetLastWin32Error());
                    Debug.WriteLine(message);
                }
            }

            else
            {
                string message = String.Format("OpenProcessToken Error: {0}", Marshal.GetLastWin32Error());
                Debug.WriteLine(message);
            }

            //We'll Close this token after it is used.
            return primaryToken;
        }

        private static IntPtr GetEnvironmentBlock(IntPtr token)
        {
            IntPtr envBlock = IntPtr.Zero;
            bool retVal = CreateEnvironmentBlock(ref envBlock, token, false);
            if (retVal == false)
            {
                //Environment Block, things like common paths to My Documents etc.
                //Will not be created if "false"
                //It should not adversley affect CreateProcessAsUser.

                string message = String.Format("CreateEnvironmentBlock Error: {0}", Marshal.GetLastWin32Error());
                Debug.WriteLine(message);
            }
            return envBlock;
        }

        public static bool Launch(string appCmdLine /*,int processId*/)
        {
            bool ret = false;

            //Either specify the processID explicitly
            //Or try to get it from a process owned by the user.
            //In this case assuming there is only one explorer.exe

            Process[] ps = Process.GetProcessesByName("explorer");
            int processId = -1; //=processId
            if (ps.Length > 0)
            {
                processId = ps[0].Id;
            }

            if (processId > 1)
            {
                IntPtr token = GetPrimaryToken(processId);

                if (token != IntPtr.Zero)
                {
                    IntPtr envBlock = GetEnvironmentBlock(token);
                    ret = LaunchProcessAsUser(appCmdLine, token, envBlock);
                    if (envBlock != IntPtr.Zero)
                        DestroyEnvironmentBlock(envBlock);

                    CloseHandle(token);
                }
            }
            return ret;
        }
    }
}

接下来,您需要找到一种方法将收集的信息从可执行文件B发送到可执行文件A.有很多方法可以执行此操作。其中之一是.Net Remoting。但是,您可以创建中间XML甚至文本文件。

也许这不是解决问题的最佳方法,但如果您需要更多本地系统< - >记录用户交互,您可以遵循一种模式。


3
2018-06-27 15:07



好吧,关键是可执行文件B将在当前用户上下文中运行。这就是GetLastInputInfo应该返回该特定用户的统计信息的原因。此类现在在我的系统中工作,该系统由在本地系统帐户下运行的Windows服务和从该服务注入已记录用户上下文的可执行文件组成。通信渠道是.Net Remoting。看一下Launch方法和这个特定的行:IntPtr token = GetPrimaryToken(processId); UPD:哇,我回答的是什么? :-) - ReVolly
抱歉,我更仔细地查看了代码并意识到你在做什么,你的描述并没有使解决方案的那个方面变得清晰。但是,我怀疑这在终端服务环境中会非常有效,因为您不知道您实际与哪个会话进行通信,当然这可能是一个可接受的限制,具体取决于用例场景,所以我只是指出这一点作为一个FYI。 - Chris Taylor
谢谢,克里斯。抱歉描述,我的英语不是很好。我没有尝试过终端服务环境中的代码,但在我的情况下,它就像一个魅力。实际上,提议的解决方案可能无法在您的方案中工作的唯一原因是错误的资源管理器令牌。这种情况需要一些额外的调查。 - ReVolly
@Virtuality嘿,您认为在Windows服务(在SYSTEM上运行)和正常流程(在CURRENT USER上运行)之间进行通信的最佳方式。顺便问一下你的代码真棒!它适用于所有平台吗? (XP / Vista / 7的)? - Danpe
@Danpe嗯,谢谢,但这不是我的,我在StackOverflow上找到了它。据我所知,它适用于XP / Vista / 7而没有任何问题。当我开始这些项目时,我提到我正在使用.Net Remoting,但不确定这是现在最好的选择。如果您正在使用.NET 4进行开发,根据您的要求,我建议您查看1)WCF(tcp或命名管道端点),2)NamedPipes(我知道.NET 4具有原生支持)。当然,这些只是一些选择。 - ReVolly


我知道这个答案已被接受,但我只想补充一下,以防人们想在终端服务环境中做同样的事情。

决明子 是一个开源库,它将.NET包装器放在Windows终端服务API上。我已将它用于我的服务器管理,它运行得非常好。您可以通过调用获取任何会话的空闲时间 ITerminalServicesSession.LastInputTime


6
2018-06-27 18:57



+1:刚看了一下图书​​馆,看起来非常好。 - Chris Taylor


执行此类操作的推荐方法是运行单独的应用程序,将用户会话数据传递给服务。您可以在用户会话开始时通过将应用程序添加到注册表中来配置应用程序 HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run 

您可以在以下知识库文章中阅读有关此方法的更多详细信息

http://support.microsoft.com/kb/308403


2
2018-06-27 15:07