一个奇怪的 interactive shell 和 systemd service 行为不一致的现象

起因是我想 self-host 一个本身设计是用在 Github Pages 上的 jekyll 博客主题,大概流程是在本地用 jekyll serve 起来以后,用 nginx 挂反代上 https。到这一步其实都还好,但是吊诡的是,我在 shell 里面用 bundle exec jekyll liveserve 跑起来以后,把这个命令做成一个 systemd service 就会一直报错。行为是这样的:

Dec 11 13:10:22 WordPress jekyll[25228]:       Generating...
Dec 11 13:10:22 WordPress jekyll[25228]:        Jekyll Feed: Generating feed for posts
Dec 11 13:10:22 WordPress jekyll[25228]:   Liquid Exception: no implicit conversion of nil into String in /_layouts/default.html
Dec 11 13:10:22 WordPress jekyll[25228]: /var/lib/gems/2.5.0/gems/jekyll-github-metadata-2.13.0/lib/jekyll-github-metadata/client.rb:133:in `join': no implicit conversion of nil into String (TypeError)
Dec 11 13:10:22 WordPress jekyll[25228]:         from /var/lib/gems/2.5.0/gems/jekyll-github-metadata-2.13.0/lib/jekyll-github-metadata/client.rb:133:in `pluck_auth_method'
Dec 11 13:10:22 WordPress jekyll[25228]:         from /var/lib/gems/2.5.0/gems/jekyll-github-metadata-2.13.0/lib/jekyll-github-metadata/client.rb:46:in `build_octokit_client'
Dec 11 13:10:22 WordPress jekyll[25228]:         from /var/lib/gems/2.5.0/gems/jekyll-github-metadata-2.13.0/lib/jekyll-github-metadata/client.rb:26:in `initialize'
Dec 11 13:10:22 WordPress jekyll[25228]:         from /var/lib/gems/2.5.0/gems/jekyll-github-metadata-

...
Dec 11 13:10:18 WordPress jekyll[25224]:         from /var/lib/gems/2.5.0/gems/jekyll-3.9.0/exe/jekyll:15:in `<top (required)>'
Dec 11 13:10:18 WordPress jekyll[25224]:         from /usr/local/bin/jekyll:23:in `load'
Dec 11 13:10:18 WordPress jekyll[25224]:         from /usr/local/bin/jekyll:23:in `<main>'

之后由于 Liquid Exception,ruby 解释器就会退出,接着触发 systemd 的 restart。但奇怪的是,我在 shell 里面运行就从来不会遇到这个错误。经简单查询以后,这个 no implicit conversion of nil into String 的错误在 jekyll 里面非常常见,很多组件都有发生这个问题,尽管查看了很多 issue 但都帮助不大。

后来仔细观察命令行运行的输出以后,发现有 GitHub Metadata: No GitHub API authentication could be found. Some fields may be missing or have incorrect data. 这样一个报错。这个报错在 systemd 的 log 里面没有,因此我怀疑这两个是否为同一个错误。在这个错误的时候,我发现了这个 issue,评论中有人提到了这样的解决方案:在 _config.yml 中加一行 github: [metadata],我试了一下,问题就得到了解决。

这就引出了另一个问题,为什么 jekyll 在 shell 下和 systemd 下的行为不一致。经查询之后,发现原因在于 systemd 和普通的 shell 在执行程序的时候使用的环境变量不一致,而在错误发生的 client.rb#L133 处需要引用 $HOME 这个环境变量,这个在 systemd 下是不存在的。因此如果想要解决这个问题,除了按照前文中的方法进行修正以外,还可以在 systemd 的 unit 里面使用 Environment= 参数手动指定要使用的环境变量。

从 portable 的角度来说,这两个解决方案应该使用前者,因为后者和代码的实现有很强的耦合性,而且仅仅加上这个环境变量也不能完全在 systemd 下模拟 shell 的运行环境。

最后放一下 nginx 的 vhost 的配置文件和 systemd service 的写法:

vhost.conf

server {
  listen 80;
  listen [::]:80;
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  ssl_certificate </path/to/your/.crt>;
  ssl_certificate_key <path/to/your/.key>;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
  ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;  ssl_prefer_server_ciphers on;
  ssl_session_timeout 10m;
  ssl_session_cache builtin:1000 shared:SSL:10m;
  ssl_buffer_size 1400;
  add_header Strict-Transport-Security max-age=15768000;
  ssl_stapling on;
  ssl_stapling_verify on;
  server_name <your_domain>;
  access_log <your_log> combined;
  index index.html index.htm index.php;
  root <your_root_dir>;
  if ($ssl_protocol = "") { return 301 https://$host$request_uri; }
  
  location ~ /(\.user\.ini|\.ht|\.git|\.svn|\.project|LICENSE|README\.md) {
    deny all;
  }
  location /.well-known {
    allow all;
  }
  location / {
    proxy_pass  http://localhost:4000;
    proxy_set_header        Host            $host;
    proxy_set_header        X-Real-IP       $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

systemd.service

[Unit]
Description=Daemon to start Jekyll service

[Service]
Type=simple
WorkingDirectory=</path/to/your/site>
ExecStart=/usr/bin/bundle exec jekyll liveserve --livereload-max-delay 1 --trace 
PIDFile=/var/run/jekyll.pid
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

注意要使用 liveserve 运行而非 serve,不然所有的链接都会是 localhost。

发表评论

您的电子邮箱地址不会被公开。