键盘组合键监听与 xterm 唤醒程序

键盘组合键监听与 xterm 唤醒程序

一、程序简介

本程序用于在 Linux 系统上全局监听键盘输入设备 ,当检测到用户按下组合键 Ctrl + Alt + T 时:

  1. 检查当前系统中是否已经存在正在运行的 xterm 进程;
  2. 如果没有运行中的 xterm,则启动一个新的 xterm
  3. 如果已经有 xterm 在运行,则不再重复启动,只输出提示信息。

程序通过直接监听 /dev/input/event* 设备,而不是依赖桌面环境或窗口管理器,因此可以在较低层次捕捉键盘事件,适合:

  • 简化桌面环境或无桌面环境(headless)场景;
  • 嵌入式系统;
  • 自定义系统级快捷键控制。

二、工作原理

1. 设备扫描

程序启动后会:

  1. 打开目录 /dev/input

  2. 遍历目录项,通过文件名中是否包含 "event" 筛选出所有输入事件设备节点,例如:

    • /dev/input/event0
    • /dev/input/event1
    • ...
  3. 对每个 event 设备路径进行记录和去重,避免重复创建监听线程。

2. 设备能力过滤(只监听有键盘按键的设备)

对每一个找到的 event 设备,程序会:

  1. open("/dev/input/eventX", O_RDONLY) 打开设备;
  2. 使用 ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), evbit) 读取该设备支持的事件类型位图
  3. 检查事件类型中是否包含 EV_KEY
    • 如果不支持 EV_KEY ,则认为该设备不产生键盘按键事件(例如鼠标、触摸板、LED 控制等),关闭并跳过;
    • 如果支持 EV_KEY,则认为该设备具备键盘功能,为其创建一个监听线程。

这样可以减少误监听非键盘类设备,避免无关输入导致混乱。

3. 多线程监听模型

  • 每个支持 EV_KEY 的设备会对应一个独立的监听线程:

    • 线程函数通过 read(fd, &ev, sizeof(struct input_event)) 持续读取输入事件;
    • 每个线程只处理来自该设备的事件;
    • 线程内部维护三个按键(Ctrl、Alt、T)的当前状态。
  • 主线程负责:

    • 扫描设备;
    • 创建监听线程;
    • 在程序退出前,pthread_join 等待所有线程结束;
    • 释放用于路径存储的动态内存。

4. 键状态与组合键检测

每个监听线程内部:

  1. 定义三个状态变量:

    c 复制代码
    int ctrl = 0, alt = 0, t = 0;
  2. 对于每个 EV_KEY 事件:

    • 根据 ev.code 判断是哪一个键:
      • 29 对应左 Ctrl(KEY_LEFTCTRL);
      • 56 对应左 Alt(KEY_LEFTALT);
      • 20 对应字母 TKEY_T)。
    • 根据 ev.value 更新状态:
      • 1:按下(press) → 状态设为 1;
      • 0:松开(release) → 状态设为 0;
      • 2:自动重复(repeat) → 一般保持原状态不变。
  3. 当三个状态同时为 1 时,判定为组合键 Ctrl + Alt + T 被按下:

    c 复制代码
    if (ctrl && alt && t) {
        // 触发组合键逻辑
    }
  4. 为避免长按 T 导致重复触发,本程序在触发逻辑后,会简单地将 t 状态清零一次:

    c 复制代码
    t = 0;

由于每个 /dev/input/eventX 都由独立线程监听、独立维护状态,因此组合键的判断是在单个设备内部完成的 。如果系统有多个键盘,每个键盘上的 Ctrl + Alt + T 都可以独立触发行为。

5. 启动前检查 xterm 是否已运行

组合键被检测到后,程序会:

  1. 调用 is_xterm_running(),该函数内部使用:

    bash 复制代码
    pgrep xterm
    • 有输出 → 认为已有 xterm 进程在运行;
    • 无输出 → 认为当前没有 xterm 在运行。
  2. 根据检测结果:

    • 如果没有运行中的 xterm

      c 复制代码
      system("xterm &");

      启动新的 xterm

    • 如果已有运行中的 xterm

      • 输出一条日志提示,不再重复启动。

三、编译方法

确保系统已安装 gcc 以及 Linux 相关头文件(一般在 linux-headers 或内核头包中)。

假设源码文件名为 main.c

bash 复制代码
gcc -o key_listener main.c -lpthread

参数说明:

  • -lpthread:链接 POSIX 线程库以支持多线程。

四、运行说明

1. 权限要求

直接访问 /dev/input/event* 通常需要较高的权限。常见方式:

  • 使用 root 用户运行:

    bash 复制代码
    sudo ./key_listener
  • 或者将当前用户加入具有访问 /dev/input/event* 权限的组(常见为 input 组),再以该用户运行。

如果运行时看到:

text 复制代码
open: Permission denied

则说明权限不足,需要提升权限或修改设备文件权限/所属组。

2. 程序启动后行为

启动程序后,终端中会看到类似输出:

text 复制代码
Listening to keyboard-capable device: /dev/input/event3
Listening to keyboard-capable device: /dev/input/event5
Device /dev/input/event2 does NOT support EV_KEY, skipping.
...

说明:

  • 某些 event 设备被认定为支持键盘事件(EV_KEY),程序为它们启动了监听线程;
  • 某些则被过滤掉,不会参与组合键检测。

程序将持续运行,等待键盘输入事件。


五、测试方法

1. 确认设备监听是否正常

  1. 启动程序:

    bash 复制代码
    sudo ./key_listener
  2. 检查终端输出,确认至少有一行类似:

    text 复制代码
    Listening to keyboard-capable device: /dev/input/eventX
  3. 如果一个都没有:

    • 检查系统是否通过 /dev/input/event* 暴露键盘;
    • 检查运行用户是否有权限访问这些设备。

2. 组合键触发测试

  1. 保证当前没有 xterm 在运行:

    bash 复制代码
    pkill xterm 2>/dev/null
  2. 在任意终端(或者桌面环境中)按下 Ctrl + Alt + T

  3. 在运行程序的终端中应看到类似输出:

    text 复制代码
    [/dev/input/event3] CTRL key event: value=1, state=1
    [/dev/input/event3] ALT key event: value=1, state=1
    [/dev/input/event3] T key event: value=1, state=1
    Ctrl + Alt + T detected on device /dev/input/event3, attempting to launch xterm...
    No xterm running. Starting xterm...
  4. 此时应弹出一个新的 xterm 窗口。

3. 已有 xterm 时的行为

  1. 保持至少一个 xterm 已打开;

  2. 再次按 Ctrl + Alt + T

  3. 程序输出应类似:

    text 复制代码
    Ctrl + Alt + T detected on device /dev/input/event3, attempting to launch xterm...
    xterm is already running. Skipping launch.
  4. 不应看到额外的新 xterm 被启动。

4. 非键盘设备测试(误触发检查)

  1. 使用鼠标、触摸板等设备进行操作;

  2. 程序在启动时对这些不支持 EV_KEY 的设备会输出:

    text 复制代码
    Device /dev/input/event2 does NOT support EV_KEY, skipping.
  3. 操作这些设备时,不应该引发组合键触发或导致奇怪的输出。

5. 多键盘场景测试(可选)

如果系统有多个键盘:

  1. 启动程序后,可能会看到多个:

    text 复制代码
    Listening to keyboard-capable device: /dev/input/eventX
  2. 在任一键盘上按 Ctrl + Alt + T,均应可触发 xterm 启动逻辑;

  3. 日志中会显示是哪个 /dev/input/eventX 触发了组合键。

6. 长时间稳定性测试(可选)

  1. 将程序放在 screentmux 中长时间运行;
  2. 观察是否有:
    • CPU 占用异常;
    • 意外退出;
    • 无响应 / 卡死等情况。

当前版本对运行中新增/拔除设备(热插拔)未做动态管理,仅在启动时扫描一次设备。


六、注意事项与扩展建议

1. 安全性

当前使用:

c 复制代码
system("xterm &");

在信任环境下较为方便,但在更严格的安全环境中,建议:

  • 改用 fork() + execlp("xterm", "xterm", NULL) 方式启动;
  • 避免通过字符串拼接构造命令,防止潜在命令注入风险。

2. 更精确地识别"键盘设备"

目前只基于"是否支持 EV_KEY"判断设备是否为"键盘能力设备"。在一些系统上,遥控器、多媒体键等设备也可能有 EV_KEY

如需只监听真正意义上的键盘,可以在后续版本中增加:

  • 读取 /sys/class/input/eventX/device/name 获取设备名称;
  • 对名称进行过滤,例如:
    • 包含 "keyboard"
    • 或者在白名单(某些厂商/型号)中。

3. 支持更多快捷键/更多程序

可以将当前硬编码的:

c 复制代码
#define CTRL_KEY_CODE 29
#define ALT_KEY_CODE 56
#define T_KEY_CODE 20

扩展为:

  • 可配置(从配置文件或命令行读取);
  • 支持多组快捷键 → 程序映射关系(例如 Ctrl+Alt+F 打开浏览器等)。

4. 动态设备管理(热插拔)

当前程序只在启动时扫描 /dev/input/event*,运行中若有键盘插拔:

  • 新插入的键盘不会被自动监听;
  • 已拔掉的键盘对应的线程会在 read 出错后退出。

后续可考虑:

  • 使用 inotifyudev 事件监控 /dev/input
  • 当检测到新设备节点创建时,动态启动新的监听线程。

七、故障排查

1. 没有任何设备被监听

现象:

text 复制代码
(no "Listening to keyboard-capable device" output)

可能原因与解决:

  1. 无权限访问 /dev/input/event*
    • 解决:使用 sudo 运行,或加入对应权限组。
  2. 键盘未通过 /dev/input 暴露(某些虚拟环境或特殊系统)
    • 解决:检��� cat /proc/bus/input/devices 确认键盘设备信息,再针对性调整代码。

2. 按下 Ctrl+Alt+T 没有反应

排查步骤:

  1. 检查程序终端是否有键值输出:
    • 若连 CTRL key event 之类的日志都没有,说明键盘事件没有到达该设备或权限问题;
  2. 检查 pgrep xterm 是否可在 Shell 中正常执行;
  3. 确认系统已安装 xterm,并可从终端直接执行 xterm

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <string.h>
#include <dirent.h>
#include <pthread.h>
#include <errno.h>
#include <sys/ioctl.h>

#define CTRL_KEY_CODE 29    // KEY_LEFTCTRL
#define ALT_KEY_CODE 56     // KEY_LEFTALT
#define T_KEY_CODE 20       // KEY_T

// 检查是否同时按下 Ctrl + Alt + T
static inline int check_key_state(int ctrl, int alt, int t) {
    return (ctrl && alt && t);
}

// 检查 xterm 是否在运行
int is_xterm_running() {
    FILE *fp = popen("pgrep xterm", "r");
    if (fp == NULL) {
        perror("popen");
        return 0;
    }

    char buffer[128];
    int running = 0;
    if (fgets(buffer, sizeof(buffer), fp) != NULL) {
        running = 1;
    }

    pclose(fp);
    return running;
}

// 启动程序(这里是 xterm)
void start_program() {
    if (!is_xterm_running()) {
        printf("No xterm running. Starting xterm...\n");
        system("xterm &");
    } else {
        printf("xterm is already running. Skipping launch.\n");
    }
}

// 判断设备是否支持键盘按键(EV_KEY)
int device_supports_keys(int fd) {
    unsigned long evbit[(EV_MAX + 7) / 8] = {0};

    // 获取支持的事件类型位图
    if (ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), evbit) < 0) {
        perror("ioctl EVIOCGBIT");
        return 0;
    }

    int bit = EV_KEY;
    int idx = bit / (sizeof(unsigned long) * 8);
    int shift = bit % (sizeof(unsigned long) * 8);

    int supported = (evbit[idx] & (1UL << shift)) != 0;
    return supported;
}

void *listen_device(void *arg) {
    char *path = (char *)arg;       // 动态分配的字符串
    int ctrl = 0, alt = 0, t = 0;
    int fd;
    struct input_event ev;

    fd = open(path, O_RDONLY);
    if (fd == -1) {
        perror("open");
        free(path);
        return NULL;
    }

    // 只监听支持键盘按键的设备
    if (!device_supports_keys(fd)) {
        printf("Device %s does NOT support EV_KEY, skipping.\n", path);
        close(fd);
        free(path);
        return NULL;
    }

    printf("Listening to keyboard-capable device: %s\n", path);

    // 持续监听事件
    while (1) {
        ssize_t n = read(fd, &ev, sizeof(struct input_event));
        if (n < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("read");
            break;
        }
        if (n != sizeof(struct input_event)) {
            // 读到不完整事件,忽略/继续
            continue;
        }

        if (ev.type == EV_KEY) {
            // value: 0=release, 1=press, 2=autorepeat
            // 我们只在 press(1) 时设置为1,release(0) 设置为0
            if (ev.code == CTRL_KEY_CODE) {
                ctrl = (ev.value == 1) ? 1 : (ev.value == 0 ? 0 : ctrl);
                printf("[%s] CTRL key event: value=%d, state=%d\n", path, ev.value, ctrl);
            } else if (ev.code == ALT_KEY_CODE) {
                alt = (ev.value == 1) ? 1 : (ev.value == 0 ? 0 : alt);
                printf("[%s] ALT key event: value=%d, state=%d\n", path, ev.value, alt);
            } else if (ev.code == T_KEY_CODE) {
                t = (ev.value == 1) ? 1 : (ev.value == 0 ? 0 : t);
                printf("[%s] T key event: value=%d, state=%d\n", path, ev.value, t);
            }

            if (check_key_state(ctrl, alt, t)) {
                printf("Ctrl + Alt + T detected on device %s, attempting to launch xterm...\n", path);
                start_program();

                // 防止长按 T 重复触发,可根据需求调整策略
                t = 0;
            }
        }
    }

    close(fd);
    free(path);
    return NULL;
}

// 检查设备是否已经添加过
int is_device_already_added(const char *path, char *dev_paths[], int dev_count) {
    for (int i = 0; i < dev_count; i++) {
        if (strcmp(dev_paths[i], path) == 0) {
            return 1;
        }
    }
    return 0;
}

int main() {
    struct dirent *entry;
    DIR *dp = opendir("/dev/input");
    if (dp == NULL) {
        perror("opendir");
        exit(EXIT_FAILURE);
    }

    pthread_t threads[128];
    int thread_count = 0;

    char *dev_paths[128];
    int dev_count = 0;

    while ((entry = readdir(dp)) != NULL) {
        if (strstr(entry->d_name, "event") != NULL) {
            char path[256];
            snprintf(path, sizeof(path), "/dev/input/%s", entry->d_name);

            if (!is_device_already_added(path, dev_paths, dev_count)) {
                // 记录设备路径(用于去重),这里给 main 自己保存一份
                dev_paths[dev_count] = strdup(path);
                if (!dev_paths[dev_count]) {
                    perror("strdup");
                    continue;
                }
                dev_count++;

                // 线程参数也要用独立的 strdup,避免使用栈上的 path
                char *thread_path = strdup(path);
                if (!thread_path) {
                    perror("strdup for thread_path");
                    continue;
                }

                if (pthread_create(&threads[thread_count], NULL, listen_device, (void *)thread_path) != 0) {
                    perror("pthread_create");
                    free(thread_path);
                    continue;
                }
                thread_count++;

                if (thread_count >= 128) {
                    fprintf(stderr, "Too many devices, stopping at 128.\n");
                    break;
                }
            }
        }
    }

    closedir(dp);

    for (int i = 0; i < thread_count; i++) {
        pthread_join(threads[i], NULL);
    }

    for (int i = 0; i < dev_count; i++) {
        free(dev_paths[i]);
    }

    return 0;
}
相关推荐
小张成长计划..18 小时前
【linux】2:linux权限的概念
linux·运维·服务器
马踏岛国赏樱花18 小时前
Windows与Ubuntu双系统,挂载D/E盘到Ubuntu下时只能读的问题
linux·windows·ubuntu
ben9518chen18 小时前
Linux操作系统基本使用
linux·运维·服务器
一个平凡而乐于分享的小比特18 小时前
CPU上电启动到程序运行全流程详解
linux·uboot·根文件系统·cpu上电到启动
以太浮标18 小时前
华为eNSP模拟器综合实验之- HRP(华为冗余协议)双机热备
运维·网络·华为·信息与通信
慧一居士18 小时前
Gitea和GitLab对比
运维·gitlab·gitea
AI科技星18 小时前
引力与电磁的动力学耦合:变化磁场产生引力场与电场方程的第一性原理推导、验证与统一性意义
服务器·人工智能·科技·线性代数·算法·机器学习·生活
不像程序员的程序媛18 小时前
Linux开机自启动systemd配置
linux·运维·服务器
GREGGXU18 小时前
Could not load the Qt platform plugin “xcb“ in ““ even though it was found.
linux·qt