分类目录归档:Linux

一个奇怪的 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。

使用 Docker 快速架设一个 IPSec VPN Server

仅作记录使用,环境为 Ubuntu 18.04 LTS

Docker Image 来自 hwdsl2/docker-ipsec-vpn-server

# Install docker
apt update
apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt update
apt install docker-ce docker-ce-cli containerd.io

# Pull the docker image
docker pull hwdsl2/ipsec-vpn-server

之后创建一个 env 文件规定 IPSec 使用的 PSK、用户名和密码,假设存储在 /home/user/.config/vpn.env

# Define your own values for these variables
# - DO NOT put "" or '' around values, or add space around =
# - DO NOT use these special characters within values: \ " '
VPN_IPSEC_PSK=your_ipsec_pre_shared_key
VPN_USER=your_vpn_username
VPN_PASSWORD=your_vpn_password

# (*Optional*) Define additional VPN users
# - Uncomment and replace with your own values
# - DO NOT put "" or '' around values, or add space around =
# - Usernames and passwords must be separated by spaces
# VPN_ADDL_USERS=additional_username_1 additional_username_2
# VPN_ADDL_PASSWORDS=additional_password_1 additional_password_2

# (*Optional*) Use alternative DNS servers
# - Uncomment and replace with your own values
# - By default, clients are set to use Google Public DNS
# - Example below shows Cloudflare's DNS service
# VPN_DNS_SRV1=1.1.1.1
# VPN_DNS_SRV2=1.0.0.1

# (*Optional*) Advanced users can set up IKEv2. See:
# https://git.io/ikev2docker

使用 systemd 开机自启动,假设文件在 /etc/systemd/system/ipsec.service:

[Unit]
Description=IPSec Docker
After=docker.service
Requires=docker.service

[Service]
User=root
Type=oneshot
RemainAfterExit=yes
ExecStartPre=-/usr/bin/docker stop ipsec-vpn-server
ExecStartPre=-/usr/bin/docker rm ipsec-vpn-server
ExecStart=/usr/bin/docker run --name ipsec-vpn-server --env-file /home/user/.config/vpn.env --restart=always -p 500:500/udp -p 4500:4500/udp -d --privileged hwdsl2/ipsec-vpn-server
ExecStop=/usr/bin/docker stop ipsec-vpn-server
ExecStopPost=/usr/bin/docker rm ipsec-vpn-server

[Install]
WantedBy=multi-user.target

之后使用 docker logs ipsec-vpn-server 就能看到本次使用的登录凭据。

在 Thunderbird 中使用 Outlook 风格回复

在之前的一篇文章中已经探讨了如何在 Linux 下使用 Office365 的各种组件,然而 Thunderbird 的默认回复风格和 Outlook 不同,在每次回复都会加入缩进,而非 Outlook 的同级顶端回复,这篇文章就为了在 Thunderbird 上尽量模拟 Outlook 的回复风格。

为了达到这个效果,我们需要两个插件:SmartTemplate4ReFwdFormatter。其中前者是收费的,5 刀一年的 standard license,也不算太贵,后者是免费的。

首先我们需要调整 Thunderbird 的默认回复行为,尽可能以 HTML 而非纯文本模式回复:

之后在 ReFwdFormatter 中删除增加回复缩进的选项:

最后使用 SmartTemplate4 更改回复的 header 样式:

样式代码如下:

<hr tabindex="-1" style="display:inline-block; width:98%">
<div id="divRplyFwdMsg" dir="ltr"><font style="font-size:11pt" face="Calibri, sans-serif" color="#000000">
<b>From:</b> %from(name,bracketMail(angle))%
<b>Sent:</b> %X:=sent% %A%, %B% %d%, %Y% %l%:%M%%p(3)%
[[<b>To:</b> %to(name,bracketMail(angle))%]][[<br><b>Cc:</b> %cc(name,bracketMail(angle))%]]
<b>Subject:</b> %subject%</font>
<div> </div>
</div>

然后就可以了

如果没有 Calibri 字体,需要安装 ttf-mscorefonts-installer 这个包。

如何压缩 VirtualBox 的 vdi 虚拟磁盘

VMware 的虚拟磁盘管理界面上有两个功能,一个是整理磁盘碎片,另一个是回收未使用的空间。这两个功能配合使用,就可以清理掉虚拟磁盘映像文件里面已经删除了的文件依然占用的空间。然而,辣鸡如 VirtualBox 并没有提供这两个功能,需要手动进行未使用空间填零和压缩磁盘映像文件两个步骤。

首先需要确保使用的映像文件格式是动态分配,这样才有压缩的可能性。

如果是 Windows 客户机,那么首先进行磁盘碎片整理,完成之后,下载 sdelete 工具,使用例如:

sdelete.exe c: -z

这样的命令对 C 盘完成填 0 操作。

如果是 Linux 客户机,那么需要使用 zerofree 命令完成这个操作。由于该命令不能在盘符分区已经挂载的情况下工作,且最近版本的 Ubuntu 在 Recovery 模式下也会挂载所有的盘符,因此如果要进行这个操作,需要使用一个 Live CD 进入系统。安装 zerofree 之后,运行:

# zerofree -v /dev/sda1

这样的命令对 /dev/sda1 进行填 0 操作,之后就可以进行压缩了。

压缩的操作我们需要使用 VBoxManage 命令:

VBoxManage modifymedium disk "/path/to/disk.vdi" --compact

在 Windows 下可能需要手动在 VirtualBox 安装路径下找到 VBoxManage.exe 进行操作。之后就压缩成功了。

迁移一个 MediaWiki

讲道理,迁移这玩意比迁移一个 WordPress 麻烦多了。

以下记录仅作为记录,因为迁移配置的环境实在不是很通用。

简单来说,我需要做的一件事是在两台我都没有 root 权限的服务器上迁移一个 MediaWiki 站点。理论上来说,按照官方提供的文档可以很简单地完成这件事,但是其中有一些细节还是需要注意。

从一个比较 high-level 的层面上来说,迁移需要做三件事,导出数据库、移动文件、导入数据库。很简单对吧,然而在两边的 web 环境不一致且不可能完全自己重装的时候,这个问题就不那么简单了。

遇到的第一个问题是,目标环境中的 PHP 没有安装 SQLite 模块,这就意味着必须使用 MySQL 作为数据库后端不能有一个较为 portable 的实现。但是很快就发现,该目标机上没有一个运行的 mysqld 进程,也就意味着我必须在没有 root 权限的情况下运行一个完整的 MySQL session。好在已经有人做过这件事,因此也找到了对应的命令:

首先初始化 MySQL 的环境,这里假定我们所有的文件都保存在 $HOME/mysql 下,使用如下命令运行 mysqld 进行初始化:

MYSQL_HOME=/homes/<user>/mysql
BASE_DIR=$MYSQL_HOME
DATADIR=$BASE_DIR/data
mysqld --user=asset-wiki-admin --datadir=$DATADIR --basedir=$MYSQL_HOME --log-error=$MYSQL_HOME/log/mysql.err --pid-file=$MYSQL_HOME/mysql.pid --socket=$MYSQL_HOME/socket --port=32303 --initialize

之后会生成一个默认的 root 密码在 mysql/log/mysql.err 最后一行,去把它记下来,之后会用到。

然后普通运行 mysqld 可以使用如下命令:

MYSQL_HOME=/homes/<user>/mysql
BASE=$MYSQL_HOME
/usr/sbin/mysqld --datadir=$BASE/data --basedir=$BASE --log-error=$BASE/log/mysql.err --pid-file=$BASE/mysql.pid --socket=$BASE/socket --port=32303 &

停止 mysqld 使用如下命令:

MYSQL_HOME=/homes/<user>/mysql
BASE=$MYSQL_HOME
/usr/bin/mysqladmin --socket=$BASE/socket shutdown -u root -p

如果要不使用交互式密码登录,那么将 -p 命令替换成 -ppassword,这里假定密码就是 password,注意这里不要空格。

如果要启动一个 MySQL 实例,那么这么做:

MYSQL_HOME=/homes/<user>/mysql
BASE=$MYSQL_HOME
/usr/bin/mysql --socket=$BASE/socket --port=32303 -u root -p

注意 32303 是我随便选的本地监听端口,可以换成任意一个可用端口。

在完成本地数据库环境搭建以后,在源服务器上 dump 数据库,在目标服务器上创建数据库和用户并授予权限,之后按照文档中的命令将其导入。

下一步就是在 web server 的目录中放入 MidiaWiki 的文件,然后访问网址进行自动安装,在填写了数据库密码之后,会自动进行数据库的适配过程。之后就会生成当前的 LocalSettings.php 文件。

为了保持原始的设定,我们可以将源服务器上的 LocalSettings.php 拷贝过来,并且将里面的内容根据新的环境做一些修改。需要修改的变量在这里可以看到。主要是修改一些路径相关的文件夹和数据库的连接方式。到这里理论上就可以结束了,但是还要记录一些问题。

关于 PDF 缩略图的生成,MediaWiki 默认通过 PdfHandler 这个插件基于 ImageMagick 和 GhostScript 完成转换和生成工作。这个需要注意的是 $wgMaxShellMemory 这个配置项,可以调整为 ‘unlimited’,以免生成时出现类似

Error creating thumbnail: convert: no decode delegate for this image format `' @ error/constitute.c/ReadImage/504. convert: no images defined `/tmp/transform_efffbb473a62.jpg' @ error/convert.c/ConvertImageCommand/3258

这样的错误。

另外一个需要注意的问题是,ImageMagick 可能禁止转换 PDF 文件,具体要查看 /etc/ImageMagick/policy.xml 或者通过 convert -list policy 查看,确认 PDF 格式的权限是 Read|Write 而非 none。如果系统配置为 none,如没有 root 权限,只能编译安装 ImageMagick,注意一定要做一个 module build。

之后在 ~/.config/ImageMagick/policy.xml 中写入对应的配置就可以了。

另外一个错误是:

Notice: Undefined offset: 0 in /home/asset-wiki/htdocs/includes/libs/mime/MimeAnalyzer.php on line 809

这个是 MediaWiki 1.33 和 PHP 兼容性导致的。具体参考这里的改动。主要就是将 809 行最后的 [0] 改成 [1] 就能解决问题。

如果上传一个 pptx 文件的时候 wiki 报错认为 MIME 类型是 application/zip,这个是 PHP fileinfo 模块的锅,可以用 $wgVerifyMimeType = false; 强行关闭 MIME 类型检查。具体看这里

在 Linux 下使用 Office 365

在 Windows 上用习惯了微软全家桶,切到 Linux 就没指望了,又不能说装个 Windows 虚拟机就为了用 Office 365,那只好尝试找找替代品了。

首先是 Outlook,包含了学校账户的 Exchange ActiveSync,个人 Outlook 邮箱还有日程功能。在 Linux 上比较常见的邮件客户端是 Mozilla Thunderbird,通过插件配置可以最低限度地支持 Outlook 提供的一些服务。

  • owl for exchange 提供 Exchange ActiveSync 的邮件支持
  • lightning 提供 Outlook 日历的基础功能支持
  • tbsync + provider for exchange activesync 提供到 Office 365 账户的日历同步功能
  • provider for google calendar 为 lightning 提供到 Google Calendar 的同步功能

有了这些插件以后,就可以依次添加自己的账户开始同步了。不能说多好用,勉强能用吧。

然后是 Microsoft To-Do,这个有好事者开发了一个跨平台的版本:klaussinani/ao。通过 snap 就可以安装。

还有 OneNote,这个似乎除了网页版就没有什么比较好用的版本,不过也有好事者用 Electron 做了一个本地网页客户端:patrikx3/onenote。还算能用,也是通过 snap 安装。

OneDrive 的同步,也有好事者写了 Linux 上可用的版本:skilion/onedrive。这个要写一下正确的安装流程,我装的时候差点就把 OneDrive 里面的文件全部删掉了,幸好有回收站。

对于 Ubuntu 18.04,安装流程是这样的:

sudo apt install libcurl4-openssl-dev
sudo apt install libsqlite3-dev

# Ubuntu 18
sudo snap install --classic dmd && sudo snap install --classic dub

git clone https://github.com/skilion/onedrive.git
cd onedrive
make
sudo make install

注意,在运行之前,一定是在运行之前,自行在 ~/.config 下创建配置文件目录,以 ~/.config/onedrive 为例,如果需要同步多个账户则应使用不同的目录名称:

mkdir -p ~/.config/onedrive
cp ./config ~/.config/onedrive/config
nano ~/.config/onedrive/config

config 文件结构类似这样:

# Directory where the files will be synced
sync_dir = "~/OneDrive"
# Skip files and directories that match this pattern
skip_file = ".*|~*"

标记了默认的同步路径为 ~/OneDrive,由于在首次运行 onedrive 的时候默认就是授权,因此如果要更改同步目录的话,在这里要先在配置文件里面写好。然后运行 onedrive –confdir=”~/.config/onedrive” 开始进行授权操作,这样就会在 sync_dir 处创建对应的文件夹,然后开始下载。

如果要自动同步,在 make install 之后,在 /usr/lib/systemd/user 下已经被创建了一个 onedrive.service 文件,类似这样:

[Unit]
Description=OneDrive Free Client
Documentation=https://github.com/skilion/onedrive

[Service]
ExecStart=/usr/local/bin/onedrive -m
Restart=no

[Install]
WantedBy=default.target

如果想要同步多个账户的话,把 ExecStart 改成类似于 onedrive -m –confdir=”~/.config/onedrivePersonal” 这样带有配置文件目录的形式。之后:

systemctl --user enable onedrive
systemctl --user start onedrive

就可以启动服务自动运行了。

如果不想使用 User Service,希望在系统启动时运行,则在 /etc/systemd/system 下创建 onedrive.service,写入类似于:

[Unit]
Description=OneDrive Free Client
Documentation=https://github.com/skilion/onedrive

[Service]
ExecStart=/usr/bin/sudo -u <user> /usr/local/bin/onedrive -m --confdir="/home/<user>/.config/onedrive"
Restart=always

[Install]
WantedBy=default.target

之后:

systemctl enable onedrive
systemctl start onedrive

切记,不能在授权完成之后,复制 config 文件并更改 sync_dir,然后直接用 -m 参数运行 onedrive,这样会使得该程序认为本地有全部删除的更改,会直接删掉 OneDrive 上所有文件。所以一定要先写配置文件再授权。

最后记录一下添加 SMB 打印机的过程,Linux 通过 CUPS 管理打印机,但是图形界面的管理未必好用,可以尝试用 localhost:631 来管理,注意 cups-2.2.7 在 Linux 版 Chrome 上有 bug,会出现 unauthorized error,这个需要升级到 2.2.8 或者使用 firefox。具体配置可以看 Arch Wiki