标签: child_process

  • 捕获 promisify  `child_process.exec` 的错误

    捕获 promisify `child_process.exec` 的错误

    这个东西文档里没写清楚,所以写篇博客记一下。

    在 Node.js 里,我们可以使用 child_process 下的命令分裂出一个进程,执行其他的命令。这些命令包括 execexecFilespawn

    我比较常使用 execspawn。前者用起来比较方便,只要传入完整的命令和参数,很接近日常命令行体验;后者传参要麻烦一些,不过可以实时获取输出,包括 stdoutstderr,比较方便给用户及时反馈。

    下面贴一下文档里的例子,spawn 的使用将来有机会再说。

    const { exec } = require('child_process');
    exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
      if (error) {
        console.error(`exec error: ${error}`);
        return;
      }
      console.log(`stdout: ${stdout}`);
      console.error(`stderr: ${stderr}`);
    });

    Node.js 8 之后,我们可以用 util.promisify() 命令将 exec 转换为 Promise 风格的方法,即不再需要 callback 函数,而是返回 Promise 实例,方便我们链式调用或者使用 Async function。

    此时,它的样子是这样的:

    const util = require('util');
    const exec = util.promisify(require('child_process').exec);
    
    async function lsExample() {
      const { stdout, stderr } = await exec('ls');
      console.log('stdout:', stdout);
      console.error('stderr:', stderr);
    }
    lsExample();

    官方文档没解释清楚错误处理,经过我的尝试,是这样的:

    1. 命令发生错误,或者被意外中断都会引发错误
    2. 如果不出错,就会正确返回 stdoutstderr
    3. 否则,错误实例里会包含 stdoutstderrcode
    4. 1~127 是各种错误编码,128+ 则是 signal + 127 的结果,通常来说是受控于我们的操作。比如使用 FFmpeg 录屏的时候,结束录制就要 Ctrl+C,此时就会得到值为 128 的 code。所以此时很可能不是真的失败。
    const util = require('util');
    const exec = util.promisify(require('child_process').exec);
    
    (async () => {
      let code, stdout, stderr;
      try {
        ({stdout, stderr} = await exec('ls'));
      } catch (e) {
        ({code, stdout, stderr} = e);
      }
    
      if (code && code > 127) {
        // 确实失败了
      }
      console.log('stdout:', stdout);
      console.log('stderr:', stderr);
    })();
  • 使用 Node.js 驱动 FFMPEG  录屏

    使用 Node.js 驱动 FFMPEG 录屏

    FFMPEG 功能非常强大,不仅能转换视频格式、压缩视频、添加字幕等,还能录制屏幕内容。使用 FFMPEG 录屏的方法很简单:

    # Linux
    ffmpeg -video_size 1024x768 -framerate 25 -f x11grab -i :0.0+100,200 output.mp4 
    
    # macOS
    ffmpeg -f avfoundation -list_devices true -i "" 
    
    # Windows
    ffmpeg -f gdigrab -framerate 30 -offset_x 10 -offset_y 20 -video_size 640x480 -show_region 1 -i desktop output.mkv 

    更详细的介绍可以参考官方文档,这里不做太多摘抄。

    使用 Node.js child_process 可以调用外部程序执行操作,详细的用法可以参考官方文档。大概来说,分为:

    1. exec/execFile 调用程序,当执行完成,一次性获得结果
    2. spawn 调用程序,过程中流式获得输出

    我的录屏对时间点有非常高的要求,力求准确。所以我只能选择 spawn,然后通过检查日志输出得到准确的录制开始时间。

    所以,我的代码大概如此:

    class FfmpegRecorder {
      // 其它函数略去
      startRecording(args) {
        this.isFileReady = new Promise((resolve, reject) => {
          this._isFileReadyResolve = resolve;
          this._isFileReadyReject = reject;
        });
        this.recording = spawn(this.ffmpeg, args);
        
        this.recording.stdout.on('data', this.onStdData);
        this.recording.stderr.on('data', this.onStdErr);
      }
      stopRecording() {
        this.recording.kill('SIGINT');
        const path = resolve(process.cwd(), 'a.mp4');
        return path;
      }
    
      onStdData(data) {
        data = data.toString();
        this.stdout += data;
        if (this.stdout.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
          this._isFileReadyResolve();
          this._isFileReadyReject = null;
        }
      }
      onStdErr(data) {
        data = data.toString();
        this.stderr += data;
        if (this.stderr.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
          this._isFileReadyResolve();
          this._isFileReadyReject = null;
        }
      }
    }

    根据我在命令行里直接跑 ffmpeg 的结果,它会先初始化,然后开始录屏。录屏开始时,会输出 Output #0, mp4, to xxx.mp4 这样的日志,所以我就会反复检查 stdoutstderr,直到关键日志出现,然后告诉外面的程序开始录制了。

    这里比较奇怪的是,日志输出应该是正常的,走 stdout 通道,结果只能从 stderr 通道获取。我为防万一,两边都留下了同样的代码。可能我对 Linux 理解不够,将来再研究一下为什么会这样吧。

    上面的代码忽略了 onErroronExit 部分,有兴趣的同学请等我开源(公司代码)。

    在 Linux,stopRecording(),即 kill('SIGINT')(相当于按下 Ctrl+C)之后,FFMPEG 会终止录屏,并且生成可以播放的视频文件。但是在 Windows,只会留下一个无法播放的文件。

    通过观察命令行直接运行 ffmpeg 的结果和 node.js 保存的结果,我发现缺失了结束录制后处理视频文件的部分。实际上 FFMPEG 录屏时只会记录视频内容,录制结束后才会生成影片的meta 信息,而播放器必须读取后者才可以正常播放。所以我猜测在 Windows 下 kill('SIGINT') 会直接彻底杀死进程,而不仅仅是发送一个信号给 FFMPEG,并让它完成后续的工作。

    做出判断后,我尝试用按下 q 的方式通知 FFMPEG 停止工作,并等待 5s。果然成功,于是我构建了新类,继承前面的 FfmpegRecorder,大概代码如下:

    const endTag = /kb\/s:\d+.\d{2}/m;
    
    class WindowsRecorder extend FfmpegRecorder {
      stopRecording() {
        // 向 child process 输入 `q`
        this.recording.stdin.setEncoding('utf8');
        this.recording.stdin.write('q');
        const p = new Promise((resolve, reject) => {
          this._stopResolve = resolve;
          this._stopReject = reject;
        });
        // 设置一个超时保护 15s
        setTimeout(() => {
          this._stopReject();
        }, 15E3);
        return p;
      }
      onStdData(data) {
        super.onStdData(data);
        if (this._stopResolve && endTag.test(this.stdout)) {
          const path = resolve(process.cwd(), 'a.mp4');
          this._stopResolve(path);
        }
      }
      onStdErr(data) {
        super.onStdErr(data);
        if (this._stopResolve && endTag.test(this.stderr)) {
          const path = resolve(process.cwd(), 'a.mp4');
          this._stopResolve(path);
        }
      }
    }

    为了功能完善,我没有选择等待固定的时间,而是继续检查日志,直到发现 endTag 标记。另外,我也留下了 15s 超时保护,避免某些暂时没遇到的问题破坏功能。

    至此,功能基本稳定完成。

  • Promise 改造 child_process.exec

    Promise 改造 child_process.exec

    child_process 是 Node.js 的一个内建模块,用于分裂出(spawn)一个子进程,执行一些特定操作。.exec() 是它的方法,接受一个参数,即要执行的 shell 命令,然后通过回调返回结果。.exec().spawn() 的不同之处在于,前者重在返回结果,后者则重在返回内容。所以当你需要执行一个命令,你并不关心执行过程中发生了什么,只要看到结果就好,那么就用 .exec();反之,假如执行过程中产生的信息对你特别有价值,你并不是特别在意结果,就应该用 .spawn()

    另外,我之前在《Node.js 8 中的 util.promisify》中介绍过,Node.js 8 引入了一个新函数,位于 util 模块,叫做 promisify(),用于将回调风格的 Node.js 函数改造成 Promise 规范的函数。

    OK,背景知识介绍结束。近期开发中,我需要执行一个命令,并且取得它的 stdoutstderrexit code,使用 promisify() 之后发现没有 exit code,于是只好重新写了一下,代码如下:

    import {exec as BaseExec} from 'util';
    
    function exec(command, options) {
      return new Promise((resolve, reject) => {
        let result = {};
        const cp = baseExec(command, options, (err, stdout, stderr) => {
          if (err) {
            err.stdout = stdout;
            err.stderr = stderr;
            reject(err);
            return;
          }
    
          result.stdout = stdout;
          result.stderr = stderr;
          if ('code' in result) {
            resolve(result);
          }
        });
    
        cp.on('exit', (code, signal) => {
          result.code = code;
          result.signal = signal;
          if ('stdout' in result) {
            resolve(result);
          }
        });
      });
    }
    

    希望对大家有用。

    新键盘到了,FC660C,静电容,试用一下,效果还不错。略硬,段落感不强,声音不大。