用 Electron 和 Rust 实现真正的全局键盘事件拦截

起因

最近在用 electron 开发一个线上的考试应用, 有一个需求是客户端处于监考模式下要拦截用户的键盘事件. 这里的拦截是全局性的,但是 globalShortcut 很多行为都拦截不了, 比如快捷键锁屏 . 在 github 翻了一圈后,没有找到合适的方案 o(╥﹏╥)o,所以自己用 rust 写一个 lib,然后用 napi-rs 打包成 nodejs 库在 electron 中使用。下面记录下整个流程, 方便参考。

mac os

如果你不关心 mac, 可以跳过这一块, 直接看 win

开发 mac os 应用,需要使用 cocoa 库,mozilla 开发了一个 rust 版本的库core-foundation-rs

使用它的 core_graphicsCGEventTap 可以拦截键盘事件

构造 CGEventTap

rust 复制代码
use core_graphics::event::{
  CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType::KeyDown,
};

fn handle_key_event() {
  let cg_event_tap = CGEventTap::new(
    CGEventTapLocation::Session,
    // 插入队首
    CGEventTapPlacement::HeadInsertEventTap,
    // 默认行为过滤
    CGEventTapOptions::Default,
    // 监听的事件
    vec![CGEventType::KeyDown],
    callback,
  );
}

构造 CGEventTap 的 5 个参数依次为

  1. 监听事件的区域 直接选 CGEventTapLocation::Session 即可
  2. 插入队首还是队尾,当一个事件触发的时候,会依次调用回掉队列上面的回调函数,前面的回调函数可以阻止后面的回调函数触发,这里插入队首部,阻止后面的默认系统行为触发
  3. 只监听事件的触发还是可以阻止后面的行为触发 这里选择 Default
  4. 监听的事件,当前只监听键盘 KeyDown 事件
  5. 回调函数 callback

添加 callback 函数判断 event类型

event 类型需要通过判断当前的 CGEventType

rust 复制代码
use core_graphics::event::{
  CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,
};

fn handle_key_event() {
  let cg_event_tap = CGEventTap::new(
    CGEventTapLocation::Session,
    // 插入队首
    CGEventTapPlacement::HeadInsertEventTap,
    // 默认行为过滤
    CGEventTapOptions::Default,
    // 监听的事件
    vec![CGEventType::KeyDown],
    |proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
      CGEventType::KeyDown => {

        Some(event.clone())
      }
      _ => Some(event.clone()),
    },
  );
}

获取 key 类型

通过 get_integer_value_field 拿到 keycode

通过 event.get_flags() 判断 CGEventFlags

rust 复制代码
use core_graphics::event::{
  CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,
};

fn handle_key_event() {
  let cg_event_tap = CGEventTap::new(
    CGEventTapLocation::Session,
    // 插入队首
    CGEventTapPlacement::HeadInsertEventTap,
    // 默认行为过滤
    CGEventTapOptions::Default,
    // 监听的事件
    vec![CGEventType::KeyDown],
    |proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
      CGEventType::KeyDown => {
        let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);

        // 判断 control 键是否摁下
         let is_control_press =
          CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;

        Some(event.clone())
      }
      _ => Some(event.clone()),
    },
  );
}

禁用 control 键

设置 key eventNULL , 就可以禁用这个键,比如禁用 control

rust 复制代码
use core_graphics::event::{
  CGEvent, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType,EventField,CGEventFlags,
};

fn handle_key_event() {
  let cg_event_tap = CGEventTap::new(
    CGEventTapLocation::Session,
    // 插入队首
    CGEventTapPlacement::HeadInsertEventTap,
    // 默认行为过滤
    CGEventTapOptions::Default,
    // 监听的事件
    vec![CGEventType::KeyDown],
    |proxy: *const std::ffi::c_void, event_type: CGEventType, event: &CGEvent| match event_type {
      CGEventType::KeyDown => {
        let key = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);

        // 判断 control 键是否摁下
         let is_control_press =
          CGEventFlags::CGEventFlagControl & event.get_flags() == CGEventFlags::CGEventFlagControl;

        if is_control_press {
          // 禁用这个快捷键
          event.set_type(CGEventType::Null);
        }

        Some(event.clone())
      }
      _ => Some(event.clone()),
    },
  );
}

加入循环队列

上面的代码,还要加入事件循环中才能生效

rust 复制代码
use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop};

let current: CFRunLoop = CFRunLoop::get_current();

match cg_event_tap {
  Ok(tap) => unsafe {
    let loop_source = tap
      .mach_port
      .create_runloop_source(0)
      .expect("sames broken");

    current.add_source(&loop_source, kCFRunLoopCommonModes.clone());

    tap.enable();

    CFRunLoop::run_current();
  },
  Err(_) => panic!("can't prevent key event"),
}

在 mac os 需要在隐私和安全性里面打开对应程序的辅助功能,该程序才能生效!!!

上面的代码可以在 mac os 下禁用 control 键, 目前的 globalShortcut实现不了这个功能

win

windows 下面实现该功能实在有点复杂,除了写键盘的事件 hook, 还要修改用户的注册表

这里拦截键盘事件使用了一个第三方库rdev来实现,rdev本质上还是调用了 windows api SetWindowsHookExA 来实现的, 因为用 windows api 很麻烦, 为了省事, 这里直接用 rdev

微软官方维护了一个 windows-rscrate,也可以通过它来实现相应的功能

rdev 拦截键盘事件

具体使用 rdev grab 的代码就不再详细说了, 可以去看它的文档

rust 复制代码
rdev::grab(move |ev| match ev.event_type {
  EventType::KeyPress(key) => {
    // 在 should_restrict 里面编写你的拦截逻辑, 判断是否拦截
    if should_restrict(key) {
      None
    } else {
      Some(ev)
    }
  }
  _ => Some(ev),
});

修改注册表拦截一些特殊按键

rdev 可以拦截绝大部分 windows 的按键,但是有一些键很特殊,是不能通过写 hook 来拦截的,比如这三个键

  1. win + l 锁屏
  2. win + g 弹出 windows game
  3. ctrl + alt + delete CAD

上面三个键只能通过修改用户的注册表来实现拦截

修改注册表需要 windows 的管理员权限,请保证你的 electron app 具有管理员权限

win + l 的注册表位于 HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\SystemDisableLockWorkstation 改为 1 即可禁用锁屏

win + g 位于 HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\GameDVRAppCaptureEnabled 设置成 0 即可禁止

cad 位于 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\taskmgr.exeDebugger 设置成 Hotkey Disabled 即可禁止

如果是在 electron 里面直接使用 nodejs 的库 regedit 即可,使用 regedit 禁用 win + l 示例如下

typescript 复制代码
// 禁用 win + l 锁屏
import regeditOrigin from "regedit"

const regedit = regeditOrigin.promisified

async function main() {
  const winLRegisterPath =
    "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"

  try {
    await regedit.createKey([winLRegisterPath])
  } catch (error) {
    console.error(error)
  }

  await regedit.putValue({
    [winLRegisterPath]: {
      DisableLockWorkstation: {
        type: "REG_DWORD",
        value: 1,
      },
    },
  })
}

regedit 在调用的时候需要将 HKEY_CURRENT_USER 需要改成 HKCU

如法炮制即可禁用剩余的 win 快捷键

合并 mac 和 win 的逻辑

rust 里面可以使用条件编译, 有 cpp 经验的应该知道这个

rust 复制代码
fn prevent_keyboard_event() {
  #[cfg(target_os = "macos")]
  {
      // mac os 相关代码
  }
  #[cfg(target_os = "windows")]
  {
      // windows 相关代码
  }
  #[cfg(not(any(target_os = "macos", target_os = "windows")))]
  {
    // 其他平台
  }
}

使用 napi-rs 打包, 用 github actions 编译成 npm 库(本质上是 .node 文件), 便可在 electron 中直接调用, 实现霸屏功能的一部分.


欢迎各位在评论区友好讨论

真不好搞 😭

相关推荐
VinciYan3 小时前
Rust使用Actix-web和SeaORM库开发WebAPI通过Swagger UI查看接口文档
rust·api·web·orm
森叶4 小时前
Electron 安装包 asar 解压定位问题实战
前端·javascript·electron
白总Server6 小时前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
森叶11 小时前
Electron-vue asar 局部打包优化处理方案——绕开每次npm run build 超级慢的打包问题
vue.js·electron·npm
新知图书11 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
diygwcom12 小时前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan12 小时前
electron react离线使用monaco-editor
javascript·react.js·electron
许野平1 天前
Rust: Warp RESTful API 如何得到客户端IP?
tcp/ip·rust·restful·ip地址
许野平1 天前
Rust:Result 和 Error
开发语言·后端·rust·error·result
春蕾夏荷_7282977251 天前
electron nsis打包windows应用程序
javascript·windows·electron·nsis