フューチャー Advent Calendar 2019 17日目の記事です。
最近は Go にはまっていますが、今回は趣向を変えてみます。前のプロジェクトでたまに使っていた、ターミナルを共有する ttycopy というツールの紹介をしようと思います。
スーパーエンジニアがサーバ上に ssh してなんやかんやしているときに、ちょっと画面を ハックしたい 見たい ...! みたいなシーンがあります。あとは、サーバ上でコマンドが正しく入力されているかとかのダブルチェックとかで画面を確認するとか。物理的な距離があったりすると、モニタに画面を映すことも難しいですよね。1
画面をみせてもらうのも一つの方法ですが、ちょっとしたツールで参照できるとスマートですよね。Perl だと ttylog というツールがあったりします。私は ttylog にインスパイアされて、シェルで動く ttycopy を作りました。シェルで動作するので ttycopy の実装をコピー&ペーストしてカジュアルに動かすこともできます。
サンプル動作
以下のように動作をします。ちょっとわかりにくいですが、あるサーバに root ユーザと tsuji ユーザでログインしていて root ユーザで ttycopy を実行しています。ターミナルで途中から tsuji ユーザでログインしているターミナルが表示されていることが分かります。
ttylog がどのように動作しているかは、ターミナル画面を勝手に共有して他人の作業を覗いてみる などでも紹介されていますが、基本的な処理の流れは以下のとおりです。
- tty のログインプロセスの PID を調べる
- PID に対し strace コマンドを使ってシステムコールをのぞき見る
- strace で出力される read システムコールの文字列を整形して表示する
ttycopy では上記の手順に沿ってシェルで実装しました。
シェル(Bash)での実装
1.tty のログインプロセスの PID を調べる
シェルでの実装は以下です。
pid=`ps fauwwx | grep sshd.*${tty} | grep -v grep | sed -e 's/^[a-zA-Z0-9]\+[ \n\r\f\t]\+\([0-9]\+\).*/\1/'`
これは grep したときに取得できる以下のようなプロセスツリーをの文字列を正規表現を用いて PID をキャプチャしています。正規表現でスッキリかけますね。
[tsuji@localhost ~]$ ps fauwwx | grep sshd.
root 1531 0.0 0.1 82568 6236 ? Ss 15:29 0:00 /usr/sbin/sshd -D
root 2830 0.0 0.2 149824 8916 ? Ss 15:29 0:01 \_ sshd: root@pts/0
root 13315 0.0 0.2 158980 10280 ? Ss 18:58 0:00 \_ sshd: root@notty
root 14956 0.0 0.2 154512 9352 ? Ss 20:10 0:00 \_ sshd: tsuji [priv]
tsuji 14959 0.0 0.1 154512 4092 ? S 20:10 0:00 \_ sshd: tsuji@pts/1
tsuji 15012 0.0 0.0 112672 2268 pts/1 S+ 20:11 0:00 \_ grep --color=auto sshd.
2.PID に対し strace コマンドを使ってシステムコールをのぞき見る
シェルでの実装は以下の部分です。
strace -e read -s16384 -q -xx -p ${pid} 2>&1
やっていることは strace を用いて、先程取得した tty に紐づくPID が発行する read システムコールを取得します。read システムコール以外にもいろいろなシステムコールが呼び出されるため、strace のオプション -e read
で read システムコールのみ抽出しています。
read システムコールは以下で定義されているシステムコールでした。strace で取得できる結果も以下のようになっています。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
例えば参照先のターミナルで以下のようなコマンドを発行したとします。
- 取得元のターミナル
[tsuji@localhost gomi]$ hoge
bash: hoge: コマンドが見つかりませんでした...
[tsuji@localhost gomi]$
このとき strace -p ${PID} -e read
で read システムコールを参照すると以下のように出力されます。
- strace しているターミナル
[root@localhost ttycopy]# strace -p 14959 -e read
strace: Process 14959 attached
read(3, "\0\0\0\20\356\202\375C&&\357q\276\210pZ)\300\26M\357T\313\303k\6p\232\351\263\32\224"..., 16384) = 36
read(11, "h", 16384) = 1
read(3, "\0\0\0\20\235\230\204Ud\36)\370\266\233\362\305\2219\253g\335M\23\212\374h\250i@\235/\216"..., 16384) = 36
read(11, "o", 16384) = 1
read(3, "\0\0\0\20\324\357\304\vn\357BbW\241m\220yS\236\362\301\337\337\237c\203\245\223\221\253;,"..., 16384) = 36
read(11, "g", 16384) = 1
read(3, "\0\0\0\20\344\215\235\300\226\236\0\323\376\r\217,\257\322\326w\323R\264\3}\266\7q\315\215\344\346"..., 16384) = 36
read(11, "e", 16384) = 1
read(3, "\0\0\0\20^``\333\263h\372Z\336\335Y2\250\203\335\221\372faj\177\260f\302Sb\35\354"..., 16384) = 36
read(11, "\r\n", 16384) = 2
read(11, "bash: hoge: \343\202\263\343\203\236\343\203\263\343\203\211\343\201\214\350\246\213\343\201"..., 16384) = 62
read(11, "\33]0;tsuji@localhost:~/gomi\7[tsuj"..., 16384) = 51
^Cstrace: Process 14959 detached
上の例だと strace: Process 14959 attached
や strace: Process 14959 detached
という文字列が表示されていますが、この出力を抑制するために strace のオプション -q
を用いています。また、表示するときに扱いやすいように -x
オプションで文字列を 16 進数に変換しています。
3.strace で出力される read システムコールの文字列を整形して表示する
目標は 2 の手順で取得した read システムコールの第 2 引数の内容を表示させることです。シェルでの実装は以下です。
sed -une "s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p" | bash
まず前半部分 sed -une "s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p"
です。正規表現で read システムコールの第 2 引数をキャプチャして、それに対して echo -n
と $''
の文字列を付与しています。よって以下のような文字列を取得できます。
- 取得元のターミナルに出力されている文字列
[tsuji@localhost gomi]$ cat hoge
hoge
[tsuji@localhost gomi]$
- strace して加工後に出力されるターミナルの文字列
echo -n $'\x00\x00\x00\x10\xc6\x1e\x56\xc6\x1f\x11\x7e\x57\x11\xaf\xdb\x2a\x91\x32\x84\x8e\x6e\x5b\x12\xc1\x72\x94\x36\x17\x12\xbb\x7c\xab\x4b\xdd\x19\x33'
echo -n $'c'
echo -n $'\x00\x00\x00\x10\x5d\x68\x72\x7c\x74\xca\x3c\xd1\x57\xfc\x14\x7d\x55\x34\x66\x15\x03\xcb\x26\x7c\x17\xbc\x7f\x7a\xf5\x25\x40\xed\xa8\x21\x39\xb3'
echo -n $'a'
echo -n $'\x00\x00\x00\x10\x05\x3d\x4a\xc2\x76\x1c\xd4\x23\x2a\x17\xc6\xa1\x1c\xf2\xdb\x14\x75\x1c\x7d\xb7\x21\xfb\xfc\xcd\x2d\x5c\xef\x06\x6c\x97\x01\x28'
echo -n $'t'
echo -n $'\x00\x00\x00\x10\x66\xb0\x8c\x40\x10\xa6\xf3\x9b\x36\x75\xd5\xc1\x65\x63\x94\x4f\x77\xd9\x10\x6d\xcf\xbb\x48\xed\x8b\x43\x58\x20\x54\x08\xde\x9b'
echo -n $' '
echo -n $'\x00\x00\x00\x10\x60\x6e\xb6\x06\x43\x16\xf5\x75\x89\x90\xb6\x42\x2c\xfe\x8b\x97\xae\xad\x47\x26\xf9\x39\xfc\xd2\x84\x37\xde\x0d\xe5\x32\xbc\x80'
echo -n $'h'
echo -n $'\x00\x00\x00\x10\x00\xe4\x3d\xb7\xd9\x79\x2e\x46\x80\xd5\xa5\xc2\xa7\x9a\xc7\x0c\xe1\x58\x7b\xd5\x97\xff\x00\xab\x72\x51\xa4\xbb\xab\x7d\xd1\xaf'
echo -n $'o'
echo -n $'\x00\x00\x00\x10\xf8\x1d\xce\xe6\x7f\x1a\x43\x94\xa2\xde\x3d\x3c\xb5\xe9\xb9\x94\x39\x43\x63\xfd\xa9\x1f\x45\x83\x64\x5c\x3a\xdf\xa8\x1a\xa4\x86'
echo -n $'g'
echo -n $'\x00\x00\x00\x10\x90\x69\x42\xa9\x48\x99\x0c\x52\xe9\x49\xbd\x4e\xa5\x17\x01\xff\xac\xec\x29\x75\x2c\xc0\x2b\x7c\x07\x85\xf2\x2f\xce\x71\x8f\x46'
echo -n $'e'
echo -n $'\x00\x00\x00\x10\xd9\x91\x38\x08\x0b\x95\x78\xd4\x80\x51\xa2\xe8\xef\x20\x06\x45\xa9\x3c\xf0\xa3\x10\x7d\x06\x32\x2d\x31\x53\x57\x40\x77\x1b\x29'
echo -n $'\r\n'
echo -n $'\x68\x6f\x67\x65\x0d\x0a\x1b\x5d\x30\x3b\x74\x73\x75\x6a\x69\x40\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x7e\x2f\x67\x6f\x6d\x69\x07'
echo -n $'[tsuji@localhost gomi]$ '
echo -n
の -n
オプションは解釈する際に改行されないようするためです。Bash の $''
の形式は以下の効果があります。Man page からの抜粋です。2
$'string' の形式を持つ単語は特殊な扱いを受けます。 この単語は string に展開され、 それから ANSI C 標準で仕様が決められている、 バックスラッシュでエスケープされている文字に置き換えられます。 バックスラッシュエスケープシーケンスは、 (もし存在すれば) 以下のようにデコードされます:
これを用いることで、文字列が保持している意味に展開することができます。
あとは | bash
で echo
の文字列をシェルに解釈させることで、文字列を表示することができます。
まとめ
strace で read システムコールの引数の文字列を取得し、リアルタイムに復元することでターミナルをカジュアルに共有することができました。他の方の作業をリアルタイムで見れるので教育的なツールではないかと思います。ターミナルをハックしてみたい方はぜひ使ってみてください。
よかったら ttycopy に star もつけてもらえるとモチベーションが上がります
-
もちろん、使うときは断ってから使いましょう。 ↩