続 mod_mrubyでmrubyのビルドサーバを書いた
この記事で、mod_mrubyで数行書くとmrubyのビルドサーバが書けるということをやっていたのですが、@matsumotoryさんが以下のようなコメントをされていたので、対応をしました。
libmruby.flags.makのような情報も返せるとさらによいなぁ / すごい!便利!! / “mod_mrubyでmrubyのビルドサーバを書いた - ujunのブログ” http://t.co/M8kNFQW86t
— MATSUMOTO, Ryosuke (@matsumotory) 2015, 9月 16
@matsumotory 有難うございます!取急ぎレスポンスヘッダに該ファイルの内容を積むようにしたのですが、他に方法が思付きませんでした。。https://t.co/IiKZSwC9lq RT @matsumotory: libmruby.flags.makのような情報も
— Jun Uchino (@u3jun) 2015, 9月 21
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
インストール&コンテナ起動
これは、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/
あとは、mysqlのCLIから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された
MySQLのCLIでコマンドヒストリをpeco検索・実行
pecoでシェルのコマンド履歴を検索・実行することが多いです。 書き捨てのワンライナーや滅多に叩かないコマンドなどでも、とりあえず一度でも実行していれば、 曖昧な記憶を頼りに再実行することができるからです。
これを、mysqlのcliでも同様のことができないかと思いました。 管理系のコマンドが全く覚えられないのはもとより、書き捨ての簡単なクエリも後から再実行できたら楽かなと。。
少し調べてみても、似たような話題にヒットしなかったので、 取り急ぎ以下のような感じでやっています。
まず、以下の2つのスクリプトを用意します。
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
あとは、mysqlのcliから上記mysql_peco.sqlをsourceするだけ。 動きとしては、履歴ファイルをpecoして一旦一時スクリプト(tmp.sql)に保存して、最後に一時スクリプトをsourceしているだけですが!
これで、記憶力が悪い自分でも、さくさくオペレーションできるようになったはず。。 egrepの部分は思いつくまま書いたのでカスタマイズしていこうと思います。
なお、以下のように.editrcにショートカットを定義しておくと、さらに楽。
.editrc
mysql:bind -s "^R" "source /path/to/mysql_peco.sql;"
vagrant-global-statusをmrubyで書いた
前提
vagrant にはもともとglobal-statusオプションがあり、以下のように仮想マシンの状態をリストすることができます。
$ vagrant global-status
id name provider state directory
-------------------------------------------------------------------------------------
941f568 default virtualbox aborted /Users/juchino/my_vagrant/centos64_oracle
41ed4ec default virtualbox aborted /Users/juchino/my_vagrant/mruby_target
9e4777a default virtualbox aborted /Users/juchino/my_vagrant/mruby_host
00dcd43 default virtualbox poweroff /Users/juchino/my_vagrant/redmine2_5_2
fc42378 core-01 virtualbox poweroff /Users/juchino/my_vagrant/coreos/coreos-vagrant
121afc9 default virtualbox aborted /Users/juchino/my_vagrant/centos56_mruby
・・・
ただ、Rubyの処理系を経由するため、結果が返ってくるまで結構待ちます。 自分のMac Book Airで計測すると、
$ time vagrant global-status
・・・
vagrant global-status 1.33s user 0.26s system 83% cpu 1.908 total
で、この処理速度を改善するため、Goで書き直されたのが、以下です。
このコマンドの処理速度は、自分のMac Book Airで計測すると、
$ time vagrant-global-status
・・・
vagrant-global-status 0.00s user 0.01s system 71% cpu 0.012 total
でした。 ちなみに、作者さんの環境での測定結果等は以下に載っております。
やったこと
このvagrant-global-statusを、mrubyで書いてCの中に組み込んでみました。
mrubyのコードのほうで大半のことをしています。 $HOME/.vagrant.d/data/machine-index/index をパースして、 必要情報(マシンのid, マシン名, プロバイダ, 状態, ファイルシステム上のパス)のみArrayにどんどんpushしていきます。 mrubyからCに渡すのは、このArrayとなります。
class Vagrant
attr_accessor :machines
def initialize()
@path = ENV['HOME'] + "/.vagrant.d/data/machine-index/index"
@machines = Array.new()
end
def get_machines_status()
file = File.open(@path)
text = file.read
file.close()
JSON::parse(text)["machines"].each do |machine|
@machines.push(machine[0].slice(0...7) + " " + sprintf("%-10s", machine[1]["name"].slice(0...10)) + " " + " " + sprintf("%-10s", machine[1]["provider"].slice(0...10)) + " " + sprintf("%-10s", machine[1]["state"].slice(0...10)) + " " + machine[1]["local_data_path"])
end
end
end
Cのほうでは、mrubyから受けとったArrayを表示しています。
mrb_value res = mrb_funcall(mrb, mrb_top_self(mrb), "main", 0);
for(int i = 0; i < RARRAY_LEN(res); i++){
char *a = mrb_str_to_cstr(mrb, mrb_ary_ref(mrb, res, i));
printf("%s\n", a);
}
ビルドの方法は以下の通り。
まず、vagrant-global-status.rbをC言語配列形式のバイトコードにします。
mrbc -Bvagrant vagrant-global-status.rb
これで、以下のようなCのコード(vagrant-global-status.c)ができます。
/* dumped in little endian order.
¦use `mrbc -E` option for big endian CPU. */
#include <stdint.h>
const uint8_t
#if defined __GNUC__
__attribute__((aligned(4)))
#elif defined _MSC_VER
__declspec(align(4))
#endif
vagrant[] = {
0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0x94,0xaf,0x00,0x00,0x04,0x7b,0x4d,0x41,
・・・
};
そして、上記を読み込むコードの方では以下のように書きます。
#include <stdio.h>
#include <string.h>
#include "mruby.h"
#include "mruby/compile.h"
#include "mruby/string.h"
#include "mruby/array.h"
#include "mruby/dump.h"
#include "vagrant-global-status.c"
int main (int argc, char *argv[]) {
extern const uint8_t vagrant[];
mrb_state* mrb = mrb_open();
mrb_load_irep(mrb, vagrant); # ここでバイトコードを読み込んでいる
mrb_value res = mrb_funcall(mrb, mrb_top_self(mrb), "main", 0);
for(int i = 0; i < RARRAY_LEN(res); i++){
char *a = mrb_str_to_cstr(mrb, mrb_ary_ref(mrb, res, i));
printf("%s\n", a);
}
mrb_close(mrb);
return 0;
}
以上のようにしてできたコード(main.c)をビルドすれば、1つのバイナリでglobal-statusができます!
測定!!
作成したmruby版vagrant-global-statusの速度を見てみました。
vagrant-global-status 0.00s user 0.00s system 65% cpu 0.011 total
Go版と大差ありません!でした。
ちなみに、今回使ったmrubyは、以下のモジュールを組み込む必要があります。
conf.gem :git => 'https://github.com/iij/mruby-io'
conf.gem :git => 'https://github.com/iij/mruby-dir'
conf.gem :git => 'https://github.com/iij/mruby-env'
conf.gem :git => 'https://github.com/mattn/mruby-json'