标签: linux

  • Ubuntu 22.04 部署 Sentry 笔记

    Ubuntu 22.04 部署 Sentry 笔记

    Sentry 可能是目前最流行的缺陷管理软件,它可以帮我收集线上产品的问题,帮我们发现各种缺陷。除了 SaaS 服务以外,它还提供独立部署版本,相信大多数用户跟我一样,都觉得独立部署比较安全,这里就分享下前阵子帮我厂搭建 Sentry 的经验。

    0. 准备

    Sentry 号称需要 4核 8G 以及 20G 硬盘,但 20G 其实完全不够,我厂产品接入一半,一周就产生 20+G 的数据量。建议至少准备 100G。

    1. 安装 Docker + Compose 插件

    Sentry 私有部署版使用 docker compose 作为部署方案,所以我们要先安装 Docker 和 Compose 插件。建议先阅读上面的文档,然后可以配合下面的步骤操作。

    卸载旧版本

    sudo apt-get remove docker docker-engine docker.io containerd runc

    配置 Docker 预编译包仓库

    推荐用这种方式来安装,方便日后升级。

    $ sudo apt-get update
    
    $ sudo apt-get install \
        ca-certificates \
        curl \
        gnupg \
        lsb-release
    
    $ sudo mkdir -p /etc/apt/keyrings
    
    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
    
    $ echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

    安装 Docker 和 Compose 插件

     $ sudo apt-get update
     $ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

    测试 Docker

    此时,正常来说,Docker 和 Compose 插件均安装完毕,我们可以跑个 hello-world 测试一下:

    $ sudo service docker start # 启动 docker engine 服务
    $ sudo docker run hello-world

    安装 Sentry

    下载

    https://github.com/getsentry/self-hosted/releases/latest

    下载完成后,解压到安装目录。以后所有操作都在这个目录完成,建议修改下目录名,并且放在公共目录里,方便大家管理。

    我用的目录名是 ~/sentry

    安装

    执行 sudo ./install.sh。如果你的当前用户不是 root,那么 sudo 就非常重要,否则可能会报告各种奇奇怪怪的错误。

    注意:以后的 sudo 也都是必须的。

    安装过程会比较久,要拉很多个镜像并生成容器。这个过程建议保持网络畅通,或者使用 screentmux 等工具保活,否则不小心断联就可能会前功尽弃。

    启动

    安装完成之后,执行 sudo docker compose up -d 启动服务即可。

    配置 Nginx 反向代理 9000 端口

    这段不详细解释了,有人提问的话再补充。总之:

    1. Nginx 反向代理 9000 端口
    2. 使用 Certbot 启动 https
    3. 或者申请个免费证书然后启动 https

    配置邮箱

    修改 ~/sentry/sentry/config.yml,完成邮箱配置。我们用的是腾讯企业邮箱,配置不复杂,就按照 SMTP 教程即可。需要注意的是,mail.from 要跟用户名一致,否则会发不出去。

    修改配置之后,都要重启整个 compose 集群才能生效。

    配置项目

    进入 Sentry,完成首次登录配置。

    然后创建项目,选好项目类型,就能看到配置教程,非常方便。


    总结

    Sentry 开源搞得很好,文档健全,基本能解决大部分问题。

    可惜版本比较多,互联网上有很多内容已经过期,大家阅读时要稍加分辨。

    有其它部署和使用的问题,可以留言交流。

  • Linux 命令行笔记

    遍历目录查找内容

    grep -rnw '/path/to/somewhere/' -e 'pattern'
    • -r 或 -R 表示遍历
    • -n 显示行号
    • -w 全文匹配
    • -l (小写 L) 只显示文件名
    • -e 匹配的表达式

    参考:How do I find all files containing specific text on Linux? – Stack Overflow

    复制多个目录

    cp -r d1 d2 d-* dist/

    查找磁盘占用

    ubuntu@VM-16-15-ubuntu:~$ sudo du -h --max-depth=1 /var/lib/docker/volumes | sort -h
    8.0K	/var/lib/docker/volumes/7e6264b4649948452587e6b562dae6a5881d17ba0254eefb6a98e32dae35f593
    8.0K	/var/lib/docker/volumes/e3a990377c6c214dd7b8c45807c8f63319988b2a29c4e58d13c0445da3e2e7d5
    8.0K	/var/lib/docker/volumes/sentry-self-hosted_sentry-kafka-log
    8.0K	/var/lib/docker/volumes/sentry-self-hosted_sentry-secrets
    12K	/var/lib/docker/volumes/373a3159029b89230c64a7dc74633a48f29fa5a10dd7fee57a16318d659e76a7
    12K	/var/lib/docker/volumes/sentry-self-hosted_sentry-smtp-log
    28K	/var/lib/docker/volumes/sentry-self-hosted_sentry-nginx-cache
    36K	/var/lib/docker/volumes/sentry-symbolicator
    92K	/var/lib/docker/volumes/sentry-self-hosted_sentry-smtp
    144K	/var/lib/docker/volumes/sentry-self-hosted_sentry-zookeeper-log
    368K	/var/lib/docker/volumes/sentry-zookeeper
    712K	/var/lib/docker/volumes/sentry-data
    1.8M	/var/lib/docker/volumes/sentry-redis
    814M	/var/lib/docker/volumes/sentry-self-hosted_sentry-clickhouse-log
    1.6G	/var/lib/docker/volumes/sentry-clickhouse
    14G	/var/lib/docker/volumes/sentry-postgres
    16G	/var/lib/docker/volumes/sentry-kafka
    31G	/var/lib/docker/volumes
    
  • node.js 里使用 fifo

    node.js 里使用 fifo

    0. 需求

    前两天 Showman 遇到一个需求:

    1. 我们需要在服务器端录制视频
    2. 录制视频的过程主要由 node.js 控制,借助 puppeteer 操作浏览器
    3. 但是也会需要执行一些 shell 命令,此时为安全考虑,我们会启动一个封闭的临时环境给用户执行
    4. 这些封闭环境是用户进程间共用的,不会随时启动随时销毁
    5. 所以 node.js 就需要在其它环境里执行一些操作,返回内容,等待执行完毕后再继续下面的

    于是我的同事就让我用 fifo。

    1. 什么是 fifo

    我以前没有用过 fifo,所以搜索了一下。

    FIFO 特殊文件(同具名管道)与管道类似,只是可以用访问文件系统的方式来访问它。它可以被多个进程同时打开和读写。当进程通过 FIFO 交换数据时,内核将直接在内部交换数据,而不会写入到文件系统中。因此,FIFO 特殊文件在文件系统中没有内容;文件系统的入口(即文件)只是作为引用方式,让各进程能够使用文件名来访问管道。

    原文:https://man7.org/linux/man-pages/man7/fifo.7.html

    管道大家应该都知道,把 A 进程的输出直接输入到 B 进程里,加快处理速度。fifo 与管道的差别就是 fifo 可以通过文件路径直接访问,用起来更简单。

    2. 在命令行里使用 fifo

    创建 fifo,使用 mkfifo 命令:

    mkfifo xxx.fifo

    写入内容到 fifo:

    echo "something" > xxx.fifo

    读取 fifo:

    cat xxx.fifo

    因为 fifo 是管道,内容直接走内核,所以实际上硬盘上不会存储任何内容。如果我们在写入之后再 cat fifo,就不会得到任何内容。

    3. 在 node.js 里使用 fifo

    在 node.js 里使用 fifo 需要用 fs.opennet.Socket。因为我需要在执行完毕后继续下一步,所以进行了 Promise 封装:

    try {
      // 为避免执行时间过长导致进程超时,不断输出些内容
      const interval = setInterval(() => {
        log('termlang is processing...');
      }, 3E4);
      await new Promise((resolve, reject) => {
        // 打开名为 $basename-$lineno.sp.fifo 的管道
        open('./$basename-$lineno.sp.fifo', constants.O_RDONLY | constants.O_NONBLOCK, (err, fd) => {
          if (err) {
            clearInterval(interval);
            reject(err);
          }
          const pipe = new Socket({fd});
          pipe.on('data', data => {
            data = data.toString();
            // 以输出内容包括 finished 或 errored 为结束标记
            if (/(finished|errored)/.test(data)) {
              resolve(data);
            }
          });
        });
      });
      clearInterval(interval);
    } catch (err) {
      log(err);
    }

    4. 总结

    作为半路出家的前端,我对系统、对 Linux 一直缺乏了解。所以类似管道这种东西,我一直也不太熟悉,这次算学会了一个新技能,记录分享一下。

  • 在 Fedora 34 上启动 VNC DISPLAY

    在 Fedora 34 上启动 VNC DISPLAY

    大家可以先阅读 使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制 了解产品目标和技术选型。

    前两天在系统更新里看到 Fedora 34 发布,作为更新党,我当然迫不及待就升级了。升级过程蛮顺利的,升级后,系统里的“在线账户”也能正常走 VPN 了,感觉还蛮好的。

    然后,前两天需要调试录视频的程序,发现新系统的 tigerVNC 有一个巨大的变化:不再支持用 vncserver 命令创建虚拟显示器,必须用 systemctl start service,目的是方便绑定系统启动,因为很多服务器的运维需要自动化。

    不过这可苦了我。我是系统运维菜鸡,基本只能照抄文章,搞了半天也没搞好。不过感谢开源,在 GitHub issue 里讨论的只言片语让我知道了其实 vncserver 是个脚本,它调用的其实是 Xvnc 这个命令。

    那就好办了,我开始按图索骥,寻找 vncserverXvnc 之间的关联。最终找到解决方法如下:

    1. 修改 /etc/X11/Xwrapper.config,加入 allowed_users=anybody。这样才能直接使用 Xvnc 创建虚拟显示器,不然会报告只有 console 用户才能创建的错误。
    2. 使用 vncpasswd 命令创建密码文件,创建后的密码文件位于 ~/.vnc/passwd
    3. 然后用 Xvnc :5 -geometry 1280x720 -PasswordFile ~/.vnc/passwd 创建显示器,跟之前的命令很类似,不过需要 -PasswordFile 选项指定密码
    4. 使用 VNC viewer 登录 VNC,输入密码。(我不知道这一步是否必须)
    5. 可以继续使用 DISPLAY=:5

    不过问题并没有完美解决,虽然我的 puppeteer JS 能跑,FFmpeg 也能录。但是 DISPLAY=:5 firefox https://cn.bing.com 只会在当前屏幕打开窗口,不知道为什么。留待以后解决吧。

    最后吐槽下,这种稳定版里换大版本的行为真的要不得,开源团队也不能滥用自己的地位。


    参考阅读

  • Linux 命令行科学上网

    Linux 命令行科学上网

    前些天因为工作需要,装了 Fedora 33 开发 Showman 的视频录制功能。准备顺势双机工作一段时间,自然也就需要给新系统配置好科学上网。这里简单记录一下过程,方便日后回查。

    0. 更新系统

    第一部当然是更新系统,保证系统组件均为最新版,这样可以规避大多数问题。

    sudo dnf update

    1. 使用 pip 安装 shadowsocks 客户端

    Fedora 33 自带 python 3.9,所以直接使用 pip 安装 shadowsocks 客户端即可:

    sudo pip install shadowsocks

    这里可以用 sudo 也可以不用,差别就在于,sudo 之后会将执行脚本安装到 /usr/local/bin;而不带 sudo 则会安装到 ~/.local/bin。两者的执行环境不一样。我无意间使用了后者,所以后面也会按照后者来记录。

    2. 配置客户端

    接下来编辑配置文件,如果是图形界面,建议使用 IDE,纯命令行的话用 vim 也可以。配置文件一般放在 /etc/shadowsocks.json。内容大概如下,顾名思义,我就不一一解释了:

    {
      "server":"server-ip",
      "server_port":8000,
      "local_address": "127.0.0.1",
      "local_port":1080,
      "password":"your-password",
      "timeout":600,
      "method":"aes-256-cfb"
    }

    与前面 Ubuntu 20.04 科学上网 一样,本文不打算介绍服务器的制备——其实我也不建议大家自己制备服务器,除非你本来就有一些资源或者需求。不然的话,以我的经验,比较出名的迷你 VPS(类似 Vultr,DO,$5/月甚至 $2.5/月),IP 大部分都在规则库里,流量大一点,比如看个视频,不出半个小时 IP 就被封了,然后一个月后解封,基本没法用。

    如果你是 macOS 或者 iPhone,或者其它支持 AnyConnect 的系统,可以考虑直接买现成的,比如链接里这个

    3. 配置系统代理

    然后启动服务:

    sslocal -c /etc/shadowsocks.json -d start
    • -c 用来指定配置文件的地址
    • -d 表示启动服务

    这里可能会遇到几个问题(也是上次我放弃命令行转投 qt5 客户端的原因)。我们来逐个解决它们:

    3.1 openssl 错误

    执行后报错:

    $ sslocal -c /etc/shadowsocks.json -d start
    INFO: loading config from /etc/shadowsocks.json
     2021-05-05 15:17:00 INFO     loading libcrypto from /root/anaconda3/lib/libcrypto.so.1.1
     Traceback (most recent call last):
       File "/root/anaconda3/bin/sslocal", line 8, in <module>
         sys.exit(main())
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/local.py", line 39, in main
         config = shell.get_config(True)
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/shell.py", line 262, in get_config
         check_config(config, is_local)
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/shell.py", line 124, in check_config
         encrypt.try_cipher(config['password'], config['method'])
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/encrypt.py", line 44, in try_cipher
         Encryptor(key, method)
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/encrypt.py", line 82, in __init__
         self.cipher = self.get_cipher(key, method, 1,
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/encrypt.py", line 109, in get_cipher
         return m[2](method, key, iv, op)
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/crypto/openssl.py", line 76, in __init__
         load_openssl()
       File "/root/anaconda3/lib/python3.9/site-packages/shadowsocks/crypto/openssl.py", line 52, in load_openssl
         libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
       File "/root/anaconda3/lib/python3.9/ctypes/__init__.py", line 395, in __getattr__
         func = self.__getitem__(name)
       File "/root/anaconda3/lib/python3.9/ctypes/__init__.py", line 400, in __getitem__
         func = self._FuncPtr((name_or_ordinal, self))
     AttributeError: /root/anaconda3/lib/python3.9/lib-dynload/../../libcrypto.so.1.1: undefined symbol: EVP_CIPHER_CTX_cleanup

    其中文件、行号可能有所不同,不过错误内容大多一致。

    这是因为 OpenSSL 升级至 1.1.0 以上后,内部 API 有一些变化,废弃了 EVP_CIPHER_CTX_cleanup() 函数而引入了 EVE_CIPHER_CTX_reset(),shadowsocks 客户端处于无人维护的状态,没有适配这些变化。

    好在修复方案并不复杂,我们只需要修改文件 ~/.local/lib/python3.9/site-packages/shadowsocks/crypto/openssl.py,将里面的 cleanup 都替换成 reset 即可。

    3.2 permission denied /var/run/shadowsocks.pid

    前面写过,因为我安装的时候没使用 sudo,所以把客户端装在当前用户的目录里。于是面临一个矛盾:

    1. 直接使用当前用户启动客户端,会报告标题里的错误
    2. 使用 root 即 sudo 启动客户端,会报告库有错误(即 3.1),因为我修改的是当前用户的本地库

    这里有两个解决方案,一是手动创建 pid,然后修改权限:

    sudo touch /var/run/shadowsocks.pid
    sudo chmod 777 /var/run/shadowsocks.pid

    这个方法重启后会失效,还得再跑一遍。所以我比较推荐另一种做法:使用 sudo -u $user -i 的方式,sudo 的同时仍然使用当前用户的环境(这个方案实测失败了,还要再研究下):

    sudo -u meathill -i sslocal -c /etc/shadowsocks.json -d start

    4. 加入自动启动

    上一步测试成功之后就可以把这段代码加入 /etc/rc.local,以便实现开机自动启动。

    5. 其它步骤

    接下来,需要配置 pac 文件、文件服务和系统代理,可以完全参考 Ubuntu 20.04 科学上网 一文。

    其中下载 gfwlist.txt 时,如果访问不到 https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt,可以试着修改 /etc/hosts,加入下一行:

    199.232.28.133 raw.githubusercontent.com

    完成剩余步骤后,配置成功。


    在 macOS/iOS 设备上使用科学上网有更简单的方法,扫码可得(其实只要你有 AnyConnect,并不限制系统平台):

  • 使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制

    使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制

    自动化录制屏幕有很多用途,比如生成教学视频、生成产品文档,等等。对比人工,自动化有很多好处:

    1. 避免创作者的设备和环境问题(比如邻居装修、麦克风不好等)
    2. 避免创作者的语言、发音问题(比如普通话不标准、不会说某种语言)
    3. 录制环境出现变化,可以方便的重录(比如换个背景图,界面有升级)
    4. 就像写博客一样,任何时候,拿出电脑或者手机都能编辑一段

    所以目前研究这方面应用的很多,我厂也是。我近期就投入大量时间在这项工作上面,现在终于有所成果,写篇博客分享一下。

    0. 准备环境

    首先推荐大家使用 Linux。Linux 开源,有很多开源免费的工具可以完成各种操作,不仅可以录屏,还可以很容易地模拟各种用户操作,给我们留下大量开发空间。

    建议选择有图形界面的 Linux 发行版,我尝试过 fedora 33 和 Debian 10 树莓派版,都很容易配置。如果使用纯命令行版本,然后自己完成安装图形界面,比如 gnome,再完成剩下来的配置,会很麻烦。

    然后记得把系统更新到最新版,以规避可能遇到的问题。

    1. 配置 vncserver

    如果只能在主屏录制,这个产品的实用性就会大打折扣。所以我们选择用 vncserver 创建虚拟屏幕,然后在虚拟屏幕上完成录制。如果需要的话,也可以随时用 vnc viewer 之类的软件连上 VNC 实时查看效果,非常方便。

    有些系统自带 vncserver,比如 Debian 10 树莓派,那就不用安装。我们选用 fedora 33,需要手动安装,这里推荐 TigerVNC,安装使用都很方便:

    sudo dnf install tigervnc

    安装完成后,使用:

    vncserver :5 -geometry 1280x720

    就可以创建虚拟显示器了。其中,:5 是显示器 id,可以顺延,比如 :6:7、甚至 :99,至于上限在哪里我暂时不知道。-geometry 1280x720 是设定显示器分辨率为 1280×720。

    另外,还可以使用下面的命令查看和关闭显示器:

     vncserver -list
     vncserver -kill :5 

    1.1 测试

    配置完成之后,可以用 Firefox 测试一下效果。

    # 安装 firefox
    sudo dnf install firefox
    
    # 在指定虚拟显示器打开 firefox
    DISPLAY=:5 firefox https://cn.bing.com
    
    # 截图,可能需要安装 xwd
    xwd -root -display :5 > screen.xwd
    
    # 转换成 png,可能需要安装 ImageMagick
    convert screen.xwd screen.png

    然后把图片下载到本地,或者启动一个 http 服务器就能看到了。

    1.2 关闭桌面

    默认的 VNC server 会启动桌面,此时可能会要求我们登录什么的。我们在这套系统当中并不需要桌面,只要有显示器即可,所以可以修改 ~/.vnc/xstartup 禁用:

    #!/bin/sh
    
    unset SESSION_MANAGER
    unset DBUS_SESSION_BUS_ADDRESS
    # 把下面这行注释掉 
    # exec /etc/X11/xinit/xinitr

    2. 使用 FFmpeg 捕获屏幕内容

    使用 FFmpeg 录屏比较简单,将输入源设置为指定显示器即可,formatx11grab,命令大体如下:

    ffmpeg -y -f x11grab -video_size 1280x720 -framerate 30 -i :5.0+0,0 -c:v h264 a.mp4

    其中,

    • -y 意思是自动覆盖前面生成的视频
    • :5.0+0,0 是使用刚才创建的 :5 显示器,用它的 0 号桌面,启动位置是 0,0 即左上角
    • -f x11grab 是使用 X server 抓取格式,Linux 下的图形界面一版是基于这个系统

    注意,上面这条命令里参数的顺序很重要,否则可能遇到 Protocol not found 等错误。

    3. 使用 node.js 驱动

    最后只要用 node.js 的 child_process.spawn() 功能调用上面的命令即可。这段代码属于公司,我就不贴了,主要分享几点经验教训:

    1. 要用 spawn,因为录制过程中我们需要用输出来判断录制状态,exec 这种只能在结束时提供输出的没法用
    2. FFmpeg 会把输出输出到 stderr,理由不明,不过记得要用 stderr 来检查
    3. 录像完成,如果在命令行,按 q 或者 ctrl+C 都可以停止录像,并开始封装视频文件。在 node.js 里,我们可以调用 cp.kill('SIGINT') 。注意,调用之后,FFmpeg 子进程并没有立刻结束,它要把前面的录像进行封装,这个过程也是需要时间的,所以如果你接下来还要对视频文件进行操作,应该等待子进程彻底结束
    4. 判断录像开始的依据,我目前用的是:输出里包含 Output #0, mp4, to 'a.mp4'
    5. 判断录像结束,视频已经生成的依据,我用的是 cp.on('exit', onExit),然后在 onExit 里处理。注意,其它情况导致 ffpmeg 子进程退出时也会触发这个函数,所以我们必须检查 code。此时,在我的机器上,code 是 255,表示它是用户手动中止的,可以当作判断依据。

    总结

    剩下来的内容基本就是怎么驱动图形界面程序运行了。一般来说用 puppeteer 比较好,可以很容易的跟 node.js 联动,我厂的 showman 也是基于这个方案来实现的,最后贴一段视频,大家看下效果:

    一段用上述技术生成的视频

    参考阅读

  • 使用 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 超时保护,避免某些暂时没遇到的问题破坏功能。

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

  • 解决 WSL Ubuntu 20.04 下使用 apt 源安装 node.js 的问题

    解决 WSL Ubuntu 20.04 下使用 apt 源安装 node.js 的问题

    随着 Ubuntu 20.04 发布,各大平台都适配发布了对应版本的系统,Windows WSL 也不例外。如果你是新系统,直接在 Microsoft Store 里搜索并安装 Ubuntu 即可;如果你是老系统,已经装过以前的版本,那么需要先卸载再安装,如果直接安装 Ubuntu 20.04 会有多个不同版本的 Ubuntu 共存。

    装完系统后,接着安装其它软件。我现在比较喜欢用包管理工具安装软件,因为容易更新,而我又是更新爱好者。所以按图索骥,找到 node.js 的二进制包安装指引,复制执行:curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - ,结果报错:gpg: can't connect to the agent: IPC connect call failed

    经过搜索,得知这是 WSL 版 Ubuntu 20.04 的问题,与 WSL1 有一些不兼容,在 WSL2 上就没这个问题了。解决方案是装一些工具:

    sudo add-apt-repository ppa:rafaeldtinoco/lp1871129
    sudo apt update
    sudo apt install libc6=2.31-0ubuntu8+lp1871129~1 libc6-dev=2.31-0ubuntu8+lp1871129~1 libc-dev-bin=2.31-0ubuntu8+lp1871129~1 -y --allow-downgrades
    sudo apt-mark hold libc6

    然后问题就解决了。

    这个 issue 里还记录了一些别的方案,包括上面方案的修正版,不过我用起来没问题,也就没继续往下看。感兴趣的同学可以研究一下。

  • 解决 Ubuntu 20.04 下无法打开蓝牙的问题

    解决 Ubuntu 20.04 下无法打开蓝牙的问题

    我的 Ubuntu 20.04 启动之后经常丢掉蓝牙。表现是设置里能看到蓝牙,状态是关闭,但是无法打开,每次点击开关过一会儿就恢复了,也没有报错。

    因为要连接音箱,没有蓝牙很不方便,就搜索了一下,最后发现下面的解决方案。虽然没理解问题根源,但是解决了就好。

    sudo rmmod btusb
    sleep 1
    sudo modprobe btusb

    参考链接:

    1. Bluetooth firmware upload failing after ‘suspend’
    2. Bluetooth used to be enabled, now disabled and won’t enable. Ubuntu 16.04
  • 重置 MariaDB root 密码

    重置 MariaDB root 密码

    以我最常用的 Ubuntu 为例,记录如何重置 MariaDB root 密码。MariaDB 是 MySQL 原作者在 MySQL 闭源之后的再起之作,基本能完全兼容 MySQL。在我的 Ubuntu 上,它的大部分命名都沿用 MySQL。

    1. 停掉 mysql 服务

    $ service mysql stop

    2. 以特殊方式启动 mysql

    使用 --skip-grant-tables 可以跳过用户权限检查,让你没有密码也能连接。此时为了避免安全问题,可以加上 --skip-networking,防止有人此时通过网络连接你的数据库。在本地环境下差别不大。

    $ sudo mysqld_safe --skip-grant-tables --skip-networking &

    这一步可能会启动失败,导致下一步连接时报错:ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ’/var/run/mysqld/mysqld.sock’ (2),此时,可以这样修正:

    $ sudo mkdir /var/run/mysqld
    $ sudo chown mysql /var/run/mysqld

    3. 连接数据库

    $ mysql -u root

    正常情况下就连上了。

    4. 重置密码

    mysql> FLUSH PRIVILEGES;
    mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'new_password';

    5. 重启 mysql

    按照参考文档里介绍的做法,需要

    $ sudo kill `cat /var/run/mysqld/mysqld.pid`
    # 或
    $ sudo kill `/var/run/mariadb/mariadb.pid`

    不过我实际操作的时候只要把(2)开启的进程关掉就好了。然后重启 mysql 服务:

    $ service mysql restart

    6. 完成

    尝试一下连接,应该可以成功了:

    $ mysql -uroot -p

    7. Ghost 连接

    虽然命令行连接成功,但是 ghost start 仍然报错,经查,是 Ghost 的数据库连接方式有点问题,修改 config.production.json,加入一行配置即可,大体如下:

    {
      "client": "mysql",
      "connection": {
        ....
        "socketPath": "/var/run/mysqld/mysqld.sock",
      }
    }
    (更多…)