[last updated:2003/10/18] [since:2003/09/25]
ttyrecを御存知でしょうか。 端末の操作を記録/再生するソフトウェアです。私は、Unix Magazine 2002年 4月号 で知りました。
友人が RubyRogue なるものを作成しました(現在も開発中)。 ぶっちゃけ、メッセージ分離型rogueをRubyに移植した様なものです。 独自に、サーバ/クライアント、疑似リアルタイムという特徴を持っています。
それで、RubyRogueのゲーム中の様子をttyrecで撮ろうと考えたのですが、 これが上手く行きません。画面が崩れてしまいます。
まあ、ttyrecの元となったscript(1)のmanページにも
script は画面を操作しないコマンドを扱ったときに最もうまく動作する。
と書いてあるので、致し方ないところではあります。
他にも、ttyrecでslを 撮った場合も、崩れてしまいました。
ほったらかしにするのも気味が悪いので、判らんなりに調べてみる事にしました。 可能性としては、
が思い浮かびました。
ttyrecのソースコードを見て、一番目の可能性は消えました。 「ひたすら読んでは書く」を繰り返す処理なので、取りこぼしが起こる はずはありません。
で、二番目の可能性が濃厚となったわけですが、凡庸な私はここから手間取りました。
まずはslの出力をファイルにリダイレクトして見てみました。
sl > test.txt
lvでファイルを見てみると…。うーん、ごちゃごちゃしてる。 (^^;
エスケープシーケンスだらけです。それでも、頑張ってinfocmp ktermの
出力と見比べてみると、何故か改行の後に「左へ移動する」というシーケンス
(^[[5D
)が入っています。
うーん、なんだろう…。改行して左に移動? でも、「ncursesを使っているのが怪しいなぁ」と思いました。
で、実験しました。(改行直前のバックスラッシュは、実際には行が継続している 事を示します。以下同様。)
ruby -e 'STDOUT.sync=true; ARGF.each_byte { |c| putc c; sleep 0.001 }' \ test.txt
では崩れるのですが、
ruby -r curses -e 'STDOUT.sync=true; Curses::init_screen; \ begin; ARGF.each_byte { |c| putc c; sleep 0.001 } \ ensure Curses::close_screen end' \ test.txt
だと崩れない事が判りました。ncursesの初期化に問題の種があるようです。
結局、ncursesのソースコードを当たってみて、
ncurses/lib_newterm.c:_nc_initscr
の関数を参考に、stty -onlcrでtermios(3)のオプションONLCRを外すと 上手く行きました。
stty -onlcr; ttyplay ttyrecord; stty onlcr
最後のstty onlcrを忘れると、後の操作で、 もれなく端末表示が崩れます。 :-)
ttyrecの配布元 <URL:http://namazu.org/~satoru/ttyrec/> にある通り、 inetdを用いると、簡単にttyplayの記録をサービスすることができます。 ビューワはtelnetです。
しかしながら、やはりslなどの記録は崩れてしまいます。 sttyを使っても上手く行きません。telnetの端末制御とかちあうようです。
そこで、最後の手段、プログラム。ワンライナーでどん!
ruby -r curses -r socket -e 'STDOUT.sync=true; Curses::init_screen; \ begin s = TCPSocket.open("server", port); while c = s.getc; putc c; end; \ s.close ensure Curses::close_screen end'
うーん、これは、ちゃんとスクリプトにした方がいいかも?
TERM環境変数がvt100とかxtermとかの端末で撮ったRubyRogueの ttyrecordをktermで再生しようとすると、2 バイト日本語文字が 全て化けてしまいます。
化けた場合でも、次のいずれかを行なえば元に戻ります。
そういえば、sshでOpenBSDにログインした場合も、同じような化け方を することがありました。
原因を調べるべく、ttyrecordをlvで見てみました。 Ctrl+中クリックメニューで文字化けを直しても、RubyRogueクライアント が実行された都度化けるようですので、プログラム起動の後に原因がある はずです。また、原因となるエスケープシーケンスは、最初に表示される 文字列の前にあるはずです。
絞られた範囲のエスケープシーケンスで怪しいものを探して…。
む、^[(B^[)0
ってのが怪しいので、echoしてみると…。
やっぱり化けました。一方だけで化けるかな…。^[)0
だけで
化けました。このエスケープシーケンスは、^[[1;24r
の直前に
存在します。^[[%i%p1%d;%p2%dr
はcsr(change_scroll_region)
だそうです。エスケープシーケンス^[(B^[)0
は、ncursesの初期化で
行なわれている可能性がありそうです。
このエスケープシーケンス^[(B^[)0
は、vt100/xtermでは、
enacs(ena_acs; enable altenate char set)として使われているようです。
ここで、infocmp ktermして探してみると、^[(0
ってのが
見つかりました。これは、smacs(enter_alt_charset_mode)でした。
因みに、vt100/xtermでは、^N
でした。
このsmacsを元に戻すのはrmacsだとterminfo(5)に書いてあったので、
調べてみます。ktermは、^[(B
みたいです。因みに、
vt100/xtermでは^O
でした。
ここで実験してみます…あれ?echo "^[(B"しても元に戻りません。 (;_;)
よーくみてみると、smacsは^[(0
で、文字化けの原因は
^[)0
です。 (^^;;;; ところが、^[)0
はterminfoには書かれて
いないようです。
仕方がないので、ktermのソースコードを調べてみることにしました。 "esc"(ESCape)で検索して引っかかったcharproc.cを中心に捜索してみました。
などを見て、実験を繰り返すと、^[)0
から元に戻すには、^[$)B
を
echoすれば良いことが判りました。
…が、こうやって調べた結果、vt100/xtermで撮ったttyrecordをktermで
再生するのは上手く行かないと引導を渡された様な感じです。
ktermのソースコードを見るに、前述のVTparse関数に存在する、
いかにも取って付けたような^[(0
対策コードが悪いような気もしますが…。
なお、ktermを起動して/するときにTERM環境変数を設定しても文字化けしました。 単純な誤魔化しは効かないようです。
以上は愛用のktermについて問題を挙げました。以下、補足です。
逆に、Debian GNU/Linux 3.0のttyrecで撮ったttyrecordを友人に送り、 別環境で再生して貰うと、どうも固まるらしいとのことです。
そこで、こちらでも別環境(OpenBSD)で試してみることにしました。 ttyrecの最新版 ttyrec-1.0.6 をコンパイルして、シリアルコンソールから ttyplayで再生してみると、確かに、途中まで調子良く動いていたttyplayが 固まってしまいました。
そこで、まずは端末の違いを疑い、Debianからsshでログインし、 ttyplayしてみました。結果、やはり同じ箇所で固まりました。
「Debianでは動くのに…。」と思ったあと、ふと思い出しました。 Debianのttyrecは、ttyrec-1.0.5です。そこで、バージョンの差分を 見てみることにしました。
ttyrec-1.0.6では、ttyplayの途中で表示速度を変えることができるように なっていて、その分のコードが追加されているようです。
止まっている場所は、gdbでの検証の結果、read関数であることが判っています。 「ということは、selectが怪しい…。」なので、select周りを見たところ、 少しだけ初期化や判定条件が不足しているようです。その部分に手を加えると、 めでたく固まらないようになりました。
パッチは、こんなんです。
diff -c ttyrec-1.0.6.orig/ttyplay.c ttyrec-1.0.6/ttyplay.c *** ttyrec-1.0.6.orig/ttyplay.c Tue Oct 22 19:01:23 2002 --- ttyrec-1.0.6/ttyplay.c Fri Sep 26 21:22:14 2003 *************** *** 82,94 **** { struct timeval diff = timeval_diff(prev, cur); fd_set readfs; assert(speed != 0); diff = timeval_div(diff, speed); FD_SET(STDIN_FILENO, &readfs); ! select(1, &readfs, NULL, NULL, &diff); /* skip if a user hits any key */ ! if (FD_ISSET(0, &readfs)) { /* a user hits a character? */ char c; read(STDIN_FILENO, &c, 1); /* drain the character */ switch (c) { --- 82,96 ---- { struct timeval diff = timeval_diff(prev, cur); fd_set readfs; + int retval; assert(speed != 0); diff = timeval_div(diff, speed); + FD_ZERO(&readfs); FD_SET(STDIN_FILENO, &readfs); ! retval = select(STDIN_FILENO+1, &readfs, NULL, NULL, &diff); /* skip if a user hits any key */ ! if (retval > 0 && FD_ISSET(STDIN_FILENO, &readfs)) { /* a user hits a character? */ char c; read(STDIN_FILENO, &c, 1); /* drain the character */ switch (c) {
ktermで漢字が文字化けするようになる問題ですが、ttyplayの出力を
フィルタして^[(B^[)0
を削除すれば良い事に気付きました。
以下、Rubyスクリプトによる、取って付けたようなフィルタ(delesc.rb)です。
#!/usr/bin/ruby ESCSEQ="\e(B\e)0" STDOUT.sync=true buf = '' while c = STDIN.getc if buf == ESCSEQ buf = c.chr else buf += c.chr if buf.size > ESCSEQ.size n, buf = buf.split(//, 2) putc n end end end puts buf unless buf == ESCSEQ
使う時は、こんな感じで。
stty -onlcr; ttyplay ttyrecord | ruby delesc.rb ; stty onlcr
「本当はterminfo/termcapを見て変換するのが良さそうだな〜」とか 思っていましたが、端末によって存在したり存在しなかったりする命令 があるようなので、この案は駄目っぽいです。
RedHat9で^[)0
を出力しても、manページは化けないようです。
manコマンドに細工がしてあるんでしょうかね。
おかげで、RedHat9のktermでは問題がないものと勘違いするところでした。
やっぱり、他環境で撮ったttyrecordファイルで、ゴミが残る問題でしょうか。
anraku@lemon.plala.or.jp