ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C#实现PS/2设备禁用:从枚举到策略落地的完整指南

C#实现PS/2设备禁用:从枚举到策略落地的完整指南 简介这是一套基于C#开发的设备管理系统源码定位很明确面向正在准备课程设计或想通过完整项目练习WinForms桌面应用开发的初学者。系统围绕设备全生命周期设计功能点覆盖设备信息添加、更新、类型维护设备维修登记与查询设备报废登记与查询设备借出和归还登记以及修改密码等操作模块划分清晰、业务流程互相衔接非常适合用于学习“增删改查”在管理系统中的实际落地。压缩包为rar格式整体大小约3.53MB目前已有92人浏览学习虽然压缩包内部文件清单暂未公开但下载后可通过源码目录快速定位各功能模块。读者能从中获得一套可直接编译运行的C#项目通过阅读和调试代码既能掌握设备管理系统的数据流转也能参考其实现方式快速做出自己的学习或课设项目具有较强的实战参考价值。1. 设备管理系统为什么要碰 PS/2 接口如果只做设备台账没人会去碰 PS/2。但银行柜面、医院工位、老式工业一体机上PS/2 键盘到现在还在服役审计要求“非白名单输入设备一律不许生效”设备管理系统里就多了一个 preventps2 模块用 C# 把 PS/2 控制器、PS/2 键盘鼠标识别出来再按策略禁用或放行。麻烦的是 PS/2 不像 USB 走标准即插即用路径设备由 i8042prt 驱动在系统早期阶段接管枚举、禁用、恢复的姿势都和普通 USB 设备不同。接下来按设备枚举、策略落地、数据回传、验证验收这条链路讲思路可以直接搬到自己的上位机或终端管理项目里。2. C# 设备枚举用 WMI 和 SetupAPI 把 PS/2 设备抓全要禁用 PS/2第一步是稳定地找到它。常见做法是先用 WMI 做粗筛再用 SetupAPI配置管理器 API确认状态两层配合才不会漏掉厂商改过设备名的机器。2.1 先按设备类过滤再按 PNP ID 精确匹配WMI 里最直觉的查法是 Win32_Keyboard 和 Win32_PointingDevice但这两个类只返回“当前正常活动”的设备设备一旦被禁用就查不到做台账可以做管控不行。我一般直接查 Win32_PnPEntity把设备类固定在 Keyboard 和 Mouse 上这样禁用前后的设备都能枚举出来。using System.Management; var scope new ManagementScope(\\.\root\cimv2); var query new ObjectQuery( SELECT DeviceID, Name, PNPClass, Status FROM Win32_PnPEntity WHERE PNPClass Keyboard OR PNPClass Mouse); using var searcher new ManagementObjectSearcher(scope, query); foreach (ManagementBaseObject mo in searcher.Get()) { string pnpId mo[DeviceID]?.ToString() ?? ; string name mo[Name]?.ToString() ?? ; if (IsPs2Device(pnpId, name)) { Console.WriteLine(${pnpId}\t{name}\t{mo[Status]}); } } static bool IsPs2Device(string pnpId, string name) { return pnpId.Contains(PNP0303) // PS/2 键盘控制器 ACPI ID || pnpId.Contains(PNP0F) // PS/2 鼠标端口 ACPI ID || name.IndexOf(PS/2, StringComparison.OrdinalIgnoreCase) 0; }这里关键不是 Name而是 DeviceID 里的 PNP ID。ACPI 固件暴露出来的硬件 ID 改不了厂商最多改设备名所以按 PNP ID 匹配是最不容易误判的方式。下面的表是我在实际机器上见过的常见组合PNP ID含义设备管理器里常见名称PNP03038042 兼容 PS/2 键盘控制器Standard PS/2 KeyboardPNP0F13PS/2 兼容鼠标端口PS/2 Compatible MousePNP0F0E第二 PS/2 鼠标端口一体机上偶尔出现Name 过滤适合做展示层PNP ID 过滤适合做策略层。两台不同厂商的主机Name 可能一个叫“Standard PS/2 Keyboard”一个叫“OEM PS/2 Keyboard”但 DeviceID 里都带着 PNP0303。2.2 用 SetupAPI 读取设备状态和问题码WMI 返回的 Status 只有 OK/Error/Degraded 几档判断“是否被策略禁用”不准确。比如用 CM_Disable_DevNode 禁用设备后WMI 的 Status 可能仍然是 OK。正确做法是 P/Invoke 配置管理器 API它直接读系统的 devnode 树反映的是设备真实动态状态。using System.Runtime.InteropServices; using System.Text; internal static class CfgMgr { [DllImport(CfgMgr32.dll, EntryPoint CM_Locate_DevNodeW, CharSet CharSet.Unicode)] public static extern int CM_Locate_DevNode( out IntPtr pdnDevInst, string pDeviceID, int ulFlags); [DllImport(CfgMgr32.dll, EntryPoint CM_Get_DevNode_StatusW)] public static extern int CM_Get_DevNode_Status( out uint pulStatus, out uint pulProblemNumber, IntPtr dnDevInst, int ulFlags); [DllImport(CfgMgr32.dll)] public static extern int CM_Disable_DevNode(IntPtr dnDevInst, int ulFlags); [DllImport(CfgMgr32.dll)] public static extern int CM_Enable_DevNode(IntPtr dnDevInst, int ulFlags); } // 调用示例 const uint DN_STARTED 0x00000001; // 设备已启动 const uint DN_DRIVER_LOADED 0x00000002; // 驱动已加载 int hr CfgMgr.CM_Locate_DevNode(out IntPtr devInst, pnpId, 0); if (hr ! 0) { Console.WriteLine($设备不存在或不可定位错误码 0x{hr:X8}); return; } uint status, problem; hr CfgMgr.CM_Get_DevNode_Status(out status, out problem, devInst, 0); if (hr 0) { bool started (status DN_STARTED) ! 0; Console.WriteLine($已启动{started}, 已加载驱动{((status DN_DRIVER_LOADED) ! 0)}, 问题码{problem}); }参数说明CM_Locate_DevNode 的第三个参数 ulFlags 传 0 即可预留给系统扩展CM_Get_DevNode_Status 的输出参数 problem 是问题码启用正常的设备 problem 为 0被禁用的设备典型值是 22CM_PROB_DISABLED。返回的 devInst 由配置管理器管理不需要手动释放。这里有个隐蔽点Win32_PnPEntity 的 DeviceID 和 SetupAPI 的 device instance ID 是同一串字符串可以直接拿来用不需要做格式转换。2.3 清单绑定到 DataGridView 的最小刷新写法设备清单一般会绑到 DataGridView 上如果每次刷新都重新 new 一个 DataTable 再赋给 DataSource几百台终端轮流上报时 UI 会明显掉帧。我一般用 BindingList 做数据源后台线程采集主线程通过 BeginInvoke 回写。public sealed class DeviceInfo { public string InstanceId { get; set; } ; public string Name { get; set; } ; public bool Enabled { get; set; } } private readonly BindingListDeviceInfo _devices new(); // Form 构造函数里 dataGridView1.DataSource _devices; private async Task RefreshLoopAsync() { while (true) { var list await Task.Run(() QueryDevices()); BeginInvoke(new Action(() { _devices.Clear(); foreach (var d in list) { _devices.Add(d); } })); await Task.Delay(5000); } }用 BindingList 的好处是增量更新时 UI 只重绘变化行Clear 再 Add 全量刷新在这种场景下也够用。关键是 Task.Run 把 WMI 和 SetupAPI 的耗时查询挪出 UI 线程BeginInvoke 把结果切回主线程更新控件这是 C# WinForm 里避免 UI 刷新卡顿的基本盘。3. preventps2 落地C# 禁用 PS/2 设备的三种做法枚举之后就是禁用。这里要区分三件事禁用驱动服务、禁用设备实例、停用类驱动三者的生效时机、恢复成本完全不同。下面三种方式我都用过按推荐程度排序讲。3.1 方式一改 i8042prt 服务的 Start 值PS/2 控制器由 i8042prt.sys 在系统启动早期接管把这个服务的 Start 值改成 4驱动不会加载PS/2 设备直接不出现。这是启动级的做法适合“这台终端就是不允许接 PS/2”的固定策略。using Microsoft.Win32; const string servicePath SYSTEM\CurrentControlSet\Services\i8042prt; using (var key Registry.LocalMachine.OpenSubKey(servicePath, writable: true)) { if (key null) { Console.WriteLine(没有 i8042prt 服务说明这台机器没有 PS/2 控制器); return; } key.SetValue(Start, 4, RegistryValueKind.DWord); // 4 禁用 }参数说明Start 的常见取值是 0系统启动加载、1系统启动时加载、2自动、3手动、4禁用。PS/2 控制器必须在启动早期就绪所以改为 1 或 2 都行但禁用一定要填 4。这个方式重启才生效恢复时把 Start 改回 1 再重启。注意这个方案的边界新平台如果没有 i8042prt 服务这段代码会直接跳过部分笔记本触摸板走的是厂商自定义 PS/2 驱动改 i8042prt 拦不住。所以它适合老工控机批量预置不适合做动态策略。3.2 方式二用 CM_Disable_DevNode 即时禁用设备实例这是最常用的做法立即生效、不用重启回滚也简单。配置管理器下发禁用后设备管理器里会显示“此设备已禁用代码 22”。public static bool SetDeviceEnabled(string instanceId, bool enable) { int hr CfgMgr.CM_Locate_DevNode(out IntPtr devInst, instanceId, 0); if (hr ! 0) { return false; // 设备枚举不到可能已被拔出 } hr enable ? CfgMgr.CM_Enable_DevNode(devInst, 0) : CfgMgr.CM_Disable_DevNode(devInst, 0); return hr 0; // 返回 0 即 CR_SUCCESS }这个函数的入参就是 2.1 节拿到的 DeviceID。执行前要确认当前进程有管理员权限否则 CM_Disable_DevNode 会直接返回 ERROR_ACCESS_DENIED。判断权限可以这样做using System.Security.Principal; bool isAdmin new WindowsPrincipal(WindowsIdentity.GetCurrent()) .IsInRole(WindowsBuiltInRole.Administrator); if (!isAdmin) { throw new UnauthorizedAccessException(需要管理员权限执行设备禁用操作); }提示如果这台机器只有 PS/2 键盘禁用前务必确认有 USB 键盘或远程桌面通道。设备被禁用后本机键盘立刻停止响应误操作只能靠远程恢复。3.3 方式三三种方案的取舍还可以通过停掉 kbdclass 服务来让所有键盘失效但 kbdclass 是键盘类驱动USB 键盘、笔记本内置键盘也会被一起禁掉误伤面太大实际部署中不建议。方式生效时机恢复成本适用场景改 i8042prt Start 值重启后改回 1 再重启固定终端长期策略CM_Disable_DevNode立即调用 Enable 或设备管理器启用动态白名单、临时管控停 kbdclass 服务立即重装驱动或改服务强管控不推荐实际项目里我会组合使用CM_Disable_DevNode 做日常动态管控i8042prt 的 Start 值做重启后的兜底防止设备管理器里被手动启用后重启又回到禁用状态。4. 设备管理系统的数据链路串口、Modbus 与 UI 防卡顿preventps2 只是策略端完整系统还要把状态回传、把策略下发到每个终端。常见做法是管理端通过串口或 Socket 连接终端设备单机上位机场景下串口 Modbus RTU 用得最多。4.1 定义一帧业务数据命令字、长度、CRC16如果设备端是单片机通常不会直接跑 Modbus而是自定义帧。帧结构我在项目里习惯这样定字段长度说明帧头2 字节固定 0xAA55命令字1 字节0x01 查询 / 0x02 禁用 / 0x03 启用数据长度1 字节数据体字节数数据体N 字节设备实例 ID、策略值CRC162 字节从帧头到数据体的校验值CRC16 的 C# 实现如下多项式是 0xA001初始值 0xFFFF这是 Modbus CRC 的标准参数static ushort Crc16(byte[] buffer, int offset, int length) { ushort crc 0xFFFF; for (int i offset; i offset length; i) { crc ^ buffer[i]; for (int bit 0; bit 8; bit) { crc (crc 1) ! 0 ? (ushort)((crc 1) ^ 0xA001) : (ushort)(crc 1); } } return crc; }代码逻辑外层循环遍历每个字节内层循环处理 8 个 bit按位异或后做右移和多项式回退。发送端和接收端用同一套实现即可完成校验。如果走 Modbus 协议CRC 由库内部处理这段可以不用。4.2 用 SerialPort 与 NModbus4 读取设备保持寄存器设备端如果支持标准 ModbusPS/2 开关状态通常会映射到保持寄存器。用 NModbus4 读取的代码很短using Modbus.Device; using System.IO.Ports; using var port new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One); port.Open(); var master ModbusSerialMaster.CreateRtu(port); byte slaveAddress 1; // 从站地址 ushort startAddress 0; // 对应 40001 ushort numRegisters 4; // 读 4 个保持寄存器 ushort[] regs master.ReadHoldingRegisters(slaveAddress, startAddress, numRegisters); bool ps2KeyboardEnabled regs[0] ! 0; // 40001: PS/2 键盘策略 bool ps2MouseEnabled regs[1] ! 0; // 40002: PS/2 鼠标策略寄存器地址的含义要和设备点位表对应我一般把 40001 固定为 PS/2 键盘开关状态40002 固定为鼠标开关状态写进接口文档里。串口参数方面工业场景 9600 波特率最稳115200 在长线上容易出现误码除非线缆很短并且做了屏蔽。4.3 循环采集数据不卡 UITask.Run 与 BeginInvoke数据采集是长周期的如果直接在 Timer 事件里跑串口读UI 会被串口等待时间卡住。我的做法是定时器只负责触发真正耗时操作放进 Task.Run结果通过 BeginInvoke 回写。public sealed class DeviceMonitor : IDisposable { private readonly CancellationTokenSource _cts new(); private System.Windows.Forms.Timer? _timer; public void Start() { _timer new System.Windows.Forms.Timer { Interval 500 }; _timer.Tick async (_, _) await PollOnceAsync(); _timer.Start(); } private async Task PollOnceAsync() { var data await Task.Run(ReadFromDevice); dataGridView1.BeginInvoke(new Action(() { for (int i 0; i data.Length i dataGridView1.Rows.Count; i) { dataGridView1.Rows[i].Cells[colStatus].Value data[i]; } })); } }这里的关键是Timer 的 Tick 在 UI 线程触发但 ReadFromDevice 里的 SerialPort 读写发生在线程池线程UI 线程只做一次 BeginInvoke 回调更新单元格。BeginInvoke 是异步的不会等 UI 绘制完成才返回所以高频轮询也不会堆积卡顿。这是 WinForm 上位机开发里最实用的一套刷新模型。5. preventps2 生效后的验证技巧问题码 22 是金标准5.1 用 CM_Get_DevNode_Status 验证不是看“设备不存在”禁用后最容易犯的错是用“设备消失了”来判断成功。实际上 PS/2 设备禁用后实例仍然在只是启动标志被清掉、问题码变成 22。判断代码很简单uint status, problem 0; int hr CfgMgr.CM_Get_DevNode_Status(out status, out problem, devInst, 0); bool disabled (hr 0) (problem 22); // CM_PROB_DISABLED记住这个区分CM_Locate_DevNode 失败是设备真正不存在CM_Get_DevNode_Status 返回 problem22 才是策略禁用生效。验收脚本里只需要前者报错、后者写日志就能精确区分硬件故障和策略状态。5.2 回归脚本每 10 秒轮询防止被设备管理器重新启用设备管理器里可以手动启用被禁用的设备这对安全策略是漏洞。我在终端代理里做了一个守护轮询定时扫描白名单外的 PS/2 设备发现被启用就再次禁用并写入 Windows 事件日志。using System.Diagnostics; while (!stoppingToken.IsCancellationRequested) { foreach (string instanceId in GetPs2DeviceIds()) { if (!IsDeviceDisabled(instanceId)) { SetDeviceEnabled(instanceId, false); EventLog.WriteEntry(DeviceGuard, $PS/2 设备被重新启用已自动禁用: {instanceId}, EventLogEntryType.Warning, 1001); } } await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); }轮询参数我固定成三个常量PollInterval10sFailureRetry3LogChannelApplication。10 秒对人工操作足够敏感对终端 CPU 占用可以忽略连续失败 3 次才告警避免设备枚举抖动造成误报日志里记录 instanceId、oldStatus、newStatus 和执行时间配合 Windows 事件日志一起使用后续审计时能精确还原每次策略变更的链路。本文还有配套的精品资源点击获取
返回列表