
03-急停热键用RegisterHotKey造一颗随时能按的刹车作者黒漂技术佬系列ALSPD-DESK 键鼠注入与保命措施三前一篇解决了「注入坐标对不对」的问题。但还有个更吓人的问题没解决万一注入逻辑真的有 bugAgent 开始疯狂乱动你的鼠标键盘而你的鼠标键盘正被它接管你反而没法用它们去关掉 Agent——这就成了「自己把自己锁在门外」。本项目的开发机就是被控机这种「夺走自己的键鼠」不是耸人听闻是真实会发生的风险。所以第一版就强制带上了一颗「随时能按的刹车」——急停热键。本文就讲这颗刹车是怎么造出来的。一、为什么非得有急停先把这个风险的画面感立起来。你在被控机上跑着 Agent 写代码远程那端或者一个 bug触发了一连串键鼠注入光标开始满屏乱飞、键盘开始乱敲。你慌了想赶紧去任务管理器结束进程——但你的鼠标移动指令和点击指令正源源不断地被 Agent 的注入覆盖掉。你越动它越乱你越想关它越接管。普通程序崩了大不了 CtrlAltDel 叫出任务管理器。但「注入」这种东西一旦失控连 CtrlAltDel 的按键都可能被你的注入逻辑「抢答」。所以必须有一套不依赖被控机正常键鼠、且优先级极高的紧急制动机制。这就是急停热键存在的全部理由。二、为什么选 RegisterHotKey而不是键盘钩子Windows 给「全局热键」提供了两种思路新手很容易选错。一种是键盘钩子SetWindowsHookEx 挂 WH_KEYBOARD_LL它能拦截、甚至吞掉系统里每一次按键功能很猛。但代价也大它要往系统消息链里插一脚对系统行为影响明显它会「拦输入」杀软对这种全局钩子天然敏感容易误报钩子逻辑一旦写崩可能把整个系统的键盘输入都搞挂。另一种是RegisterHotKey它只是「向系统登记一个组合键」声明「这个键按下时通知我一下」。系统只在那个特定组合键被按下的瞬间回调你一次不拦截任何输入、不装钩子、对系统影响最小杀软也不敏感。对于「急停」这种用途我们要的恰恰是「最小侵入 最高可靠」。我们不需要拦截键盘只需要一个在失控时能被系统 reliably 叫醒的开关。所以项目源码选了 RegisterHotKey并把它封装成PanicHotkey类。源码开头的注释写得很直白不装键盘钩子、不拦输入对系统影响最小AV 也不敏感。三、独立线程 消息循环等那一声 WM_HOTKEYRegisterHotKey 注册之后系统会在该组合键被按下时往注册时所在线程的消息队列里丢一条WM_HOTKEY。所以要拿到这个通知必须有一个线程在跑 Windows 消息循环。项目源码的做法是急停热键跑在一个独立的守护线程里专门GetMessageW循环等消息def_run(self)-None:kernel32ctypes.WinDLL(kernel32,use_last_errorTrue)self._thread_idkernel32.GetCurrentThreadId()hotkey_id1okuser32.RegisterHotKey(None,hotkey_id,self.modifiers|MOD_NOREPEAT,self.vk)ifnotok:errctypes.get_last_error()self.log([急停] ⚠️ 热键 %s 注册失败错误码 %d—— 可能已被其他程序占用。请改用别的组合或依赖「空闲自动断连」兜底。%(self.spec,err))self.registeredFalsereturnself.registeredTruemsgMSG()try:whileself._runninganduser32.GetMessageW(ctypes.byref(msg),None,0,0)0:ifmsg.messageWM_HOTKEY:self.log([急停] 热键被按下 —— 立即停止注入)try:self.callback()exceptExceptionase:self.log([急停] 回调出错%s: %s%(type(e).__name__,e))breakfinally:try:user32.UnregisterHotKey(None,hotkey_id)exceptException:pass几个值得停下理解的细节MOD_NOREPEAT这个标志让热键「按一次只触发一次」即使你长按不放也不会反复回调。急停场景下一次就够重复触发没必要。独立守护线程消息循环放在自己的线程里不阻塞主流程daemonTrue保证主程序退出时它不会吊着进程。注册失败明确报错并给错误码如果组合键被别的程序占用了比如某些全局快捷键工具RegisterHotKey会失败get_last_error()拿到错误码代码会明确打日志告诉你失败和原因而不是静默失效——静默失效意味着你以为有急停、实际没有危急时刻才发现刹车是空的那就太晚了。退出时finally里一定会UnregisterHotKey注销避免热键一直被占用。四、parse_hotkey把 “ctrlaltshiftq” 解析成系统能懂的码热键在配置里是一串人读得懂的字符串比如默认的ctrlaltshiftq。项目源码用parse_hotkey把它拆成(modifiers, vk)defparse_hotkey(spec:str)-Tuple[int,int]:mods0vkNoneforpartin(specor).lower().replace( ,).split():ifnotpart:continueifpartin(ctrl,control):mods|MOD_CONTROLelifpartalt:mods|MOD_ALTelifpartshift:mods|MOD_SHIFTelifpartin(win,super,meta):mods|MOD_WINelifpartin_HOTKEY_NAMES:vk_HOTKEY_NAMES[part]# 特殊键方向键、F1~F24、esc 等eliflen(part)1:vkord(part.upper())# 单字母/数字else:raiseValueError(无法识别的热键组成%r%part)ifvkisNone:raiseValueError(热键缺少主键%r%spec)returnmods,vk它支持的组合键ctrl/control、alt、shift、win/super/meta。它支持的主键很广单个字母、数字F1~F24以及esc、tab、space、enter、backspace、delete、insert、home、end、pageup、pagedown、四个方向键、pause、scrolllock、printscreen。也就是说你可以很自由地改。默认的ctrlaltshiftq之所以选这么「重」的组合就是为了防止误触——三键齐按才触发正常打字不可能撞上。如果你发现它和别的软件冲突改个panic_hotkey比如ctrlaltshiftf12再重启即可。五、按下之后做三件事热键被按下的瞬间回调callback在热键线程里执行。项目里它指向_on_panic做的事很干脆——三连def_on_panic(self)-None:self.log([保命] 急停停止注入、松开按键、停止 Agent需手动重启才能再用)ifself._injectorisnotNone:self._injector.pause(急停热键被按下)self._injector.release_all()# 松开所有按键避免远端卡键self._panic.set()self.stop_flag.set()# 彻底停止 Agent暂停注入先把注入器pause掉后续任何键鼠事件都会被拒。松开所有按键调用release_all()把可能还按在半路上的鼠标键和修饰键统统抬起避免「卡住的按键」留在远端这个机制我们后面会细讲。彻底停止 Agent设stop_flag让主循环退出进程结束。六、为什么刻意连「重连」也一起停掉这里有个反直觉的设计决定值得单独拿出来说。Agent 正常是有「断线自动重连」能力的——Viewer 一掉线它会退避几秒重新连回去。那么「急停」如果只是断开当前会话会发生什么断开 → 自动重连 → Viewer如果还开着一回来 → 注入又恢复了。那急停就完全失去意义了。你按了急停以为安全了结果几秒后注入自己又活过来了你又被接管。所以这个「刹车」必须是彻底停车连重连逻辑一起停掉进程直接退出。急停之后再要用必须手动重新启动 Agent——这是有意为之的「摩擦」确保恢复控制权是人为、清醒的决定而不是系统的自动行为。七、stop()用 PostThreadMessageW(WM_QUIT) 收尾热键线程自己跑着消息循环。当我们要主动停掉整个 Agent比如正常退出、或测试收场时得有个办法让那个GetMessageW循环优雅地退出来。直接TerminateThread太粗暴会漏掉finally里的注销。项目源码用PostThreadMessageW往热键线程丢一条WM_QUITdefstop(self)-None:self._runningFalseifIS_WINandself._thread_id:try:ctypes.WinDLL(user32).PostThreadMessageW(self._thread_id,WM_QUIT,0,0)exceptException:passGetMessageW收到WM_QUIT会返回 0不再大于 0循环自然结束进入finally注销热键。干净利落不留尾巴。八、手动验证急停热键的步骤自动化测试再全也替你按不了真实的物理按键——所以急停热键的「真实按键」必须你亲自验一次。项目给的手动验证清单大致是这样注意本文不写具体启动命令按你本地的运行方式即可先只读验证保持allow_input false在 Viewer 里乱动鼠标键盘确认本机鼠标键盘完全没反应只读模式不创建注入器后面一篇会讲。开急停、真实注入开启注入后一启动就立刻按一次CtrlAltShiftQ确认 Agent 终端打印急停日志、且进程直接退出不会自动重连。这一步过了你就有了一颗随时能按的刹车。确认好使之后再做真实注入先把光标移过去再移回来这种无害可逆的小动作逐步验证移动/点击/键盘。如果提示「热键注册失败」说明组合被占用改panic_hotkey换一个再重启即可。九、小结急停热键是整套保命设计里最硬的一颗刹车背景风险真实存在——开发机就是被控机bug 会夺走自己的键鼠选RegisterHotKey而非键盘钩子不装钩子、不拦输入、对系统影响最小、杀软不敏感独立线程跑GetMessageW消息循环等WM_HOTKEY注册失败明确报错并给错误码parse_hotkey支持丰富的组合键与主键默认用三键重组合防误触按下后三件事暂停注入 → 松开所有按键 → 彻底停止 Agent刻意连重连也停掉否则自动重连会让急停形同虚设恢复需手动重启stop()用PostThreadMessageW(WM_QUIT)优雅收尾。急停是「出了事能立刻停车」。但最好的保命是让它根本没机会出事——所以在急停之外项目还布了几道「平时就兜着」的兜底只读模式、空闲自动断连、断线释放按键。它们和急停一起构成纵深防御。