ぶていのログでぶログ

思い出したが吉日

reddish-shell v0.9.0 開発進捗 | 内蔵Rubyコマンド/break,continueの実装/for文の追加/シグナルハンドラーの見直し | mruby3.0対応の予定

reddish-shellのバージョンアップ報告。 粛々と開発つづけてきて、やっと普通のシェルくらいの機能にはなってきたかな?しかし、まだ実用までは長い。。 今回は特にシグナルハンドリング周りがとても大変だった…。

commit一覧

core mgemをすべて使うようにした

いままで、最低限必要なcore mgemのみ使うようにしていた。 しかし、これは不便だし、特にバイナリサイズを気にするほどのものでもないのですべてのcore mgemを使うようにした。 だいぶ、CRubyとの差もなくなって開発しやすくなって最高。

内蔵Rubyコマンドの実装

reddishの開発当初から実装しようと思っていた、内蔵Rubyコマンドを実装した。 reddishを使えば、CRubyなどをインストールしなくてもRubyが使えるという便利な機能。 ちょっとしたスクリプトはシェルスクリプトで書いて、複雑な処理が必要だったり速度が必要な場合は、内蔵Rubyでスクリプトを作って動かすみたいなのを想定している。

コマンド名は iruby にした。internal ruby ? implemented ruby ? rubyコマンドと同じインターフェイスにしようと思っているけど、今は -v / -e オプション、スクリプトの読み込みのみの実装になっている。

reddish> iruby -v
iruby use mruby 2.1.2 (2020-08-06)
reddish> iruby -e 'puts "hello"'
hello
reddish> cat test.rb
puts "hello"
reddish> iruby test.rb
hello

irubyの実装は簡単だと思ったのだけど、案外考えることが多かった。 というのも、mrubyではKernel#bindingがサポートされていない。 なので、雑にinstance_evalなどでスクリプトを実行してしまうと、reddishが動いているmruby VMが壊される恐れがある。 そこで、少し富豪的ではあるがmruby VMをもう1つ生成して、その中で実行するようにしている。

// 新しいmruby VMを作る
mrb_state* m = mrb_open();
mrbc_context* c = mrbc_context_new(m);

// 実行するRubyスクリプトのファイル名を設定する
// バックトレースなどを表示するときに参照される
mrbc_filename(m, c, filename);

// Rubyのコードをcodeに格納し↓を実行する
mrb_load_string_cxt(m, code, c);

if (m->exc) {
  // 例外が発生しRubyコードの実行が終了した場合の処理
}

また、スクリプト内でProcess#exitが呼び出されてしまうと、exitシステムコールが呼び出されてしまいreddishごとexitしてしまうため、SystemExitをexceptionするようにメソッドを再定義している。 こんな感じで今は実装しているのだがこれだけではまだちょっと足りなく、Rubyスクリプトで無限ループが発生するとreddish側から停止することができなくなる。 そこで、この問題を解決するために、forkしてから実行するようにしようと思っている。

break/conitnue/nextの実装

whileやfor文などの制御構文を制御するために、break/continue/nextの実装をした。 nextはcontinueと同じくループ文の先頭に強制的に戻る。Rubyでの構文なので取り入れた感じ。 どのように実装するか悩んでいたが、bashの実装を参考にすることにした。 具体的には、bashではloop_level、breaking、continuingという3つのグローバル変数が定義されていて、ループ文に入るごとにloop_level++して抜けるとloop_level--している。 breakやcontinueコマンドが呼び出されたら、breakingやcontinuing変数に値を設定して、whileやfor文ではbreaking/continuingの値がセットされていたら相応の処理をするという流れになっている。 同じように、reddishでも同様のインスタンス変数を設定して制御をしている。

ローカル変数と環境変数の使い分けを正しくした

いままで、シェル上で定義した変数はすべて環境変数にセットしていた。 しかし、これはあまりよろしくはないので、ローカル変数と環境変数の使い分けをするようにした。 具体的には、すでに環境変数として定義されている変数は環境変数としてセットし、そうじゃない場合ローカル変数として扱うようにした。 ローカル変数から環境変数に、その逆をするにはexportコマンドを使う必要があるがまだ実装していないので、そのうち実装する予定。

この機能を実装するにあたって、bashの挙動を確認したのだが、exportコマンドの挙動を正しく理解していなかった。 exportは対象の変数にexportフラグをつけるという挙動であった。 内部的にはdeclareに-x フラグをつけている。 -xフラグがついている変数への読み書きは環境変数への読み書きになる。 今までexportしたときにだけ環境変数を読み書きするだけだと勘違いしていたのであった。

readコマンドの実装

指定されたfd(デフォルトでは0)から1行読み込んで、変数に格納する内部コマンド。 複数の変数に分割して格納するようにしているので、最低限の機能はあると思う。

パイプの処理を正しくした

例えば、 echo hoge | while read FOO; echo $FOO; end みたいなコマンドはsyntax errorとなっていたので修正した。

for文を追加した

新しい制御構文としてfor文を追加した。

reddish> for FOO in a b c; echo $FOO; end
a
b
c

なお、パーサーの問題で、in句を省略したときに、do句を省略することはできない。

for FOO; echo $FOO;end     #=> syntax error
for FOO;do echo $FOO;end #=> syntax ok

CRubyの制御構文を見ても、in句は省略できないことになっているのでこれを直すつもりはない。 bashにおいて、in句を省略すると $@ から読み込むのだが、reddishでは$@を実装していないので、$@と表示される…。 これもそのうち実装する予定。

SIGTTIN/SIGTTOU/SIGTSTPシグナルの扱いを見直した

reddishでは外部コマンドを実行するときは、forkしてプロセスグループを設定してからexecveするという流れになっている。 これでたいていのコマンドは問題ないのだが、bashなどttyにアクセスする必要があるコマンドの場合、刺さってしまう。 なぜなのか不思議だったのだが、原因はbashに対していttyがSIGTTINシグナルを送っていることであった。 ttyでは自身を所有していないプロセスグループにあるプロセスからの読み込み・書き込みが行われると、ttyが対象のプロセスにSIGTTIN/SIGTTOUを送るという仕様になっているのであった。 参考: https://qiita.com/angel_p_57/items/ff1c0d054714b7982ca5

これを回避するためには、tcsetpgrpを使って一時的にttyの所有プロセスグループを変更する必要がある。 https://linuxjm.osdn.jp/html/LDP_man-pages/man3/tcgetpgrp.3.html

シグナルハンドラーを見直した

いままで、雰囲気でシグナルの処理をしていた。 しかし、whileやfor文などのループを実装したことで見直す必要がでてきた。 というのも、今までの実装ではループ文で無限ループしたときに止める方法がなかった。 というより、ループ中にSIGINTを送るとreddishプロセスごと落ちてしまう。 そこで、sigactionとシグナルハンドラを使って、SIGINTがきたときに割り込まれたとわかるようにinterrupt_stateグルーバル変数のフラグを立てるという処理にし対応した。

対応した内容を書くと1行なのだがここにたどり着くまでかなりの時間を要した・・・。 他のシェルがどういう風に実装しているのかを調べるために、bashやfish-shell、kshやzshのコードを読み込んでいた。 そして、sigactionとシグナルハンドラを使う方法にたどり着いたのだが、このシグナルハンドラの実装についての資料が少なくそれも実装に時間がかかる要因であった。 シグナルハンドラは非同期的に呼び出されるので、何も考えずに実装するとバグが発生してしまう。 JPCERTの記事codezineの記事に詳しい。

これらの記事の中で、volatile sig_atomic_t型以外のグローバル変数にはアクセスしてはいけないと書いてあるのだが、bashでは普通にint型の変数や構造体に対して読み書きをしていてなんでだ・・・っとなっていた。 ふつうのLinuxプログラミングオライリーのLinuxシステムプログラミングを読んだりしたのだが、sigactionについては書いてあったがシグナルハンドラについては詳しくかいておらずよくわからなかった。 おそらく、未定義な動作ではあるが、実情は正しく動くからこの実装になっているのではないかと思う。多分。 sig_atomic_tも実態はint型なので、アトミックに操作できるようになっていたりするのだろうか。

ちなみに、fishではC++のメモリバリア機能を使っていい感じに対応していた。また、kshではlibastというC言語の便利関数を集めたAT&Tが作ったらしい(ソースのコメントに書いてあった)ライブラリを内蔵していてその中でアトミックな操作をしているようだった。

mruby3.0リリース🎉

ついにmruby3.0がリリースされましたね! reddishももちろんmruby3.0に対応していこうとおもっている。 雑にビルドしたらもちろん通らなかった。 どうも、今までbuild_config.rbに依存するmgemを書いていたのだが、正しくはmrbgems.rakeにspec.dependencyで定義するようだ。 それを直しても、bintestが通らなくなってしまったので、次のバージョンでは直したい。