You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js监听SIG信号时createReadStream致process.exit()挂起

解决Node.js监听/dev/input设备时自定义信号监听器导致的进程无法退出问题

我之前也碰到过类似的坑——当处理/dev下的字符设备文件时,Node.js的ReadStream会陷入阻塞式系统调用,单纯调用stream.close()或者fs.closeSync()根本没法中断这个调用,直接导致事件循环卡壳,进程死活退不出去。结合你的代码,这里有几个关键修复点:

核心修复步骤

  • 用stream.destroy()替代stream.close():它能直接中断底层的阻塞读取,比close()的清理更彻底
  • 清除定时任务:你的setInterval会让Node.js事件循环一直保持活跃,必须手动清除
  • 统一资源清理逻辑:把设备断开、进程退出时的资源释放抽成通用方法,避免遗漏
  • 给事件循环留缓冲时间:确保流销毁、文件描述符关闭的操作完成后再退出进程

修改后的完整代码

const fs = require('fs');
let intervalId; // 保存定时器ID,方便后续关闭

let monitoredDevices = {
    devices: [],
    addDevice: function(path) {
        fs.open(path, 'r', (e, fd) => {
            if(e) return;
            let dev = { path: path, fd: fd, stream: null};
            dev.stream = fs.createReadStream(null, { fd: dev.fd, mode: 'r'});
            const deviceIndex = this.devices.push(dev) - 1; // 修正索引,确保splice能正确删除
            
            if(dev.stream) {
                dev.stream.on('data', (data) => {
                    console.log('DATA('+dev.path+'):', data);
                });
                dev.stream.on('error', (e) => {
                    if(e.code == 'ENODEV') {
                        // 设备断开时自动清理
                        this.cleanupDevice(dev, deviceIndex);
                    }
                });
            }
        });
    },
    hasDevice: function(path) {
        return this.devices.some(x => x.path === path);
    },
    // 新增统一的设备清理方法,复用逻辑
    cleanupDevice: function(dev, index) {
        // 先销毁流,中断底层阻塞调用
        if(dev.stream) {
            dev.stream.destroy();
            dev.stream = null;
        }
        // 关闭文件描述符,容错处理避免报错
        if(dev.fd !== null) {
            try {
                fs.closeSync(dev.fd);
            } catch (err) {
                console.error(`关闭设备${dev.path}的文件描述符失败:`, err);
            }
            dev.fd = null;
        }
        // 从设备列表中移除并清理对象
        if(index !== undefined && index >= 0) {
            this.devices.splice(index, 1);
        }
        delete dev;
    }
};

// 保存定时器ID,方便退出时清除
intervalId = setInterval(function() {
    fs.readdir('/dev/input/by-id', (err, files) => {
        if(err) return;
        files.forEach(file => {
            const fullPath = '/dev/input/by-id/' + file;
            try {
                if(fs.lstatSync(fullPath).isDirectory()) return;
                if(monitoredDevices.hasDevice(fullPath)) return;
                monitoredDevices.addDevice(fullPath);
                console.log('EVENT:', file);
            } catch (statErr) {
                console.error(`检查设备文件${file}失败:`, statErr);
            }
        });
    });
}, 500);

let _shutdownInProcess = false;
function exitHandler(signal='') {
    if(!_shutdownInProcess) {
        _shutdownInProcess = true;
        console.log('\033[31m 捕获退出信号 ' + signal + ', 正在清理资源... \x1b[0m');
        
        // 第一步:清除定时器,让事件循环有机会结束
        clearInterval(intervalId);
        
        // 第二步:批量清理所有监听的设备
        monitoredDevices.devices.forEach((dev, index) => {
            console.log(`正在关闭设备: ${dev.path} (文件描述符: ${dev.fd})`);
            monitoredDevices.cleanupDevice(dev, index);
        });
        
        // 第三步:给事件循环留100ms处理清理回调,再强制退出
        setTimeout(() => {
            process.exit(0);
        }, 100);
    }
}

// 保留你的信号监听器
process.on('SIGINT', () => exitHandler('SIGINT'));
process.on('SIGUSR1', () => exitHandler('SIGUSR1'));
process.on('SIGUSR2', () => exitHandler('SIGUSR2'));
process.on('SIGTERM', () => exitHandler('SIGTERM'));

关键修复点说明

  1. stream.destroy()的必要性
    /dev/input下的设备是字符设备,Node.js读取这类设备时会发起阻塞的read()系统调用。close()只是关闭流的上层接口,无法中断底层的系统调用;而destroy()会直接终止这个调用,彻底释放相关资源。

  2. 清除定时器的作用
    Node.js的事件循环会因为存在活跃的定时器而持续运行,必须用clearInterval移除定时任务,否则即使所有设备资源都清理了,进程还是会卡在事件循环里。

  3. 统一清理逻辑的好处
    把设备断开和进程退出的清理逻辑抽成通用方法,既能避免重复代码,也能确保两种场景下的资源释放逻辑一致,不会出现遗漏。

  4. 延迟退出的原因
    流的销毁、文件描述符的关闭都是异步操作,给100ms的缓冲时间能让这些操作完成,避免进程在资源还没完全释放时就强制退出。

这样修改后,你按下CTRL+C或者执行killall node时,进程应该能正常退出,不会再出现终端挂起需要kill -9才能结束的情况。

内容的提问来源于stack exchange,提问作者AJ Cole

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:45:07