You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Python编写的Windows服务中检测用户活动/空闲状态

在Windows服务中检测控制台会话的用户活动(Python实现)

服务运行在会话0,而GetLastInputInfo只能获取当前调用会话的输入数据,所以直接在服务里调用它永远返回0。要解决这个问题,核心思路是在用户的控制台会话(通常是会话1及以上)中运行一个辅助进程,由它来获取用户活动信息,再通过进程间通信(IPC)把数据传给服务。

具体实现方案

1. 核心步骤拆解

  • 服务端:枚举系统中活跃的用户控制台会话,创建命名管道用于IPC,然后在目标会话启动辅助进程。
  • 辅助进程:在用户会话中调用GetLastInputInfo计算用户空闲时间,定期将数据通过命名管道发送给服务。

2. 代码实现

辅助进程脚本(user_activity_monitor.py)

这个脚本需要在用户会话中运行,负责采集用户活动数据:

import ctypes
import win32api
import win32file
import time

def get_idle_time():
    class LASTINPUTINFO(ctypes.Structure):
        _fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_uint)]
    
    lii = LASTINPUTINFO()
    lii.cbSize = ctypes.sizeof(LASTINPUTINFO)
    win32api.GetLastInputInfo(ctypes.byref(lii))
    # 计算当前时间与最后输入时间的差值,即空闲时间
    return win32api.GetTickCount() - lii.dwTime

def main():
    # 连接到服务创建的命名管道
    pipe_name = r'\\.\pipe\UserActivityPipe'
    try:
        pipe = win32file.CreateFile(
            pipe_name,
            win32file.GENERIC_WRITE,
            0,
            None,
            win32file.OPEN_EXISTING,
            0,
            None
        )
        while True:
            idle_ms = get_idle_time()
            # 将空闲时间发送到服务
            win32file.WriteFile(pipe, str(idle_ms).encode('utf-8'))
            time.sleep(5)  # 每5秒采集一次
    except Exception as e:
        print(f"辅助进程出错:{e}")

if __name__ == '__main__':
    main()

Windows服务主脚本

负责管理辅助进程、接收用户活动数据:

import win32serviceutil
import win32service
import win32api
import win32process
import win32pipe
import win32file
import wtsapi32
import ctypes
import win32con

class UserActivityMonitorService(win32serviceutil.ServiceFramework):
    _svc_name_ = "UserActivityMonitorService"
    _svc_display_name_ = "用户活动监控服务"

    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
        self.pipe_handle = None
        self.aux_process_handle = None

    def SvcDoRun(self):
        self.ReportServiceStatus(win32service.SERVICE_RUNNING)
        
        # 创建命名管道,用于和辅助进程通信
        pipe_name = r'\\.\pipe\UserActivityPipe'
        self.pipe_handle = win32pipe.CreateNamedPipe(
            pipe_name,
            win32pipe.PIPE_ACCESS_INBOUND,
            win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT,
            1, 65536, 65536, 0, None
        )

        # 枚举系统中的活跃用户会话,筛选出非会话0的控制台会话
        active_sessions = []
        ptr = ctypes.c_void_p()
        session_count = ctypes.c_uint()
        if wtsapi32.WTSEnumerateSessions(wtsapi32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ctypes.byref(ptr), ctypes.byref(session_count)):
            session_info_array = ctypes.cast(ptr, ctypes.POINTER(wtsapi32.WTS_SESSION_INFO * session_count.value))[0]
            for session in session_info_array:
                # 只筛选活跃状态且非会话0的会话
                if session.State == wtsapi32.WTSActive and session.SessionId != 0:
                    active_sessions.append(session.SessionId)
            wtsapi32.WTSFreeMemory(ptr)

        # 在第一个活跃会话中启动辅助进程
        if active_sessions:
            target_session_id = active_sessions[0]
            # 获取当前进程的令牌,用于跨会话创建进程
            current_process = win32api.GetCurrentProcess()
            token_handle = win32process.OpenProcessToken(current_process, win32con.TOKEN_ALL_ACCESS)
            
            # 启动辅助进程的命令,注意替换为实际的脚本路径
            cmd_line = f'python "C:\\path\\to\\user_activity_monitor.py"'
            startup_info = win32process.STARTUPINFO()
            proc_info = win32process.CreateProcessAsUser(
                token_handle,
                None,
                cmd_line,
                None,
                None,
                False,
                win32process.CREATE_NO_WINDOW,
                None,
                None,
                startup_info
            )
            self.aux_process_handle = proc_info[0]

        # 等待辅助进程连接管道,并接收数据
        win32pipe.ConnectNamedPipe(self.pipe_handle, None)
        while True:
            # 检查服务是否收到停止信号
            if win32event.WaitForSingleObject(self.hWaitStop, 1000) == win32event.WAIT_OBJECT_0:
                break
            # 读取辅助进程发送的空闲时间数据
            try:
                data = win32file.ReadFile(self.pipe_handle, 1024)[1]
                if data:
                    idle_ms = int(data.decode('utf-8'))
                    # 这里可以根据需求处理空闲时间,比如写入日志、触发操作等
                    print(f"当前用户空闲时间:{idle_ms} 毫秒")
            except Exception as e:
                print(f"接收数据出错:{e}")

    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.hWaitStop)
        
        # 清理资源:终止辅助进程、关闭管道
        if self.aux_process_handle:
            win32process.TerminateProcess(self.aux_process_handle, 0)
            win32api.CloseHandle(self.aux_process_handle)
        if self.pipe_handle:
            win32file.CloseHandle(self.pipe_handle)
        self.ReportServiceStatus(win32service.SERVICE_STOPPED)

if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(UserActivityMonitorService)

3. 注意事项

  • 依赖安装:需要先安装pywin32库,执行pip install pywin32。
  • 权限要求:服务必须以管理员权限运行,否则无法调用CreateProcessAsUser和WTS相关API。
  • 路径配置:确保辅助脚本的路径在用户会话中可访问,Python环境在用户会话中也能正常运行。
  • 会话切换处理:如果用户注销后重新登录,服务需要重新枚举会话并重启辅助进程,可以在服务中定期检测会话状态更新。

内容的提问来源于stack exchange,提问作者JRiggles

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 17:43:16