録箱

[last updated:2003/10/18] [since:2003/09/25]

ttyrec絡みの話。

ttyrec

ttyrecを御存知でしょうか。 端末の操作を記録/再生するソフトウェアです。私は、Unix Magazine 2002年 4月号 で知りました。

RubyRogue

友人が RubyRogue なるものを作成しました(現在も開発中)。 ぶっちゃけ、メッセージ分離型rogueをRubyに移植した様なものです。 独自に、サーバ/クライアント、疑似リアルタイムという特徴を持っています。

ttyrec + RubyRogue = ??

それで、RubyRogueのゲーム中の様子をttyrecで撮ろうと考えたのですが、 これが上手く行きません。画面が崩れてしまいます。

まあ、ttyrecの元となったscript(1)のmanページにも

script は画面を操作しないコマンドを扱ったときに最もうまく動作する。

と書いてあるので、致し方ないところではあります。

他にも、ttyrecでslを 撮った場合も、崩れてしまいました。

何故、崩れるのか?

ほったらかしにするのも気味が悪いので、判らんなりに調べてみる事にしました。 可能性としては、

  1. 描画のタイミングとキャプチャのタイミングが合わない。
  2. cursesが変な事をしている。

が思い浮かびました。

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を忘れると、後の操作で、 もれなく端末表示が崩れます。 :-)

ttyplayサーバとtelnet

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'

うーん、これは、ちゃんとスクリプトにした方がいいかも?

VT100/xterm etc. → kterm(on Debian) 問題

TERM環境変数がvt100とかxtermとかの端末で撮ったRubyRogueの ttyrecordをktermで再生しようとすると、2 バイト日本語文字が 全て化けてしまいます。

化けた場合でも、次のいずれかを行なえば元に戻ります。

  1. Ctrl+中クリックでメニューを出し、EUC漢字モードに設定し直す。
  2. Ctrl+中クリックでメニューを出し、完全リセットを行なう。

そういえば、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) {

VT100/xterm etc. → kterm(on Debian) 問題に対する一つの解

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を見て変換するのが良さそうだな〜」とか 思っていましたが、端末によって存在したり存在しなかったりする命令 があるようなので、この案は駄目っぽいです。

kterm文字化けについての余談:RedHat 9

RedHat9で^[)0を出力しても、manページは化けないようです。 manコマンドに細工がしてあるんでしょうかね。 おかげで、RedHat9のktermでは問題がないものと勘違いするところでした。

残された問題

やっぱり、他環境で撮ったttyrecordファイルで、ゴミが残る問題でしょうか。

録箱

anraku@lemon.plala.or.jp