App by webrick
ruby webrick
前回はOpenBSDに備付のWEBサーバーでCGIをやってみた。じゃ、OpenBSD以外のOSはどうするん?
普通に考えたらapacheを入れる。そんなの大袈裟すぎる。ひょんな亊からgolangに備付のhttpの解説に行き当ってしまっていた(前回の最後の部分)。で、目覚めた訳よ。
WEBサーバーと構える必要が全く無いって亊。ブラウザーからのリクエストを受け付けて、それに返事出来れば、りっぱなWEBサーバーだって亊。
じゃ、golangでいいじゃんとも思ってしまうんだけど、コンパイラーだからなあ。たまには、軽くて便利なインタープリタに触ってみるか。猫の杓子もpythonは止めておいてrubyを登場してもらいます。
備付のWEBサーバー有ったよな。ええとwebrickってやつ。標準添付品のはずなんだけど、ruby 3.0になった時、分家に出されてしまったようだ。もし入っていなかった、gemから入れるべし。
ob$ doas gem install webrick Fetching webrick-1.7.0.gem Successfully installed webrick-1.7.0 Parsing documentation for webrick-1.7.0 Installing ri documentation for webrick-1.7.0 Done installing documentation for webrick after 2 seconds 1 gem installed
何で分家に出された? お家騒動ってか内紛があったのだろう。ruby3になって、性能が劇的にあがった。matz曰く 3 X 3 って亊らしい。ruby3になると3倍速くなります。webrickは、作りが古い為、この恩恵に与れない。ずっと居座っていると看板の足を引っ張る亊になる。なら、本体から外して分家に出しちゃ。これ、オイラーの勝手な邪推。どうなんでしょ?
ob$ ruby -run -e httpd . [2022-04-12 06:38:35] INFO WEBrick 1.7.0 [2022-04-12 06:38:35] INFO ruby 3.0.3 (2021-11-24) [x86_64-openbsd] [2022-04-12 06:38:35] INFO WEBrick::HTTPServer#start: pid=60970 port=8080
un.rb って言うunixもどきの真似をさせるモジュールにより、上記のように起動出来る。 カレント・ディレクトリィーがhtdocs兼cgi-binになる。ようするになんでもござれ状態だ。
少し資料を探っておく。
ruby sqlite3
rubyからもsqlite3を使えるようにする。 リナの場合、ヘッダー無いぞのイジメに会わないように、防御してからgemする。
sudo apt install libsqlite3-dev ; sudo gem install sqlite3
前回書いたrustのやつと似せてみた。
require 'sqlite3' def search(sw) ew = sw + "%" db = SQLite3::Database.new("/usr/share/dict/ejdict.sqlite3") stmt = db.prepare("SELECT word,mean FROM items WHERE word like ? limit 3") stmt.bind_param(1, ew) stmt.execute().each do |res| puts("<dt>#{res[0]}</dt>\n") puts("<dd>#{res[1]}</dd>\n") end stmt.close(); db.close() end puts("Content-type: text/html; charset=UTF-8\n") search("clap")
辞書は、 無料 英和辞書データ ダウンロード から頂いたものを、単語帳が有る場所に保存しておいた。 こんな有用な辞書を公開下さって、感謝です。 m(_ _)m
small server
app.rb
require 'webrick' srv = WEBrick::HTTPServer.new({ :DocumentRoot => './', :BindAddress => '0.0.0.0', :Port => 8080}) srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb') srv.mount('/foo.html', WEBrick::HTTPServlet::FileHandler, 'hoge.html') trap("INT"){ srv.shutdown } srv.start
view.rb
#!/usr/local/bin/ruby require 'cgi' cgi = CGI.new html = <<-EOF <html><head><title>cgi test</title></head><body> <h1>Hello</h1> </body></html> EOF puts cgi.header puts html
hoge.html
<html><head><title>test html</title></head><body> <h1>Test html</h1> </body></html>
BindAddressは何処からでもかかってきなさいというpython仕様に合わせてあるんで、良い子は注意してね。http://localhost:8080/foo.html とかすると、アクセス出来るよ。
古めかしいHTTPServletなんてクラス名が使われてるのが分家された一因なのだろうか。今時Javaと一緒にして貰いたくはないわって、いきまいている人が居るんだな。 Java Servlet
cgi化
これで役者は揃った。早速webrickなサーバーを介して、世界の標準入出力装置であるブラウザーから、辞書を引けるようにしてみよう。
sql.rbをcgi対応にする。ここで登場するのは、cgiモジュールだ。懐しいこったい。
#!/usr/bin/env ruby require 'cgi' : cgi = CGI.new() ew = cgi['ew'] puts( cgi.header(options = "text/html; charset=UTF-8") ) puts("<html><head><title>search</title></head><body>\n") search(ew) puts("</body></html>\n")
CGI.new("html3.0")とか指定出来るけど、なくても普通は困らない。これって、権威の象徴W3Cと現場の綱引きの象徴だからね。下手に指定すると、災難が降り掛かるぞ。詳細は、 どうしてHTML5が廃止されたのか を参照。
cgi用のスクリプトは、オフラインで確認出来るように配慮されてる(by cgi/core.rb)。
sakae@pen:/tmp/t$ ./sql.rb (offline mode: enter name=value pairs on standard input) ew=ruby # Ctl-d to continue Content-Type: text/html; charset=UTF-8 <html><head><title>search</title></head><body> <dt>ruby</dt> <dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / ルビー色の,深紅の, 日本発のプログラミング言語</dd> </body></html>
[2022-04-13 06:45:24] ERROR CGIHandler: sql.rb:\nenv: ruby: No such file or directory\n [2022-04-13 06:45:24] ERROR CGIHandler: sql.rb exit with 127 [2022-04-13 06:45:24] ERROR Premature end of script headers: sql.rb 10.0.2.2 - - [13/Apr/2022:06:45:24 JST] "POST /ejdict HTTP/1.1" 500 317 http://localhost/ -> /ejdict
OpenBSD特有な亊かも知れないけど、シェバングに #!/usr/bin/env ruby とかやって、勝手に正しい位置を探して貰おうとすると、上記のエラーになった。手抜きをせず、絶対PATHで指定してあげよう。
app
webric/config.rb
:DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"]
matzの発明品、index.rhtmlなんてのも顔を出している。このセットは、URLでDIRを指定した時、そのDIR内にこれらのファイルがあると、それが使われますよって指定だ。apacheとかの有名なやつだと、これらの他にも有効になってるぞ。例えば、home.html とかね。
で、これを踏まえて、app.rbのルーティングを次のように指定した。
srv.mount('/ejdict', WEBrick::HTTPServlet::CGIHandler, 'sql.rb') srv.mount('/', WEBrick::HTTPServlet::FileHandler, 'index.htm')
サーバーの公開エリアだどうなってるか、隠すんだな。対応するindex.htmは、
<html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head><body> English word to Japanese <br> <form method=POST action="/ejdict"> <input type="text" name="ew"> <br> <input type="submit" value="trans"> </form></body></html>
これはユーザーに公開されるけど、これを見たって、内部で実行されるcgiスクリプトは判明しない。多少はセキュリティーに寄与するのかな。
single file
ここまでで、app.rb index.htm sql.rb と3つのファイルを用意すれば使えるようになった。 でも、これって取回しに不便。それにsqlite3な辞書にアクセスする度にrubyのプロセスが余分に走る。これは許せないぞ。
出来る亊なら、app.rb これ一つにまとめたい、と、竹村健一的な欲求が出て来た。歳がバレるな。
と言う亊で、index.htmとsql.rbをapp.rbに吸収したやつにしてみた。アプリが走ると今居るdirに問答無用でindex.htmが作成される。まあ、3文字のサフィックスはWindows屋さんが好んで使うんで許せ。それから、書込み出来る場所って亊が必要。/tmpがお勧め。勝手に作られたindex.htmは放置してるんで、これも許せ。これらに不満が有るなら、勝手に改造されたし。
で、問題が … VirtualBOX上のOpenBSD ruby 2.7.2p137 では、問題なく動くんだけど、Debianな環境では、検索出来無い。辞書引きを拒否しちゃうんだ。なんでかな?
# Usage: run this script and then firefox http://localhost:8080/ # warning index.htm file auto maked in current dir require 'webrick' require 'sqlite3' KICK = 'index.htm' def setup_file(kick) src = '<html><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head><body> English word to Japanese <br> <form method=POST action="/ejdict"> <input type="text" name="ew"> <br> <input type="submit" value="trans"> </form></body></html> ' File.write(kick, src) end def httpd(kick) srv = WEBrick::HTTPServer.new({ :DocumentRoot => './', :FancyIndexing => false, :BindAddress => '0.0.0.0', :Port => 8080}) srv.mount('/', WEBrick::HTTPServlet::FileHandler, kick) srv.mount_proc("/ejdict") do |req, res| qry = req.query ew = qry['ew'] + "%" db = SQLite3::Database.new("/usr/share/dict/ejdict.sqlite3") stmt = db.prepare("SELECT word,mean FROM items WHERE word like ? limit 5") stmt.bind_param(1, ew) body = "<html><head><title>ejdict</title></head><body>\n" body << "<h2>SQL search #{ew} limit 5</h2>\n" stmt.execute().each do |rb| body << "<dt>#{rb[0]}</dt>\n" body << "<dd>#{rb[1]}</dd>\n" end stmt.close(); db.close() body << "</body></html>\n" res.status = 200 res['Content-Type'] = 'text/html; charset=UTF-8' res.body = body end trap("INT"){ srv.shutdown } srv.start end ### main ######### setup_file(KICK) httpd(KICK)
これで目出度くサーブレットになった。webrickもさぞかし満足してる亊でしょう。cgiの名前も消えて、20年のブランクを解消したぞ。もうこれで平成の原人と呼ばれなくなるな。
bugを探して1万行
Debianでは辞書を引いてくれない問題の検証。別の角度からって亊でcurlを使ってみた。まずは優等生なOpenBSDで確認。
vbox$ curl -sSv -X POST 'http://localhost:8080/ejdict' --data 'ew=ruby' * Trying 127.0.0.1:8080... * Connected to localhost (127.0.0.1) port 8080 (#0) > POST /ejdict HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.79.0 > Accept: */* > Content-Length: 7 > Content-Type: application/x-www-form-urlencoded > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Content-Type: text/html; charset=UTF-8 < Server: WEBrick/1.6.0 (Ruby/2.7.2/2020-10-01) < Date: Wed, 13 Apr 2022 04:01:35 GMT < Content-Length: 266 < Connection: Keep-Alive < <html><head><title>ejdict</title></head><body> <h2>SQL search ruby% limit 5</h2> <dt>ruby</dt> <dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / ルビー色の,深紅の, 本発のプログラミング言語</dd> </body></html> * Connection #0 to host localhost left intact
こちらは、Debian(64bit)の結果。綺麗さっぱりと辞書を引くのを拒否って塩梅。
sakae@pen:~$ curl -sSv -X POST 'http://localhost:8080/ejdict' --data 'ew=ruby' : <html><head><title>ejdict</title></head><body> <h2>SQL search ruby% limit 5</h2> </body></html> * Connection #0 to host localhost left intact
tmux de logging
Ruby初心者のためのByebugチュートリアルを見るとトレースが出来るみたい。
ログに落そうと byebug -t gd.rb |& tee LOG とかやると、固まってしまった。何故って、追求はしないで、ならば、tmuxでログって方向に舵をきる。ああ、突然出て来たgd.rbは、app.rbを改名したもの。期待を込めて、グッドデザイン賞ね。自分で言ってれば世話がないな。兎も角、FreeBSDでも普通に動いたぞ。
.tmux.conf
# start logging bind-key C-p pipe-pane -o '/bin/sh -c "while read -r LINE; do echo \"\${LINE}\" >> /tmp/TMUX.log; done "' \; display-message "Logging start." # stop logging bind-key C-q pipe-pane \; display-message "Logging end."
prefix C-p, prefix C-q で、ロギングをコントロール出来るはず。それからログの場所も/tmpを指定するも、全く無視されて$HOMEに出来ちゃった。出来上がったのは10000行を越てた。
別なロギングの方法として、tmuxが内部に蓄えているデータをファイルに落とす亊が出来る。
tmux capture-pane -p -S -1000 > TMUX.log
-pで標準出力へ。-S で、今迄ログされてる行を遡って行数分を出力(遡るんで、行数はマイナスで指定する)。ちょい使いには、こちらの方が便利そう。aliasで、tlogとか設定しておくとよい。
[sakae@fb ~]$ head ~/20220414-073227-4-1.0.log [2022_0414_073240_N] byebug -t gd.rb Tracing: /tmp/ruby-cgi/gd.rb:4 require 'webrick' [2022_0414_073240_N] [2022_0414_073240_N] [1, 10] in /tmp/ruby-cgi/gd.rb [2022_0414_073240_N] 1: # Usage: run this script and then firefox http://localhost:8080/ [2022_0414_073240_N] 2: # warning index.htm file auto maked in current dir [2022_0414_073240_N] 3: [2022_0414_073240_N] => 4: require 'webrick' [2022_0414_073240_N] 5: require 'sqlite3'
後は、出来の惡いDebianと対比させればいいんだな。難儀なこったい。
道しるべ
千里も道も一歩から、って昔の人は良い亊を言ってるな。でも、現代は忙しいのよ。出て来たログをほいほいとスキップしましょ。
/tmp/ruby-cgi/gd.rb:40 stmt.execute().each do |rb| /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:62 reset! if active? || done? /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:95 !done? /usr/local/lib/ruby/gems/2.7/gems/sqlite3-1.4.2/lib/sqlite3/statement.rb:64 bind_params(*bind_vars) unless bind_vars.e\ mpty?
こんな風に、gd.rbを頼りに進めば大丈夫。潜っていけよ。
差分を取ってみたぞ。
冒頭に出て来た検索プログラムをref.rbとしてトレース。辞書を引かない奴(一体形)をgd.rbとした。分かり易い所で、データベースをオープンする所から掲載する。
diff -y REF.log GD.log な結果。醜かったらスマソ。
ref.rb:5 db = SQLite3::Database.new("/usr/share/dict/ejdict | gd.rb:34 db = SQLite3::Database.new("/usr/share/dict/ejdi database.rb:66 mode = Constants::Open::READWRITE | Cons database.rb:66 mode = Constants::Open::READWRITE | Cons database.rb:68 if file.encoding == ::Encoding::UTF_16LE database.rb:68 if file.encoding == ::Encoding::UTF_16LE database.rb:75 mode = Constants::Open::READONLY if op database.rb:75 mode = Constants::Open::READONLY if op database.rb:77 if options[:readwrite] database.rb:77 if options[:readwrite] database.rb:82 if options[:flags] database.rb:82 if options[:flags] database.rb:89 open_v2 file.encode("utf-8"), mode, zv database.rb:89 open_v2 file.encode("utf-8"), mode, zv database.rb:92 @tracefunc = nil database.rb:92 @tracefunc = nil database.rb:93 @authorizer = nil database.rb:93 @authorizer = nil database.rb:94 @encoding = nil database.rb:94 @encoding = nil database.rb:95 @busy_handler = nil database.rb:95 @busy_handler = nil database.rb:96 @collations = {} database.rb:96 @collations = {} database.rb:97 @functions = {} database.rb:97 @functions = {} database.rb:98 @results_as_hash = options[:results_as_ database.rb:98 @results_as_hash = options[:results_as_ database.rb:99 @type_translation = options[:type_transl database.rb:99 @type_translation = options[:type_transl database.rb:100 @type_translator = make_type_translato database.rb:100 @type_translator = make_type_translato database.rb:725 if should_translate database.rb:725 if should_translate database.rb:732 NULL_TRANSLATOR database.rb:732 NULL_TRANSLATOR database.rb:101 @readonly = mode & Constants::O database.rb:101 @readonly = mode & Constants::O database.rb:103 if block_given? database.rb:103 if block_given? ref.rb:6 stmt = db.prepare("SELECT word,mean FROM items WHE | gd.rb:35 stmt = db.prepare("SELECT word,mean FROM items W database.rb:147 stmt = SQLite3::Statement.new( self, sq database.rb:147 stmt = SQLite3::Statement.new( self, sq database.rb:148 return stmt unless block_given? database.rb:148 return stmt unless block_given? ref.rb:7 stmt.bind_param(1, ew) | gd.rb:36 stmt.bind_param(1, ew) ref.rb:8 stmt.execute().each do |res| | gd.rb:38 body = "<html><head><title>ejdict</title></head> > gd.rb:39 body << "<h2>SQL search #{ew} limit 5</h2>\n" > gd.rb:40 stmt.execute().each do |rb| statement.rb:62 reset! if active? || done? statement.rb:62 reset! if active? || done? > utils.rb:192 @queue.clear > utils.rb:166 now = Process.clock_gettime(Process: > utils.rb:167 wakeup = nil > utils.rb:168 to_interrupt.clear > utils.rb:169 TimeoutMutex.synchronize{ > utils.rb:170 @timeout_info.each {|thread, ary| > utils.rb:182 to_interrupt.each {|arg| interrupt(* > utils.rb:183 if !wakeup > utils.rb:184 @queue.pop statement.rb:95 !done? statement.rb:95 !done? statement.rb:64 bind_params(*bind_vars) unless bind_var statement.rb:64 bind_params(*bind_vars) unless bind_var statement.rb:65 @results = ResultSet.new(@connection, s statement.rb:65 @results = ResultSet.new(@connection, s resultset.rb:73 @db = db resultset.rb:73 @db = db resultset.rb:74 @stmt = stmt resultset.rb:74 @stmt = stmt statement.rb:67 step if 0 == column_count statement.rb:67 step if 0 == column_count statement.rb:69 yield @results if block_given? statement.rb:69 yield @results if block_given? statement.rb:70 @results statement.rb:70 @results resultset.rb:133 while node = self.next resultset.rb:133 while node = self.next resultset.rb:104 if @db.results_as_hash resultset.rb:104 if @db.results_as_hash resultset.rb:108 row = @stmt.step resultset.rb:108 row = @stmt.step resultset.rb:109 return nil if @stmt.done? resultset.rb:109 return nil if @stmt.done? resultset.rb:111 row = @db.translate_from_db @stmt.type | gd.rb:44 stmt.close(); db.close() statement.rb:118 must_be_open! | gd.rb:45 body << "</body></html>\n" statement.rb:126 if closed? < statement.rb:119 get_metadata unless @types < statement.rb:136 @columns = Array.new(column_count) do < statement.rb:137 column_name column < statement.rb:137 column_name column < statement.rb:139 @types = Array.new(column_count) do |c < statement.rb:140 column_decltype column < statement.rb:140 column_decltype column < statement.rb:120 @types < database.rb:717 @type_translator.call types, row < database.rb:722 NULL_TRANSLATOR = lambda { |_, row| row } < resultset.rb:113 if row.respond_to?(:fields) < resultset.rb:122 row = ArrayWithTypesAndFields.new(ro < resultset.rb:125 row.fields = @stmt.columns < statement.rb:102 get_metadata unless @columns < statement.rb:103 return @columns < resultset.rb:126 row.types = @stmt.types < statement.rb:118 must_be_open! < statement.rb:126 if closed? < statement.rb:119 get_metadata unless @types < statement.rb:120 @types < resultset.rb:127 row < resultset.rb:134 yield node < ref.rb:9 puts("<dt>#{res[0]}</dt>\n") < <dt>ruby</dt> < ref.rb:10 puts("<dd>#{res[1]}</dd>\n") < <dd>〈C〉紅玉,ルビー / 〈U〉ルビー色,深紅色(しんこうしょく) / < resultset.rb:104 if @db.results_as_hash < resultset.rb:108 row = @stmt.step < resultset.rb:109 return nil if @stmt.done? < ref.rb:12 stmt.close(); db.close() <
ランデブーが失敗した、まれなケースを踏んじゃった? Debian 32,64bitの両方で辞書引の拒否。BSD系は問題ないって亊から、十分に考えられるな。それとも、こういう使いかたは想定してませんって亊? これ以上は、オイラーの手に余るな。
たのしいOSSコードリーディング: Let’s read WEBrick こういう楽しいのがあるんで、トレース結果と突き合せてみるかな。何かヒントが見付かるかも知れないぞ。
諦めきれないオイラーはOpenBSDで採取して比べてみた。左がOpenBSDで右がDebian。 OpenBSDには、diff -y がなくて、代わりにsdiffが用意されてた。-w は画面サイズ指定。
vbox$ sdiff -w 126 OB.log GD.log
resultset.rb:108 row = @stmt.step resultset.rb:108 row = @stmt.step resultset.rb:109 return nil if @stmt.done? resultset.rb:109 return nil if @stmt.done? resultset.rb:111 row = @db.translate_from_db @stmt.type | gd.rb:44 stmt.close(); db.close() statement.rb:118 must_be_open! | gd.rb:45 body << "</body></html>\n" statement.rb:126 if closed? < statement.rb:119 get_metadata unless @types < statement.rb:136 @columns = Array.new(column_count) do < statement.rb:137 column_name column < statement.rb:137 column_name column < statement.rb:139 @types = Array.new(column_count) do |c < :
statement.rb:139以降を省略しちゃったけど、この後の流れはREF.logと同一だった。
rust-cgi
面白い物を見付た。cgiをサポートしないWEBサーバーにcgiを追加するものらしい。 早速行ってみよー。 rust-cgi
// How many bytes do we have to read for request body // A general stdin().read_to_end() can block if the webserver doesn't close things let content_length: usize = env_vars.get("CONTENT_LENGTH") .and_then(|cl| cl.parse::<usize>().ok()).unwrap_or(0); let mut stdin_contents = vec![0; content_length]; stdin().read_exact(&mut stdin_contents).unwrap();
前回苦労した所が、こんなにもスマートに記述出来るんだ。まだまだ、オイラーはひよこだな。
python + ruby
ああ、それからrust-cgiに出て来る説明に疑問が有っtので、pythonのコードを覗いてみたよ。そしたら、–cgiってオプションを付けるとCGI対応になる亊が判明。
PythonでHTTPDして、rubyのcgiを動かすっていうコラボをやってみた。
sakae@pen:/tmp/t$ tree . ├── htbin │ └── sql.rb └── index.html
htbinの代わりにcgi-binでもOK。サーバーをキックするindex.htmlを調整してね。
sakae@pen:/tmp/t$ python3 -m http.server --cgi 8080 Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ... 127.0.0.1 - - [16/Apr/2022 15:03:30] "GET / HTTP/1.0" 200 - 127.0.0.1 - - [16/Apr/2022 15:03:38] "POST /htbin/sql.rb HTTP/1.0" 200 - 127.0.0.1 - - [16/Apr/2022 15:03:58] "POST /htbin/sql.rb HTTP/1.0" 200 -
ちょいとした疑問が福を呼ぶな。それもこれもrustをやっていればこそのものです。
たまにrubyをやると、緩くていいなと思うぞ。型について余り気にする必要無いしね。 rubyで関数を呼ぶ時、思わず引数の型を書こうとして、指が止ってしまったのは、秘密事項だ。