VNC越しに起動したアプリとの間でクリップボードを共有したい(tmuxでもOK)

今回は、リモートサーバーにSSH接続したマシンとクライアント間のクリップボードを共有する時に詰まった部分があったので、まとめてみたいと思います。

やりたいことをざっくりとイメージ図にしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255755/cd9640ea-c987-087e-427d-4b55b5dd83d3.png)


shell内でDISPLAYをVNC用に設定したアプリケーション`App. A`の中で作られた文字列をshellの方のクリップボードに格納したいということです。

# 環境について

Windows 10のクライアントPCにWSL1 (`Penguin 20.04.1`)を入れて、リモートサーバーにSSHしています。
Xクライアントは`X410`を使っています。
接続先のリモートサーバーは`CentOS 6.9`です。
私は普段、リモートサーバー上ではターミナルを多重化してセッションを保持出来る`tmux 2.3`を使っています。
いずれのshellも`zsh`を使っています。
Xのクリップボードにアクセスするコマンドとして`xsel 1.2`を使用します。

# 基本的な使い方

本題に入る前に、基本的なVNCの使い方・`xsel`の使い方を軽くまとめておきます。
本題は次のSectionです。

## DISPLAYを指定してプログラムを起動する

X ServerのDISPLAYを指定してあるプログラムを開く時には

```shell
DISPLAY=:1.0 (shell command)
```

と記述します。このようにすることでVNC上のDISPLAYにXを出力できます。SSHでリモートサーバーにアクセスして、その上でXをローカルに飛ばすと通信で重くなるので、VNCで出力させた方が早いです。

## クリップボードを共有する

Xはまた、クリップボードの管理機能も持っています。

ここでは、`xsel`というパッケージを使用してクリップボードのコンテンツを読み書きします。
`ssh`でログインする時に必ず`-X`オプションをつけるか`.ssh/config`で`ForwardX11 yes`をつけてX forwardingをしておかないとSSHした先とのクリップボード共有が出来ません。
Windows 10とWSLとのクリップボード連携はWindows用のX client (`X410`とか`VcXsrv`)をインストールしてWSLでの環境変数`DISPLAY=:0.0`を通しておけば大丈夫(なはず)です。


使い方を簡単に説明すると

```shell
echo "hoge" | xsel -bi
```
でクリップボードに"hoge"がコピーされ

```shell
xsel -bo
```
でクリップボードの中身を出力します。

# 本題

## VNCで画面出力しているプログラム上でクリップボードを共有するには?

ここで、一つの問題が生じます。DISPLAYを指定して起動させたプログラム(`App. A`)では、当然`DISPLAY`変数が指定されたものになります(~~某政治家みたい~~)。

例えば以下のように、ただ`$DISPLAY`を出力するだけのshell scriptを書きます。

```sh:print_display.sh
#!/bin/sh

echo $DISPLAY
```

これを普通に実行すると

```shell
% sh print_display.sh
localhost:10.0
```

と出力されますが、これをDISPLAYを指定して実行すると

```shell
% DISPLAY=:1.0 sh print_display.sh
:1.0
```

となります。これはこのshell script内ではDISPLAY変数が変更されて実行されていることになります。

これが、Xで管理されているクリップボードだとどうなるでしょうか。

```shell
% echo "from localhost" | xsel -bi # DISPLAYを指定しない(現在のDISPLAY)で"from localhost"を格納
% echo "from localhost to 1.0" | DISPLAY=:1.0 xsel -bi # DISPLAYを1.0にして、"from localhost to 1.0"を格納
% xsel -bo 
from localhost  # 現在のDISPLAYで動いているXのクリップボードを参照
% DISPLAY=:1.0 xsel -bo
from localhost to 1.0 # 1のDISPLAYで動いているXのクリップボードを参照
```

という結果になります。ではここで、VNCで動いているshellにてクリップボードの中身を参照してみます。

```shell:at_VNC
xsel -bo
:1.0
```

と、クリップボードの中身がVNC上で共有されています。

## VNCにXを飛ばしているプログラム内で、今のshellのクリップボードを共有したい

では、イメージ図の`App. A`にてshellのクリップボードを利用する場合にはどうすれば良いでしょうか。
これの解は`env`コマンドを使用して、このコマンドに対してのみshellのXを指定するということをします。
[こちら](https://labunix.hateblo.jp/entry/20130525/1369486329)を参考にさせていただきました。

このようなshell scriptを書いて実験します。

```sh:change_display_test.sh
#!/bin/sh

echo "change display" | env DISPLAY=localhost:10.0 xsel -bi
```

`env DISPLAY=localhost:10.0`で、環境変数`$DISPLAY`をshellのXに変更して実行するようにしています。
このスクリプトを`:1.0`ポート(`App A`を想定)で実行します。

```shell
% echo $DISPLAY
localhost:10.0 # 現在のDISPLAYはlocalhsot:10.0
% DISPLAY=:1.0 sh change_display_test.sh
% xsel -bo # 現在のXのクリップボードを見る
change display   
```
大元のプログラムでは`DISPLAY`を`:1.0`に指定して走らせていますが、そのプログラムの中で唯一クリップボードに格納する処理だけ`DISPLAY`の指定を解除すれば、クリップボードがVNC DISPLAYではなくshellのものと共有出来るようになるわけです。
これで`DISPLAY`を変えて動かしたプログラムにて現在のXのクリップボードが参照できました。


# tmux使いだともうひと手間

これで普通にSSHでログインしたshellで`App. A`を使う場合にはOKです。
しかし、ここで`tmux`を使っている方はもうひと手間必要です。

## sessionが持っている`DISPLAY`変数は起動した`DISPLAY`変数が保持される

tmuxで新たにsessionを立ち上げた時、このsessionの`DISPLAY`変数は破棄されるか途中で変更するまでずっと立ち上げた時の`DISPLAY`変数が使われます。

例えば`localhost:10.0`のshellで`tmux`を立ち上げると、ずっと`DISPLAY`変数は`localhost:10.0`のままです。 **デタッチして再アタッチしたshellが`localhost:10.0`でなくても** 。

実験をします。まず、1つめのshellを作ります。この中で新たにtmuxのsessionを起動させ、デタッチします。

```shell-session:1(10.0)
(client) % ssh (server)
(server) % echo $DISPLAY
localhost:10.0 # 起動したshellのDISPLAY変数
(server) % tmux
(server) % echo $DISPLAY
localhost:10.0 # tmuxのsessionが持っているDISPLAY変数
[detached (from session 1)] # デタッチさせます
```

ここで、`localhost:10.0`のshellを保持したまま新たにもう1回shellを作り、前のshellで作ってデタッチしたsessionにアタッチします。

```shell-session:2(11.0)
(client) % ssh (server)
(server) % echo $DISPLAY
localhost:11.0 # 起動したshellのDISPLAY変数
(server) % tmux a -t 1 # 先程デタッチさせたsessionをアタッチ
(server) % echo $DISPLAY
localhost:10.0 # tmuxを起動した時のDISPLAY変数のまま
```

と、起動しているshellとtmux上の`DISPLAY`変数の不一致が起きます。この状態で`localhost:10.0`に対応する1つめのshellを終了させてtmux上で取得出来るで`xsel`を動かそうとします。

```shell-session:1(10.0)
(server) % exit
```

```shell-session:2(11.0)_tmux_session
(server) % echo "hoge" | xsel -bi # DISPLAY変数が本当は死んでいるlocalhost:10.0なので、Errorになる
xsel: Can't open display: (null)
: Connection refused
```

と、存在しない`DISPLAY`変数を持ってこようとするので、Errorとなります。

## 回避策

本来であればアタッチしたshellの`DISPLAY`変数が欲しいですよね。それ、出力できます!!!

`tmux showenv`コマンドを使用します。このコマンドの引数に`DISPLAY`を指定すると、アタッチしたshellの`DISPLAY`変数を取得できます。

```shell-session:2(11.0)_tmux_session
(server) % tmux showenv DISPLAY
DISPLAY=localhost:11.0
```

アタッチしたshellの`DISPLAY`変数を取ってこれました。あとはこれを`env`コマンドと組み合わせるだけです。

# 最終形態

```shell:change_display_test.sh
#!/bin/sh

echo "hoge" | env $(tmux showenv DISPLAY) xsel -bi
```

これを実行すると

```shell-session:2(11.0)_tmux_session
(server) % DISPLAY=:1.0 sh change_display_test.sh
```

クライアント(WSL)の方に戻り、クリップボードの中身を確認すると

```shell-session(client)
(client) % xsel -bo
hoge
```

と、狙った挙動になりました!