TigerVNC-1.11.0にてvncserver/vncsessionまわりのエラーを調査した話

[FreeBSD Advent Calendar 2020](https://qiita.com/advent-calendar/2020/freebsd) 10日目の記事です。
今日はTigerVNCパッケージの更新でハマった話を書こうと思います。

## 背景

[1日目の記事](https://qiita.com/furandon_pig/items/948ae9ecb8bb9dd5e52c)でFreeBSD+Jailによるコンテナ環境を気軽に作成・破棄できるようになりました。さらに[VNC](https://ja.wikipedia.org/wiki/Virtual_Network_Computing)と組み合わせることで、GUI環境も構築できます。
FreeBSDでは[TigerVNC](https://tigervnc.org/)パッケージが提供されており、ことを用いることで簡単にVNCサーバを立ち上げることができます。

ところが、いつものように[TigerVNC](https://tigervnc.org/)をインストールして動かそうとしたところ、これまでの手順では動かせなくなっており、あれこれ調べて対応したというのが今日のお話になります。
## 設定手順の一連の流れ

今日の話はデバッグのための試行錯誤が長々と続くため、最初に一連の設定手順を載せておきます。
(なのでこの部分だけ見ればOKという話でもあります...)

```sh
$ # 必要なパッケージのインストール。
$ sudo pkg install -y \
    tigervnc-server tigervnc-viewer \
    perl5 xauth xterm xrdb xsetroot \
    ja-uim-mozc uim-gtk ja-font-sazanami \
    leafpad open-motif
$ # VNCの設定ファイル用ディレクトリを作成。
$ mkdir .vnc
$ vncpasswd .vnc/passwd
$
$ # Xvncを起動したときに実行させるスクリプト。
$ cat <<_EOF > .vnc/xstartup
#!/bin/sh

export GTK_IM_MODULE=uim
export QT_IM_MODULE=uim
export XMODIFIERS=@im=uim
export XIM=uim

mozc_process=`ps ax | grep mozc | grep -v grep | wc -l`
[ $mozc_process -eq 0 ] && /usr/local/bin/mozc start &

uim_xim_process=`ps ax | grep uim-xim | grep -v grep | wc -l`
[ $uim_xim_process -eq 0 ] && uim-xim &

xsetroot -solid midnightblue

exec mwm
_EOF
$
$ # Xvncコマンドを直接実行し、xterm経由でxstartupを実行する。
$ Xvnc -PasswordFile ~/.vnc/passwd -verbose 2 & \
    sleep 2 \
    && xterm -iconic -display :0 /home/fbsd/.vnc/xstartup &
```

## vncserverコマンドでのエラー

FreeBSDで[TigerVNC](https://tigervnc.org/)を用いたGUI環境を構築する場合には、以下のようなパッケージをインストールします。

```sh
$ sudo pkg install -y \
    tigervnc-server tigervnc-viewer \
    perl5 xauth xterm xrdb xsetroot \
    ja-uim-mozc uim-gtk ja-font-sazanami \
    leafpad open-motif
```

これまでは必要なパッケージをインストールしたのち、 `vncserver :0` でVNCサーバを起動していたのですが、パッケージをアップデートしたタイミングでコマンドが見つけられなくなってしまいました。

```sh
$ vncserver :0
-bash: vncserver: command not found
```

### どのバージョンから変更が入っている？

VNCが利用できている既存の環境と比較すると、TigerVNCのバージョンが `1.10.1_1` から `1.11.0` に上がっていました。


 * `vncserver` でVNCが起動できる環境。
 
```sh
$ pkg info tigervnc-server | head -n4
tigervnc-server-1.10.1_1
Name           : tigervnc-server
Version        : 1.10.1_1
Installed on   : Tue May 19 01:20:05 2020 JST
```

 * パッケージアップデート後の環境。

```sh
$ pkg info tigervnc-server | head -n4
tigervnc-server-1.11.0
Name           : tigervnc-server
Version        : 1.11.0
Installed on   : Sun Oct 11 02:10:00 2020 JST
```

### インストールされるファイルの差分確認

インストールされるファイルの差分をみると、いくつか差異があります。

```sh
$ diff -u tigervnc-server-1.10.1_1.txt tigervnc-server-1.11.0.txt
--- tigervnc-server-1.10.1_1.txt        2020-10-11 02:29:50.500981000 +0900
+++ tigervnc-server-1.11.0.txt  2020-10-11 02:30:02.870729000 +0900
@@ -1,16 +1,22 @@
 /usr/local/bin/vncconfig
 /usr/local/bin/vncpasswd
-/usr/local/bin/vncserver
 /usr/local/bin/x0vncserver
 /usr/local/bin/Xvnc
+/usr/local/etc/pam.d/tigervnc
+/usr/local/etc/tigervnc/vncserver-config-defaults
+/usr/local/etc/tigervnc/vncserver-config-mandatory
+/usr/local/etc/tigervnc/vncserver.users
 /usr/local/lib/xorg/modules/extensions/libvnc.so
+/usr/local/libexec/vncserver
 /usr/local/man/man1/vncconfig.1.gz
 /usr/local/man/man1/vncpasswd.1.gz
-/usr/local/man/man1/vncserver.1.gz
 /usr/local/man/man1/x0vncserver.1.gz
 /usr/local/man/man1/Xvnc.1.gz
+/usr/local/man/man8/vncserver.8.gz
+/usr/local/man/man8/vncsession.8.gz
+/usr/local/sbin/vncsession
 /usr/local/share/doc/tigervnc/tigervnc-server-LICENCE.TXT
 /usr/local/share/doc/tigervnc/tigervnc-server-README.rst
-/usr/local/share/licenses/tigervnc-server-1.10.1_1/catalog.mk
-/usr/local/share/licenses/tigervnc-server-1.10.1_1/GPLv2+
-/usr/local/share/licenses/tigervnc-server-1.10.1_1/LICENSE
+/usr/local/share/licenses/tigervnc-server-1.11.0/catalog.mk
+/usr/local/share/licenses/tigervnc-server-1.11.0/GPLv2+
+/usr/local/share/licenses/tigervnc-server-1.11.0/LICENSE
```

### vncserverをフルパス指定で実行すれば解決？

`vncserver` コマンドは `/usr/local/libexec` 以下に移動されていました。

```
バージョン1.10.1_1  /usr/local/bin/vncserver
バージョン1.11.0    /usr/local/libexec/vncserver
```

フルパス指定で `/usr/local/libexec/vncserver` を実行すれば解決するかと思ったのですが、別のエラーが出てしまいました。

```sh
$ /usr/local/libexec/vncserver :0 ; echo $?
vncserver: Couldn't find suitable Xsession.
2
```

そしてここから長いデバッグの旅が始まったのです...。

### 頑張ってデバッグする

#### Xsessionファイルの作成

以下に同様のエラーが報告されていた。Xsession ファイルが存在しないことが原因のようです。

 * [TigerVNC does not start after update](https://bbs.archlinux.org/viewtopic.php?id=258937)

手動で `Xsession` ファイルを作成します。

手動で Xsession ファイルを作成する。

```sh
$ ls /etc/X11/Xsession
ls: /etc/X11/Xsession: そのようなファイルまたはディレクトリはありません
$ echo '#!/bin/sh' | sudo tee /etc/X11/Xsession
$ sudo chmod +x /etc/X11/Xsession
```

#### パスワードファイルの作成

`Xsession` 関連のエラーは解消しましたが、今度はVNC用のパスワードファイルが無いというエラーに遭遇しました。

```sh
$ /usr/local/libexec/vncserver :0
VNC authentication enabled, but no password file created.
```

対応方法は自明で、 `~/.vnc/passwd` ファイルを作成することで対応します。


```sh
$ mkdir .vnc
$ vncpasswd .vnc/passwd
Password:
Verify:
Would you like to enter a view-only password (y/n)? n
```

#### デスクトップセッションファイルの作成

今度はデスクトップセッション周りのエラーに遭遇しました。

```sh
$ /usr/local/libexec/vncserver :0 ; echo $?
Could not find a desktop session to run
255
```

ソースコードを参照してみると、エラーメッセージは以下の場所で出力されているようです。

 * [TigerVNC/tigervnc/unix/vncserver/vncserver.in](https://github.com/TigerVNC/tigervnc/blob/a55fe7e2745a62e4b8b63688d1bac523bfbd08e4/unix/vncserver/vncserver.in#L188)

ソースコード中の前後の処理をみるど、どうやら `/usr/share/xsessions/*.desktop` でマッチするファイルが存在しないことが要因のようです。

```perl
if (!$sessionname) {
  foreach $file (glob("/usr/share/xsessions/*.desktop")) {
    ($name) = $file =~ /^.*\/(.*)[.]desktop$/;
    %session = LoadXSession($name);
    if (%session) {
      $sessionname = $name;
      last;
    }
  }
}

if (!$sessionname) {
  die "Could not find a desktop session to run\n";
}
```

なるほど、たしかに `/usr/share/xsessions` ディレクトリの中身は空っぽになっています。

```sh
$ ls /usr/share/xsessions/*.desktop | wc -l
       0
```

`/usr/share/xsessions/mwm.desktop` ファイルを作成して以下の内容を記載します。

```
[Desktop Entry]
Name=MWM
Exec=mwm
TryExec=/home/fbsd/.vnc/xstartup
```

#### xinitでエラー

これで大丈夫かなと思ったのですが、今度は `xinit` でエラーが出てしまいました...。

```sh
$ /usr/local/libexec/vncserver :0
...
xinit: connection to X server lost

waiting for X server to shut down
```

`xinit` とその中から実行されそうなスクリプトは以下の場所にあるようです。とはいえ、TigerVNCパッケージを更新する前は普通に動作していたため、 `xinit` 側が要因である可能性は低そうです。

```sh
$ find /usr/local/etc/ | grep -i xinit | xargs file
find: /usr/local/etc/cups/ssl: パーミッションが拒絶されました
/usr/local/etc/X11/xinit:         directory
/usr/local/etc/X11/xinit/xinitrc: POSIX shell script, ASCII text executable
```

そもそも `/usr/local/libexec/vncserver` コマンドは `libexec` ディレクトリに配置される形に変更されているので、このコマンドを直接実行するという事自体が推奨されない使い方になった、という話なのかもしれません。
どうやら `/usr/local/sbin/vncsession` が内部的に `vncserver` を実行しているようです。が、こちらも実行すると以下のようなエラーになってしまいます...。

```sh
$ sudo vncsession fbsd :0
Failure daemonizing
```

#### ソースコードと照らし合わせてみる

どうやらこれはTigerVNCのソースコードと照らし合わせながら調べないと原因特定ができなさそうです。
`vncsession` で発生しているエラーをソースコードから検索してみます。

```sh
$ find . -type f -name vncsession
./unix/vncserver/vncsession
$ sudo ./unix/vncserver/vncsession fbsd :0
Failure daemonizing
$ find . -type f | xargs grep 'Failure daemonizing'
./unix/vncserver/vncsession.c:            fprintf(stderr, "Failure daemonizing\n");
```

該当箇所の前後の処理を見てもイマイチエラーの原因が分かりません...。
`vncsession` → `vncserver` の順で呼び出していることまでは把握できているので、最終的に実行される `xinit` コマンドがどう実行されるかを調べるほうが早道かもしれません。

```c
        /* Wait for child to finish startup */
        len = read(fds[0], buf, 1);
        if (len != 1) {
            fprintf(stderr, "Failure daemonizing\n");
            _exit(EX_OSERR);
        }
```

#### xinitコマンドはどう実行されている？

`xinit` は以下の個所から実行されるようです。 `@cmd` という配列にオプション等を追加していき、最終的に `xinit` コマンド文字列が作り出されています。

 * [TigerVNC/tigervnc/unix/vncserver/vncserver.in](https://github.com/TigerVNC/tigervnc/blob/a55fe7e2745a62e4b8b63688d1bac523bfbd08e4/unix/vncserver/vncserver.in#L188)

```perl
@cmd = ("xinit");

push(@cmd, $Xsession, $session{'Exec'});

push(@cmd, '--');

# We build up our Xvnc command with options
push(@cmd, "@CMAKE_INSTALL_FULL_BINDIR@/Xvnc", ":$displayNumber");

foreach my $k (sort keys %config) {
  push(@cmd, "-$k");
  push(@cmd, $config{$k}) if defined($config{$k});
  delete $default_opts{$k}; # file options take precedence
}

foreach my $k (sort keys %default_opts) {
  push(@cmd, "-$k");
  push(@cmd, $default_opts{$k}) if defined($default_opts{$k});
}
```

私の環境では以下のような `xinit` コマンドになっていました。

```
xinit -- /usr/local/bin/Xvnc :0 -auth /home/fbsd/.Xauthority -desktop tmp01.furandon.net:0
```

#### xinitのオプション

[xinit(1)](https://www.freebsd.org/cgi/man.cgi?query=xinit&apropos=0&sektion=1&manpath=FreeBSD+12.2-RELEASE+and+Ports&arch=default&format=html)のマニュアルを見ると、 `--` の後にserverと付随するオプションを指定するという形になっています。
ということは、TigerVNCでは、 `xinit` から `Xvnc` が実行されるという流れになっているという話ですね。

```sh
$ man 1 xinit
NAME
       xinit - X Window System initializer

SYNOPSIS
       xinit [ [ client ] options ... ] [ -- [ server ] [ display ] options
       ... ]
```

#### Xvncを直接実行する

これは `Xvnc` を直接実行すれば良さそうです。以下のようなコマンドを実行することで、無事にVNCが利用できる状態になりました。

```sh
$ Xvnc -PasswordFile ~/.vnc/passwd -verbose 2 & \
    sleep 2 \
    && xterm -iconic -display :0 /home/fbsd/.vnc/xstartup &
```

### その他のハマりどころ

今回の調査を行っている際に `~/.ssh/known_hosts` の内容と接続元PCの情報に齟齬が出ている状態だと、Xvncを直接実行する形であってもVNCに接続できないという点にハマりました。
仮想マシン等で環境を構築していると、接続元PCのIPアドレスを使いまわしたりするため、この現象に遭遇しやすいかもしれません。この場合は、 `~/.ssh/known_hosts`  の該当エントリを削除することで問題が解消します。

## まとめ

TigerVNCパッケージの更新でハマった話を紹介しました。思っていたよりも深くまで調べないと解消方法が見つからないため苦労しました。
最終的には `Xvnc` コマンドを直接実行する方法で解消しましたが、 `.xstartup` コマンドの実行はちょっとトリッキーな方法で実行しているため、より良い方法を見つけたいところです。