gitリポジトリを公開するための☆過去書き換え☆テクニック

また時間操作か!

githubにリポジトリ公開したいお…でも

       ____ 
     /      \ 
   /  _ノ  ヽ、_  \ 
  / o゚((●)) ((●))゚o \  ほんとはgithubで公開したいんだお…
  |     (__人__)    | 
  \     ` ⌒´     / 

       ____ 
     /      \ 
   /  _ノ  ヽ、_  \ 
  /  o゚⌒   ⌒゚o  \  でもMySQLのパスワードとか
  |     (__人__)    |   公開できない情報が入ってるんだお…
  \     ` ⌒´     / 

       ____ 
     /⌒  ⌒\ 
   /( ●)  (●)\ 
  /::::::⌒(__人__)⌒::::: \   だから過去を書き換えるお!
  |     |r┬-|     | 
  \      `ー'´     /

 と、いうわけで、書き換えです。

githubにソースコードを置きたい…でも公開できない情報が…。ありそうなのはこんなケース?

  • MySQLのパスワードが書いてある…
  • 完全に非公開で遊ぶために書いてたから著作権上アレゲなコンテンツが…
  • 残念な感じのコミットログが…

今更その部分を隠してコミットしたって、昔のコミットログを辿られたら意味ないじゃない!んじゃあ、修正済みのやつだけコミットログ無しで公開?もっと意味ないじゃん

そこで過去の書き換えです。gitはもうぶっちゃけ何でもできるので、過去のコミットを再度やりなおして全部書き換える事ができます。以上のケースについて、修正してみましょう。

 MySQLのパスワードが書いてある…

こんなサンプルリポジトリを考えてみましょう(github)。

ファイルの一部分に公開したくない情報があるケースです。たとえばパスワード。ためしにこんなファイルを考えてみましょう。

// 以下のコードはあくまでサンプルであり、何の意味もありません。

var db = /* なんか初期化処理 */ null;

db.init({password: test}); /* パスワードを追加しちゃった! */
// データベースに何か値を突っ込む
db.save({id: 1, name: "ちさ"});
db.load(); // 何かロード処理的なもの
db.close();

db.initの行でパスワードをハードコーディングしてしまいました。公開するのには問題があるかもしれません。繰り返しますが、この状態からパスワードを隠してコミットしても過去のファイルはそのままなので、意味がありません。

さて、git logはこんな感じです。

commit e236c0d55d0d7fc4744dd5f7edc34b9c22d8cada
Author: psi <ledyba@users.sourceforge.jp>
Date:   Fri Feb 22 11:05:50 2013 +0900

    ロード処理的なものを追加した(という想定)

commit e27ea6a1cf49021b9686c7e757fbf9adf60afb3f
Author: psi <ledyba@users.sourceforge.jp>
Date:   Fri Feb 22 11:05:10 2013 +0900

    パスワードを追加しちゃったコミット。取り消したい。

commit 7c132ae20f6c1db1bff876e8d0626d41e07a8294
Author: psi <ledyba@users.sourceforge.jp>
Date:   Fri Feb 22 11:04:30 2013 +0900

    最初のコミットです。

この「パスワードを追加しちゃった」コミットで、ソースコードに直接パスワードを書いてしまっています。

git diff-tree e27e -p
e27ea6a1cf49021b9686c7e757fbf9adf60afb3f
diff --git a/sample.js b/sample.js
index 0a2fa57..9c892d8 100644
--- a/sample.js
+++ b/sample.js
@@ -2,7 +2,7 @@

 var db = /* なんか初期化処理 */ null;

-db.init();
+db.init({password: test}); /* パスワードを追加しちゃった! */
 // データベースに何か値を突っ込む
 db.save({id: 1, name: "ちさ"});
 db.close();

password: testの部分をpassword: xxxxに変えて、隠しましょう。

そのために、まず新しいブランチを用意します。

git checkout -b rewritten

として新たなブランチをつくった上で、

git rebase -i <修正したいコミットの一個前のコミット>

を実行して書き換え開始です。rebase -iでインタラクティブモードを使うと、コミットを直接書き換えることができるようです。するとインタラクティブなので、こういう感じでエディタが現れます。 →notepad++でgit logを編集したいとき

20130222-01

これで、テキストの上のほうに

pick e27ea6a パスワードを追加しちゃったコミット。取り消したい。
pick e236c0d ロード処理的なものを追加した(という想定)

とあるので、改変した「pick e27ea6a」を「edit e27ea6a」に書き換えてエディタを終了します。すると…

git rebase -i 7c132
Stopped at e27ea6a... パスワードを追加しちゃったコミット。取り消したい。
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

と指示が出てくるので、書き換えましょう。書き換えたソースがこれ。

// 以下のコードはあくまでサンプルであり、何の意味もありません。

var db = /* なんか初期化処理 */ null;

db.init({password: xxxx}); /* パスワードは隠しておいた */
// データベースに何か値を突っ込む
db.save({id: 1, name: "ちさ"});
db.close();

ちゃんと隠しました。

指示されたとおり–amendでコミットします。その前にgit addを忘れないように

git add sample.js
git commit --amend
[detached HEAD 6a7b87a] パスワードを追加したコミットだったけど、取り消しておいた
 1 file changed, 1 insertion(+), 1 deletion(-)

これで書き換えられましたとさ。で、残りのコミットをgit rebase –continueで再度適用しなおします。

$ git rebase --continue
Successfully rebased and updated refs/heads/rewritten.

gitk –allってやって全コミットを可視化すると…

20130222-02

ちゃんと枝分かれしてます。masterを消してrewritteをmasterにrenameすれば、完全に書き換えたことになります。

ここで注意しないと行けないのが、「ロード処理的なもの~」というコミットが2つ存在することです。rebase -iして書き換えると、その時点以降のコミットはすべて別のコミット扱いになるので、注意が必要です。

最初のコミットはどうするの?

上では修正したいコミットの一個前のコミットを指定しましたが、じゃあその一個前のコミットがない、最初のコミットはどうすればいいの?その場合のサンプルも用意してみました→github:ledyba/__sample__RewriteGitHist2

この時は若干面倒くさいです。

まず、テンポラリなブランチを作って…

git checkout -b tmp

reset –hardで一番最初まで戻ります。

git reset --hard <一番最初のコミット>

この状態で、消したい部分を修正して、コミットを修正しちゃってください!

vi <修正したいファイル>
git add <修正したいファイル>
git commit --amend

こうすると、tmpブランチはmasterブランチとは一切つながりのない孤立したブランチとなります。よく見てみてください。線が繋がってません。

20130222-03

ではここからどうするかというと、この新しい孤島のtmpに、書き換え以前のmasterのコミットを適用しなおします。

そのためにmasterの最初のコミットに、oldrootというタグをつけて、rebase –ontoを使います。

git rebase --onto tmp oldroot master

すると(うまくマージが動けば)修正済みのmasterブランチが出来上がってくるはずです。

20130222-04

昔の書き換えたいコミットである、oldrootが今度は孤島になりました。

過去コミットのAuthor/Comitterを書き換えたい

以前適当に使っていたので、設定する名前とメールアドレスも適当でした。マシンごとに違った設定だったり、セットアップしなおすたびに適当に違うメールアドレス入れたり。

$git config --global user.name fuga
$git config --global user.email hoge@gmail.com

でもそうすると、誰がコミットしたのか訳がわからなくなってしまって、困ってしまいます。ので、公開するときにはこれを統一したい。そう思いました。

その方法ですが、stack overflowに書いてあるのでそのまま受け売りすると、filter-branchを使うと書き換えられるようです。

 git filter-branch --commit-filter '
        if [ "$GIT_COMMITTER_NAME" = "<Old Name>" ];
        then
                GIT_COMMITTER_NAME="<New Name>";
                GIT_AUTHOR_NAME="<New Name>";
                GIT_COMMITTER_EMAIL="<New Email>";
                GIT_AUTHOR_EMAIL="<New Email>";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

とやって、コミット時にいろいろ書き換えられます。このcommit filterのなかに書いてあるのはシェルスクリプトで、sedを使ってファイルを書き換えることもできますし、上記のように環境変数を指定することで、メタデータも書き換えられます。書き換えられるメタデータは以下:

  • GIT_AUTHOR_NAME
  • GIT_AUTHOR_EMAIL
  • GIT_AUTHOR_DATE
  • GIT_COMMITTER_NAME
  • GIT_COMMITTER_EMAIL
  • GIT_COMMITTER_DATE

特定のファイルを全部除去したい

今度は例えば、画像処理プログラムのサンプルで、著作権的によろしくない画像を使ってた…みたいなケースです。ありそう(過去を思い出しながら)

上記のfilter-branchではコマンドラインを使うと任意のシェル・コマンドが使えますので、

git filter-branch --commit-filter 'rm -f <消したいファイルとか>' HEAD

ってやれば、ファイルとか全部消せます。examplesにいろいろかいてあるので試してみてね

おわり

rebaseの話はあんまり見ないので、そちらを重点に、以前やったことがある書き換えについて書いてみました。gitのブランチを詰将棋みたいに書き換えるゲームもあったりするようで、gitのこの辺の機能は強力であると同時に少し難しいですね…。

Snap@Haskellでファイルアップロード時にenctype指定を忘れるとエラーも出ずに動かない

悲しい話

情弱でもSnapでファイルアップロードしたい!

HaskellのWebフレームワークSnap、使ってますか!最近ちょっと触ってて、ファイルアップロードがしたいなぁと思ってこんなハンドラを書きました。Snapでは、URLに紐ついたハンドラ(結局モナド)を登録して、特定のURLにアクセスされると、そのハンドラが実行されます。

こんな感じ:

handleUpload :: Handler App App ()
handleUpload = method GET (redirect "/") <|> method POST handlePost
    where
        process part = continue (\a -> (yield (partFileName part) a))
        handlePost = do
            ret <- handleMultipart defaultUploadPolicy (\p -> process p)
            writeText $ T.pack $ show ret

ふーむ。この状態でこんなフォームを書いて送信してみます。

<form method='post' action='/upload'>
  <table id='info'>
    <tr>
      <td>file: </td><td><input type='file' name='image' size='20' /></td>
    </tr>
    <tr>
      <td></td>
      <td><input type='submit' value='send' /></td>
    </tr>
  </table>
</form>

よくみるとformでのenctype指定がないのですが、最初私は気づきませんでした。さてブラウザからアクセス。

20130218-01 アップロード。

20130218-02うーん、acceptできるハンドラがない、ねぇ…。最初、ルーティングがうまくいってないのかな、と思ったのですが、GETで/uploadにアクセスするとちゃんとリダイレクトがかかるので、そこは間違ってないだろう、となると、このPOSTのハンドラが変みたいだ、と目星をつけました。

finally catch try ->どれも動かない

handleMultiPartを消してreturnにするとちゃんとレスポンスが表示されるので、どうやらここで例外?が発生してる模様。じゃあ`catch`だ!とやってみても…

handleUpload = method GET (redirect "/") <|> method POST handlePost
 where
 process part = continue (\a -> (yield (partFileName part) a))
 handlePost = do
 ret <- (handleMultipart defaultUploadPolicy (\p -> process p)) `catch` (\(e :: SomeException) -> [show e])
 writeText $ T.pack $ show ret

こうしても、さっきとまったく同じ、「No handler accepted.」。すべての例外をキャッチできるはずのSomeExceptionを使ってもだめ。例外がキャッチできてないからか、finallyを使っても何も起こりません。tryを使ってもEitherが帰ってこず、処理がスキップされちゃいます。

これ、C++で-fno-exceptionした時となんか似てます。例外処理が動いてないのでは?…もっとも、C++の場合はもっと不思議なことが起こったりですが…。

Multi-Partのチェックとその失敗処理は例外と直行している

中身を見るしかないので読みましょう。まぬあるにて、どうぞ。 HaskellのドキュメントシステムHackageでは、rdocみたいにソースが読めます。すばらしい。

長いのですが、今回エラー処理が起こされているのはここでした。

    -- not well-formed multipart? bomb out.
    when (ct /= "multipart/form-data") $ do
        debug $ "handleMultipart called with content-type=" ++ S.unpack ct
                  ++ ", passing"
        pass

passというので実際に処理を抜けているようですね。このpassのドキュメントはこれ。Exception関係のものがシグネチャにあらわれていないのを見ればわかるように、例外とは一切関係なく処理をスキップ致します。finallyを使っても何も起こせないのは、こういう事だったのですね。

debugを有効にしたほうがいいかもしれない

環境変数にDEBUG=1を指定するとデバッグモードが有効になります。

すると、こんな感じのエラーログがひたすら出力されます。

 [       8] handleMultipart called with content-type=application/x-www-form-urlencoded, passing
 [       8] Server.httpSession: finished running user handler
 [       8] Server.httpSession: handled, skipping request body
 [       8] httpSession/skipToEof: BEGIN
 [       8] httpSession/skipToEof: continue

これならコンテント・タイプがおかしいんだな~ってすぐ気づけますね…。開発中はずっとこれをつけといたほうがいいかもしれませんね!

ちなみにうまくいくと

20130218-03Justでファイル名が帰ってくるだけの簡単なサンプルプログラムでした。ファイルは扱ってないです…。

ネタもと

ぼんやりと「Haskellのエラー処理とMonadCatchIOの落とし穴 – 純粋関数型雑記」を見てたら思い至った話です。

「例外、失敗」を処理する方法がたくさんあるのは柔軟で良いと思うのですが、ルールをちゃんとあわせておくか、あるいは別種の「失敗」があることをちゃんと把握しておかないと意図しない挙動になっちゃうのは、悲しいですね…。

明日「未踏事業 成果報告会」@秋葉でしゃべります

こんにちは!

ここ一ヶ月で「時を司るスクリプト言語 ど~なっつ」やそれを使った出前授業について書いて来ましたが、実はそれらは全て国の機関IPAの未踏事業の支援を受けて行なってきたものだったのです!

で、ついにその支援期間も終わりまして、その成果を報告する成果報告会が明日、秋葉のUDXで行われます。もちろん支援を受けた私もその中で喋ります。

11 17:35-18:10 「CPUの理解を容易にするシステムと解説サイトの構築」

秋葉のUDX、しかも17時半ですので、仕事や学校の帰りにすこし時間のある方はぜひいらして下さい!

みどころ?

ど~なっつで開発された教材を使って今月中旬に行われた、出前授業についてレポートします。特設サイト~。

時を司るスクリプト言語 ど~なっつについて、実際のデモを交えながら、「時を司る」さまをデモります!

ファミコンでコンピュータを理解する出前授業してきた

こんにちは。今IPAの未踏事業で開発をやっているのですが、その関係で兵庫県立西宮香風高校で出前授業をさせて頂きました〜!ちなみに新幹線で新大阪に降りたのはこれが人生初めてです!(ぇ

20120118

沢山のライブラリやOSがコンピュータの低レイヤの部分を完全に覆いさってしまい、プログラマでもコンピュータのごくごく基本的なアセンブラやメモリの動きを実感を持って理解しているとは限らない現代において、ファミコンのいわゆる「改造」を通じてそれを伝えることは出来ないだろうか?という企画です。

20120118-2

ゲーム機を再現したソフトウェアであるエミュレータと、それをターゲットとした簡易デバッガの組み合わせです。機材の関係で写真だとパソコンで作ってますが、Nexus7でも動いています。

20120118-3

命令実行や命令とメモリとの対応をなんとか可視化できる状態で授業に持ってこれました。60FPSでレンダリング出来る他、右下の「戻る」ボタンでは時を司るスクリプト言語ど〜なっつの能力を借りて、理解できなかった所まで巻き戻してやり直す、なんてことも出来るようになっています。

ファミコンに限らずコンピュータは全て「数字の計算」で動いている事を実感してもらったあと、メモリの検索や比較で目的の値を絞り込んでいき、キャラクタをワープさせてめり込ませたり、残機数を増やしてみてもらいました。

20120118-4

当初想定してた以上にハードな改造をして、当たり判定を全てすり抜けるようにしたり、隠しメッセージまで表示させてる人も居たりと、想定外な感じの出前授業となりました。

当初の目的はなんとか果たせたのではないかな〜と思います。兵庫県立西宮香風高校の皆様、ありがとうございました。

IPAの成果報告会でも喋ります〜〜

今週末土曜日に秋葉のUDXでやる「第19回未踏事業」成果報告会で実物展示や、「何度でもやり直せる」システム面についての話を行なってくる予定です〜。17:35からの予定なので、お仕事や学校の帰りにたまたま秋葉を通るような方が居たらぜひいらしてください!

【備忘録】boost1.52をソースからインストール

毎回毎回どうやるんだっけ…とか検索してて不毛なので自分用にまとめました

私はboostをWindowsのMinGWでC++11のスレッド・アトミックサポートがうまく動かないのでその代替として使っています。環境の組み合わせは:

  • Windows 7 x86_64
  • mingw32 x86
  • gcc 4.7.2 x86

ダウンロード

boostの公式サイトからソースのtarballをダウンロードしてどっかに保存してtar xvf。

boost::atomicの追加

採択されたらしいのになぜか入ってないboost::atomicを手動で入れる

公式サイトからダウンロードして中身を全部boostのフォルダにマージすればOKな模様。とりあえず使えたのでこれいいのかなと…。

bjamのブートストラップ

解凍したディレクトリに入って

cmd
bootstrap.bat gcc

するとb2.exeが出来て、これがboostのビルドシステム。bjam.exeっていうのもできてるけどこれはどうやら古い(?)ビルドシステムの模様。折角なのでb2を使いました。

b2の設定&ビルド&コンパイル

b2 --help

で設定が見れる。

b2 --show-libraries

でライブラリの一覧が見える。後で–without-****するときにこの名前が必要。

今回使った設定

これらを使って今回紡いだオプションがこちら

b2 --prefix=<install先> --build-type=complete \
--toolset=gcc --variant=debug,release \
--without-python --without-graph \
--without-graph_parallel --without-mpi \
link=static threading=multi runtime-link=static \
release debug stage -j4

これでちゃんと動くようなら上記の最後に install を追加したb2を走らせてインストール。

終わり

お疲れ様でした。

ど~なっつ、時を司るプログラミング言語。

あけましておめでとうございます!!今年は友人のサークルで売り子をしていたのですが、なんと完売御礼でした!!やったねたえちゃん!(謎

さて元日も過ぎて餅にも飽きてきた頃かと思いますので、半年間掛けて未踏IT人材育成事業で開発していたもののうち、なんとか外に出せそうなスクリプト言語部分について紹介します!

ど〜なっつ、時を司るスクリプト言語

20120102

ど~なっつは

  • 処理系とその外界の状態を過去に戻し、未来に戻せ、
  • 任意の時点でのセーブデータを書き出し、後で実行を再開できる、

ちょっと変わった「時を司るプログラミング言語」です!

時を司るプログラミング言語

普通、プログラミング言語って上から下に実行されて、起こった事は元に戻りません。副作用ってやつです。覆水盆に返らず。

int main(){
    int x = 10; //このときxは10。
    printf("x is %d", x);
    x = 20; //これ以降、ずっと20で、以前10だった痕跡は一切残らない。
    printf("x is %d", x);
    return 0;
}

タイムリープでフロー制御。

でも、戻りたい時だってあるかもしれません。例えば、ユーザーのログイン処理なんかそうでしょうか。普通のプログラミング言語、例えばRubyだと、こんな感じでパスワード認証を書くでしょうか?

while true do
    puts "パスワードを入力して下さい!"
    passwd = gets
    if passwd == "正しいパスワード" then
        puts "認証完了"
        break
    end
    puts "間違ったパスワードです"
end

puts "こんにちは、正しいユーザーさん!"

すっきり書けました。

でも…。やりたい事は

  • 正しいパスワードが入力されるまで繰り返し

ではなくて、

  • 正しいパスワードが入力されたらログインしていいけど、そうじゃなかったら門前払い

なのではないでしょうか?「ど~なっつ」では、こんな感じで同じプログラムを書けます!

//このあとの命令を使って、この時点まで時を戻せるようになります。
save_time=Homura.tick();

System.print("パスワードを入力してください。");
//パスワードを読み込みます。
passwd=System.readline();

if(passwd == "abcdef"){
  //パスワードの認証に成功しました。
  System.println("こんにちは!正しいユーザーさん。");
} else {
  // 上で記録した時間まで、スクリプトエンジン全体を戻します。
  System.println("パスワードが間違っています!");
  Homura.seek(save_time);
};

「Homura」というオブジェクト1を使って、パスワードが間違っていたときはログイン処理前にまでタイムリープさせてしまいます。変数、コールスタック、全ての「環境」が文字通り「戻ります」。

これで不自然な繰り返しを使わずにユーザーログイン処理が書けてしまうのです!(ドヤァ

私は何度でも繰り返す。

gotoでも同じように書けますが、変数も全て巻き戻されるので、安全に巻き戻す事ができます。この事を利用すると、「時が戻る事を利用したループ」がつくれます。普通に起こすと無限ループになりそうですが、ほむらオブジェクトに突っ込んだ値はタイムリープ前の事を覚えているので、コレを使います。交わした約束忘れないよ

//普通の変数は、時間操作の影響を受けますが…
tabeta=0;

//Homuraは時間操作前の事を覚えていて、時間操作の影響を受けません。
Homura.counter=0;

//このあとの命令を使って、この時点まで時を戻せるようになります。
save_time=Homura.tick();

if(Homura.counter < 10){
  // ドーナッツを食べましょう。
  // この足し算の結果は、時間操作で戻ってしまいます。
  tabeta++;

  // ドーナッツを食べた回数を表示しましょう
  System.println(tabeta, "番目のドーナッツを食べた!");

  // Homuraに入った値は時間操作の影響を受けないので、
  // 時間操作を行なってもこの足し算の結果は戻りません。
  Homura.counter+=1;

  // 上で記録した時間まで、スクリプトエンジン全体を戻します。
  Homura.seek(save_time);
} else {
  //ど~なっつでは、else節を省略することはできません。
};

これを実行すると、

% donut time_op.donut
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!
 1番目のドーナッツを食べた!

このように何度ループをめぐっても1番めのドーナッツを食べ続けることができますが、Homuraは時間操作の影響を受けないので、無限ループには陥らずに済みます。

「継続」と違って スクリプト外部への副作用も制御できる

こういったループはSchemeのcall/ccやRubyのFiberような「継続」を使えば記述することができます。しかし、「継続」のアプローチでは戻せないものがあります。それは、言語処理系の環境です。

つまり、Scheme内から外界(たとえばウィンドウシステム)に、「ウインドウを表示する」と言った副作用を起こしていた場合、「ウインドウを表示する」以前の継続に戻っても、戻るのはSchemeの中でだけであって、ウインドウは表示されたままになってしまいます2

「ど〜なっつ」では言語処理系外に起こした副作用も、「副作用」に対応した「反副作用」みたいなものを登録して管理させることができます。時間操作が行われた瞬間にその「反副作用」を用いて副作用を打ち消させることができます。

でも、「反副作用」を作れない場合もありますよね?例えば、ネットワークでデータを送信するような操作は絶対取り消せませんから、「反副作用」は作れません。そのときにその時刻以前には戻れないようにする機構も作られています。コンソールへの出力も取り消せませんから、今回使ってるSystem.printlnなども本当は取り消せないメソッドとして登録すべきなのですが、副作用のないメソッドとして登録されています。HaskellのUnsafeIOみたいな感じです。

普通にスクリプト言語

関数も使えますし、関数はクロージャーですし、第一級オブジェクトです。JavaScriptのような感じで使えます。

f = func(idx, acc){
    if( idx <= 0 ){
        acc; //最後に評価された値が戻り値になる
    }else{
        return f(idx-1, acc+idx); //戻り値を明示することもできます。
    };
};
System.println(f(10, 0)); // prints 55

またプロトタイプ型オブジェクト指向言語で、__proto__にオブジェクトを指定するとJSのようにプロトタイプを巡ってくれます(この辺のドキュメントも書かなきゃ…)。全体的にはJavaScript: The Good Parts ―「良いパーツ」によるベストプラクティスを参考に設計されています。

 

まだまだ開発中で、リポジトリも未踏でのプロジェクトと同じリポジトリに入っていて大変使いづらいとは思うのですが、テンションを上げるために公開してみました。

ダウンロード

ダウンロードはど~なっつのサイトからお願いします!githubでのソースは本体プロジェクトの「ちさ」と同じ所に入ってます。

ライセンス

ど~なっつのライセンスはGPLv3となっています。

他に開発中のもの

他に開発中の本体プロジェクト「ちさ」は、このど~なっつをネイティブで使用して、時を司り、さらに任意の時点での実行状態を保存できるGUIツールキット兼電子コンテンツエンジンです。が、まだあんまり出来てないので公開はまだしません。

Thanks

ど~なっつはIPAの未踏IT人材発掘・育成事業に採択された「CPUの理解を容易にするシステムと解説サイトの構築」の一部分として開発され、支援を受けています。

 

  1. もちろんアレです []
  2. もちろんアドホックに実装した場合は除きます []

クリスマスはNoiz2saをChromeで遊ぼう!

Native Clientは死んだんだ
いくら呼んでも帰っては来ないんだ
もうあの時間は終わって、君も人生と向き合う時なんだ


弾幕の海にたゆたう、アブストラクトシューティングNoiz2sa。

ChromeにNoize2saを移植してみた

Google ChromeにNoiz2saを以前移植して公開してあったのですが、こちらでは書きそびれてたので、クリスマスというタイミングに書くことにします。

ABA GamesのSTG “Noiz2sa”をブラウザで遊べるようにしてみました。

20121224

Chrome ウェブストア – noiz2sa

 ChromeのNative Clientという仕組みを使っていて、これを使うと比較的簡単にネイティブのゲームをブラウザ上で動かすことが出来るようになります!

GamepadのAPIがうまく動かないので残念ながらゲームパッドでは動く動かないのですが、キーボードでならガリガリ動くのでJoyToKeyでも使ってやってみてください!…だったら普通にWindows版で遊ぶ?そ、そうですよね…。

特に何もしてないけどクロスプラットフォーム

NativeClientは、何もしなくてもクロスプラットフォームに対応していて、今回の拡張もLinux/Windows/MaxOSXで動きます。ちょうべんりー。ちなみに開発自体はLinuxでやってます。

NativeClientのしんどいところ

NativeClientはセキュリティ上の要請から、色々制限があります。普通に計算をさせる分には困らないですし、グラフィックやサウンドの出力も、NaCl Portsがあるお陰で普通に使う分には困りませんが、ファイルアクセスやネットへの接続などでは独自のライブラリを使わなければなりません。

今回ゲームということでネットワークは使っていないのですが、タイトルの「noiz2sa」のロゴ画像(ビットマップ)や弾幕の定義データ(XML)、BGMや効果音のファイルなど、比較的数は少ないもののファイルアクセスがあり、これに対処する必要がありました。

ファイルはすべてゲームバイナリ内に

Native ClientにはURLLoaderというクラスがあるので、これを使えば任意のURLの内容を取得できます。これをローカルファイルの代わりに使えば良さそうです…が、今回はゲームなのでローディングを避けたいのと、大した容量でもない(8MBくらい)なので、全部バイナリ内に入れてしまうことにしました。

具体的にいうと…

struct FileEntry{
const char* filename;
const size_t size;
const char* data;
};
extern const struct FileEntry gFileImages[];
extern const size_t gNumberOfFileImages;

Noiz2saNaCl/nacl/file_data.h at master ・ ledyba/Noiz2saNaCl ・ GitHub

というような構造体をつくって、

static const char ImageData[8810309] = [具体的なデータ>];
const size_t gNumberOfFileImages=96;
const struct FileEntry gFileImages[96] = {
{
.filename="boss/forward_3way.xml",
.size=818,
.data=&ImageData[0],
},
(ファイルのエントリをたくさん)
};

Noiz2saNaCl/nacl/file_data.c at master ・ ledyba/Noiz2saNaCl ・ GitHub

として全データを格納します。

これを読み書きするためのfopen、fread、fcloseなどの関数を独自に定義して

extern size_t nacl_ftell (FILE *file);
extern int nacl_fseek (FILE *file, size_t offset, int whence);
extern FILE *nacl_fopen (const char *filename, const char *modes);
extern size_t nacl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
extern size_t nacl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
extern int nacl_fclose(FILE *fp);
extern int nacl_feof(FILE *stream);
extern int nacl_ferror(FILE *stream);
extern void nacl_rewind(FILE *stream);
extern int nacl_fgetc(FILE *stream);
extern int nacl_fflush(FILE *stream);

最後にマクロで普通のfopenを乗っ取ります

#define fopen(n,m)  nacl_fopen(n,m)
#define fclose(f) nacl_fclose(f)
#define ftell(f) nacl_ftell(f)
#define fseek(f,o,w) nacl_fseek(f,o,w)
#define fread(p,s,n,f) nacl_fread(p,s,n,f)
#define fwrite(p,s,n,f) nacl_fwrite(p,s,n,f)
#define feof(f) nacl_feof(f)
#define ferror(f) nacl_ferror(f)
#define rewind(f) nacl_rewind(f)
#define getc(f) nacl_fgetc(f)
#define fgetc(f) nacl_fgetc(f)
#define clearerr(f) nacl_clearerr(f)
#define fileno(f) -1
#define fflush(f) nacl_fflush(f)

Noiz2saNaCl/nacl/storage.h at master ・ ledyba/Noiz2saNaCl

ソースコードを改変できるので別に乗っ取る必要はないのですが、NativeClientの元の記事がそういう感じで乗っ取ってたのでそうしときました。

ディレクトリ内のファイルを列挙するAPIであるreaddirなどもおんなじ感じで乗っ取っております。

この方法はGoogle先生謹製の記事を参考にしていて、ソースも一部拝借してます。Case Study: Porting MAME to Native Client – Native Client — Google Developers

さてどうしたものか

NativeClient、ファイルが絡むと一気にめんどくさくなります。既存の技術(Flashとか)を置き換えるには少し不便ですね。

WordPressに移行した&支援スクリプト書いた

諸事情でWordPressをインストールして別箇所で使っていたのですが、これは素晴らしいですね!!!
何が素晴らしいって、もう

  • UIは使いやすいし!
  • アップデートは自動だし!
  • 固定ページとかの編集も簡単だし!
  • テンプレートを簡単に変更できるし!
  • プラグインは豊富だし!
  • かわいいテンプレート一杯あるし!
  • かわいいテンプレートがたくさんあるし!
  • かわ(ry

もう移行しない理由とかないよねーないよねー、ということでちょっと重い腰を上げて移行!することにしました。言語別にパッケージが分かれてるのは正直ダサいと思いますが、不満はそれぐらいです。

はてな記法を捨てたい。

はてな記法のプラグインを使ってて依存していたため移行をためらっていたのですが、今回この負の連鎖を断ち切るべく、プラグインの変換エンジン部分を適当に引っ張ってきて、エクスポートしたデータのはてな記法の部分を変換する(!)スクリプトを書きました。

  • github: ledyba/mt-hatena-converter

「ブログ記事のエクスポート」で引っ張ってきたexport*****.txtファイルを、このスクリプトに渡して下さい

perl format.pl export******.txt

ごめんなさい:追記部分の変換を忘れてました。使用時には注意してくださいね…。

今回Perl使ったのは初めてなのですが、空文字列をfalseとして認識する使用で随分ひっかかりました。もうやめようよそういう仕様…ね?

 

gmtime/localtimeと閏秒の面倒くさい関係

お久しぶりです、ψ(プサイ)です。

今日は今やってるプロジェクトとは一切関係なく、gmtime/localtimeの実装が気になったので、読んで分かったことをまとめておきます。

軽くおさらいですが、gmtime/localtimeは、unixtime(1970年1月1 00:00:00 AM UTCからの経過秒数)をtm構造体に入った年・月・日・時間・分・秒に変換してくれるCの標準ライブラリ関数です。unixtimeのままでは人間が到底読めないので、表示する時などに必要な関数です。

思えば、このgmtime/localtimeの実装は結構めんどくさそうです。すごくざっくりで良いなら、

年=(int)1970+(unixtime/(24*365*3600))

みたいな感じで簡単に算出できますが、これでは全然駄目な点がたくさんあります。

  • うるう年は?
  • うるう秒は?
  • (localtimeの時)時差は?

どうやるか自体は大体想像できると思いますが、折角なので読んでみました。そんな具体的な話には興味ないという方は飛ばして下さい。

newlibのgmtimeを読んでみよう

newlibは標準Cライブラリの軽量な実装です。組み込みの分野でよく使われていて、コンパクトなのが特徴だそうです。

ためしに、このライブラリでのgmtimeを読んでみます。newlib/libc/time以下のフォルダにあります。

gmtime.c

struct tm *
_DEFUN (gmtime, (tim_p),
	_CONST time_t * tim_p)
{
  _REENT_CHECK_TM(_REENT);
  return gmtime_r (tim_p, (struct tm *)_REENT_TM(_REENT));
}

gmtimeはグローバル変数のtm構造体を返すのですが、ユーザの指定したtm構造体に値を格納するgmtime_rという関数もあるので、それに処理を投げているようです。その先は…

gmtime_r.c

struct tm *
_DEFUN (gmtime_r, (tim_p, res),
	_CONST time_t * tim_p _AND
	struct tm *res)
{
  return (_mktm_r (tim_p, res, 1));
}

localtimeと処理を共用しているようで、その共通の関数に投げてます。3つ目の引数の「1」は、gmtを使うという意味のようです。

mktime_r.c

struct tm *
_DEFUN (_mktm_r, (tim_p, res, is_gmtime),
	_CONST time_t * tim_p _AND
	struct tm *res _AND
	int is_gmtime)
{
  long days, rem;
  time_t lcltime;
  int y;
  int yleap;
  _CONST int *ip;
   __tzinfo_type *tz = __gettzinfo ();
  /* base decision about std/dst time on current time */
  lcltime = *tim_p;

この関数は結構長いので、ちょっとずつ読んで行きましょう。まずはローカル変数の定義です。昔のCの規格でも動くように、ローカル変数は必ず関数(もっというとスコープ)の一番最初で宣言されているみたいです。std/dstという文字列が見えますね…サマータイムと通常時間の事です。私は最初、すっかり存在を忘れてました。。。もしオレオレ実装を作っていたらすっかり忘れていた所です。

  days = ( (long)lcltime) / SECSPERDAY;
  rem = ( (long)lcltime) % SECSPERDAY;
  while (rem < 0) 
    {
      rem += SECSPERDAY;
      --days;
    }
  while (rem >= SECSPERDAY)
    {
      rem -= SECSPERDAY;
      ++days;
    }

1970年の元旦からの日数と、残りの秒数を求めているみたいです。

if文の分岐先が中々不思議ですね。あまりを求めるときのあまりは、法(割る数)の符号に合わせて決めるはずだったと思うので、マイナスになることは無いし、法を超える事も無いはずだと思うのですが、もしかするとCの仕様ではそう決まってないのかも…?? よくわかりません(

  /* compute hour, min, and sec */  
  res->tm_hour = (int) (rem / SECSPERHOUR);
  rem %= SECSPERHOUR;
  res->tm_min = (int) (rem / SECSPERMIN);
  res->tm_sec = (int) (rem % SECSPERMIN);
  /* compute day of week */
  if ( (res->tm_wday = ( (EPOCH_WDAY + days) % DAYSPERWEEK)) < 0)
    res->tm_wday += DAYSPERWEEK;

時間・分・秒・曜日は特に何も考えなくても機械的に決定できるので、問題ないです。

 /* compute year & day of year */
  y = EPOCH_YEAR;
  if (days >= 0)
    {
      for (;;)
	{
	  yleap = isleap(y);
	  if (days < year_lengths[yleap])
	    break;
	  y++;
	  days -= year_lengths[yleap];
	}
    }
  else
    {
      do
	{
	  --y;
	  yleap = isleap(y);
	  days += year_lengths[yleap];
	} while (days < 0);
    }
  res->tm_year = y - YEAR_BASE;
  res->tm_yday = days;

少し長いですが、1,970年の元旦からの日数から、年を求めています。isleap関数はうるう年の時は1を、そうでないときは0を返す関数で、year_lengthsは{365,366}の配列です。このふたつを組み合わせるとその年の日数をうるう年を考慮した上で知ることができます。

基本的には1年ずつ年数を調べて、最初に計算した1970年からの経過時間であるdaysから引いていって、1年に満たなくなるまでループを回す、っていう、素朴な実装です。

最初に分岐がありますが、相変わらず、days<0になるのはいまいちどういうケースなのか想像できないです。。。

そして最後の-YEAR_BASEですが、tm構造体のtm_yearの値は1900年からの年数を表す事になってるので、これで引いています。

  ip = mon_lengths[yleap];
  for (res->tm_mon = 0; days >= ip[res->tm_mon]; ++res->tm_mon)
    days -= ip[res->tm_mon];
  res->tm_mday = days + 1;

ここまでで、daysは1年のうちでの日数を表しているので、年でやったのと同じように何月なのかを計算しています。mon_lengthsは…

static _CONST int mon_lengths[2][MONSPERYEAR] = {
  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
  {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
} ;

こんな感じの配列で、うるう年を考慮した月の日数の一覧です。

最後の+1ですが、tm_mdayは1から始まるきまりになってるので、その調整です。

そして最後の最後にgmtimeとlocaltimeでの分岐処理が入ります。

  if (!is_gmtime)
//省略
  else
    res->tm_isdst = 0;

gmtの時はtm_isdts=0に設定してるだけで簡単です。gmtにはサマータイムとかは無く、またunixtimeはGMTの1970年の元旦からの秒数なので時差もありません。

localtimeの時の処理はかなり骨がおれます。if文の中身を読んで行きましょう。

      if (_daylight)
	{
	  if (y == tz->__tzyear || __tzcalc_limits (y))
	    res->tm_isdst = (tz->__tznorth 
			     ? (*tim_p >= tz->__tzrule[0].change 
				&& *tim_p < tz->__tzrule[1].change)
			     : (*tim_p >= tz->__tzrule[0].change 
				|| *tim_p < tz->__tzrule[1].change));
	  else
	    res->tm_isdst = -1;
	}
      else
	res->tm_isdst = 0;

_daylightはグローバル変数で、サマータイムを使う可能性があるときにセットされます。

res->tm_isdstの判定なのですが、すいません、よくわかりません。__tznorthがfalseの時、条件が反転するみたいです。謎い…。サマータイム、嫌すぎる

→コメント覧参照ですが、これはどうやら南半球でのサマータイム(10月〜翌年3月ぐらい)の判定と、北半球でのそれとでの差のようです。これも最初まったく考えが至りませんでした。…北半球から出たことないです…。

      offset = (res->tm_isdst == 1 
		  ? tz->__tzrule[1].offset 
		  : tz->__tzrule[0].offset);
      hours = (int) (offset / SECSPERHOUR);
      offset = offset % SECSPERHOUR;

      mins = (int) (offset / SECSPERMIN);
      secs = (int) (offset % SECSPERMIN);
      res->tm_sec -= secs;
      res->tm_min -= mins;
      res->tm_hour -= hours;

サマータイムを考慮しつつ、時差を取得して、その分を足したり引いたりしている処理です。この時、機械的に足すので、時間が24を超えたり、マイナスになるかもしれません。ので、次で調整します。

      if (res->tm_sec >= SECSPERMIN)
	{
	  res->tm_min += 1;
	  res->tm_sec -= SECSPERMIN;
	}
      else if (res->tm_sec < 0)
	{
	  res->tm_min -= 1;
	  res->tm_sec += SECSPERMIN;
	}
      if (res->tm_min >= MINSPERHOUR)
	{
	  res->tm_hour += 1;
	  res->tm_min -= MINSPERHOUR;
	}
      else if (res->tm_min < 0)
	{
	  res->tm_hour -= 1;
	  res->tm_min += MINSPERHOUR;
	}
      if (res->tm_hour >= HOURSPERDAY)
	{
	  ++res->tm_yday;
	  ++res->tm_wday;
	  if (res->tm_wday > 6)
	    res->tm_wday = 0;
	  ++res->tm_mday;
	  res->tm_hour -= HOURSPERDAY;
	  if (res->tm_mday > ip[res->tm_mon])
	    {
	      res->tm_mday -= ip[res->tm_mon];
	      res->tm_mon += 1;
	      if (res->tm_mon == 12)
		{
		  res->tm_mon = 0;
		  res->tm_year += 1;
		  res->tm_yday = 0;
		}
	    }
	}
       else if (res->tm_hour < 0)
	{
	  res->tm_yday -= 1;
	  res->tm_wday -= 1;
	  if (res->tm_wday < 0)
	    res->tm_wday = 6;
	  res->tm_mday -= 1;
	  res->tm_hour += 24;
	  if (res->tm_mday == 0)
	    {
	      res->tm_mon -= 1;
	      if (res->tm_mon < 0)
		{
		  res->tm_mon = 11;
		  res->tm_year -= 1;
		  res->tm_yday = 364 + isleap(res->tm_year + 1900);
		}
	      res->tm_mday = ip[res->tm_mon];
	    }
	}

かなり長いですが、これはかなり単純で、例えば分が60を超えていたら調整のためにその上の時間を足して、…という処理を延々年まで行なっていってるだけです。

以上です。いかがだったでしょうか。一つ一つの処理はそこまで複雑ではないのですが、やはり長いですし、細かい例外もちゃんとフォローしてありましたね。ライブラリは偉大です。

閏秒は????????

さて、ここまでで気づきませんでしたか。

閏秒という概念が登場しなかった、という事実を。

そう、実はposix標準ではgmtime/localtimeは閏秒を考慮しないらしいですえっそれ普通に困るのでは

が、しかし、glibcのソースを眺めてみると、めんどくさくてあまり読んでないのですが、次の内部関数を見ると、どうやらうるう秒を考慮していそうです。

time/tzfile.c

void
__tzfile_compute (time_t timer, int use_localtime,
		  long int *leap_correct, int *leap_hit,
		  struct tm *tp)
  /* Apply its correction.  */
  *leap_correct = leaps[i].change;
  if (timer == leaps[i].transition && /* Exactly at the transition time.  */
      ( (i == 0 && leaps[i].change > 0) ||
       leaps[i].change > leaps[i - 1].change))
    {
      *leap_hit = 1;
      while (i > 0
	     && leaps[i].transition == leaps[i - 1].transition + 1
	     && leaps[i].change == leaps[i - 1].change + 1)
	{
	  ++*leap_hit;
	  --i;
	}
    }
}

むー、なんか複雑になってきました。

ということは:同じ時刻、違うunixtime?

閏秒を考慮する場合としない場合で、当然時刻の計算が違ってくるはずです。glibはうるう秒を考慮に入れてるっぽいので、適当に決めたunixtime=1354320000で、差をチェックしてみましょう。

ソースはこちらです。

#include 
#include 
int main()
{
	struct tm* result;
	time_t constTime = 1354320000L;
	result = gmtime(&constTime);
	printf("%04d/%02d/%02d %02d:%02d:%02d\n",
			result->tm_year+1900,
			result->tm_mon+1, //1月=0
			result->tm_mday,
			result->tm_hour,
			result->tm_min,
			result->tm_sec);
	return 0;
}

まずはnewlibでやってみます。なんかprintfがうまく動かないので、gdbでステップ実行しました(情弱)

結果がこんな感じ

# gdb ./gmtime_test
(略)
(gdb) print *result //resultの中身を表示するよ
$2 = {tm_sec = 0, tm_min = 0, tm_hour = 0, tm_mday = 1, tm_mon = 11, 
  tm_year = 112, tm_wday = 6, tm_yday = 335, tm_isdst = 0}
# -> 2012/12/01 00:00:00

で、Fedora 17のデフォルトのglibc(2.15)とリンクした方でテストすると、

# ./gmtime_test
2012/12/01 00:00:00

あれれ???一緒ですね…

さらに泥沼が待っているとはこの時まだ私は知らないのであった…

タイムゾーンの設定が、2つある

glibcのところで、うるう秒を考慮にいれているようなソースがありましたね。うるう秒というのはうるう年とは違って、ランダムに挿入されます。ので、どこかに「うるう秒の一覧」を記録しておかないと、正しく計算することができません。

「うるう秒一覧」が何処にあるかというと、fedora 17の場合、/usr/share/zoneinfoというフォルダの中にあり…

Africa	    Chile    GB		Indian	     Mideast   posixrules  US
America     CST6CDT  GB-Eire	Iran	     MST       PRC	   UTC
Antarctica  Cuba     GMT	iso3166.tab  MST7MDT   PST8PDT	   WET
Arctic	    EET      GMT0	Israel	     Navajo    right	   W-SU
Asia	    Egypt    GMT-0	Jamaica      NZ        ROC	   zone.tab
Atlantic    Eire     GMT+0	Japan	     NZ-CHAT   ROK	   Zulu
Australia   EST      Greenwich	Kwajalein    Pacific   Singapore
Brazil	    EST5EDT  Hongkong	Libya	     Poland    Turkey
Canada	    Etc      HST	MET	     Portugal  UCT
CET	    Europe   Iceland	Mexico	     posix     Universal

こんな感じでたくさん入ってます。この中には時差その他のデータが入っていて、うるう秒のデータも含めることができるそうです。詳しくは:tz database – Wikipedia

これを日本について見てみると…

% zdump Japan -v
Japan  -9223372036854775808 = NULL
Japan  -9223372036854689408 = NULL
Japan  Sat Dec 31 14:59:59 1887 UTC = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
Japan  Sat Dec 31 15:00:00 1887 UTC = Sun Jan  1 00:00:00 1888 JST isdst=0 gmtoff=32400
(略)
Japan  Fri Sep  7 16:00:00 1951 UTC = Sat Sep  8 01:00:00 1951 JST isdst=0 gmtoff=32400
Japan  9223372036854689407 = NULL
Japan  9223372036854775807 = NULL

となってて、2012年の7月にあった最新のうるう秒などのデータが入ってるようには見えません。

実はright/Japanというファイルもあって、こちらを見てみると…

% zdump right/Japan -v
right/Japan  -9223372036854775808 = NULL
right/Japan  -9223372036854689408 = NULL
right/Japan  Sat Dec 31 14:59:59 1887 UTC = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
(略)
right/Japan  Fri Sep  7 16:00:00 1951 UTC = Sat Sep  8 01:00:00 1951 JST isdst=0 gmtoff=32400
(略)
right/Japan  Sat Jun 30 23:59:60 2012 UTC = Sun Jul  1 08:59:60 2012 JST isdst=0 gmtoff=32400

2012年7月の表記があるので、ちゃんとうるう秒が反映されているようだ、とわかります。

タイムゾーンを正しい方に設定してみる

やっと正しい方が分かった所で、この正しい方にタイムゾーンを設定してみましょう。TZ環境変数に正しい方のファイルへのパスを指定すると、どのタイムゾーンのファイルを使うか設定することができます。

glibcの方から先にしてみましょう。

% TZ=/usr/share/zoneinfo/right/Japan ./gmtime_test  
2012/11/30 23:59:35

ふぅ。。。。さっきは12/01でしたが、うるう秒を考慮した結果25秒ほど過去に戻って11月になってしまいました。

一応、うるう秒を考慮にいれていないはずであるnewlibも試して見ました。

% TZ=/usr/share/zoneinfo/right/Japan gdb ./gmtime_test_newlib
(gdb) print *result
$1 = {tm_sec = 0, tm_min = 0, tm_hour = 0, tm_mday = 1, tm_mon = 11, 
  tm_year = 112, tm_wday = 6, tm_yday = 335, tm_isdst = 0}
# -> 2012/12/01 00:00:00

おおお、やっと差が出ましたね!つ、疲れた…。まとめましょう。

ちなみに毎度毎度TZ環境変数にいれなくても、システム全体で正しい方を使うようにもできます

まとめ

  • gmtime/localtime、簡単そうな処理ですがサマータイムとかうるう秒を考えたらやっぱりライブラリ使いましょう
  • gmtime/localtimeはposix標準ではうるう秒を考慮しません
  • glibcでは考慮しますが、デフォルトでは無効化されていて、タイムゾーンファイルの指定を変更することで「正しい」時間を表示するように出来ます。
  • する場合としない場合で、2012年現在25秒も違います。

なんていうか、…大変ですね(しみじみ)。一番の問題はやっぱりposix標準で「うるう秒を考慮しない」としてる点だと思います。今更変えられないのが非常に…悔しいですね…。

SElinuxはミリしらだけどアクセス許可を追加してみた

 SELinuxってたまに聞くけど中身は全然知らなかったのですが、今回Fedora 17のChromeでNativeClientが動かなかったので設定することにしました。

SELinuxって何じゃらホイ

 SELinuxって名前は聞いたことあって、セキュリティ系の機能らしいというのは知っているのですが、それ以上のことはよく知りませんでした…。

 というわけで、調べてみました。第1回 セキュアOS機能「SELinux」の基本的な仕組み。ちょっと古い記事ですが、まあ問題ないでしょう。

私の理解したところによると。

 今までの普通のLinuxには、chmodで設定するような、ユーザーやグループを用いたアクセス制御がありますが、この場合は万が一root権限で動くプロセスを奪取されるとすべてにアクセスし放題になってしまってよろしくありません。というわけで、SELinuxではそれとは別にプログラム単位で色々な制限を、ファイルへのアクセス制限だけでなく、もっと色々な行動への制限が掛けられるようになっている…ということみたいです。

 …間違ってたらゴメンネ。例えばhttpdにはネットワーク接続の権限が必要だけど、passwdへのアクセスは要らないから、これを制限しよう、という発想だと理解しました。これをTE(Type-Enforcement)と言うそうです。

 RABCっていう別の仕組みもあるそうですが、今回引っかかったのはTEなのでこちらに話を絞ります

Fedora 17でGoogle ChromeのNative Clientが動かない

 さて。今回引っかかったのはGoogle ChromeのNative Clientです。SDKに入っているexampleを試しに動かしてみましたが…こんな感じで動いてくれません。

 20120825_01.png

 うまく行くとこうなるはずなんですけどー!

20120825_02.png

 で、まずはGoogleで検索してみたのですが、どうやら今回のSELinuxが問題で動かないようになっているようだ、という事が分かりました。

まずは全体を無効にしてみる

 まずは、SELinux全体を無効にしてみました。

% sudo setenforce Permissive

こうすると全体のSELinuxが無効になります。で、この状態でさっきのExampleが綺麗に表示されたので、SELinuxが問題だと分かります。

あとで有効に戻すには次のコマンドを使って下さい。無効にしたままは流石にどうかと思うので…

% sudo setenforce Enforcing

さらに細かい粒度で有効か無効かを調べる

 とりあえずSELinuxの問題であることがわかったので、Chromeのどういうモジュールなのかなあというのを調べるために、プロセスのドメインごとに有効無効を切り替えられるツールを使います

% sudo yum install policycoreutils-gui

20120825_03.png

どうやらChromeにはsandboxとsandbox_naclという2つのドメインが割り当てられているようです(この割り当て自体はFedoraの中の人がやってるようです)。どう考えても後者が怪しいので、Permissiveボタンを押してnaclの方をPermissiveモード(SELinux無効)にしてみると…やっぱり動作しました。これが問題みたいです。

AuditのログからSELinux用のモジュールを作る

 SELinuxを一時的に解除して動くようにしましたが、SELinuxはアクセス制御は行なっていませんが、ログを、Auditdというデーモンのログに記録しています。Fedoraの場合は/var/log/audit/audit.logに入っております。

 このAuditというデーモン、syslogdみたいな感じらしいです。でもSyslogdよりリッチなシステムらしいです。詳しくはLinux Audit 〜OSレベルの監査ログ徹底活用〜が参考になります。

 このログの中で、chrome nacl…といったキーワードを調べていくと、chrome_sandbox_nacl_tというのがnacl関連だと思われるので、それでgrepします。

%sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log

 さらにこれでログを抽出した上で、audit2allowというプログラムを用いて、SELinuxでの設定ファイル、モジュールを作る事ができます。

%sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log | audit2allow -a -l r
require {
type chrome_sandbox_nacl_t;
type chrome_sandbox_t;
class unix_dgram_socket { read write };
}
#============= chrome_sandbox_nacl_t ==============
allow chrome_sandbox_nacl_t chrome_sandbox_t:unix_dgram_socket { read write };

さらに、-M module名とすると、モジュールのコンパイルもしてくれます。

% sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log | audit2allow -a -l -r -M chrome_sandbox_nacl

 で、最後にsemodule -iを使ってインストールします。

% sudo module -i chrome_sandbox_nacl.pp

 以上です。さっきのGUIでnaclだけPermissiveにしていたのを元に戻してEnforcingにしても、ちゃんとnaclが動作するようになるはずです!

モジュールを作らなくても大丈夫な場合もある

 今回はモジュールを作りましたが、正直言って結構大工事でしたね。それ以外にも、getsebool/setseboolで設定できるフラグや、ファイルのタイプを変更する方法でもSELinuxの制限を何とか出来たりもするようです。が、今回は駄目でした。

SELinuxは分かりづらい。

 SELinuxは他のLinuxのデーモンなどと違い、設定ファイルがバイナリにコンパイルされた状態で保存されています。(/etc/selinuc/targetd/以下)。速度が重要な所ですから、多分そのためだとは思うのですが、いまいち分かりづらいです。

 上記で使ったaudit2allowの代わりに、audit2whyというのを使うと制限された理由を教えてくれたりもするようです。

type=AVC msg=audit(1345883976.560:36379): avc:  denied  { read write } for  pid=15643 comm="nacl_helper_boo" path="socket:[667837]" dev="sockfs" ino=667837 scontext=unconfined_u:unconfined_r:chrome_sandbox_nacl_t:s0-s0:c0.c1023 tcontext=unconfined_u:unconfined_r:chrome_sandbox_t:s0-s0:c0.c1023 tclass=unix_dgram_socket
Was caused by:
Missing type enforcement (TE) allow rule.
You can use audit2allow to generate a loadable module to allow this access.

Missingって…。Fedoraの中の人のミスじゃないかなあ、これ…。

参考にしたところ