如何在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
相关产品推荐
相关产品推荐

