mruby Advent Calendar 2016 の 2日目の記事です。 昨日は おごもりさんのRuby Miniature Book の記事でした。
今日は、最近こそこそ作っているfusumaとmruby-fuseが、なんとなく面白いおもちゃにはなってきたので、せっかくということで紹介したいと思います。
fusuma (FUSe Upon Mruby-script Assistance)
mrubyとFUSEを用いて、ファイルシステムを作れるコマンドベースのミドルウェアです。
インストールはx86_64なLinuxバイナリは用意してありますのでひとまずそちらを解凍し、パスの通るところに配置すればOKです。各ディストロで fuse/ilbfuse
相当と libcurl
相当を別途インストールす必要があるでしょう。
とりあえず、libfuseの hello.c に対応するような簡単なファイルシステムクラスを実装してみます。こんな感じで。
FUSE.program_name = "fusuma" FUSE.path = "/tmp/foo" FUSE.fsname = "fusuma" FUSE.subtype = "fusuma" FUSE.uid = FUSE.gid = 1000 FileStat = Struct.new(:st_mode, :st_nlink, :st_size) class Example # This is called just before on_getattr def initialize(path, *a) @path = path case @path when "/hello" @value = "Hello, mruby fuse!!\n" when "/world" @value = "Hello, yet another mruby fuse!!\n" end end def on_getattr case @path when "/" return FileStat.new(FUSE::S_IFDIR|0755, 2, nil) when "/hello" return FileStat.new(FUSE::S_IFREG|0444, 1, @value.size) when "/world" return FileStat.new(FUSE::S_IFREG|0644, 1, @value.size) else return nil end end def on_open if ["/hello", "/world"].include? @path return 0 else return nil end end def on_readdir return nil if @path != "/" return ["hello", "world"] end def on_read_all return nil if @path == "/" return [@value, @value.size] end def on_truncate(size) @value = "" return 0 end def on_write(buf, offset) @value << buf return buf.size end end FUSE.run Example
このRubyスクリプトを fusuma
コマンドで実行します。すると、ターミナルに張り付きます。
$ mkdir /tmp/foo $ fusuma example.rb
別のセッションから確認すると、ちゃんと fuse としてマウントされていて、また、ファイルやディレクトリを認識していることがわかります。
$ mount | grep fusuma fusuma on /tmp/foo type fuse.fusuma (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,default_permissions)
続いて、ファイルに書き込むとSlackに通知するような謎ファイルシステムを作ってみます。全体はこんな感じで。
FileStat = Struct.new(:st_mode, :st_nlink, :st_size) class SlackFS def initialize(path, hook_url) @path = path @hook_url = hook_url end def on_getattr case @path when "/" return FileStat.new(FUSE::S_IFDIR|0755, 2, nil) when "/notify" return FileStat.new(FUSE::S_IFREG|0222, 1, 0) else return nil end end def on_open @path != "/slack" ? 0 : nil end def on_readdir return nil if @path != "/" return ["notify"] end def on_write(message, offset) req = HTTP::Request.new req.method = "POST" req.body = <<EOJ payload={"text":#{message.inspect}} EOJ req.headers['Content-Type'] = "application/x-www-form-urlencoded" res = Curl.new.send(@hook_url, req) puts "Post: #{res.body} / #{res.status_code}" return message.size end end FUSE.program_name = "fusuma" FUSE.path = "/dev/slack" FUSE.fsname = "slackfs" FUSE.subtype = "slackfs" FUSE.uid = FUSE.gid = 1000 FUSE.run SlackFS, ENV["SLACK_INCOMING_HOOK"]
マウント先を準備し、これをマウントすると、 /dev/slack/notify
と言うファイルが...
$ sudo mkdir /dev/slack $ sudo chown vagrant: /dev/slack $ export SLACK_INCOMING_HOOK="https://hooks.slack.com/services/..." $ ./mruby/bin/fusuma sample/slackfs.rb & $ mount | grep slack slackfs on /dev/slack type fuse.slackfs (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000,default_permissions)
$ ls -l /dev/slack/ total 0 --w--w--w- 1 vagrant vagrant 0 Dec 31 1969 notify
書き込んでみましょう
$ echo 'I :heart: mruby system programming!' > /dev/slack/notify
なんだかよくわからないですけど便利そうなファイルシステムですね!
fusuma の設計思想
内部的には、libfuseの struct fuse_operations
に対応するような各フックを、Rubyのクラスでインスタンスメソッドとして実装してあげて、それを FUSE.run
に渡せば、あとはファイル操作をフックとして様々な処理をmrubyで書けるようになります。
インスタンスは getattr
のタイミングで作成し、pathをキーにしてプールに保存しています。なので、同じpathを持ったファイル(インスタンス)への操作は、fusumaのプロセスがいる限りは保持されています。
今のところ、以下のフックを実装しています。カッコがRuby側で実装すべきメソッドとなります。今後の展開として、残りをひたすら地道に書く感じです...。
- getattr(
on_getattr
) - readdir(
on_readdir
) - truncate(
on_truncate(size)
) - open(
on_open
) - read(
on_read
/一気に全コンテンツを返せるon_read_all
もある) - write(
on_write(buf, offset)
) - release(
on_release
)
免責事項
本ツールとmruby gemはlibfuseの練習も兼ねて作っています。今のところ、特にマルチスレッド時の挙動など、細かいところの詰めがまだまだです。
こうしたらいいよ!という方針があれば是非プルリクエストをくださいませ...
ということで、なんとなく自作のファイルシステムが欲しくなった際などに、お試しいただければと思います。
ところで、明日のmruby Advent Calendarは 空席です 。是非、ご参加をお待ちしております...!!!1