如何通过AWS Lambda或SSM可靠获取Windows EC2实例全会话类型的实时用户活动(鼠标/键盘)时间戳?
如何通过AWS Lambda或SSM可靠获取Windows EC2实例全会话类型的实时用户活动(鼠标/键盘)时间戳?
这个问题我之前帮客户处理过,核心原因就是SSM Run Command的执行上下文——它默认跑在Windows的Session 0(服务会话),而GetLastInputInfo是会话隔离的API,只能获取当前运行会话的用户输入状态。Session 0是后台服务专用的,没有任何用户交互,所以你每次拿到的都是同一个静态值,完全反映不了桌面会话的真实活动。
下面给你几个可行的解决方案,从快速调整到长期可靠方案都有:
方案一:让SSM命令在用户的交互式会话中执行(临时应急)
如果不想部署代理,可以先通过枚举所有活跃的交互式会话,再在每个会话中单独执行空闲时间检测逻辑。这样能绕过Session 0的限制,拿到真实的用户活动数据。
具体步骤:
- 先在EC2实例上提前部署Sysinternals的
PsExec.exe(可以通过SSM State Manager批量推送,或者手动下载到C:\Tools\目录),它能帮我们在指定的用户会话中启动进程。 - 用以下PowerShell脚本作为SSM Run Command的执行内容:
# 1. 获取所有活跃的交互式会话(排除Session 0) $activeSessions = quser | Where-Object { $_ -notmatch '^USERNAME' -and $_ -notmatch 'Disc' } | ForEach-Object { $parts = $_ -split '\s{2,}' [PSCustomObject]@{ Username = $parts[0] SessionId = $parts[2] } } # 2. 补充NICE DCV会话(如果quser没识别到) if (Get-Command "dcv" -ErrorAction SilentlyContinue) { $dcvSessions = dcv list-sessions | Where-Object { $_ -match '^[a-f0-9-]+' } | ForEach-Object { $sessionId = $_ -split '\s+' | Select-Object -First 1 $dcvDetails = dcv describe-session $sessionId | ConvertFrom-Json [PSCustomObject]@{ Username = $dcvDetails.owner SessionId = $dcvDetails.'user-session-id' } } $activeSessions += $dcvSessions | Where-Object { $_.SessionId -notin $activeSessions.SessionId } } # 3. 在每个活跃会话中执行空闲时间检测 $latestActivity = $null foreach ($session in $activeSessions) { $idleOutput = & C:\Tools\PsExec.exe -i $session.SessionId -s powershell -Command @" Add-Type @" using System; using System.Runtime.InteropServices; public class IdleTime { [StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; } [DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); public static uint GetIdleTime() { LASTINPUTINFO lii = new LASTINPUTINFO(); lii.cbSize = (uint)Marshal.SizeOf(lii); GetLastInputInfo(ref lii); return ((uint)Environment.TickCount - lii.dwTime) / 1000; } } "@ $idle = [IdleTime]::GetIdleTime() (Get-Date).ToUniversalTime().AddSeconds(-$idle).ToString("o") "@ $sessionActivity = [DateTime]::Parse($idleOutput) if (-not $latestActivity -or $sessionActivity -gt $latestActivity) { $latestActivity = $sessionActivity } Write-Output "会话 $($session.SessionId)(用户 $($session.Username))最后活动时间:$idleOutput" } # 4. 输出所有会话中最新的活动时间 if ($latestActivity) { Write-Output "全局最新用户活动时间:$($latestActivity.ToString('o'))" } else { Write-Output "无活跃用户会话" }
缺点:
- 每次调用SSM都有延迟,实时性差
- 需要维护
PsExec.exe的权限和安全例外(可能被Windows Defender拦截) - 会话枚举逻辑需要适配不同的远程桌面工具(比如Moonlight可能需要额外处理)
方案二:自定义后台代理服务(推荐,长期可靠)
如果需要跨所有会话类型的实时、可靠数据,部署一个轻量级的代理服务是最优解。它能持续监控所有会话的活动状态,主动上报到存储服务,Lambda直接查询即可。
具体实现思路:
- 代理脚本:编写PowerShell脚本,定期(比如每10秒)检查所有交互式会话的空闲时间,取所有会话中最新的活动时间戳,上报到DynamoDB/CloudWatch Metrics。
- 注册为Windows服务:用NSSM(Non-Sucking Service Manager)把脚本包装成Windows服务,随机器自动启动,后台运行。
- Lambda查询:Lambda直接调用DynamoDB API,根据EC2实例ID获取最新的活动时间戳。
代理脚本示例:
# 配置参数 $awsRegion = "us-east-1" $dynamoTable = "EC2UserActivity" $checkInterval = 10 # 秒 # 获取当前EC2实例ID $instanceId = (Invoke-RestMethod -Uri http://169.254.169.254/latest/meta-data/instance-id -ErrorAction Stop) # 初始化AWS工具 Set-AWSCredential -ProfileName "EC2AgentRole" -Region $awsRegion while ($true) { $allIdleTimes = @() # 1. 从WMI获取所有会话的空闲时间(覆盖RDP/控制台/大部分远程工具) $desktops = Get-WmiObject -Class Win32_Desktop -ErrorAction SilentlyContinue if ($desktops) { $allIdleTimes += $desktops | ForEach-Object { $_.IdleTime } } # 2. 补充DCV会话的空闲时间 if (Get-Command "dcv" -ErrorAction SilentlyContinue) { $dcvSessions = dcv list-sessions | Where-Object { $_ -match '^[a-f0-9-]+' } foreach ($session in $dcvSessions) { $sessionId = $_ -split '\s+' | Select-Object -First 1 $dcvDetails = dcv describe-session $sessionId | ConvertFrom-Json $desktop = Get-WmiObject -Class Win32_Desktop -Filter "SessionId = $($dcvDetails.'user-session-id')" -ErrorAction SilentlyContinue if ($desktop) { $allIdleTimes += $desktop.IdleTime } } } # 3. 计算并上报最新活动时间 if ($allIdleTimes.Count -gt 0) { $latestIdle = ($allIdleTimes | Measure-Object -Minimum).Minimum $lastActivity = (Get-Date).ToUniversalTime().AddMilliseconds(-$latestIdle) # 写入DynamoDB $item = @{ InstanceId = $instanceId LastActivity = $lastActivity.ToString('o') Timestamp = $lastActivity } Write-DDBItem -TableName $dynamoTable -Item $item -ErrorAction SilentlyContinue Write-Host "$(Get-Date -Format 'HH:mm:ss') 上报最新活动时间:$($lastActivity.ToString('o'))" } Start-Sleep -Seconds $checkInterval }
部署注意事项:
- 给EC2实例的IAM角色添加
dynamodb:PutItem权限 - 用NSSM把脚本注册为服务:
nssm install EC2UserActivityMonitor "powershell.exe" "-File C:\Scripts\ActivityMonitor.ps1" - 确保代理脚本的执行权限,避免被Windows Defender拦截
方案三:使用Win32_Desktop WMI类(快速验证)
如果不想用额外工具,可以直接通过WMI的Win32_Desktop类获取所有会话的空闲时间,它能返回每个交互式会话的IdleTime(毫秒级),取最小的IdleTime对应的就是最新的活动时间。
SSM命令示例:
$desktops = Get-WmiObject -Class Win32_Desktop if ($desktops) { $latestIdle = $desktops | Sort-Object IdleTime | Select-Object -First 1 -ExpandProperty IdleTime $lastActivity = (Get-Date).ToUniversalTime().AddMilliseconds(-$latestIdle) Write-Output $lastActivity.ToString('o') } else { Write-Output "无活跃用户会话" }
缺点:
- 部分远程工具(比如Moonlight)的会话可能没有对应的
Win32_Desktop实例 - WMI的IdleTime更新频率可能不如直接调用
GetLastInputInfo高
最终建议
如果你的场景需要实时性高、跨所有会话类型的可靠数据,方案二的自定义代理服务是最优选择,它能持续监控、主动上报,Lambda查询也非常高效。如果只是临时排查问题,方案一或方案三可以快速验证。
另外,关于SSM Run Command的Session 0问题,目前AWS没有提供直接的配置项让它在用户会话中执行,所以必须通过跨会话执行工具(如PsExec)或代理服务来绕过这个限制。




