ujunのブログ

mruby-hibari on mod_mrubyが動いた。

mruby-hibariというmrbgemがあり、主要なWebサーバならなんとM/WレベルでRackテイストなWebアプリを書けるようです。

kysnm.hatenablog.com

kysnm.hatenablog.com

Apacheでやってみたところ、少し修正して、なんとか動きました!!

まず、通常通り以下のようにmod_mrubyをビルド。

$ git clone https://github.com/matsumoto-r/mod_mruby
$ cd mod_mruby
$ vim build_config.rb  # 依存gemにkentaro/mruby-hibariを追加します。
$ sh test.sh
$ sh build.sh

で、こうして生成されたmod_mruby.soモジュールをApacheにて利用します。

サンプルにならえば、以下のようにLocationとmrubyHandlerMiddleCodeでhello, world!!ができます。

curl!!

$ curl http://lcoalhost/rack_base?hoge=fuga
$ 

と思いましたが、なにも返ってきません。

でもH2OでもNginxでもうまくいってるっぽいんだけどと思いつつ、mruby.confで何をやってもうまくいってくれない。。

さんざん見直した末、以下が種明かしだと気付きました。

github.com

'nginx' || 'apache''nginx'と評価されるようですね。なので、

case engine
when 'nginx' || 'apache' # => when 'nginx'

となるため、caseの中に'apache'がマッチする条件がなくなってしまうということのよう。

複数条件に正しくマッチさせるには、

case engine
when 'nginx', 'apache'

とする必要があるようです。

このPR、送ったら即座にmergeしていただけました!

curl!!

$ curl http://localhost/rack_base?hoge=fuga
Hello, World!hoge: fuga

ちゃんと出ました。

自分が普段お世話になっているmrubyとそのmrbgems達のコントリビュータの末席を汚すことになり、鼻血がとまりません。

続 mod_mrubyでmrubyのビルドサーバを書いた

この記事で、mod_mrubyで数行書くとmrubyのビルドサーバが書けるということをやっていたのですが、@matsumotoryさんが以下のようなコメントをされていたので、対応をしました。

libmruby.flags.makはmrubyビルド時にlibmruby.aと同パスに生成されますが、以下のようなフォーマットになっていて、libmruby.aをスタティックリンクする際にこのファイルからオプションを得るみたいな用途で必要になります(参考:人間とウェブの未来 - マルチプラットフォームでmrubyを使ってHTTP通信する方法)。

MRUBY_CFLAGS = -g -std=gnu99 -O3 -Wall -Werror-implicit-function-declaration -Wdeclaration-after-statement -Wwrite-strings -I\"/path/to/mruby/include\"
MRUBY_LDFLAGS =  -L/path/to/mruby/lib
MRUBY_LDFLAGS_BEFORE_LIBS = 
MRUBY_LIBS = -lmruby -lm -lreadline -lncurses

この情報をサーバから返すにはどうしたらいいか。。。

現状、libmruby.aをレスポンスボディで返しているので、さらにボディに追加するのは難しいってことで、 無理やりレスポンスヘッダに設定することで乗り切ることにしました!!

生成されたlibmruby.flags.makを読み込み、(左辺)=(右辺)のような形でマッチした部分をキャプチャし、 (key, value) = (左辺, 右辺) としてレスポンスヘッダに突っ込んでいます。

@@ -8,6 +8,16 @@
 # build mruby 
 `cd /var/www/html/mruby; rake`
 
+# set values in libmruby.flags.mak to response headers
+File.open("/var/www/html/mruby/build/host/lib/libmruby.flags.mak") do |file|
+  reg = Regexp.compile("([^=]+)\s=(.*)\n")
+  file.each do |line|
+    if reg =~ line
+      r.headers_out[$1] = $2
+    end
+  end
+end
+
 # return the generated static library
 r.filename = "/var/www/html/mruby/build/host/lib/libmruby.a"

ということで、このサーバに対してcurlすると、レスポンスヘッダの中に欲しい情報が入るようになりました。 (以下の、MRUBY_CFLAGS, MRUBY_LDFLAGS, MRUBY_LDFLAGS_BEFORE_LIBS, MRUBY_LIBS)

$ curl -v -d "`cat /path/to/build_confg.rb`" -o libmruby.a http://hostname:8080/mruby-build

・・・

> POST /mruby-build HTTP/1.1
> User-Agent: curl/7.37.1
> Host: hostname:8080
> Accept: */*
> Content-Length: 848
> Content-Type: application/x-www-form-urlencoded
> 
} [data not shown]
* upload completely sent off: 848 out of 848 bytes
100   848    0     0  100   848      0     13  0:01:05  0:01:04  0:00:01     0< HTTP/1.1 200 OK
< Date: Mon, 21 Sep 2015 13:04:40 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< MRUBY_CFLAGS:  -g -std=gnu99 -O3 -Wall -Werror-implicit-function-declaration -Wdeclaration-after-statement -Wwrite-strings -I\"/var/www/html/mruby/include\"
< MRUBY_LDFLAGS:   -L/var/www/html/mruby/build/host/lib
< MRUBY_LDFLAGS_BEFORE_LIBS:
< MRUBY_LIBS:  -lmruby -lm -lcrypto
< Last-Modified: Mon, 21 Sep 2015 13:05:44 GMT
< ETag: W/"6fbc80-520418a0948d2"
< Accept-Ranges: bytes
< Content-Length: 7322752
< 

・・・

@matsumotoryさん有難うございます!!

mod_mrubyでmrubyのビルドサーバを書いた

build_config.rbをそのままPOSTすると、mrubyの静的ライブラリを返してくれるサーバを書きました。

なにが嬉しいかといえば、ローカルにCRuby等を用意しなくても、以下を実行してlibmruby.aをコロッと作れるので、 開発環境に1台このビルドサーバを置いておけば、任意の場所からおもむろにmrubyを生成することができるのです。 あとは、これをGoに組み込んだりして遊ぶんです。

$ curl -d "`cat /path/to/build_config.rb`" -o libmruby.a http://hostname:8080/mruby-build

github.com

インストール&コンテナ起動

これは、setup.shを実行するだけです。

#!/bin/bash

git clone https://github.com/matsumoto-r/mod_mruby
cp -p ./Dockerfile mod_mruby
cp -p ./mruby-build.rb mod_mruby/docker/hook
cp -p ./mruby.conf mod_mruby/docker/conf

cd mod_mruby

docker build -t local/mruby-build-server .
docker run -d -p 8080:80 local/mruby-build-server 

まずmod_mruby本体をcloneしてきています。

このままmod_mrubyの手順にしたがってdocker buildすると、サンプルのapache設定ファイルとフックに渡すmrubyスクリプトを使ったDockerイメージが出来上がります。

ですが、ここでは、この2つをそれぞれ用意したmruby.conf, mruby-build.rbで置き換えしています。また、 Dockerfileも自前のもので置換しています。

置換したDockerfileについては、mod_mrubyオリジナルのものに、以下を追記したものになります。

# clone mruby 
RUN cd /var/www/html; git clone https://github.com/mruby/mruby
RUN chown -R www-data:www-data /var/www/html/mruby

# create the module directory and add files 
RUN mkdir /etc/apache2/mruby-build
ADD docker/conf/mruby.conf /etc/apache2/mruby-build/mods-available/mruby.conf
ADD docker/hook/mruby-build.rb /etc/apache2/mruby-build/mruby-build.rb

乱暴ですが、ドキュメントルートにmrubyのリポジトリをcloneしてきて、POSTされたbuild_config.rbを使ってここでmrubyをビルドすることを考えています。
※ もともとmod_mrubyの生成時に使っている/usr/local/src/mod_mruby/mrubyがあるはずですが、なんとなくそれはそれで手をつけたくはないのでそっとしときます。

次に、置換したmruby.confは以下です。

<IfModule mod_mruby.c>
  <Location /mruby-build>
    mrubyTranslateNameFirst /etc/apache2/mruby-build/mruby-build.rb
  </Location> 
</IfModule>

mrubyTranslateNameFirstフックにmruby-build.rbを渡しているだけです。

そして、/mruby-buildルートにアクセスが来た時実行されるmruby-build.rbが以下です。

r = Apache::Request.new()                                                                                

# store the sent mruby_config.rb to /var/www/html/mruby  
File.open("/var/www/html/mruby/build_config.rb", "w") do |file|
  file.write(r.body.to_s)
end

# build mruby 
`cd /var/www/html/mruby; rake`

# return the generated static library
r.filename = "/var/www/html/mruby/build/host/lib/libmruby.a"

Apache.return(Apache::OK)

POSTされたbuild_configを/var/www/html/mruby/build_config.rbに保存して、これを使ってmrubyをビルドしています。 最後に、r.filenameにlibmruby.aを指定することで、クライアントに静的ライブラリを返しています。

setup.shが最後まで動けば、コンテナが起動しているはずです。 あとはcurlで、libmrubyを生成してください。

$ curl -d "`cat /path/to/build_config.rb`" -o libmruby.a http://hostname:8080/mruby-build

このコンテナ、このままHerokuデプロイすることも考えたのですが、 POSTされたスクリプトをそのまま実行部に渡すというのがちょっと怖かったので、 やっぱり見送りました。。

ローカルでちょこちょこ遊ぶにはちょうどよい作りなんじゃないかと思います。

この調子で、mod_mrubyでどんどんオレオレサーバを量産していきたいです。

MySQLのUDFをGoで書く

前回、Go1.5を使ってC共有ライブラリを生成しました。

この時は、単純にsoをビルドしてpythonのREPLからエクスポートされた関数が実行できるかを見たのですが、このテクノロジーを使ってMySQLのUDFをGoで書いてみたのが今回です。 環境は、以下です。

$ cat /etc/redhat-release 
CentOS release 6.6 (Final)
$ go version
go version go1.5 linux/amd64
$ mysql
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.1.73    |
+-----------+
1 row in set (0.00 sec)

コードは、以下です。 github.com



まずは、ビルドします。

$ go build -buildmode=c-shared -o main.so main.go

次に、生成されたライブラリを、mysqlプラグイン配置先にコピーします。

$ sudo cp -p main.so  /usr/lib64/mysql/plugin/

あとは、mysqlCLIからUDFを定義し、実行します。 いろいろ考慮は足りていないとは思いますが、OSのシェルコマンドを文字列として渡せば実行結果が返ってくる的なものになっています。

mysql> create function myexec returns string soname 'main.so';
Query OK, 0 rows affected (0.00 sec)

mysql> select myexec("hostname");
+---------------------------------+
| myexec("hostname")              |
+---------------------------------+
| vagrant-centos66.vagrantup.com
 |
+---------------------------------+
1 row in set (0.01 sec)


書いてみてですが、Goで書けるようになったこと自体は面白くて、いろいろ妄想が膨らみますが、 以下のように、cgoで頑張って型変換をこねくり回しているので、煩雑という感じが。。。

//export myexec
func myexec (initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *C.ulong, is_null *C.char, error *C.char) *C.char { 
  out, err := exec.Command(C.GoString(*args.args)).Output()
  if err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
  result = C.CString(string(out))
  *length = C.ulong(utf8.RuneCountInString(C.GoString(result)))
  return result
}

前回のように、ここにさらにmrubyまで導入して、 CとGoとmrubyでUDFを書くこともやってみたいけど更に複雑化するだけかも。。

go-mrubyでmrubyをC共有ライブラリに突っ込む

Go1.5から、C共有ライブラリを生成するオプションが追加されたらしい。

そこで、Goにmrubyを組み込んでC共有ライブラリを作成し、使ってみた。 環境は、

まずは、go-mrubyをダウンロード。

go get -d github.com/mitchellh/go-mruby

続いて、リポジトリのreadme通りmrubyをビルド。(ここで独自にmrbgemを組み込みたい場合は、makeしないで自前でビルドしたほうがよい)

cd $GOPATH/src/github.com/mitchellh/go-mruby
make

次に、試しに以下のようなコードを任意の場所に用意。 LoadString()で、引数の文字列をmrubyのコードとして実行し戻り値を受け取っている。

// example.go
package main                                                                                                                                                                                                                      
                                                                                                                                                                                                                                  
import ( 
  "C"                                                                                                                                                                                                                     
  "github.com/mitchellh/go-mruby"                                                                                                                                                                                                 
)                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                  
//export example                                                                                                                                                                                                                      
func example() {                                                                                                                                                                                                                      
  mrb := mruby.NewMrb()                                                                                                                                                                                                           
  defer mrb.Close()                                                                                                                                                                                                               
                                                                                                                                                                                                                                  
  _, err := mrb.LoadString(`5.times {|num| puts "*" * num}`)                                                                                                                                                                      
  if err != nil {                                                                                                                                                                                                                 
  ¦ panic(err.Error())                                                                                                                                                                                                            
  }                                                                                                                                                                                                                               
}                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                  
func main() {                                                                                                                                                                                                                     
} 

上記example.goと同ディレクトリに、最初に生成しておいた静的ライブラリ(libmruby.a)を置くことも忘れない。

cp -p $GOPATH/src/github.com/mitchellh/go-mruby/libmruby.a /path/to/example

最後に、以下でsoが生成されるぞお!

go build -buildmode=c-shared -o example.so example.go

と思ったら、エラーが発生した。

# command-line-arguments
/path/to/linker: running clang failed: exit status 1
clang: error: no such file or directory: 'libmruby.a'

いろいろ試してみたけど、これは、 go-mrubyのmruby.go内で、cgoのインポート時にLDFLAGSに指定しているlibmruby.aが見つからないと言っているので、

とりあえず、以下のようにmruby.go自体を書き換えてしまう。

@@ -3,7 +3,7 @@ package mruby
 import "unsafe"
 
 // #cgo CFLAGS: -Ivendor/mruby/include
-// #cgo LDFLAGS: libmruby.a -lm
+// #cgo LDFLAGS: /full/path/to/libmruby.a -lm
 // #include <stdlib.h>
 // #include "gomruby.h"
 import "C"

再度ビルドして、example.soが生成された!

example()がエクスポートされているかを確認する。よさそう。

$ nm example.so | grep -i example                    
0000000000071a90 T __cgoexp_a4c9f6073c4b_example
00000000000026e0 T _example
0000000000071ad0 t main.example
00000000001fc400 s main.example.f

ということで、例によってpythonのREPLでexample()を実行してみる。出た!

>>> import ctypes
>>> lib = ctypes.CDLL("./example.so")
>>> lib.example()

*
**
***
****
1



以上、非常に手軽にgoでC共有ライブラリを書けて、実は中身はmrubyでした!! ということができました。

次に、LoadString()でmrubyを読みこむ部分を、ファイルから読めるようにすると良いかなと思う。 そうすればリビルドすることなく挙動をコントロールできるという恩恵が得られます。

それは次回のエントリにしよう。。。

初Pull Requestがmergeされた

さきほど、自分のGitHubページを見ていたら、先日送信した初プルリクがマージされたことがわかった。

github.com

自分のアカウントがcontributorに並んだ!

と思って眺めていたら、あのmattnさんもcontributorに入っていた。 嬉しいなあ。

MySQLのCLIでコマンドヒストリをpeco検索・実行

pecoでシェルのコマンド履歴を検索・実行することが多いです。 書き捨てのワンライナーや滅多に叩かないコマンドなどでも、とりあえず一度でも実行していれば、 曖昧な記憶を頼りに再実行することができるからです。

これを、mysqlcliでも同様のことができないかと思いました。 管理系のコマンドが全く覚えられないのはもとより、書き捨ての簡単なクエリも後から再実行できたら楽かなと。。

少し調べてみても、似たような話題にヒットしなかったので、 取り急ぎ以下のような感じでやっています。

まず、以下の2つのスクリプトを用意します。

mysql_peco.sql

system /path/to/mysql_peco.sh
source /path/to/tmp.sql

mysql_peco.sh

#!/bin/zsh                                                                                                                                                                                                                    
                                                                                                                                                                                                                               
BUFFER=$(sed -e "s/\\040/ /g" $MYSQL_HISTFILE | sed -e 's/\\//g' | egrep ";$" | egrep -i "^select|^update|^insert|^show|^commit|^use|^pager|^desc" | awk '!a[$0]++' | peco) 
echo $BUFFER > /path/to/tmp.sql 

あとは、mysqlcliから上記mysql_peco.sqlをsourceするだけ。 動きとしては、履歴ファイルをpecoして一旦一時スクリプト(tmp.sql)に保存して、最後に一時スクリプトをsourceしているだけですが!

これで、記憶力が悪い自分でも、さくさくオペレーションできるようになったはず。。 egrepの部分は思いつくまま書いたのでカスタマイズしていこうと思います。

なお、以下のように.editrcにショートカットを定義しておくと、さらに楽。

.editrc

 mysql:bind -s "^R" "source /path/to/mysql_peco.sql;"