yunomuのブログ

酒とゲームと上から目線

Android SDKのSQLiteDatabase#execSQLのbindArgsの型

AndroidSDKのSQLiteDatabase#execSQLのマニュアル
[http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#execSQL(java.lang.String, java.lang.Object[])]
よくあるsqlとbindArgs(Object配列)を渡す構造なんですが、このbindArgsのマニュアルを見ると

Parameters

sql the SQL statement to be executed. Multiple statements separated by semicolons are not supported.
bindArgs only byte[], String, Long and Double are supported in bindArgs.

byte[], String, Long and Doubleのどれかしかサポートしてないよと書いてあります。

すると「え、でもInteger入れたいんだけどどうなのよ」ってなるわけじゃないですか。
で、中身を見てみると

SQLiteDatabase.java

    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        executeSql(sql, bindArgs);
    }

ここはnullを弾いてるだけ。

SQLiteDatabase.java

    private int executeSql(String sql, Object[] bindArgs) throws SQLException {
        acquireReference();
        try {
            if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) {
                boolean disableWal = false;
                synchronized (mLock) {
                    if (!mHasAttachedDbsLocked) {
                        mHasAttachedDbsLocked = true;
                        disableWal = true;
                    }
                }
                if (disableWal) {
                    disableWriteAheadLogging();
                }
            }

            SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs);
            try {
                return statement.executeUpdateDelete();
            } finally {
                statement.close();
            }
        } finally {
            releaseReference();
        }
    }

ここはSQLiteStatementのコンストラクタに渡して、その後にSQLiteStatement#executeUpdateDeleteを呼んでる。

SQLiteStatement.java

    SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) {
        super(db, sql, bindArgs, null);
    }

コンストラクタでは親クラスのコンストラクタに渡しているだけ。おそらく単に親のフィールドに格納していて、getBindArgs()で取ってきたりしているんだろう。というかしてた。ちょっと気を使った書き方になってたのでライブラリ作る人は見てみてもいいかも。

SQLiteStatement.java

    public int executeUpdateDelete() {
        acquireReference();
        try {
            return getSession().executeForChangedRowCount(
                    getSql(), getBindArgs(), getConnectionFlags(), null);
        } catch (SQLiteDatabaseCorruptException ex) {
            onCorruption();
            throw ex;
        } finally {
            releaseReference();
        }
    }

executeUpdateDeleteメソッドでは、getBindArgsで親クラスから取ってきた値をSQLiteSession#executeForChangedRowCountに渡している。

SQLiteSession.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) {
            return 0;
        }

        acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
        try {
            return mConnection.executeForChangedRowCount(sql, bindArgs,
                    cancellationSignal); // might throw
        } finally {
            releaseConnection(); // might throw
        }
    }

ここでは、2箇所でbindArgsが使われているけれど、1つ目のexecuteSpecialの中では使われていなかったので、2つ目のSQLiteConnection#executeForChangedRowCountに渡されている方が本命。

SQLiteConnection.java

    public int executeForChangedRowCount(String sql, Object[] bindArgs,
            CancellationSignal cancellationSignal) {
        if (sql == null) {
            throw new IllegalArgumentException("sql must not be null.");
        }

        int changedRows = 0;
        final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount",
                sql, bindArgs);
        try {
            final PreparedStatement statement = acquirePreparedStatement(sql);
            try {
                throwIfStatementForbidden(statement);
                bindArguments(statement, bindArgs);
                applyBlockGuardPolicy(statement);
                attachCancellationSignal(cancellationSignal);
                try {
                    changedRows = nativeExecuteForChangedRowCount(
                            mConnectionPtr, statement.mStatementPtr);
                    return changedRows;
                } finally {
                    detachCancellationSignal(cancellationSignal);
                }
            } finally {
                releasePreparedStatement(statement);
            }
        } catch (RuntimeException ex) {
            mRecentOperations.failOperation(cookie, ex);
            throw ex;
        } finally {
            if (mRecentOperations.endOperationDeferLog(cookie)) {
                mRecentOperations.logOperation(cookie, "changedRows=" + changedRows);
            }
        }
    }

真ん中あたりのbindArgumentsで使われている。これ以降には出てこないので、たぶんここでstatementの中に入っちゃってるんだろう。nativeExecuteForChangedRowCountまで見れば処理の本体が見られるだろうけど、今回の興味はここではない。
ところでこのtry節のネストっぷりは、なるほどなぁと思った。次からこうしよう。

SQLiteConnection.java

    private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
        final int count = bindArgs != null ? bindArgs.length : 0;
        if (count != statement.mNumParameters) {
            throw new SQLiteBindOrColumnIndexOutOfRangeException(
                    "Expected " + statement.mNumParameters + " bind arguments but "
                    + bindArgs.length + " were provided.");
        }
        if (count == 0) {
            return;
        }

        final int statementPtr = statement.mStatementPtr;
        for (int i = 0; i < count; i++) {
            final Object arg = bindArgs[i];
            switch (DatabaseUtils.getTypeOfObject(arg)) {
                case Cursor.FIELD_TYPE_NULL:
                    nativeBindNull(mConnectionPtr, statementPtr, i + 1);
                    break;
                case Cursor.FIELD_TYPE_INTEGER:
                    nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).longValue());
                    break;
                case Cursor.FIELD_TYPE_FLOAT:
                    nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
                            ((Number)arg).doubleValue());
                    break;
                case Cursor.FIELD_TYPE_BLOB:
                    nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
                    break;
                case Cursor.FIELD_TYPE_STRING:
                default:
                    if (arg instanceof Boolean) {
                        // Provide compatibility with legacy applications which may pass
                        // Boolean values in bind args.
                        nativeBindLong(mConnectionPtr, statementPtr, i + 1,
                                ((Boolean)arg).booleanValue() ? 1 : 0);
                    } else {
                        nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
                    }
                    break;
            }
        }
    }

ここではfor文の中でなんぞ判定して、数値ならNumberにキャストしてからlongValueなりdoubleValueを呼び出してる。これならIntegerでもいけるっぽい。
ところでこのDatabaseUtils.getTypeOfObjectって何だ。

DatabaseUtils.java

    public static int getTypeOfObject(Object obj) {
        if (obj == null) {
            return Cursor.FIELD_TYPE_NULL;
        } else if (obj instanceof byte[]) {
            return Cursor.FIELD_TYPE_BLOB;
        } else if (obj instanceof Float || obj instanceof Double) {
            return Cursor.FIELD_TYPE_FLOAT;
        } else if (obj instanceof Long || obj instanceof Integer
                || obj instanceof Short || obj instanceof Byte) {
            return Cursor.FIELD_TYPE_INTEGER;
        } else {
            return Cursor.FIELD_TYPE_STRING;
        }
    }

このように、実際にはここでinstanceofを使って型を判別しております。見たところIntegerでもShortでもFloatでも良さそうですね。まあFloatをintValueで変換とかはしてくれなさそうですが、おおむね問題は起きないようにしてあるっぽいです。

で、マッチしなかったらStringとみなされて、さっきのbindArgumentsに戻ってみると、そこでさらにBooleanかどうかの判定が行われて、trueなら1、falseなら0、BooleanじゃなければObject#toStringが呼ばれる、と。
これ、このswitch文は必要なのかな。DatabaseUtils.getTypeOfObjectは消して、このswitch文と同化してしまえばいい気がするけどどうなんだろう。getTypeOfObjectがどこかの使い回しだったりするのかな。まあするんだろうなUtilsだし。

最後にtoStringしてるってことは、enumにtoStringを実装しておいてぶっ込むとかもできるし、細かく楽ができそうですね。

SSHがつながらない

前回と同じネットワーク構成で。
f:id:yunomu:20140311120402p:plain
AからVPN経由でCへのSSH接続を試みる。

% ssh 192.168.2.11
ssh_exchange_identification: read: Connection reset by peer

つながらない。

Aからインターネット経由でC(eth0)へのSSH接続は普通にできる。

% ssh server_public_ip
Last login: Fri Mar 14 14:36:19 2014 from 192.168.1.2
[yunomu@host ~] $

だいたいこういう感じ。

/var/log/secureで関係ありそうなのは以下の1行だけ。

Mar 14 13:41:41 host sshd[30405]: Did not receive identification string from 192.168.1.2

他の/var/log/messagesとかは何も出てなかった。

TCPWrapperやxinetdは使っていなくて、生sshdで動いている。
なのでhosts.allowやhosts.denyは空で問題ないはず。

死んだsshdのプロセスがウヨウヨいたりして新規接続の確立ができてないのかなと思ったけど特にそういう気配も無し。

というかそもそも/var/log/secureの通り一応sshdが動くところまでは行ってるわけで。sshdが動いた上で、クライアントからのデータが何も来ないからしばらくしてコネクションが切られているっぽい。

さらに面倒くさいことに、2日前まではインターネット経由でもVPN経由でもつながっていたし、この記事を書き始めてから試したらつながった。
つながったじゃないか! なんなんだこれ!

というわけで、またよくわからないうちに事件が解決してしまった。いや解決してないんだけど。

ネットワークがつながらない

基本がわかっていないとなにもかもわからなくなる。

出来事

外で借りてるサーバ(C)を使ってこんな感じのネットワークを作った。
f:id:yunomu:20140311120402p:plain

  • AはWindows 7
  • Aの隣に何台かノート(D)やら携帯(E)やらがある
  • BはUT-VPNクライアントが動く程度のLinux(CentOS 5系だったと思う)
  • CはVPSLinux(これもCentOS 5系のはず)
  • BとCはUT-VPNで接続していて、仮想HUBはCにある
  • ルータはなんか数千円の安いやつ
  • 白い四角はルーティングテーブル

この状態で、CにはDBやらWebサーバやらいろいろ置いてあって、AからVPN経由で触って遊べるようにしたつもり。

で、しばらくなんの問題もなく遊んでいたんですが、なんかAのネットワークとCの通信がうまくいったりいかなかったりする。
症状はこんな感じ。

  • AとEからはCのサービス(HTTP)が見られる
  • DからはCのサービス(HTTP)が見られない
    • Bのeth0ではHTTPのrequest/responseパケットをキャプチャできる
  • A, D, EからC(192.168.2.11)にpingを飛ばすと全て応答する
  • CからAやDにpingを飛ばすとどちらも応答しない
    • Bのeth0ではicmpパケットのecho/replyをキャプチャできる

何がうまくいかないのかさっぱりわからん。なんでDのTCPだけ通信できないんだ。

よく見てみると、DからCにTCPで通信する時、行きはルータ->B->Cと経由しているけど、帰りはBからルータを経由せずにDに帰るようになっている。
ということでBのルーティングテーブルを変更して192.168.1.0/24を192.168.1.1に向けてみた。
するとDはCとTCPでも通信できるようになったが、今度はAとEがCと通信できなくなった。

まあそういういい加減な対処は無かったことにして。

なんとなく、ルータを経由したりしなかったりするのが良くないんじゃないかという気がするので、さしあたりDに192.168.2.0/24->192.168.1.10のルーティングを追加して、まあなんとか使える感じにしてみた。
一晩寝ると、CからAへのpingは通るようになっていたけども、CからDへは通らず。

なんか、何年も前に何か問題が起きたわけじゃないけども、こういう感じの問題に対処するために何かした記憶がおぼろげにあるような気がしないでもないけども、全然思い出せない。

よくわからない

問題は、何が悪いかよくわかっていないこと。割とつながってるやつもいるし、ネットワーク的には間違っていないようにも見える。
つながったりつながらなかったりするってことは、ARPテーブルがどうのこうのいう感じかなぁとも思ったものの、いじってみても特に変化はなさそう。

こういう時に、これがIPとして正しい動きなのか、TCPとして正しい動きなのか、Ethernetとして正しい動きなのか、それともそれぞれの機器の実装の問題なのか、そのあたりの切り分けができない。
自信を持って「これはIP的にはちゃんとできてるハズなんだけどねぇ~」みたいな事が言えない。
本当に教科書的な意味で、基本がよくわかっていない。

私のネットワークの知識はなんかルータいじってる過程でいつの間にか身についたものなので、プロトコルに関しては実のところよく知らなかったりするのです。誰だよ私がネットワークに詳しいなんて言ったボンクラは。(私です)

そろそろTCP/IPの本を一冊くらい読んでまともに勉強してみようかなぁと思いました。

それはそれとして件の問題は片付いていないんですが。

haskell-relational-recordとMySQLの識別子の大文字小文字

haskell-relational-record(hrr)とMySQLを使ってプログラムを書いてみようと思った。

書き方自体はこのあたり。
https://github.com/khibino/haskell-relational-record
https://github.com/krdlab/haskell-relational-record-driver-mysql

元々haskell-relational-recordはPostgreSQLのために作られているので(訂正:IBM-DB2 + ODBCで動かしていたのが最初だそうです)、MySQLから使おうとするとちょっと詰まるのかもしれない。
まずdefineTableFromDBが動かなかったんですけど、それはちょっと原因がよくわからないので保留。

そもそもの問題が適当に作られたテーブルの形式だったりするわけなんですが、おおむねこういう感じになっている。

mysql> connect myapp
Connection id:    126780
Current database: myapp
mysql> desc USER;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| NAME         | varchar(32)  | NO   | PRI | NULL    |       |
| MAIL_ADDRESS | varchar(128) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

MySQLではスキーマとデータベースの区別が無いというか、同じ物を指すっぽいので、データベース名もスキーマ名も同じ"myapp"になっている。そして、テーブル名とフィールド名を大文字のスネークケース。どこの常識だこれはという感じ。まあこれはそういうものなので仕方ないものとする。

これを元にテーブル定義を作るとこうなる。defineTableFromDBが動かないので自分で定義する。

{-# LANGUAGE TemplateHaskell, MultiParamTypeClasses, FlexibleInstances #-}

module User where

import Database.HDBC.Query.TH (defineTableDefault')
import Database.Record.TH (derivingShow)
import Language.Haskell.TH
import DataSource

defineTableDefault' "myapp" "USER"
    [ ("NAME", conT ''String)
    , ("MAIL_ADDRESS", conT ''String)
    ] [derivingShow]

でき上がるテーブルとクエリの定義はこうなる。

*Main> :i USER
data USER
  = USER {nAME :: !String,
          mAILADDRESS :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM MYAPP.user

とてもきもちわるい。その上この時点で既に破綻している。

  • データ型名が全部大文字で気持悪い
  • フィールド名の'_'が消えて先頭だけが小文字になっていて気持悪い
  • クエリの関数名が同様に"uSER"て何よ
  • クエリの中身のデータベース名が大文字に、テーブル名が小文字になっている

この、最後のやつが困る。他はまあ気持悪いだけなのでいいのだけど。
どう困るかというと、このクエリを発行するとそんなテーブル無いよと言われる。MySQLはテーブル名の大文字と小文字を区別することがある。

MySQLのドキュメントにはこういう事が書いてある。

5.2.2. 識別子の大文字小文字の区別
MySQL において、データベースはデータディレクトリ内のディレクトリに対応しています。データベース内の各テーブルも、データベースディレクトリ内の少なくとも 1 つ(記憶エンジンによってはそれ以上)のファイルに対応しています。トリガーもファイルに対応しています。そのため、ベースとなるオペレーティングシステムで大文字と小文字が区別される場合、データベース名とテーブル名でも大文字と小文字が区別されます。
MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 5.2.2 識別子の大文字小文字の区別

つまりMySQLのデータがWindowsファイルシステム上に置かれている場合はデータベース名やテーブル名の大文字小文字は区別しないけれど、UnixLinuxの多くのファイルシステム上に置かれていた場合は区別しますという事。なんじゃそりゃという感じだけども、これも仕方ない。

これをなんとかしてhrrを使うためには、ソースをいじる必要がある。
問題はテーブル定義と同時に作成されるクエリ関数なわけなので、それを定義している場所を探すと、tableSQLという関数が見つかる。場所はここ。
haskell-relational-record/relational-query/src/Database/Relational/Query/TH.hs at master · khibino/haskell-relational-record · GitHub

tableSQL :: String -> String -> String
tableSQL schema table = map toUpper schema ++ '.' : map toLower table

とてもわかりやすいですね。
これの"map toUpper"と"map toLower"を消してあげればいいのです。PostgreSQLでは基本的にテーブル名は小文字なのでこうなってるんだと思います。スキーマ名は忘れたけど大文字なんだっけ?

ということで、hrrはまだhackageに登録されていないので皆様各々githubからcloneして使っておられると思いますので、この辺りをいじる心理的障壁は比較的少ないんじゃないかと思います。やっちゃいましょう。branch作ってcommitしておくと本家が更新した時に追随が楽だと思います。いや検証してPull Request送れという話はもっともなんですが。

ともあれこれで一応クエリは利用可能になります。めでたしめでたし。

*Main> uSER
SELECT NAME, MAIL_ADDRESS FROM myapp.USER

ところで実はMySQLはデータベース名とテーブル名についてはなんか面倒な仕様があるんですが、フィールド名の大文字小文字は区別しないので、テーブル定義のところはフィールド名を小文字のスネークケースにすると少しだけ幸せになります。

defineTableDefault' "myapp" "USER"
    [ ("name", conT ''String)
    , ("mail_address", conT ''String)
    ] [derivingShow]

ごらんよ、焼け石に水って感じだけども。これでも普通に使えます。

*Main> :i USER
data USER
  = USER {name :: !String,
          mailAddress :: !String}
     -- Defined at User.hs:10:1
instance Show USER -- Defined at User.hs:10:1
instance TableDerivable USER -- Defined at User.hs:10:1
instance ProductConstructor (String -> String -> USER)
  -- Defined at User.hs:10:1
*Main> uSER
SELECT name, mail_address FROM myapp.USER

Frappuccinoの使い方

前回、Frappuccinoの件をあまりにももののついでみたいに書いてしまったんですが、使おうとすると例によってドキュメントが無くてソースを読む事になって、なったので、その時の足跡です。
https://github.com/steveklabnik/frappuccino

Source

前準備として、streamとイベント発生装置(Source)を作る。公式のReadmeではButtonとなってるやつ。

class Source
  def send(event)
    emit(event)
  end
end

source = Source.new
stream = Frappuccino::Stream.new(source)

このemitは、ソースを見てみると、まずStream.newで渡されたインスタンスにFrappuccino::Sourceを継承させてる。
frappuccino/stream.rb

module Frappuccino
  class Stream
    include Observable

    def initialize(*sources)
      sources.each do |source|
        source.extend(Frappuccino::Source).add_observer(self)
      end 
    end
(略)

それでemitはSourceに定義されている。中では変更フラグを立てて、イベントをObserverに通知している。
frappuccino/source.rb

require 'observer'

module Frappuccino
  module Source
    def self.extended(object)
      object.extend(Observable)
    end 

    def emit(value)
      changed
      notify_observers(value)
    end 
  end 
end

というわけでSource.sendの中でemitを呼び出すことができるようになっている。Observableは標準添付のobserverに入ってるやつです。そんなの標準であったのかという感じですが。

Streamにはmap, drop, take, select, zip, merge, scan, count, inject, merge, update, on_valueというメソッドがあって、分類すると以下のような感じになります。

  • Streamを返す
    • map
    • drop
    • take
    • select
    • zip
    • merge
    • scan
  • Proptertyを返す
    • inject
    • count
  • その他
    • update
    • on_value

Stream系

Streamを返すやつらは、分類したり変換したり統合したり、まあメソッド名を見ればでわかるような事をやります。Java8のStreamと同じような感じです。
mapは変換、dropは指定した個数のイベントを無視、takeは指定した個数のイベント以降を無視、selectは条件に合うイベントのみを拾うようなStreamを返します。だいたい想像通りに動くと思います。

mapはブロックじゃなくてHashを取ることもできて、

  def map(hash = nil, &blk)
    blk = lambda { |event| hash.fetch(event) { hash[:default] } } if hash
    Map.new(self, &blk)
  end

みたいになってて、

stream.map(:e1 => 1, :e2 => 2, :default => 0)

みたいなHashを渡すと、キーと一致するイベントがきたら特定の値に変換するというのが簡単に書けるんですが、まああんまし使わないと思います。

zipとmergeはStreamの統合で
zipは

stream3 = stream1.zip(stream2)

とやると、stream1とstream2の両方のイベントが1つずつそろった時にstream3が発火するような感じ。ソース見ると本当に単にバッファを2つ作って、両方そろったら発火させているだけで、あーこんなに簡単に作れるのかーという気がする。

mergeは

stream3 = Stream.merge(stream1, stream2)

zipと似てるんですけど、これはstream1とstream2のどちらかが発火したら発火する。

scanは畳み込みみたいな感じで、前回のイベントを参照しながらmapみたいな処理ができる。
例えば、イベントの数を数えるのはこう。

stream2 = stream1.scan(0) {|a| a + 1 }

イベントとして飛んでくる整数の合計値を取る場合はこう。

stream2 = stream1.scan(0) {|a, v| a + v }

ブロックの第一引数が蓄積変数というか前回の値で、第二引数が今回のイベントになってる。集計以外では、値のストリームを前回との差分のストリームに変換したりとか、そんなふうに使うんじゃないでしょうか。

Property系

Property系は、StreamではなくPropertyオブジェクトを返します。

module Frappuccino
  class Property
    def initialize(zero, stream)
      @value = zero
      stream.add_observer(self)
    end 
        
    def now
      @value
    end
    
    def update(value)
      @value = value
    end
  end
end

ご覧の通り、nowとupdateがあって、nowは現在の値を返します。updateはどうでもいいというか使わないと思います。
「現在の」と言ったのは、イベントが発生するとnowの値が変わるからです。公式のReadmeにあるとおりなんですが。

injectは、scanの結果をPropertyにしたやつで、scanの合計値を数えるのと同じ事をやると、

counter = stream1.inject(0) {|a| a + 1 }

これでcounter.nowで現在のイベントの個数が取得できます。以下と全く同じです。

stream2 = stream1.scan(0) {|a| a + 1 }
counter = Frappuccino::Property.new(0, stream2)

countはinjectのラッパーで、個数を数えるのに特化しています。引数を渡すと内部でselectされます。

counter = stream1.count #=> イベントの個数を数える
counter1 = stream1.count(:event1) #=> :event1の個数を数える
counter2 = stream1.count {|e| e[:value] >= 2 } #=> 条件に合うイベントの個数を数える

その他

updateはイベントを発生させます。
Source以外の場所からemit相当の事をするために使うっぽい。というかこれがあれば実はemitを使わなくてよくて、実は前回の例はこう書ける。(Emitterクラスを定義してない)

ws = WebSocket::Client::Simple.connect url
stream = Frappuccino::Stream.new
ws.on :message do |msg|
  stream.update msg
end

公式のReadmeの例に引っ張られすぎだった。

on_valueは、イベントが発生したら引数で渡したブロックを評価する。Streamをイベントに変換するメソッドです。便利!

おわり

実装が思いのほか単純で、読むのが楽なのが素晴らしいですね。まあドキュメントあるにこしたことはないんですけど。
ところでこのライブラリって何に使うんでしょうね。

Chrome remote-debuggingの話と見せかけてWebSocketとかfrappuccinoとか

最近ちょっとブラウザで遊んでいたので、共有というか。

Google Chromeにremote debuggingという機能があります。
https://developers.google.com/chrome-developer-tools/docs/debugger-protocol

要するにJavaScript開発者にはおなじみのDeveloper toolsが使っているAPIなんですが、最近モバイル対応だのなんだののために整備されて使いやすくなったとかなんとか。最近て、何年前の話か知りませんけども。

使うためには、remote debugging portを有効化してChromeのプロセスを起動する必要があります。Chromeのプログラム名は環境によって違うので、それらしいやつを起動してください。

% google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp

とやると起動します。9222はなんか、デフォルトのポートらしいです。
user-data-dirはプロファイルのディレクトリで、指定しないと既存のChromeプロセスがforkするだけでremote-debuggingは有効になりません。既存のプロセスが無ければうまくいくんですが。

起動すると、
http://localhost:9222/
にアクセスできるようになります。このトップページは現在開いているタブの一覧です。それぞれのページを開くと、おなじみのdeveloper toolsが開きます。developer toolsってJavaScriptで書かれてたのかよという。

http://localhost:9222/json
にアクセスすると、開いているタブの情報がJSONで取れます。
この中にある”webSocketDebuggerUrl”というやつが、remote-debugging APIのエンドポイントです。その名の通り、WebSocketで通信します。

ここからは手打ちでは辛いのでRubyで操作します。
まず、どうでもいいんですがwebSocketDebbuggerUrlの取得部分。

require 'json'
require 'open-uri'

# ページ一覧のJSONを取ってくる
def pagelist(host = "localhost", port = 9222)
    JSON.parse open("http://#{host}:#{port}/json") {|f|
        f.read
    }   
end

# とってきたやつからタイトルに”GitHub”が含まれる最初のものを探す
w = pagelist.find {|e| e["title"].include? “GitHub }

# webSocketDebuggerUrlを取得
debugUrl = w["webSocketDebuggerUrl"] || begin
    puts "debugUrl is nil"
    exit 1
end

ここではタイトルに”GitHub”を含む最初のタブのdebugger urlを取得していいます。これとは別にdeveloper toolsを使っていたりするとこのインタフェースが無くなったりするので、念のためにエラー処理を入れてます。

で、WebSocketでの通信ではwebsocket-client-simpleを使います。たぶんこれが一番楽です。

% gem install websocket-client-simple
# (さっきの続き)
require 'websocket-client-simple'

# 接続
ws = WebSocket::Client::Simple.connect debugUrl

# 以下はイベントハンドラ

ws.on :message do |msg|
    data = JSON.parse(msg.data)
    print "response: ", data["params"], $/
end

ws.on :open do
    puts "open"
end

ws.on :close do |s|
    puts "close"
end

基本はこんな感じ。イベントドリブンになっていて、message, open, closeというイベントが起きます。messageは受信した時、openは接続完了した時、closeは閉じた時。
それとは別に、sendというメソッドがあります。

試しにTimelineで流れてくるイベントを取ってみます。
https://developers.google.com/chrome-developer-tools/docs/protocol/1.1/timeline
ここに載ってるstartというメッセージを送ると、ブラウザ側からイベントのログがドロドロと流れてきます。
WebSocketのopenのタイミングが微妙なのでopenハンドラに直に送信処理を書きます。

ws.on :open do
    ws.send JSON.dump("id" => 1, "method" => "Timeline.start")
end
sleep 5

こうすると5秒ほどイベントが流れてきます。流れてこないという人はJavaScriptイベントが起きていない可能性があるのでブラウザのページ上でマウスを動かしたりなんたりしてみてください。
あと、このidというのは、まあなんかidらしいです。適当に決めてます。

{
  "record"=>{
    "startTime"=>1388377232581.642,
    "type"=>"Program",
    "data"=>{},
    "children"=>[],
    "endTime"=>1388377232581.6663
  }
}

イベントの1個の形式がこんな感じで(Rubyに整形済み)、いわゆるTimelineの情報が入っているんですが、このstartTime/endTimeというやつが、エポックからのまさかのマイクロ秒で、ミリ秒じゃねーのかよってつまんないところでつまづきました。

frappuccino

messageイベントのハンドラを書くのが面倒くさい。ので、frappuccinoでイベントをストリームに変換してみようと思いました。いわゆるFRPライブラリです。
https://github.com/steveklabnik/frappuccino

% gem install frappuccino
require 'frappuccino'

# イベントストリームを作るやつ
class Emitter
  def send(data)
    emit(data)
  end 
end
emitter = Emitter.new
stream = Frappuccino::Stream.new(emitter)

# debug: 送られてきたイベントを画面に出力する
#stream.on_value do |v|
#  puts v
#end

# eventRecordedだったら画面に出力する
stream
  .select {|d| d["method"] == "Timeline.eventRecorded"}
  .on_value {|d| puts d } 

# eventRecordedじゃなかったら"log"というファイルに書き出す
stream
  .select {|d| d["method"] != "Timeline.eventRecorded"}
  .on_value {|d| open("log", "a+") {|f| f.puts d } } 

# (中略: 最初に書いたWebSocketを接続したりする処理)

# データを受け取ったらストリームに流す
ws.on :message do |msg|
  emitter.send JSON.parse(msg.data)
end

これはわかりやすくなったんだろうか?
でもまあ、今回みたいにselectが複数あったりとか、あと畳み込みをしたくなった時には結構いいかもしれません。この例だとzipやmergeはあんまし使わないかなぁ。

しかしストリームをイベントに変換してそれをまたストリームに変換とういのもなんか妙な話ですね。

ノートの使い方

ノートの話をたまに書いているので、じゃあ次はペンの話でも書こうかと思ったけど、ノートの使い方の話は書いてないよねと指摘を受けたので書いてみます。そういやそうだよね。断片的には書いてるけど。

まず基本として、ノートは記憶の補助として使っています。というかそれ以外で何かあるのかな。
その、記憶の中でもかなり短期、1日とか数日とか、おおむね1週間以内の情報を置くために使っています。読み返すとしても数ページ、多くても10ページとかそういう感じで、それ以上前の事はあまり覚えてもいません。それと関係あるかは微妙ですが字は雑です。

それと、1ページの中で日付をまたぐ事もめったにしないので、1〜2行で終わってたり、タイトルだけだったり、ヘタすると日付だけだったりもします。
まあもったいない使い方のような気もしないでもないですけど、余白も情報だと思いますし、詰めて書いて追記できないのもバカバカしいし、「もったいないと思うならリサイクルに出せばいい」と解決になってるんだかなってないんだかわかんない理屈もあります。単に何も気にしていないというだけですが。

書いている内容は、リストです。テーマごとに項目を列挙しているだけです。そのテーマというのが日記(と呼んでる今日の仕事)だったり開発の検討項目だったり議事録だったり作業ログだったりする。で、だいたい1日に数ページ進む。
これを、今自分が何をやっているかを思い出すために使っています。あー議事録だけは普通のメモですけども。

基本的にはスタックというか、気が散ったり別の仕事をしたり飯を食ったり帰って寝たりした後でも作業を迅速に再開できるように残している感じです。なのでまあ、数日前の内容にあまり意味はありませんし、割り込みが無い時や無視していい時、あと割り込まれても復帰が簡単な時はそもそもノートを使ったりしません。
実際就職してから去年くらいまではあんまし使ってなかった。逆に大学でソースコード読みとかしてた頃は数日で1冊とかのペースで消費してる時もありました。call traceとか一瞬目を離したら忘れるし。

あとは、暇つぶしだったりやる気無い時の仕事してるふりだったり、まあそれにしてもリストですね。やりたくない事リストを書いて渋々覚悟を決めたり、やらなくていい理由を考えたり。

それ以外の情報は、予定や締め切りはGoogleカレンダーや会社のグループウェアに書きますし、もう少し広いプロジェクトや進捗状況や議事のまとめは別の資料に書きますし、検討項目も別の資料やプロジェクトのtracとかRedmineとかに書きますし、思いつきはGithubのissueとかTwitterとか、ブログのネタは溜まるとストレスになるから書かないで忘れるに任せる。いや書きかけのテキストファイル結構ありますけども。

というわけで、本当に今やってることや今からやることや今考えたことを書いているだけです。
なのであまり職場以外で持ち歩く必要がなくて、ノートの記事とかで忙しくなったら小さいノートがほしいとか言ってるのは外で仕事の事を考える必要が出た時の事を考えていました。今のところ職場にいる時間を使えば済む程度だからまあいいかなぁと。
あと、こういう使い方なので持ち歩くにしても切り替え時期に2冊持つくらいで済むので、カバンにやさしい。

ノートっていうのは非常に自由度が高い道具なので使い方はそれこそ千差万別で、私とは逆に長期の記憶として使ったり、メモ帳だったり、スケジュール帳だったり、プロジェクト管理だったり、いや後半はノートっていうより手帳かな。違いはよくわかりませんが。
長期の記憶として使う場合は鉛筆で下書きをしたり、下書き用のノートを別に持ってたりするらしいですよ。重要な事は何回書いてもいい。気持ちはわからんでもないけど私は面倒くさい。

ノートなんぞ使わなくていい生活がしたいものです。