Edited at
LivesenseDay 24

ターミナル画面を勝手に共有して他人の作業を覗いてみる

More than 3 years have passed since last update.

この記事は 2015年 Livesense Advent Calendar 24日目の記事です。

今日はクリスマスイブということで、いつもお世話になっている敏腕若手インフラグループリーダーに感謝の気持ちを込めてクリスマスプレゼントを送りたいと思います。


プレゼント何にしよう?

さてプレゼントを送ることを決めたは良いものの何をあげようか迷っていました。

そんな時以前話したある会話を思い出しました。

インフラグループリーダー 「踏み台サーバとかのターミナル画面って簡単に共有できる方法ないのかな?」

インフラグループリーダー 「特に新人とかはエースエンジニアの障害対応とか見てるだけで勉強になるんだよね」

インフラグループリーダー 「オペレーション時の確認者とかも作業者のPC覗きこむんじゃなくて自分のPCで見たいよね」

自分 「他人のターミナルに書き込むことはできるんでできそうですよね」

インフラグループリーダー 「いいの見つからないな... 作ってよ」

自分 「ちょっと調べてみます」

他人のターミナルに書き込む方法は @sion_cojp さんの 上司のターミナルにマトリックスを走らせよう! の記事のようにやり方は知っていたので逆も簡単だろうと高をくくりこれをプレゼントすることに決めました。


どんなものが欲しいのか


  • 踏み台サーバとかでチームメンバーがやってる作業の様子が見たい


    • 各人が障害対応時どんなことをしているかがわかる

    • エースエンジニアの作業は見ているだけでも勉強になる

    • できれば振り返りとかもしたい



  • 見られていることを意識したくない


    • いくら画面を共有したいとはいえ作業する人がやりにくい環境になっては元も子もない



どんなものが欲しいのか考えたところで、どうやったら実現できるかを調べてみました。


調査結果

簡単にインターネットで調べてみたところターミナル画面の共有方法は以下の様なものがありました。


  • tmux、screenで仮想端末を共有


    • 作業者に協力して使ってもらう必要がある



  • scriptで共有


    • 作業者に協力して共有してもらう必要がある



  • ttylogで共有


    • いいものあった!!



tmuxやscreenを使えば出来るよという声が多かったのですが、チーム全員が使いこなす必要があることやキーバインドをカスタマイズしている時に不便なのでこの案は見送りました。

scriptコマンドでログを共有する方法も作業者側が共有をしなければいけないので今回は見送りました。

そして最後になんとか見つけたのが ttylog というツール。

調べてみたところ同じサーバ上の他のターミナル画面を覗くことができ、かつ覗かれている方には全く影響がない様子。

ログをファイルに保存し、見返すことができるなど欲しい機能はほとんど入っている。

これは来たと思い実際に使ってみることにしました。


ttylogで遊んでみる

ttylogで遊んでみました。環境はCentOS7を使用しました。


インストール

まずは必要と思われるもののインストール

$ yum -y install perl perl-Time-HiRes perl-ExtUtils-MakeMaker strace

必要なものを見てビックリ。ぱ、ぱーる...

私はPerlは暗号だと思っています。

そんなことにはめげずGitでソースコードを取得してmakeします。

$ git clone https://github.com/gitpan/ttylog

$ cd ttylog
$ perl Makefile.PL
$ make
$ make test
$ make install

これでインストールおしまいです。


実際に使ってみる

ttylog.gif

左が作業者のターミナル、右が傍観者のターミナル画面になります。

ほぼリアルタイムできれいに表示されています。

見たい相手のttyはwコマンドやwhoコマンドで確認することが出来ます。

これを使えばターミナル画面を勝手に共有することが出来ます。

リーダー やりました!!


どうやって実現しているか調べてみた

どうやって実現しているのか気になったのでPerlのソースコードを読んでみました。

繰り返しますが私はPerlは暗号だと思っています.

やっていることは大きく分けて3つでした


  • ttyのログインプロセスのpidを調べる

  • pidに対しstraceコマンドを使ってシステムコールを覗き見る

  • システムコールからターミナル画面に出力されていた情報を抜き取り整形して表示


ttyのログインプロセスのpidを調べる

psコマンドを使って調べていました。

概略は以下のようなものでした。

my $ps = `ps fauwwx`;

if ($ps =~ /\n(\S+)\s+(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\?\s+\S+\s+\S+\s+\S+\s+\S+[\|\\_ ]+\S*\bsshd\b.*\n\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+$tty\s/) {
my $pid = $2;
}

Perlの正規表現エンジンが優秀とはいえ恐ろしい...


pidに対してstraceコマンドを使ってシステムコールを覗き見る

strace コマンドを使ってシステムコールを覗き見ています。

他人のターミナルを覗くという今回の肝の部分です。

概略は以下のようなものでした。

exec "strace","-e","read,write","-s16384","-x","-o",$write,"-p",$pid

exec で strace コマンドを実行しています。

-e オプションで read と write のシステムコールを見ています。


システムコールからターミナル画面に出力されていた情報を抜き取り整形して表示

最後にstraceの結果を受け取り、画面に表示するために整形をしています。

概略は以下のようなものでした。

while(<TRACE>) {

if ($output && /read\($fd_terminal, "(.*)"/) {
my $s = $1;
$s =~ s/\\x(..)/chr hex $1/eg;
$s =~ s/\\t/\t/g;
$s =~ s/\\r/\r/g;
$s =~ s/\\n/\n/g;
$s =~ s/\\\\/\\/g;
if (open OUT, ">>$output") {
OUT->autoflush(1);
print OUT $s;
close OUT;
}
}
}

straceの結果は一部バイナリを想定してASCIIコードになっています。

それをchr hex $1の部分で文字に変換しています。

Perlの正規表現エンジンが優秀とはいえ恐ろしい...

全体的にやはりPerlってすごいなという感想でした。


プレゼントにするなら自分で作らないと!!

ttylogのソースコードを読んでみてなんだか自分でも実装できるような気がしてきました。

せっかくクリスマスプレゼントにするなら手作りの方が気持ちがこもっているはず!!

幸いにも私は普段よくGo言語を書いているため、作ってしまえばPerlよりもバイナリにして配れる分メリットも大きいはず。

(お前が作ったものなんて気持ち悪いよとか言わないでください)


Goで実装してみた

ということでttylogを参考にしながらクリスマスプレゼントを実装してみました。

その名も informer (わりと苦労したのは内緒)。

ソースコードは以下においてあります。

https://github.com/nashiox/informer

動作としてはlistコマンドでユーザとttyを調べる。

$ informer list

nashio pts/0 192.168.189.3 28:53 -zsh
nashio pts/8 192.168.189.3 1:23m sshd: nashio [priv]
nashio pts/9 192.168.189.3 5.00s sshd: nashio [priv]

watchコマンドでpts/0のターミナルを覗き見。

-oオプションで振り返り用のログを指定できます。

$ sudo informer watch -o /tmp/hoge.trace pts/0

reviewコマンドでwatchで見た内容を後から振り返れます。

$ sudo informer review /tmp/hoge.trace

informer.gif

具体的には以下のように実装しました。


ttyを調べる

listコマンドの実装です。

wコマンドの出力をそのまま表示しています。

out, _ := exec.Command("w", "-hs").Output()

fmt.Println(string(out))


ttyのログインプロセスのpidを調べる

ttylogとほぼ同じ実装です。

Go言語だと正規表現扱うの面倒ですね…

out, _ := exec.Command("ps", "fauwwx").Output()

psreg := regexp.MustCompile(
`\n(\S+)\s+(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\?\s+\S+\s+\S+\s+\S+\s+\S+[\|\\_ ]+\S*\bsshd\b.*\n\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+` + tty + `\s`,
)

if !psreg.Match(out) {
fmt.Errorf("Unable to locate corresponding ssh session for [%s]", tty)
os.Exit(2)
}

pid := string(psreg.FindSubmatch(out)[2])


pidに対してstraceコマンドを使ってシステムコールを覗き見る

ttylogとほぼ同じような実装です。

traceの結果をファイルに出力しています。

cmd := exec.Command("strace", "-e", "read", "-s16384", "-q", "-x", "-p", pid, "-o", output)

cmd.Start()


システムコールからターミナル画面に出力されていた情報を抜き取り整形して表示

最後にstraceの結果書き込んだファイルから読み取り、画面に表示するために整形しています。

ここが最も苦戦しました。

straceの結果の整形をGo言語で行うのがなかなか難しく、未だ日本語がうまく表示できません。

t, _ := tail.TailFile(output, tail.Config{Follow: true})

defer t.Kill(nil)

outreg := regexp.MustCompile(
fmt.Sprintf(`read\(%d, "(.*)"`, keys[len(keys)-1]),
)
asciireg := regexp.MustCompile(`\\x(..)`)
for line := range t.Lines {
if outreg.Match([]byte(line.Text)) {
s := string(outreg.FindSubmatch([]byte(line.Text))[1])
s = asciireg.ReplaceAllStringFunc(s, func(ss string) string {
ascii, err := strconv.ParseInt(strings.Replace(ss, `\x`, "", -1), 16, 64)
return string(ascii)
})
s = strings.Replace(s, `\n`, string(0x0a), -1)
s = strings.Replace(s, `\r`, string(0x0d), -1)

fmt.Print(s)
}
}


おわりに

ターミナル画面の共有ができるようになると作業時の画面共有ができたり、新人教育なんかにも使えるんじゃないでしょうか?

他にも勉強会の際にも前の大画面で表示するのではなく手元で見れるのは効率が上がるような気がします。

うまく使えばチーム力の向上・作業の効率化に繋がるんじゃないかと思いました。

いつもお世話になっているインフラグループリーダーに私のクリスマスプレゼントが届いてくれると嬉しいです。

とはいえ個人のターミナルはプライベート空間だと思います。

本記事の内容は用法用量を守って正しくお使いください。