2009/12/31


最近、GoのGTKバインディングを作ってるのだけど、先日ようやく簡易twitterクライアントを作れるまでに至ったのだが、そのtwitterクライアントの「タイムライン更新」ボタンを押した時に画面をブロックさせずに画面を更新する方法を考えてた。
Perl-GTKならばCoroを使ってこうするだろうか。 use strict;
use warnings;
use Gtk2 '-init', '-threads-init';
use AnyEvent;
use Coro;
use Coro::Timer;

my $window = Gtk2::Window->new('toplevel');
$window->signal_connect(
    destroy => sub {
        exit;
    }
);
my $vbox = Gtk2::VBox->new;

my $button = Gtk2::Button->new('click me');
$button->signal_connect(
    clicked => sub {
        $button->set_sensitive(0);
        async {
            Coro::Timer::sleep 3;
            Gtk2::Gdk::Threads->enter;
            $button->set_sensitive(1);
            Gtk2::Gdk::Threads->leave;
        };
    }
);
$vbox->add($button);

$window->add($vbox);
$window->show_all;

Gtk2->main;
ここでCoro::Timer::sleepしているのは、生のsleepを使うとCoroがスケジュール割り当てとして介入出来なくなるのを回避する為。非同期で動作はするものの、スレッド動作になるので「Gtk2::Threads->enter」と「Gtk2::Threads->leave」でUIに関連する処理をブロックしなければなりません。つまり例えばボタン押下時の処理が5段階あり、その都度UIの何かしらを変更しなければならない場合、「Gtk2::Threads->enter」と「Gtk2::Threads->leave」でそれぞれブロックしなければならない事になります。
平たく言うと、スレッドを使う予定の無かったUI処理を、スレッド対応にするには多少のコード修正が必要になるという事です。
今回、goで作った簡易twitterクライアントの「タイムライン更新」ボタンを非同期対応するのに費やしたコードはたった2行。元のコード
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        button.SetSensitive(false);
        if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
            // タイムライン取得、表示処理
        }
        button.SetSensitive(true);
    }, nil);
となっていたコードに「go」で実行されるブロックを追加しただけ。
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        button.SetSensitive(false);
        go func() {
            if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
                // タイムライン取得、表示処理
            }
            button.SetSensitive(true);
        }();
    }, nil);
何が言いたいのかというと、groutineはgo組み込みであって、Coroがsleepやhttp_getを置き換えようとしている(CORE::GLOBAL上書きもあるけど)物とは違い、言語として非同期をサポートしているという事。もちろんgoにおいてもボタンクリック時にsleepを呼ぶとUIはブロックされるのだけど、実際sleepなんて使わないし、そもそもgoroutineはスレッドじゃない。
そう、goroutineはスレッドじゃない。
A Tutorial for the Go Programming Language

Go has its own model of process/threads/light-weight processes/coroutines, so to avoid notational confusion we call concurrently executing computations in Go goroutines.

http://golang.org/doc/go_tutorial.html
実際にはgoが内部でscheduleを行って実行する処理連鎖で実現している。なのでgtkを呼び出す場合においてもgdk_threads_enter/gdk_threads_leaveを呼び出す必要もない。なぜなら並行とは言えど同時実行ではないのでコンテキストスイッチも意識しなくて良い。
故に上記の様な 「go func(){}()」ブロックだけで非同期処理が書けるという訳。例えばcursesでCUIアプリを書いても画面が崩れる事は無いだろう(未確認)。

UIの開発に有利な言語にはある程度、共通する物があると思う。
  • 匿名関数をコールバックとして扱える
  • サードパーティでも良いので非同期処理の仕組みがある
  • GCが効く
この内、2個か3個あてはまるとUIを扱う上で有利になると思う。Javascriptはこの点(組み込みで非同期が扱える:というかそういうものしか扱えない?)素晴らしいし、jQueryを使えばAjax/UIとして作り手側はかなりの恩恵が得られる。Perlは「sub {}」で匿名関数が使えるがので優秀だが非同期となるとスレッドに頼らざるを得ないのが難点。rubyも同様。pythonは匿名関数が使えないのが少し悲しい(でもpygtkは好きだ)。結局の所、メインループと非同期処理をどう扱うかがネックになっている。例えば各言語でCOMを扱う場合、STA(Single Thread Apartment)しか対応していないCOMは、言語側でメインループをグルグル回すしかない。まぁ言語として用途が違うんだ(JavascriptはそもそもUI記述言語だろ...とか)から、比べちゃならんだろ...とも言えるし。
goにおいては上記3点あてはまるが、デストラクタが無いのが一番の難点。
これからのUIはどんどん非同期な物になって行くんだろうな。

10年程前のUIと言えば、非同期なUIはあまり見られなかった。VB6においても定期的に「DoEvents」を実行してプログレスバーを更新するという、なんちゃって非同期処理が横行していた頃だ。最近はVB.NETでコンポーネント自身がスレッドサポート(実際にはデリゲート)しているので簡単に非同期処理が書ける様になったが、これら全てにおいてもやはり
非同期対応するつもりの無かったUI処理を、非同期対応にするには多少のコード修正が必要になる
には違い無いし、その点でgoには期待している。
Posted at by



2009/12/22


Go binding for GTK GoのGTKバインディングを現在作業中なのですが、ようやく簡単なTwitterクライアントくらいならば作れる程度に出来上がってきたのでご報告。
mattn's go-gtk at master - GitHub

Go binding for GTK

http://github.com/mattn/go-gtk
イベントハンドラがJavaScriptっぽく書けるので、結構見通し良いコードになります。
package main

import "gtk"
import "gdk-pixbuf"
import "unsafe"
import "http"
import "json"
import "io"
import "os"
import "strconv"
import "strings"
import "reflect"

func url2pixbuf(url string) *gdkpixbuf.GdkPixbuf {
    if r, _, err := http.Get(url); err == nil {
        n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
        t := r.GetHeader("Content-Type");
        b := make([]byte, n);
        io.ReadFull(r.Body, b);
        var loader *gdkpixbuf.GdkPixbufLoader;
        if strings.Index(t, "jpeg") >= 0 {
            loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/jpeg");
        } else {
            loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/png");
        }
        loader.SetSize(24, 24);
        loader.Write(b);
        loader.Close();
        return loader.GetPixbuf();
    }
    return nil;
}

func main() {
    gtk.Init(&os.Args);
    window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
    window.SetTitle("Twitter!");
    window.Connect("destroy", func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        gtk.MainQuit();
    }, nil);

    vbox := gtk.VBox(false, 1);

    scrolledwin := gtk.ScrolledWindow(nil, nil);
    textview := gtk.TextView();
    textview.SetEditable(false);
    textview.SetCursorVisible(false);
    scrolledwin.Add(textview);
    vbox.Add(scrolledwin);

    buffer := textview.GetBuffer();

    tag := buffer.CreateTag("blue", map[string] string {
        "foreground": "#0000FF", "weight": "700" });
    button := gtk.ButtonWithLabel("Update Timeline");
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
            n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
            b := make([]byte, n);
            io.ReadFull(r.Body, b);
            j, _ := json.Decode(string(b));
            arr := reflect.NewValue(j).(*reflect.SliceValue);
            for i := 0; i < arr.Len(); i++ {
                data := arr.Elem(i).(*reflect.InterfaceValue).Elem().(*reflect.MapValue);
                icon := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("profile_image_url")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                var iter gtk.GtkTextIter;
                buffer.GetStartIter(&iter);
                buffer.InsertPixbuf(&iter, url2pixbuf(icon));
                name := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("screen_name")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                text := data.Elem(reflect.NewValue("text")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                buffer.Insert(&iter, " ");
                buffer.InsertWithTag(&iter, name, tag);
                buffer.Insert(&iter, ":" + text + "\n");
            }  
        }
    }, nil);
    vbox.PackEnd(button, false, false, 0);

    window.Add(vbox);
    window.SetSizeRequest(300, 400);
    window.ShowAll();
    gtk.Main();
}
実行すると以下の様な画面になります。
go-gtk-twitter-client
まだ、完成じゃないです。がんばります!
Posted at by



2009/12/13


やっぱり少し私とは認識が違う気がする。
おごちゃんの雑文 » Blog Archive » 「はまちちゃん」をspamと断じる思考停止
この穴を「カンチョウ」で済ませているところが、はまちちゃんのサジ加減なのだ。

「スキだらけの後ろ」があることは、彼にはわかっている。わかっていてそこを突かないのは一見親切で礼儀正しい態度に見える。でも、そういった非常に突きやすい、単純な穴があることは、わかる人にはわかってしまっていて、気がついてないのは運営者だけという状態になっているのが、「Amebaなう」だったわけだ。はまちちゃんのやったのは「カンチョウ」かも知れないけれど、やれることがわかっている悪意の者であれば、

後ろから刺す

ことだって出来るわけだ。「悪の組織」がはまちちゃんの彼女をどーこーしたら… とかだってないとは言えない。Amebaには首相官邸のブログだってあるわけで、ポストにちょっとした仕掛けを入れるだけで、

公式チャネルからデマを流す

ことだって可能になる。仕手筋とかなら5分デマが流れりゃ十分満足だろう。Amebaには日本の最終兵器とも言われるデスブログだってあるわけでw
http://www.nurs.or.jp/~ogochan/essay/archives/2156

カンチョーで済ましてあげてるだけ、やさしい?

違うでしょ、肩をトントンと叩いて「ちょっと、スキだらけですよ。世の中ひどい人ばかりなので、気をつけないとカンチョーされますよ。」と言ってあげるのがやさしいんじゃないの?
おまけに傍観者とし面白い話かもしれないけどカンチョーでどんだけ人が動くか想像してもいいんじゃないかな?復旧にはSE一人じゃなかったかもしれないし、対策検討会の様な物もあったかもしれない。SE一人終日動かせば1日で原価数万飛びますよ。それ十分被害でしょ。
カンチョーがどんな印象を与えたのかは受けた者でないと分かんないよ。
はまちちゃんと脆弱性報告のあり方 - 世界線航跡蔵
これは「隙があった」んじゃないだろう。「開腹したまま内臓が露出している」んだ。

ところが、どうも現実の医師とは違ってこの世界の、特にAmebaみたいな大きな会社の開発者はその辺の意識が甘い。「手術したけど、まー、内臓が見えててもすぐに死ぬ訳じゃないし、適当に皮被せておけばいいよね」とか思ってる。下手したら開腹後に塞ぐべきという常識的なことを考えすらしない。で、そこら中を内臓が見えたままの患者が歩いている。
http://yugui.jp/articles/851
それも違う気がする。
この件で傍観している人たちは技術者だ。そしてその脆弱性は技術者でしか分からない。これはいわば院内感染ですよ。脆弱な病棟に気付かず通院/入院している一般の人達と、そこに「院内感染だー!」と叫ぶ人でしょ。彼らにはそれが冗談なのか何なのか分かってない。確かに今回のはまちちゃんの行為を「カンチョウ」と命名したのは私だが、このカンチョウで不安になった人がいたかもしれないし、Amebaの印象を悪くした人もいるかもしれない。

カンチョーで痔になってたら軟膏塗ってくれるの?はたまたカンチョーで筋うんこ付いてたら、パンツ洗ってくれるの?
カンチョーだってやり方次第では十分犯罪ですよ。


ただし言いっておきたいのは、私はこういったサービス全て注意勧告してあげるべきとは思ってない。注意して効かなかったサービスは勝手に潰れるだろうし、言い方変えれば潰れるべきだと思う。
私は脆弱性を見つけて注意勧告する事はとても大切だと思うが、それを手法交えて公に広める事については賛同出来ない。セキュリティを扱う人たちにとってはそれがステータスかもしれないけし、知名度を上げるチャンスなのかもしれないけど、私はそんなチャンスは欲しくないし、それで知名度を上げられたとしても嬉しくない。

私ならばもっと技術者らしい知名度の上げ方をする。ただそれだけの話ですよ。

ちなみに全然関係ない話ですが、私が中学生だった時の友達は、「起立!礼!着席!」のタイミングで前に座っていた同級生にカンチョウを食らわそうとしてたが、思っていた以上にズボンが固かった為、中指第二関節を骨折した事がありますよ!カンチョウには十分気をつけましょう。
Posted at by



2009/12/10


Tatsumakiって面白そうとは思ってたけど、触った事なかったので、勉強してみた。
なるほど面白そう。と思ったのとPSGIでも動かせるんだーと感動しました。今日は勉強がてら、以前紹介した事もあるDiggのDUI Streamと混ぜて遊んでみた。
Digg the Blog » Blog Archive » DUI.Stream and MXHR

We call this technique MXHR (short for Multipart XMLHttpRequests), and we wrote an addition to our Digg User Interface library called DUI.Stream to implement it. Specifically, DUI.Stream opens and reads multipart HTTP responses piece-by-piece through an XHR, passing each chunk to a JavaScript handler as it loads.

http://blog.digg.com/?p=621
miyagawa's Tatsumaki at master - GitHub

Plack-based nonblocking Web framework for IO-bound delayed response, server push (streaming) and long-poll comet

http://github.com/miyagawa/tatsumaki
ルートとなるHTMLからjavascriptでストリームへのエンドポイントへリクエストさせ、そこからmultipart/mixedで送出されるgif画像(静止画)を断続的に送出し、DUI Streamでパースしながら1個のIMGタグを高速に切り替えます。
名前をTatsumakiSenpukyakuと名付けました。
mattn's TatsumakiSenpukyaku at master - GitHub

竜巻旋風脚

http://github.com/mattn/TatsumakiSenpukyaku
実行するとポート9999でサーバが起動し、「せーのー」と勢いを付けたあとに、高速な「竜巻旋風脚」が始まります。
竜巻旋風脚
javascriptからタイマでリクエストしているのではなく、サーバから断続的に、切断せずに送出されています。上記DUIのリンク先にある画像を使ったデモでも分かる通り、ストリーミングによりパラパラと表示されていた画像コンテンツがまるで動画の様に見せる事が出来ます。
ネタっぽい作りですが、へーこんな事出来るんだー...てな感じで動かしてみて下さい。
Posted at by



2009/12/04


GtkTreeModelとか面倒くさい部分に差し掛かってます。でもこれないとコンボボックスもツリーもリストも作れないんすよね。
go-gtk-20091204
いまんところこんな感じ。
イベントクロージャが内包出来てJavaScriptを書いているかの様にGTKプログラミングが出来ています。
    //--------------------------------------------------------
    // GtkButton
    //--------------------------------------------------------
    button := gtk.ButtonWithLabel("Button with label");
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        print("button clicked: ", button.GetLabel(), "\n");
        dialog := gtk.MessageDialog(
            button.GetTopLevelAsWindow(),
            gtk.GTK_DIALOG_MODAL,
            gtk.GTK_MESSAGE_INFO,
            gtk.GTK_BUTTONS_OK,
            entry.GetText()
        );
        dialog.Response(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
            println("Dialog OK!")
        }, nil);
        dialog.Run();
        dialog.Destroy();
    }, nil);
    buttons.Add(button);
完成はまだまだ先です。頑張ります。
Posted at by



2009/12/02


os0xさん。お疲れ様でした。次の活躍も期待しています。
株式会社ALBERTを退社します - 0xFF

ファイル整理とかしていて2005年当時に書いたプログラムとかが出てきて、あまりのアレさに感慨深くなりました…

http://d.hatena.ne.jp/os0x/20091201/1259626454
私も以前フォルダの中を色々探索してたら、8年前のソースコードが出て来て懐かしくなりました。某IRCチャネルでは一度晒した事があるのですが、ネタとして面白そうだったのでブログに転載します。
8年前の私は、何故かtelnetに興味があり、Windows標準のtelnetは何故色が出ないんだ!なぜあんなショボいんだ!と思って、自前でtelnetを作っていました。
最初はvectorに登録する予定でしたが、面倒くさくなってやめてしまいました。
xterm互換です。NTLM認証対応です。Win32 APIでエスケープシーケンス表現しています。utf-8には対応していません><。とても長いです。変に凝ってます。そしてAPIゴリゴリです。懐かしいと感じると同時に、センスの無さに笑いました。

sTelNet.c
sshが標準な今、telnetのソースコードなんて無意味に近いですが、興味のある方だけ、どうぞw
Posted at by



2009/11/28


やっぱり新しい言語が出来たらGUIだよね!
って事でGTKバインディング作ります!

mattn's go-gtk at master - GitHub

gtk extension for go

http://github.com/mattn/go-gtk
Goでは継承が使えないので、例えばGtkBoxを継承したGtkVBox/GtkHBox、GtkWidgetを継承したGtkWindow/GtkButton/GtkLabel...が上手く表現出来ません。
出来ないというか、GtkButtonにGtkWidgetと同じメソッドを生やそうと思うと、同じコードを書かなければなりません。
色々と模索した結果、gtk.Window()やgtk.Button()で返すのは常にGtkWidgetとし、GtkWindowやGtkButton独自のメソッドを呼び出したい場合には
(&gtk.GtkWindow{window.Widget}).SetTitle("GTK Go!");
こういう風に型変換して貰う事にしました。まぁコンポジションぽい物?
C言語バージョンにおいても同じ処理をする時には gtk_window_set_title(GTK_WINDOW(w), "GTK Go!");
とする訳だし、似てると言えば似てる訳だし...
要は、Widget毎にGtkWidgetからGtkXXXへの変換関数なんか作ってらんねぇよ。バカ!
という訳です。

現状、コールバック(gtk_signal_connect)も動作しています。少しハマった理由としては、goroutineを勝手にスレッドだと思っていたので、スレッド間通信を想定してpipeのコードを書いていましたが、よく考えたらscheduled coroutineだし、自分のwrite pipeを自分でreadしてどうするよ...って気付いたのです。
結局変数ポーリングに変えて現状上手く動いています。
GoでGTKのプログラミングをすると、以下の様なコードになります。
package main

import (
  "os";
  "gtk";
  "unsafe";
)

func main() {
    gtk.Init(&os.Args);
    window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
    (&gtk.GtkWindow{window.Widget}).SetTitle("GTK Go!");

    vbox := gtk.VBox(0, 1);

    label := gtk.Label("ハローワールド");
    (&gtk.GtkBox{vbox.Widget}).PackStart(label, 0, 1, 0);

    entry := gtk.Entry();
    (&gtk.GtkEntry{entry.Widget}).SetText("入力エリア!");
    vbox.Add(entry);

    button := gtk.ButtonWithLabel("こんにちわ!こんにちわ!");
    button.Connect("clicked", func(widget *gtk.GtkWidget, data unsafe.Pointer){
        println("button clicked");
        println((&gtk.GtkButton{button.Widget}).GetLabel());
    }, nil);
    vbox.Add(button);

    window.Add(vbox);

    window.ShowAll();
    gtk.Main();
}
そして実行画面。
go-gtk
まだ、GtkWindow/GtkButton/GtkLabel/GtkVBox/GtkHBox/GtkEntryのそれぞれ一部のメソッドしか動いていませんが、宜しければ遊んでみて下さい。
そしてもし「おおし!おいらも作るお!」って方が居られたら、ぜひgithubでforkして下さい。

Posted at by



2009/11/25


そろそろgoでライブラリを作る頃かなーと思って、migemo(cmigemo)を使う物を書いてみた。
mattn's go-migemo at master - GitHub

migemo extension for go

コードの中ではKoRoNさんのcmigemoを使った。コードは少ないけど実は少しハマって、今日はそれを書き記したい。

migemoでは、正規表現文字列やパターン文字列をunsigned char*で引数として扱っているんですが、cgoを使ったC言語ライブラリの取り込みを行う場合、char*と型が合わなくてコンパイルエラーが発生する。しかしC言語の様に *C.uchar(p)
等と書けない(これだとucharの参照になってしまう)Go君は、致し方なくchar*を引数に持つwrapper関数を用意するしかないんだけど、実はcgoに食わせるgoファイルでは
package migemo

/*
#include <migemo.h>
static char* _migemo_query(migemo* object, const char* query) {
  return (char*)migemo_query(object, (const unsigned char*)query);
}
static void _migemo_release(migemo* object, const char* str) {
  migemo_release(object, (unsigned char*)str);
}
*/
import "C";
と言った様に、Cのコードが書ける。ここにGoで扱い易い型のwrapperを書けば良い。今回の例だとunsigned char*の引数を持つ関数をchar*で渡せる(C.CString)関数を用意している事になる。

これにより、いちいち別ファイルにwrapper関数用意したりMakefileにwrapperをビルドする為のターゲットを書かなくても良い。

これは便利だ。

話戻してmigemo拡張ですが、現状Open/Close/Load/Queryの4メソッドを持っています。
Queryで渡したパターンによるマッチし得る複数の正規表現が得られます。
package main

import (
  "fmt";
  "regexp";
  "strings";
  "migemo";
)

func main() {
    var pattern = "goGengo";
    var match = "go言語";
    m := migemo.Open("../dict/utf-8.d/migemo-dict");
    s := migemo.Query(m, pattern);
    if (regexp.MustCompile(s).Match(strings.Bytes(match))) {
        fmt.Printf("%s は %s にマッチします!\n", pattern, match);
    } else {
        fmt.Printf("%s は %s にマッチしません!\n", pattern, match);
    }
}
実行すると goGengo は go言語 にマッチします!
と出力されます。
地味に使えるかも。

Posted at by



2009/11/24


最初Goがリリースされた時には、自分がPortingするんだーとか意気込んでましたが、google codeにホスティングされている方のほうが良い物作ってくれそうだったので、ずっと見守ってました。
hectorchu-go-windows - Project Hosting on Google Code

go for Windows

http://code.google.com/r/hectorchu-go-windows/
Windows PEのリンカも入って、適当な物ならばコンパイル&リンク出来る様になってます。
ただ一般的なWindows Portingの問題として、POSIXなAPIであるpipeやfork、ソケットディスクリプタ等といったWindowsで完全に模倣出来ない部分が残っています。例えばhttpクライアントを扱うhttpパッケージは、netパッケージに依存し、かつ内部ではos.Fdを使う為にそのままのコードではWindowsで実行出来ません。さらにgodoc等ファイルおよび外部プログラムを扱う様な物は内部でos.Pipe()、os.Fork()が呼ばれており、こちらもWindowsなAPIで置き換えなければなりません。
現在の所、src/pkg/osとは別にsrc/pkg/windows/osというフォルダで、Win32 APIを使ってosパッケージを模倣され様としています。移植という点ではこれからになりますね。Issue Trackerが開かれていないのでバグ報告出来ないですが、もし気になる事があればメーリングリストに投げようかなと思っています。

ちなみに、スレッドまわりの実装は既に入っているのでgoroutineは動きます。
ぜひ遊んでみましょう!
Posted at by



2009/11/23


昨日書いたGoのIRCボットに手を入れ、mecabで要素解析する様にした。色々弄ってたら、某所で有名な「悪く言うなbot」になってた。
goによるmecab wrapperの実装がある事をtokuhiromさんに教えてもらったので、さっそく使わせて頂いた。
package main

import (
  "fmt";
  "http";
  "bytes";
  "json";
  "strings";
  "io";
  "mecab";
)

func get_json(url string, data map[string]string) json.Json {
  q := "";
  for key, val := range data {
    if (len(q) > 0) { q += "&" }
    q += fmt.Sprintf("%s=%s", key, http.URLEscape(val));
  }
  bb := &bytes.Buffer{};
  bb.Write(strings.Bytes(q));
  r, err := http.Post(url + "?" + q, "application/x-www-form-urlencoded", nil);
  if err == nil {
    b, _ := io.ReadAll(r.Body);
    r.Body.Close();
    j, _, _ := json.StringToJson(string(b));
    return j
  }
  return nil
}

func get_sid(nick string) string {
  j := get_json("http://webchat.freenode.net/e/n", map[string]string{"nick":nick});
  return j.Elem(1).String();
}

func login(nick *string) string {
  sid := get_sid(*nick);
  for {
    j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
    if j == nil {
      break;
    }
    for i := 0; i < j.Len(); i++ {
      data := j.Elem(i);
      println(data.String());
      if (data.Elem(0).String() != "c") {
        continue;
      }
      if (data.Elem(1).String() == "433") {
        *nick += "_";
        sid = get_sid(*nick);
      }
      if (data.Elem(1).String() == "376") {
        return sid;
      }
    }
  }
  return "";
}

func join(sid string, room string) bool {
  j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"JOIN " + room});
  if j == nil {
    return false;
  }
  return true;
}

func say(sid string, room string, msg string) bool {
  j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"NOTICE " + room + " :" + msg});
  if j == nil {
    return false;
  }
  return true;
}

func listen(sid string, nick string, room string) {
  for {
    j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
    if j == nil {
      continue;
    }
    for i := 0; i < j.Len(); i++ {
      data := j.Elem(i);
      println(data.String());
      if (data.Elem(0).String() != "c") {
        continue;
      }
      if (data.Elem(1).String() != "PRIVMSG") {
        continue;
      }
      line := data.Elem(3);
      if (line.Elem(0).String() == nick) {
        continue;
      }
      s := line.Elem(1).String();
      m := mecab.New2("");
      r := mecab.SparseToStr(m, s);
      ss := strings.Split(r, "\n", 999);
      for l := 0; l < len(ss); l++ {
        st := strings.Split(ss[l], "\t", 2);
        if (len(st) < 2) {
          continue;
        }
        if (strings.Index(st[1], "固有名詞") != -1 ||
            strings.Index(st[1], "人名") != -1 ||
            strings.Index(st[1], "組織") != -1) {
          say(sid, room, st[0] + "は良い奴だ。悪く言うな。俺が許さん。");
        }
      }
    }
  }
}

func main() {
  nick := "go-japan";
  room := "#mattn";
  sid := login(&nick);
  if (len(sid) > 0) {
    if (join(sid, room)) {
      listen(sid, nick, room);
    }
  }
}
channelに固有名詞や人名等が現れると「XXXは良い奴だ。悪く言うな。俺が許さん。」と発言します。
mecabライブラリについては、やっぱりfeatureとかsurfaceを扱いたいのでこれからに期待したいと思います。まぁ、現状でも上記の様にstrings.Split使えば出来なくないんですけどね。

なんとなくだけど、だんだん「C++書きの為のGo言語」と言われてる理由が分かって来た気がする。
Posted at by




とは言っても、freenodeのWebインタフェース使ってJSONやり取りして、発言するだけの物。以前C++で作った全裸botの簡易版といった所か。
動かすと、freenodeの"golang-daisuki"(仮)にJOINして「郷です!ジャパーーーン!」と発言した後に死亡します。
嫌がらせには持って来いですね。

package main

import (
  "fmt";
  "http";
  "bytes";
  "json";
  "strings";
  "io";
)

func get_json(url string, data map[string]string) json.Json {
  q := "";
  for key, val := range data {
    if (len(q) > 0) { q += "&" }
    q += fmt.Sprintf("%s=%s", key, http.URLEscape(val));
  }
  bb := &bytes.Buffer{};
  bb.Write(strings.Bytes(q));
  // freenodeはPOSTメソッドでGETクエリをおくらなきゃ駄目
  r, err := http.Post(url + "?" + q, "application/x-www-form-urlencoded", nil);
  if err == nil {
    b, _ := io.ReadAll(r.Body);
    r.Body.Close();
    j, _, _ := json.StringToJson(string(b));
    return j
  }
  return nil
}

func get_sid(nick string) string {
  j := get_json("http://webchat.freenode.net/e/n", map[string]string{"nick":nick});
  return j.Elem(1).String();
}

func login(nick string) (string, string) {
  sid := get_sid(nick);
  for {
    j := get_json("http://webchat.freenode.net/e/s", map[string]string{"s":sid});
    if j == nil {
      break;
    }
    for i := 0; i < j.Len(); i++ {
      data := j.Elem(i);
      println(data.String());
      if (data.Elem(0).String() != "c") {
        continue;
      }
      if (data.Elem(1).String() == "433") {
        nick += "_";
        sid = get_sid(nick);
      }
      if (data.Elem(1).String() == "376") {
        return sid, nick;
      }
    }
  }
  return "", "";
}

func join(sid string, room string) bool {
  j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"JOIN " + room});
  if j == nil {
    return false;
  }
  return true;
}

func say(sid string, room string, msg string) bool {
  j := get_json("http://webchat.freenode.net/e/p", map[string]string{"s":sid, "c":"NOTICE " + room + " :" + msg});
  if j == nil {
    return false;
  }
  return true;
}

func main() {
  nick := "go-japan";
  sid, _ := login(nick);
  if (len(sid) > 0) {
    if (join(sid, "#golang-daisuki")) {
      say(sid, "#golang-daisuki", "郷です!ジャパーーーン!");
    }
  }
}
作ってみた感想としては、httpまわりにもう少しPOSTに便利なメソッドが欲しいのと、Postメソッドのデフォルトがcheckedになっている(Postメソッドでは変えれない)ので古くさいサーバで動かないんじゃないかと思ったり。

ただC++版に比べて幾らか短く書けたし、今回のコードでは例外等処理してないけど、例外処理を書きたくなった時にも簡単に実装が出来るのも分かった。

ところでpublicメソッドはメソッド名を先頭大文字にするってルールはいいけど、少しだけオモチャっぽく感じてしまうのは私だけだろうか。
Posted at by



2009/11/19


ふと思いつきで書いたコードを実行してしまって死にかけた。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <memory.h>

int main(void) {
  char *p = NULL;
  struct sigaction sa_sigint;
  memset(&sa_sigint, 0, sizeof(sa_sigint));
  sa_sigint.sa_handler = (void (*)(int))fork;
  sa_sigint.sa_flags = SA_RESTART;
  if (sigaction(SIGSEGV, &sa_sigint, NULL) < 0) {
    perror("sigaction");
    exit(1);
  }
  sleep(1);
  puts("僕は死にましぇん!");
  return *p = 0;
}
Ctrl-CとかCtrl-Zとか押しまくってようやく止めた。
良い子は真似しちゃ駄目よ。
Posted at by




今日、TypePadが提供するMicroblogを作ってみた。
http://mattn.typepad.com/
とてもグラフィカルで、reblogもついててfavoriteもついててカッコイイのだが、これを見て最近twitterのBeta機能として盛り込まれているReblogボタンの存在に疑問を感じた。
twitter-reblog-button
twitterではこれまでRT(Retweet)する際にはユーザがRTという接頭語を入れ、さらにその前にコメントを付与してポストするのだけれど、実はこのボタン、押すとそのまま自動でRetweetされる。まぁコメントが入れれない訳なんですが、私が疑問を感じたのはそこじゃない。

「何故ボタンにしたのか」

私が思うにtwitterが起こした革命の一つが「テキストの可能性」へ着目した事だと思う。140字以内で、かつ太字に出来る訳でもアンカーを貼れる訳でもなく、かつ宛名をGUIで指定出来る訳でもない。単なるテキストだけで、宛先、タグ、評価、位置などを表しているのだ。昨今リッチコンテンツが流行する中、メインとなるコンテンツの入力は、twitterサービス開始当初と変わらずテキストエリアなのだ。
このテキストによる表現は、replyこそ当初から入っていたが他の物については誰が取り決めた訳でもなく、ユーザが勝手に始め、後追いでオフィシャルが機能追加していったのが実状だ。
そしてそれは今や明文化され、一種のルール(nanoformat/twitter-syntax)になっている。
microblogging-nanoformats · Microformats Wiki
http://microformats.org/wiki/twitter-nanoformats
Twitter Syntax · Microformats Wiki
http://microformats.org/wiki/twitter-syntax
なのになぜ、ボタンにしたのか...。テキストでの表現があってこそ、Greasemonkeyによる拡張や、他のサービス連携が発展したのだと私は思うし、twitter自身が例えば画像添付や自動処理なんてすべきじゃないとも思う。

サービスを面白くする要因は、サービスが提供する機能以上に、ユーザが作るコンテンツが重要なんだと信じているが、この自動化の流れはユーザが作る新たなルール(コンテンツ)を阻害する可能性もあると思う。
twitterのあるべき姿は140字のテキストエリアであって、ボタン一つでアクションを起こせる様な、流行に流される物じゃない。今後このRetweetボタンがどの様に機能していくか分からないが、コンテンツを作るのはサービス提供側じゃなく、ユーザなんだという部分を忘れないでいて欲しいし、これからのtwitterがそうある事を信じたい。
Posted at by



2009/11/14


amachangのtwitterタイムラインの奴をhttp.Getでやってみた。
早速 Go 言語を試してみる! - IT戦記

http.Get は何故か動かなかったので net.Dial を使った。

http://d.hatena.ne.jp/amachang/20091111/1257928890

こうかな?わかりません><
package main

import (
    "fmt";
    "json";
    "io";
    "http";
)

func main() {
  if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
    b, _ := io.ReadAll(r.Body);
    j, _, _ := json.StringToJson(string(b));
    for i := 0; i < j.Len(); i++ {
      data := j.Elem(i);
      fmt.Printf("%s: %s\n",
        data.Get("user").Get("screen_name"),
        data.Get("text"));
    }  
  }
}

今日、始めてGo触ってみたけど、重大な欠点を見つけた。
# vi twitter.go
コードかきかき...
# 8g twitter.go
うし!コンパイルだ!
twitter.go:7: imported and not used: net
おろっエラーだ
# vi twitter.go
直すぞ!直すぞ!
# !8
もっかいコンパイルだ!
bash: !8: event not found
><
言語仕様で、multiple-valueをちゃんとmultiple-valueで受けないといけないのが堅すぎる気がする。
pythonとかC++とか言われてるけど、私はluaなんじゃないかと思った。
Posted at by



2009/11/13


goを弄ってるといちいち8g(5g?)とか8l(5l?)とかでコンパイル、ビルドする手間が必要なんだけど、これってquickrun.vimの設定で便利になるんじゃないか?と思ったので設定した。
thincaさんバージョンのquickrun.vimを入れた後、vimrcで以下の様に記述する。
let g:quickrun_config = {
\  'go': {
\    'command''8g',
\    'exec': ['8g %s', '8l -o %s:p:r %s:p:r.8', '%s:p:r %a', 'rm -f %s:p:r']
\  }
\}
環境によっては8g/8lを書き換える必要があります。
あとは拡張子goのファイルで<leader>r(mapleaderを設定してなければ\r)で、コンパイル、リンク、実行までやってくれて、まるでスクリプト言語を書いている様な開発効率が得られる!

thinca++
Posted at by



2009/11/12


Gyazoアプリがgithubにあがってるのを知った。
gyazo's Profile - GitHub

Gyazo is Open Source!

http://github.com/gyazo
GyazoWinならびにGyazoのLinux版にもお世話になっているので結構嬉しい。今日見たらESCキーで中断出来る様になってた。嬉しい。
よし!私もGyazoアプリ作るぞ!と思って、考えたけど結局思いついたのがこれしか無かった。

コマンドプロンプトから実行すると、コマンドプロンプトをキャプチャしてgyazoするアプリ、GyazoCmd!

mattn's GyazoCmd at master - GitHub

gyazo interface to windows command prompt

http://github.com/mattn/GyazoCmd
実行するとこんな風になります!
gyazocmd
だっ....誰得!汗
Posted at by



2009/11/08


たまにはプログラミングに全く関係ない事でも書いてみる。


先日、はてなブックマーク経由でとある対戦アクションゲーム大会の動画を見てて、最近ゲームなんて全くしていなかった私はある事に気が付いた。

最近のゲームは必殺技名を叫ばない。

なんでだろ...。昔のゲームと言えば「昇竜拳!」「竜巻旋風脚!」「バーンナックル!」「虎煌拳」等と叫んだものだ。
子供達はそれで必殺技を覚え、学校で話し、真似して遊んだもんだ。でも最近のゲームは必殺技を叫ばない。どうやって友達と学校で情報共有するんだろうか。攻略本やネットで調べるのか?それとも最近じゃ真似する時も叫ばないのか?

叫ぶからこそ必殺技っぽいと思っていた時代は古いのか?例えば学校で「カイザーウェーブ!」って叫びながら大きく胸を突き出し、腕を振り出す真似を叫ばずやったとしたら、はたしてそれが「カイザーウェーブ」と分かるだろうか。
叫ぶからこそ、ゲームを知らない人でも「あー、ゲームの必殺技かなんかだろうな...」って気付くだろうし。叫んだからこそ女子が見て「男子幼稚!」ってなったものが、はたして叫ばなかったらそれがゲームの必殺技と分かるだろうか。知らない人が見たらカイザーウェーブの動きなんて、公園に現れる変質者の挙動に見え兼ねない。きっと女子に「男子変態!」って言われるだろう。
あの、古き良き時代は何処に行ってしまったんだろう...。

全然関係ないけど、必殺技って技を繰り出す前、もしくは繰り出している最中に叫ぶ物だと思うんだけど、そういう点でいうと北斗の拳のケンシロウは少し卑怯だと放映当時の私は子供ながら感じていた。
ケンシロウは技を繰り出してから技名を叫ぶのだ。

アタタタタタタタ...お前はもう死んでいる!

これだと、技の最中に失敗してたとしても言い訳付くんですよ。

アタタタタタタタ...(あっ!ミスった)...お前はもう...全身打撲!

経絡秘孔を付き間違ったとしても、「あっ!外した!」と感じたならば、必殺技名を言わなければいいんですよ。ただ単に指が体に食い込んで痛いだけの技になるんですよ。

はい、どうでもいいですね。

最近は必殺技を叫ばないのが普通なんだろうか。
必殺技を叫ぶのは昭和なんだろうか...
Posted at by



2009/11/05


できなくはない...かな。
Vim-users.jp - Hack #98: VimScriptで疑似乱数を生成する

VimScriptには、残念ながら疑似乱数を生成するための関数が存在しません。

reltimestr()やreltime()はVimが+reltimeでコンパイルされていないと動作しません。

http://vim-users.jp/2009/11/hack98/
+reltimeでなくても乱数を発生させられる。しかも擬似乱数じゃなく。
:call libcallnr("", "srand", localtime())
:echo libcallnr("", "rand", -1)
-1はダミーね
Windowsの場合は :call libcallnr("libcmt.dll", "srand", localtime())
:echo libcallnr("libcmt.dll", "rand", -1)
とか :call libcallnr("msvcrt.dll", "srand", localtime())
:echo libcallnr("msvcrt.dll", "rand", -1)
とかで動くと思う。ためしてない。
Posted at by



2009/10/30


最近、kazuhoさんが作った「C」で遊んでいるのですが(いまごろかい!)、これWindowsでも使いたいなーなんて思ったのでポーティングしてみた。
C - a pseudo-interpreter of the C programming language

Perl や Ruby では、ワンライナーで処理が書けて便利です。でも、なぜか C では書くことができません。仕事上の都合で、小さな処理を C 言語で書く必要があったので、ワンライナーも書くことのできる C 言語のインタプリタ(?)を作ってみました。

http://labs.cybozu.co.jp/blog/kazuho/archives/2006/01/large_c.php

目指せバイナリアン (C-0.06)

C-0.06 をリリースします。

http://labs.cybozu.co.jp/blog/kazuho/archives/2006/05/c-0_06.php
これがあるとコマンドプロンプトから C:¥>C
puts("kazuho");
^D
kazuho

C:¥>
こんな事が出来たり、 C:¥>C -e "printf("""hello world¥n""");
hello world

C:¥>
こんな事が出来たりします。コマンドプロンプトなのでクォートのエスケープ2重打ちがめんどくさいですが(実際にはクオートの中のクォートなので3重になります)、なれれば簡単ですし昔なつかしnyacusなんかを使えばシングルクォートでも行けるはずです。(cygwin?何それ)
コンパイルや実行にはmingw32が必要です。
コンパイルは以下の様に簡単。
C:¥C-0.06¥>gcc -o C.exe C.c
mingw32が出力するa.exeに対応しています。ちょっと弄ればMSVCにも対応出来るんじゃないかな。

kazuhoさんに感謝しつつ、Version画面に「Win32 Porting」として名前を入れさせて頂いています。
mattn's C-win32 at master - GitHub

win32 port of C(a pseudo-interpreter of the C programming language)

http://github.com/mattn/C-win32
Windowsユーザでコマンドプロンプト使いで、mingw32が入っててC言語をこよなく愛する皆さんにどうぞ...。

対象範囲せま!
Posted at by




昨日IRCでsecurity guyの人と話してて、twitterに新しく導入された機能「list」には少し不満があるという話をしてた。
ざっと上げると
  • 個人単位のlistであり、追加するにはそのlistの持ち主でしか追加出来ない。
  • UIが痛い。listについての画面遷移、追加削除がやりにくい。
  • 他人のlistを自分のlistにimportしたいが出来ない。
こんな感じ。まだまだbetaだからこれから良くなるのかもしれないけど、さすがにimportは欲しいよね...って事で3分程度で作ってみた。
APIは昨日調べてなんとなく分かってたので、だいぶ楽に作れた。
#!/usr/bin/env perl
use strict;
use warnings;
use LWP::UserAgent;
use JSON;
use Config::Pit;

# のび太の物は俺の物
my $copy_from = $ARGV[0] || 'hasegawayosuke/javascript';
# 俺の物は俺の物
my $copy_to = $ARGV[1] || 'mattn_jp/javascripter';

my $config = pit_get("twitter.com", requires => {
    username => 'username in twitetr.com',
    password => 'password in twitetr.com',
});

my $ua = new LWP::UserAgent;
$ua->env_proxy;
$ua->credentials(
    "twitter.com:80", "Twitter API",
    $config->{username} => $config->{password}
);
my $json = from_json($ua->get("http://twitter.com/$copy_from/members.json")->decoded_content);

for (@{$json->{users}}) {
    $ua->post("http://twitter.com/$copy_to/members.json", {id => $_->{id}});
}
使い方は # perl copy-to-mylist-from-others.pl hasegawayosuke/javascript mattn_jp/javascripter
こんな感じ。実行するときにはジャイアンのテーマを脳内再生しながら実行して下さい。
Posted at by



2009/10/29


C言語ってlambdaが書けないのでGUIのコールバックなんかを作る場合には関数を用意しなければならないのですが...
Lambda abstractions in C++ vs. Scheme

1. Simple lambda-expressions

#define Lambda(args,ret_type,body) \
class MakeName(__Lambda___) { \
public: ret_type operator() args { body; } }
http://okmij.org/ftp/c++-digest/Lambda-CPP-more.html
なるほどねー。これなら Lambda((int a, int b), int, return a+b) foo;

std::cout << foo(1, 2) << std::endl; // 3
こんな書き方も出来る訳か。ただGUIなんかのコールバックだと関数ポインタが必要だし、operator()のアドレスなんて取る必要もないからstaticで宣言した関数ポインタで十分かな。

fltkだとこんな感じにmainの外に関数宣言しなくても良くなるね。
/* g++ -I Develop/fltk c.cxx -L Develop/fltk/lib -lXi -lfltk2 -lXft -lXinerama -lXcursor */
#include <fltk/Window.h>
#include <fltk/Button.h>
#include <fltk/ask.h>
#include <fltk/run.h>
#include <stdlib.h>

int main(void) {
#define lambda(args,ret_type,block) \
    class { public: static ret_type func args {block;} }

    lambda((fltk::Widget* wid, void* data), void, fltk::alert("don't click!")) button_clicked;

    fltk::Window w(0, 0, 300, 100, "lambda window");
    w.begin();
    fltk::Button b(10, 10, 280, 80, "click me!");
    b.callback(&button_clicked.func, 0);
    w.end();
    w.show();

    return fltk::run();
}

fltk-lambda-callback

mainしか宣言したく無い人にはいいかも。
先日知ったんだけど、最近のVisual Studioに付いてるコンパイラだと auto func [&](int a, int b) -> int { return a+b; };

std::cout << func(1, 2) << std::endl;
こんな書き方が出来るらしい
Posted at by



2009/10/23


githubが高速化に成功した様です。
How We Made GitHub Fast - GitHub
github

Now that things have settled down from the move to Rackspace, I wanted to take some time to go over the architectural changes that we’ve made in order to bring you a speedier, more scalable GitHub.

...

For our data serialization and RPC protocol we are using BERT and BERT-RPC.

http://github.com/blog/530-how-we-made-github-fast
データのシリアライズおよびRPC(リモートプロシージャコール)としてBERT-RPCを選んだ様です。
BERT and BERT-RPC 1.0 Specification

BERT and BERT-RPC are an attempt to specify a flexible binary serialization and RPC protocol that are compatible with the philosophies of dynamic languages such as Ruby, Python, PERL, JavaScript, Erlang, Lua, etc. BERT aims to be as simple as possible while maintaining support for the advanced data types we have come to know and love. BERT-RPC is designed to work seamlessly within a dynamic/agile development workflow. The BERT-RPC philosophy is to eliminate extraneous type checking, IDL specification, and code generation. This frees the developer to actually get things done.

http://bert-rpc.org/
BERT-RPCはBERT(Binary ERlang Term)という名前の通り、ERlangのterm_to_binary/1で生成されるフレキシブルなバイナリデータ交換の仕組みで、これをRPCに利用した物がBERT-RPCです。

BERT

BERT (Binary ERlang Term) is a flexible binary data interchange format based on (and compatible with) Erlang's binary serialization format (as used by erlang:term_to_binary/1).

http://bert-rpc.org/
erlangで使える、integer,float,atom,tupple,bytelist,list,binaryをシリアライズして転送する事が出来ます。仕様書はerlangの拡張仕様ページに掲載されています。
External Term Format

The external term format is mainly used in the distribution mechanism of Erlang.

http://erlang.org/doc/apps/erts/erl_ext_dist.html
詳しくはgithubのブログエントリでも説明されています。
最近ではMessagePackやProtocolBuffer、Thriftなどバイナリフォーマットを使ったRPCが流行ですが、このErlangのバイナリフォーマットは無駄の無いシリアライズ形式になっているかと思います。
実際には型識別と、それに後続するデータで構成され、型によってはデータ部の前にデータ長が付加されます。
Erlangのシリアライズでは、Erlangのprotocolバージョン131から開始されますが、その前に全体長を付与した物がRPCで使われています。RPCの本体はtuppleと、その中のATOMで構成されています。
実装を見たい方は以下のrubyによる実装を見る事が出来ます。
mojombo's bert at master - GitHub

BERT (Binary ERlang Term) serialization library for Ruby.

http://github.com/mojombo/bert

mojombo's bertrpc at master - GitHub

BERTRPC is a Ruby BERT-RPC client library.

http://github.com/mojombo/bertrpc
またerlangによるシリアライズ実装、およびBERT-RPCを使ったRPCサーバの実装は以下から参照出来ます。
mojombo's erlectricity at master - GitHub

Erlectricity exposes Ruby to Erlang and vice versa

http://github.com/mojombo/erlectricity

mojombo's ernie at master - GitHub

Ernie is an Erlang/Ruby BERT-RPC Server.

http://github.com/mojombo/ernie
例えば上記githubブログへのリンク先で説明されている様に # calc.rb
require 'ernie'

mod(:calc) do
  fun(:add) do |a, b|
    a + b
  end
end
というサーバのコードに対しては require 'bertrpc'

svc = BERTRPC::Service.new('localhost', 9999)
svc.call.calc.add(1, 2)
# => 3
というクライアントのコードが書けるのですが、内部ではErlangでシリアライズされ、先頭にデータ長が付与された以下のイメージでデータ送信が行われます。
{call,calc,add,[1,2]}
そしてバイト列としては以下の様になります。
0000000: 0000 0021 8368 0464 0004 6361 6c6c 6400  ...!.h.d..calld.
0000010: 0463 616c 6364 0003 6164 646c 0000 0002  .calcd..addl....
0000020: 6101 6102 6a                             a.a.j
先頭4バイトにデータ長(0x21:33バイト)、バージョン(0x83:131)、tupple(0x68)、tuppleアイテム数(4)、atom ext(0x64)...と続きます。

今日はこのBERT-RPCで動く足し算サーバををC言語から呼び出すサンプルを書いてみました。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

// send BERT-RPC command
//
//   request => {call,calc,add,[1,2]}
//   result  => {reply,3}
int
main(int argc, char *argv[]) {
  int sock;
  struct sockaddr_in serv_addr;
  short nport = htons(9999);
  struct hostent *host_ent;
  char *host = (char*)strdup("127.0.0.1");
  char *ptr;
  FILE *sockfp;

  ptr = strchr(host, ':');
  if (ptr) {
    *ptr++ = 0;
    if (atoi(ptr) == 0) {
      struct servent *serv_ent;
      serv_ent = getservbyname(ptr, "tcp");
      if (serv_ent)
        nport = serv_ent->s_port;
    } else {
      nport = htons(atoi(ptr));
    }
  }
  ptr = host;
  if ((host_ent = gethostbyname(ptr)) == NULL) {
    sock = -1;
    return -1;
  }
  memset((char *)&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  memcpy(&serv_addr.sin_addr, host_ent->h_addr, host_ent->h_length);
  serv_addr.sin_port = nport;

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    return;
  }

  if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) {
    close(sock);
    sock = -1;
    return;
  }

  char  n1;
  short n2;
  int   n4;

  // total length
  n4 = htonl(33);               // 4byte
  write(sock, &n4, sizeof(n4)); // number of bytes

  n1 = 131;
  write(sock, &n1, sizeof(n1)); // version 131

  // {call,module,function,arguments}
  // ex: {call,calc,add,[1,2]}
  n1 = 104;
  write(sock, &n1, sizeof(n1)); // tupple
  n1 = 4;
  write(sock, &n1, sizeof(n1)); // number of tupple items

    n1 = 100;
    write(sock, &n1, sizeof(n1)); // atom
    n2 = htons(4);
    write(sock, &n2, sizeof(n2)); // number of atom items

      // atom1
      write(sock, "call", 4);          // call

      // atom2
      n1 = 100;
      write(sock, &n1, sizeof(n1));    // atom ext
      n2 = htons(4);                   // 2byte
      write(sock, &n2, sizeof(n2));    // atom length
      write(sock, "calc", 4);          // atom name

      // atom3
      n1 = 100;
      write(sock, &n1, sizeof(n1));    // atom ext
      n2 = htons(3);                   // 2byte
      write(sock, &n2, sizeof(n2));    // number of atom items
      write(sock, "add", 3);           // atom name

      // atom4
      n1 = 108;
      write(sock, &n1, sizeof(n1));    // list extension
      n4 = htonl(2);                   // 4byte
      write(sock, &n4, sizeof(n4));    // number of list elements

        n1 = 97;
        write(sock, &n1, sizeof(n1));  // small int
        n1 = 1;
        write(sock, &n1, sizeof(n1));  // int 1

        n1 = 97;
        write(sock, &n1, sizeof(n1));  // small int
        n1 = 2;
        write(sock, &n1, sizeof(n1));  // int 2

      n1 = 106;
      write(sock, &n1, 1); // nil ext(term of list extension)

  char buf[256];
  ptr = buf;
  int x = 0, n, l;
  l = recv(sock, buf, sizeof(buf), 0);
  printf("recv size %d\n", l);
  for (n = 0; n < l; n++) {
      x = (x + 1) % 80;
      printf("%02X ", (unsigned char)buf[n]);
      if (x == 0) putchar('\n');
  }
  putchar('\n');

  // [ 13bytes ] []
  // 00 00 00 0D 83 68 02 64 00 05 72 65 70 6C 79 61 03

  // length
  n4 = (unsigned int) (
          (*(ptr+0) << 24) +
          (*(ptr+1) << 16) +
          (*(ptr+2) <<  8) +
          (*(ptr+3) <<  0));
  printf("total length = %d\n", n4);
  ptr+=4;

  // version
  n1 = *ptr;
  printf("version = %d\n", (unsigned char)n1);
  ptr++;

  // small tupple extension
  n1 = *ptr;
  if (n1 == 104) printf("tupple extension\n");
  ptr++;

  // number of tupple items
  n1 = *ptr;
  printf("number of tupple items %d\n", n1);
  ptr++;

  // atom ext
  n1 = *ptr;
  if (n1 == 100) printf("atom extension\n");
  ptr++;

  // atom length
  n2 = (unsigned short) (
          (*(ptr+0) <<  8) +
          (*(ptr+1) <<  0));
  printf("atom length = %d\n", n2);
  ptr+=2;

  // atom name
  char atom[200] = {0};
  memcpy(atom, ptr, n2);
  printf("atom name = %s\n", atom);
  ptr+=n2;

  // small int
  n1 = *ptr;
  if (n1 == 97)printf("small int\n");
  ptr++;
  n1 = *ptr;
  printf("int %d\n", n1);

  close(sock);
  sock = -1;
  return 0;
}
  
  

分かりやすい様にコメントに種別を書いていますが、実用するには構造化したり抽象化する必要があります。ただし言語によっては型が見合わない場合もあるので、擬似するコードを書かなければならない可能性もあります。まだrubyとjavascriptにしか実装がない様なので、どなたかチャレンジしてみてはいかがでしょうか。
Posted at by



2009/10/16


まぁ手順通りだけど...
Ubuntu 9.04で作りました。以下手順。

必要なソフトウェアのインストール
# sudo apt-get install texlive-xetex latex-cjk-xcjk pandoc texlive-latex-recommended
ちなみにtexlive-latex-recommendedを入れないと"kvoptions.styが無いよ!"と怒られるので注意。

progitのリポジトリをcloneする
# git clone git://github.com/progit/progit.git
git入れてない人はプロジェクトページのdownloadボタンで...

フォントを設定する
latexというフォルダに移動し、config.xmlというファイルがあるのでfontを設定する。"Japan"なんて郷ひろみばりのフォント持ってないのでIPAGothicを指定した。
diff --git a/latex/config.yml b/latex/config.yml
index c8f0d61..305e262 100644
--- a/latex/config.yml
+++ b/latex/config.yml
@@ -9,7 +9,8 @@ zh:
   font: AR PL UMing CN
   fig: "图 "
 ja:
-  font: Japan
+  font: IPAGothic
+  mono: IPAGothic
   fig: "図"
   tab: "表"
 ru:

あとは実行
# ./makepdf ja
jaフォルダの中にmain.pdfというファイルが出来る。

progit-ja
ウマー! 印刷して持ち歩こう。
Posted at by



2009/10/14


Google Waveにinviteされた。人からじゃなくて前に申し込んでたのが来たらしい。
ブログで招待しようかと思ったけど、いつもお世話になっている人たちにIRCで「いりますか?」と尋ねたら40分くらいで完売。
って事でタイトルは「Google Waveに招待します」じゃなく「Google Waveに招待しました」になっちゃいました。

wave-invited
Posted at by



2009/10/09


tokuhiromさんがnanowwwという、C++から簡単にHTTPが扱えるクライアントライブラリを書いてくれたので、botを書いてみた。
tokuhirom's nanowww at master - GitHub

C++ lightweight, fast, portable HTTP client library

http://github.com/tokuhirom/nanowww
picojsonは以前ご紹介した通り、kazuhoさんが書いたC++からSTLと親和性の高いJSONパーサです。
今回はtokuhiromさんが、C++で、もちろんSTLと親和性の高いHTTPクライアントライブラリを書いてくれました。
これを使えば、例えばhttp://example.com/fooというURLにPOSTで"name=hasegawa"を送信するとした場合
  nanowww::Client client;
  nanowww::Request req("POST", "http://example.com/foo", "name=hasegawa");
  nanowww::Response res;
  req.set_header("Content-Type", "application/x-www-form-urlencoded");
  if (client.send_request(req, &res))
      std::cout << res.content() << std::endl;
  else
      std::cout << client.errstr() << std::endl;
これだけのコードでHTTP通信出来てしまいます。
C++らしくていいですね。CURLよりはインタフェースが優れていると思います。ただ現状はPROXYに対応していなかったり、Windowsで使えなかったりしますので、用途を絞られますが、今後に期待したいと思います。
さて今日はこれを使って、IRCに常駐し、指定のチャネルに入り、指定の単語が現れると「それXXX関係ないだろ!jk」と発言する、「関係ないbot」を書きました。実際にIRCプロトコルを喋っている訳でなく、webchat.freenode.netのAjax通信を、nanowwwとpicojsonによる擬似Ajax通信を行って実現しています。
以下、少々長いですが全ソースコード。
#include "picojson.h"
#include "nanowww.h"

using namespace std;
using namespace picojson;
using namespace nanowww;

#ifdef _WIN32
static char* utf8_to_str_alloc(const char* utf8) {
    size_t in_len = strlen(utf8);
    wchar_t* wcsdata;
    char* mbsdata;
    size_t mbssize, wcssize;

    wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len,  NULL, 0);
    wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
    wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, wcsdata, wcssize + 1);
    wcsdata[wcssize] = 0;

    mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
    mbsdata = (char*) malloc((mbssize + 1));
    mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
    mbsdata[mbssize] = 0;
    free(wcsdata);
    return mbsdata;
}
#endif

value get_json(string url, map<string, string>& postdata) {
  value v;
  char error[256];
  string data;
  map<string, string>::iterator it;

  for (it = postdata.begin(); it != postdata.end(); it++) {
      if (data.size()) data += "&";
      data += it->first + "=";
      data += it->second;
  }

  Client client;
  Request req("POST", url.c_str(), data.c_str());
  Response res;
  req.set_header("Content-Type", "application/x-www-form-urlencoded");
  if (client.send_request(req, &res)) {
    const char* ptr = res.content().c_str();
    string err = parse(v, ptr, ptr + strlen(ptr));
    if (!err.empty()) cerr << err << endl;
  }
  return v;
}

int
main(int argc, char* argv[]) {
  map<string, string> postdata;
  value v;
  bool loop;

  if (argc != 4) {
      std::cerr << "usage: kankeinaibot [nick] [channel] [word]" << std::endl;
      return -1;
  }
  string nick = argv[1];
  string channel = argv[2];
  string word = argv[3];

  postdata.clear();
  postdata["nick"] = nick;
  v = get_json("http://webchat.freenode.net/e/n", postdata);
  string sid = v.get<array>()[1].get<string>();

  loop = true;
  postdata.clear();
  postdata["s"] = sid;
  while (loop) {
    v = get_json("http://webchat.freenode.net/e/s", postdata);
    array a = v.get<array>();
    for (array::iterator it = a.begin(); it != a.end(); it++) {
      if (!it->is<array>()) continue;
      array line = it->get<array>();
      if (line[0].to_str() == "c") {
        if (line[1].to_str() == "443") {
            postdata["nick"] += "_";
            v = get_json("http://webchat.freenode.net/e/n", postdata);
            sid = v.get<array>()[1].get<string>();
            break;
        }
        if (line[1].to_str() == "376") {
            loop = false;
            break;
        }
      }
    }
  }

  loop = true;
  postdata.clear();
  postdata["c"] = "JOIN #";
  postdata["c"] += channel;
  postdata["s"] = sid;
  v = get_json("http://webchat.freenode.net/e/p", postdata);
  while (loop) {
    v = get_json("http://webchat.freenode.net/e/s", postdata);
    array a = v.get<array>();
    for (array::iterator n = a.begin(); n != a.end(); n++) {
      array line = n->get<array>();
      if (line[0].to_str() == "c") {
        array aa = line[3].get<array>();
        if (aa.size() > 1 && aa[0].to_str() != nick) {
          cout << aa[0].serialize() << endl;
          cout << aa[1].serialize() << endl;
          if (!strncmp(aa[1].to_str().c_str(), word.c_str(), word.size())) {
              map<string, string> saydata;
              saydata["s"] = sid;
              std::stringstream s;
              s << "PRIVMSG #" << channel << " :" <<
                  "それ" << word << "関係ないだろ!jk";
              saydata["c"] = s.str();
              cout << saydata["c"] << endl;
              v = get_json("http://webchat.freenode.net/e/p", saydata);
          }
        }
        string s = line[3].serialize();
#ifdef _WIN32
        char* p = utf8_to_str_alloc(s.c_str());
        cout << p;
        cout << " ";
        free((void*)p);
        cout << endl;
#else
        cout << s << endl;
#endif
      }
    }
  }
  
  return 0;
}
CURLを使った場合のレスポンスwriterを書かなくてもいいので、かなりスッキリしましたね。

ソースはgithubにも置いてあるのでよろしければ試してみて下さい。
mattn's kankeinai-bot at master - GitHub

関係ないbot

http://github.com/mattn/kankeinai-bot

えっ?全然botじゃない?

bot関係ないだろ!jk

Posted at by



2009/10/08


Javaで...
class test{public static void main(String[]a){for(int n=1;n<101;n++)System.out.println(n%15>0?n%3>0?n%5>0?""+n:"Buzz":"Fizz":"FizzBuzz");}}
むー139バイトで46位。遠いなー。

じゃぁRubyで...
puts (1..100).map{|i|i%15==0%1?:FizzBuzz:i%3==0?:Fizz:i%5==0?:Buzz:i}
むむー。72バイトで65位。遠いよー。

なっ...ならばLuaで!!!汗...
for v=1,100 do print(v%15>0 and(v%3>0 and(v%5>0 and v or"Buzz")or"Fizz")or"FizzBuzz")end
88バイトで11位。ムキーーーーッ!

寝る!
Posted at by



2009/10/07


以前、Growl For Windows紹介記事gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書いたのですが、github上でpsinnottさんという方がforkしてUDP送信機能を付けてくれました。オープンソースの素晴らしい所ですね。
さらにlibgrowlというライブラリとしても使える様にしたので、アプリケーションに簡単に組み込む事が出来ます。
GNTPプロトコルは今のところWindowsだけしか使えませんが、UDP送信であればアイコンは出ないものの、他のプラットフォームでもGrowl出来る様になっています。現在WindowsとLinuxで動作確認出来ています。
なお、Windowsの場合はGrowlNotifyというDLLエントリポイントをrundll32用にエクスポートしてありますので
C:\>rundll32 growl.dll,GrowlNotify localhost:23053,MyApp,MyNotify,これはすごい,よーわからんけどね!,http://mattn.kaoriya.net/images/logo.png
と実行する事でGrowl For WindowsでGrowlされる様になっています。gntp-sendという付属コマンドもありますがコマンドラインからrundll32経由で使えて便利ですね!
誰得な機能っていわないで!><
Posted at by




blosxomにはnotfoundプラグインがあるので入れた。デフォルトのテンプレートのままだと見栄えが宜しくないので、"page.notfound"というファイルを作って各テンプレートファイルを引っ付けた物をベースに文言などを書いた。
見た目はこんな感じ。
Posted at by



2009/10/05


spiritlooseさんがPSGIなapacheモジュールを書いてくれたのでWindowsにポーティングしてみた。
mod_psgi を実装してみた - spiritlooseのはてなダイアリー

PSGI を実装したApache2モジュール。

http://d.hatena.ne.jp/spiritloose/20091002/1254467284
ちょっと気持ち悪いpatchになるけど、一応動いている。
巷にあるライブラリなどではWindowsポーティングされた時にUN*Xライクな型を自前で定義している事があるんだけど、Apacheモジュールのapr.hなんかや、Perlなんかでuid_t/gid_t/pid_tなんかが定義されている事が多い。

apr.h
typedef int uid_t;

perl.h
typedef long uid_t;

もちろんこのヘッダを同時に読み込むとエラーになるんですが、こういう場合に私がよく使う手として
#define uid_t _uid_t
#include <apr.h>
#undef uid_t
#include <perl.h>
てな具合に前の宣言を逃してやって、後の宣言を有効に出来る。今回はこの気持ち悪いhackを使ってWindowsにポーティングしました。

Commit 03fb93ebf27f0b98c3fbcc616ce736129e747912 to mattn's mod_psgi - GitHub
http://github.com/mattn/mod_psgi/commit/03fb93ebf27f0b98c3fbcc616ce736129e747912
今の所ご機嫌よく動いていて use strict;
use warnings;
my $count = 0;
my $handler = sub {
    my $content = "Hello World".$count++;
    return [ 200, [ "Content-Type" => "text/plain", "Content-Length" => length($content) ], [ $content ] ];
};
# vim:set ft=perl:
こんなPSGIなアプリもうまくちゃんとリクエスト毎にカウントアップされています。
もちろんpreforkだと同じ結果が現れる事がありますが
まだマージして頂いていないので、捨てコードになるかもしれませんが...

AWSWORD:perl:
Posted at by



2009/10/02


tthttpd(tinytinyhttpd)にiratqqさんがIPv6対応をコードを入れてくれました。通常起動でIPv4、IPv6どちらも扱える様になっていて、引数で # tthttpd -4
とすればIPv4だけで # tthttpd -6
とすればIPv6だけで起動する様になります。iratqq++
また、静的なファイルをサーブする場合にはこれまでreadしながらsendしていたのですが、sendfile(2)を使う様にしました。Windowsの場合は昨日Plackの高速化対応した時の様にTransmitFileを使うようにしました。
小さいサイズのファイルだと差は出ませんが、数メガくらいのファイルから格段に差が出て速くなりました。
mattn's tinytinyhttpd at master - GitHub

tiny tiny httpd

http://github.com/mattn/tinytinyhttpd

Posted at by




最近熱いPSGI/Packですが、サーバのStandaloneではかなり高速なパフォーマンスが出ているらしく、試してみようと思った所、Windowsでは動かない箇所があったのでいろいろやった作業履歴。
まず、Standaloneサーバが速いと言われる理由としてsendfile(2)を使っているのですが、Windowsにはsendfile(2)がありません。しかしソケット前提であるならばTransmitFileというAPIがあります。
sendfile(2)と同じようにZeroCopyでファイルから指定ディスクリプタに流し込むAPIです。
Plackが使っているSys::Sendfileに対してこのAPIを使う様にWindowsポーティングしました。
Sys::Sendfileのauthorにもpull requestを送りました。次にPlackにてWindowsで使えないAPIを叩いている部分に対するpatchを書きました。
これで一応、高速化対応は完了。
Plackに付属の画像を返すpsgiアプリケーションを起動してベンチマークを取ってみました。
# plackup -s Standalone -a eg/dot-psgi/image.psgi
ベンチマーク結果は以下の通り。まず高速化対応前。
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Plack-Server-Standalone/0.01
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        2397678 bytes

Concurrency Level:      1
Time taken for tests:   1.219 seconds
Complete requests:      10
Failed requests:        0
Write errors:           0
Total transferred:      23978730 bytes
HTML transferred:       23976780 bytes
Requests per second:    8.21 [#/sec] (mean)
Time per request:       121.876 [ms] (mean)
Time per request:       121.876 [ms] (mean, across all concurrent requests)
Transfer rate:          19213.49 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   4.9      0      16
Processing:   109  119  10.9    125     141
Waiting:        0    5   7.5      0      16
Total:        109  120  12.9    125     141

Percentage of the requests served within a certain time (ms)
  50%    125
  66%    125
  75%    125
  80%    141
  90%    141
  95%    141
  98%    141
  99%    141
 100%    141 (longest request)
そして対応後 This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient).....done


Server Software:        Plack-Server-Standalone/0.01
Server Hostname:        localhost
Server Port:            8080

Document Path:          /
Document Length:        2397701 bytes

Concurrency Level:      1
Time taken for tests:   0.578 seconds
Complete requests:      10
Failed requests:        0
Write errors:           0
Total transferred:      23978960 bytes
HTML transferred:       23977010 bytes
Requests per second:    17.30 [#/sec] (mean)
Time per request:       57.813 [ms] (mean)
Time per request:       57.813 [ms] (mean, across all concurrent requests)
Transfer rate:          40504.51 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    3   6.6      0      16
Processing:    31   55  22.4     47      94
Waiting:        0    3   6.6      0      16
Total:         31   58  24.5     47     109

Percentage of the requests served within a certain time (ms)
  50%     47
  66%     47
  75%     63
  80%     94
  90%    109
  95%    109
  98%    109
  99%    109
 100%    109 (longest request)
だいたいRequests perl secondで2倍くらい速くなってます。すばらしい。

miyagawa++ kazuho++ Plack++
Posted at by



2009/09/30


ASINを指定すると、Amazonのアフィをヨロシク表示してくれるblosxomのプラグインでawsxomというのがあるのですが、以前それをItemSearchにも対応させ、以降何回か使ってました。ただ最近は記事を書く際に文章で頭が一杯になってしまい、毎回アフィを貼るのを忘れてしまうという難病にかかってしまったせいでawsxomをamazonの仕様変更に追従させるのを忘れてました。
で、案の定先ほどの記事をポストした際にASIN書いたら見事に記事が壊れて泣くハメに...

ええいと重い腰を上げて修正してみました。
修正方法はhail2uさんが書いた物をベースに修正しました。
あまりawxsomの原形を留めていないので修正後のファイルで...
#!/usr/bin/perl
# ---------------------------------------------------------------------
# awsxom: AWSからデータを取得して書影その他を作成(ECS v4対応版)
# Author: Fukazawa Tsuyoshi <tsuyoshi.fukazawa@gmail.com>
# Version: 2006-11-24
# http://fukaz55.main.jp/
# Modified: Yasuhiro Matsumoto <mattn.jp@gmail.com>
# ---------------------------------------------------------------------
package awsxom;

use strict;
use LWP::UserAgent;
use CGI qw/:standard/;
use FileHandle;
use URI::Escape;
use Digest::SHA::PurePerl qw(hmac_sha256_base64);

# --- Plug-in package variables --------
my $asoid = "XXXXXX-22";            # AmazonアソシエイトID
my $devkey = "XXXXXXXXXXXXXXXXXXXX";        # デベロッパートークン
my $secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";    # シークレットキー
my $cachedir = "$blosxom::plugin_state_dir/aws_cache";  # XMLのキャッシュ用ディレクトリ
my $EXPIRE = 24 * 7;                # データを再読込する間隔(単位:時間)
my $default_template = "awsxom";        # デフォルトのテンプレートファイル名

my $VERSION = '1.4';
my $ua_name = "awsxom $VERSION";
my $endpoint = "ecs.amazonaws.jp";
my $unsafe   = "^A-Za-z0-9\-_.~";
my $debug_mode = 0;

# ---------------------------------------------------------------------
sub start {
    # キャッシュ用ディレクトリの作成
    if (!-e $cachedir) {
        my $mkdir_r = mkdir($cachedir, 0755);
        warn $mkdir_r
        ? "blosxom : aws plugin > \$cachedir, $cachedir, created.\n"
        : "blosxom : aws plugin > mkdir missed:$!";
        $mkdir_r or return 0;
    }

    1;
}

# ---------------------------------------------------------------------
sub story {
    my ($pkg, $path, $filename, $story_ref, $title_ref, $body_ref) = @_;
    $$body_ref = conv($$body_ref);
    1;
}

# ---------------------------------------------------------------------
sub foot {
    my($pkg, $currentdir, $foot_ref) = @_;
    $$foot_ref = conv($$foot_ref);
    1;
}

# ---------------------------------------------------------------------
sub conv {
    $_ = shift;

    # ASIN/ISBNが書かれていたら置き換える
    # テンプレート指定版
    s/(?:ASIN|ISBN):([A-Z0-9]{10}):(.*?):/to_html_asin($1,$2)/ge;

    # テンプレート無指定版
    s/(?:ASIN|ISBN):([A-Z0-9]{10})/to_html_asin($1,$default_template)/ge;

    # テンプレート無指定版
    s/(?:AWSWORD):([a-zA-Z0-9_]*?):/to_html_word($1,$default_template)/ge;

    return $_;
}

# ---------------------------------------------------------------------
# ASINからAmazonのアフィリエイト用HTMLを作成
sub to_html_asin {
    my ($asin, $template) = @_;    # ASINとテンプレ名称
    my $cache = "$cachedir/$asin.xml";
    my $outfile = "$cachedir/$asin.html";

    my $q = CGI->new;
    $q->param("AWSAccessKeyId", $devkey);
    $q->param("AssociateTag", $asoid);
    $q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3],  $_[2], $_[1], $_[0] ) }->(gmtime(time))));
    $q->param("Service", "AWSECommerceService");
    $q->param("Operation", "ItemLookup");
    $q->param("ItemId", $asin);
    $q->param("ResponseGroup", "Medium,Offers");
    $q->param("Version", "2009-01-06");
    my @p = $q->param();
    foreach (@p) {
        $_ = escape($_) . "=" . escape($q->param($_));
    }
    my $qs = join("&", sort(@p));
    my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
    my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);

    # 取り込み直す必要はあるか?
    if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
    # AWSから情報を取得してキャッシュファイルに保存
        # UserAgent初期化
        my $ua = new LWP::UserAgent;
        $ua->agent($ua_name);
        $ua->timeout(60);
        my $rtn = $ua->mirror($url, $cache);
    }

    # キャッシュからXMLを読み込んで解析
    my $content = getFile($cache);
    my %detail = parseXML($content, $asin);

    # テンプレートを展開。エラーの場合はエラー文字列を返す
    my $form;
    if (!defined($detail{"ErrorMsg"})) {
        #$form = &$blosxom::template($blosxom::path, $template, 'html');
        my $fh = new FileHandle;
        if ($fh->open("< $blosxom::datadir/$template.html")) {
            $form = join '', <$fh>;
            $form =~ s/\$(\w+)/$detail{$1}/ge;
            $fh->close();
        }
    }
    else {
        $form = "<p>" . $detail{"ErrorMsg"} . "</p>";
    }

    return $form;
}

# ---------------------------------------------------------------------
# ASINからAmazonのアフィリエイト用HTMLを作成
sub to_html_word {
    my ($word, $template) = @_;    # ASINとテンプレ名称
    my $cache = "$cachedir/$word.xml";
    my $outfile = "$cachedir/$word.html";

    my $q = CGI->new;
    $q->param("AWSAccessKeyId", $devkey);
    $q->param("AssociateTag", $asoid);
    $q->param("Timestamp", sprintf("%04d-%02d-%02dT%02d:%02d:%02d.000Z", sub { ($_[5]+1900, $_[4]+1, $_[3],  $_[2], $_[1], $_[0] ) }->(gmtime(time))));
    $q->param("Service", "AWSECommerceService");
    $q->param("Operation", "ItemSearch");
    $q->param("Keywords", $word);
    $q->param("SearchIndex", "Books");
    $q->param("ResponseGroup", "Medium,Offers");
    $q->param("Version", "2009-01-06");
    my @p = $q->param();
    foreach (@p) {
        $_ = escape($_) . "=" . escape($q->param($_));
    }
    my $qs = join("&", sort(@p));
    my $signature = hmac_sha256_base64("GET\n$endpoint\n/onca/xml\n$qs", $secret) . "=";
    my $url = "http://$endpoint/onca/xml?$qs&Signature=" . escape($signature);

    # 取り込み直す必要はあるか?
    if (!(-e $cache) || (-M $cache > ($EXPIRE / 24))) {
    # AWSから情報を取得してキャッシュファイルに保存
        # UserAgent初期化
        my $ua = new LWP::UserAgent;
        $ua->agent($ua_name);
        $ua->timeout(60);
        my $rtn = $ua->mirror($url, $cache);
    }

    # キャッシュからXMLを読み込んで解析
    my $content = getFile($cache);
    $content =~ s!.*?(<Item>.*?</Item>).*!$1!is;
    my $asin = "";
    $asin = $1 if ($content =~ /<ASIN>([^<]*)<\/ASIN>/);
    return "" if !$asin;
    my %detail = parseXML($content, $asin);

    # テンプレートを展開。エラーの場合はエラー文字列を返す
    my $form;
    if (!defined($detail{"ErrorMsg"})) {
        #$form = &$blosxom::template($blosxom::path, $template, 'html');
        my $fh = new FileHandle;
        if ($fh->open("< $blosxom::datadir/$template.html")) {
            $form = join '', <$fh>;
            $form =~ s/\$(\w+)/$detail{$1}/ge;
            $fh->close();
        }
    }
    else {
        $form = "<p>" . $detail{"ErrorMsg"} . "</p>";
    }

    return $form;
}

# ---------------------------------------------------------------------
# ファイルを読み込む
sub getFile {
    my $cache = shift;
    my $fh = new FileHandle;

    $fh->open($cache);
    my @data = <$fh>;
    $fh->close();
    my $content = join('', @data);
    return undef if (!$content);

    return $content;
}

# ---------------------------------------------------------------------
sub parseXML {
    my ($buf, $asin) = @_;
    my %detail;

    # Amazonへのリンク
    $detail{"Link"} = "http://www.amazon.co.jp/exec/obidos/ASIN/$asin/ref=nosim/$asoid";

    # 個々の要素の抽出
    $detail{"Asin"} = $1 if ($buf =~ /<ASIN>([^<]*)<\/ASIN>/);
    $detail{"ProductName"} = $1 if ($buf =~ /<Title>([^<]*)<\/Title>/);
    $detail{"Catalog"} = $1 if ($buf =~ /<Binding>([^<]*)<\/Binding>/);
    $detail{"ReleaseDate"} = $1 if ($buf =~ /<PublicationDate>([^<]*)<\/PublicationDate>/);
    $detail{"ReleaseDate"} = $1 if ($buf =~ /<ReleaseDate>([^<]*)<\/ReleaseDate>/);
    $detail{"Manufacturer"} = $1 if ($buf =~ /<Manufacturer>([^<]*)<\/Manufacturer>/);
    $detail{"ImageUrlSmall"} = $1 if ($buf =~ /<SmallImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"ImageUrlMedium"} = $1 if ($buf =~ /<MediumImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"ImageUrlLarge"} = $1 if ($buf =~ /<LargeImage>[^<]*?<URL>([^<]*)<\/URL>/);
    $detail{"Availability"} = $1 if ($buf =~ /<Availability>([^<]*)<\/Availability>/);
    $detail{"ListPrice"} = $1 if ($buf =~ /<LowestNewPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"OurPrice"} = $1 if ($buf =~ /<ListPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"UsedPrice"} = $1 if ($buf =~ /<LowestUsedPrice>.*?<FormattedPrice>([^<]*)<\/FormattedPrice>/);
    $detail{"Author"} = $1 if ($buf =~ /<Author>([^<]*)<\/Author>/);
    # エラー?
    if ($buf =~ /<Errors>.*?<Message>([^<]*)<\/Message>/) {
        $detail{"ErrorMsg"} = $1;
    }

    return %detail;
}

# ---------------------------------------------------------------------
# デバッグ用
sub print_debug {
    return if (!$debug_mode);

    my $fd = new FileHandle;
    $fd->open("/path/to/log/output/directory/logfile.log", "a");
    print $fd "$_[0]";
    $fd->close();
}

sub escape {
  my $s = shift;

  $s =~ s/([^\0-\x7F])/do {
    my $o = ord($1);
    sprintf("%c%c", 0xc0 | ($o >> 6), 0x80 | ($o & 0x3f));
  }/ge;

  return uri_escape($s, $unsafe);
}

1;
ちなみに、オリジナルに追加した機能は「AWSWORD:perl」と書くとItemSearch結果の1番目を表示する...というモノグサ機能です。


Posted at by




久々tthttpd(tinytinyhttpd)を触っていて、動いてるだろうと思ってたkeep-aliveのコードが動いていなかった事に愕然としながら修正してたら、以前smegheadさんが報告してくれていたmingw32で落ちる問題の原因を発見。
記憶の底で、_beginthread()したらCloseHandle()しなきゃいねないと思い込んでいたんですが、どうやら_beginthread()じゃなくて_beginthreadex()の場合だけだった(参照)。_beginthread()の場合はスレッド終了時にリソース回収されるらしい。
他いろいろと修正してkeep-aliveが動くようになり、ベンチマーク取ってみた。
テストは「helloworld」と書かれたhello.txtをabでGETする物。

まずはconnection closeの場合
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        
Server Hostname:        localhost
Server Port:            8080

Document Path:          /hello.txt
Document Length:        11 bytes

Concurrency Level:      10
Time taken for tests:   0.993 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      160000 bytes
HTML transferred:       11000 bytes
Requests per second:    1006.77 [#/sec] (mean)
Time per request:       9.933 [ms] (mean)
Time per request:       0.993 [ms] (mean, across all concurrent requests)
Transfer rate:          157.31 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:     1   10  13.0      6     128
Waiting:        0   10  12.9      6     128
Total:          2   10  13.0      6     129

Percentage of the requests served within a certain time (ms)
  50%      6
  66%      7
  75%      7
  80%      7
  90%     15
  95%     31
  98%     59
  99%     86
 100%    129 (longest request)
そしてconnection keep-aliveの場合
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        
Server Hostname:        localhost
Server Port:            8080

Document Path:          /hello.txt
Document Length:        11 bytes

Concurrency Level:      10
Time taken for tests:   0.559 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Keep-Alive requests:    1000
Total transferred:      185472 bytes
HTML transferred:       11088 bytes
Requests per second:    1789.60 [#/sec] (mean)
Time per request:       5.588 [ms] (mean)
Time per request:       0.559 [ms] (mean, across all concurrent requests)
Transfer rate:          324.14 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       1
Processing:     0    6  19.7      1     263
Waiting:        0    5  19.0      0     262
Total:          0    6  19.8      1     263

Percentage of the requests served within a certain time (ms)
  50%      1
  66%      4
  75%      4
  80%      4
  90%     11
  95%     22
  98%     46
  99%     80
 100%    263 (longest request)
ちなみに同じマシン(Ubuntu9.10 on CeleronM 1.5GHz 500M)で取ったapache2のベンチ。
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:        Apache/2.2.11
Server Hostname:        localhost
Server Port:            80

Document Path:          /~mattn/hello.txt
Document Length:        11 bytes

Concurrency Level:      10
Time taken for tests:   0.771 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Keep-Alive requests:    1000
Total transferred:      464920 bytes
HTML transferred:       11000 bytes
Requests per second:    1297.49 [#/sec] (mean)
Time per request:       7.707 [ms] (mean)
Time per request:       0.771 [ms] (mean, across all concurrent requests)
Transfer rate:          589.09 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.5      0       7
Processing:     6    8   3.3      7      28
Waiting:        6    7   3.1      6      27
Total:          6    8   3.3      7      28

Percentage of the requests served within a certain time (ms)
  50%      7
  66%      7
  75%      7
  80%      7
  90%     12
  95%     16
  98%     18
  99%     27
 100%     28 (longest request)
あまりパフォーマンスは意識してなかった割には、まぁまぁな速度(Request per secondでapache2の1.5倍速くらい?)が出たんじゃないかと思います。ただ機能も小さいですしapacheにも幾らかモジュールが入っちゃってますから参考値にしかすぎませんが...
とりあえずmingw32で不意に落ちる問題が解決したのでこれからパフォーマンスも気にしながらやって行こうかなーと思います。



Posted at by



2009/09/24


apacheでperlを扱うなら、CGI/FCGI/mod_perlといった選択肢があるのですが、mod_perliteという軽量なapacheモジュールも存在します。
sodabrew's mod_perlite at master - GitHub

A lightweight Apache module for Perl scripts

http://github.com/sodabrew/mod_perlite/
mod_perlは結構複雑ですが、mod_perliteは仕組みも簡素で分かりやすく速度パフォーマンスもそこそこあるという代物になっています。

mod_perliteについてはhidekさんの紹介記事を見ていただくと分かるかと思います。実はこのmod_perlite、絶賛の声はあれどPOSTメソッドが動かないという重大なバグを持ったまま今日に至ります。
先日からWindows向けにportingしている最中にその事実に気づき、「ええぃどうせならPOST出来る様にしてしまえ!」とpatchを書いたのが今日の昼。githubでpull requestしたのでauthorの方が今頃吟味検討してmergeするかどうかを判断しておられる頃かと思います。
変更差分を見られたい方はこの辺で...

何個か動作確認を行い、blosxomMTOS(Movable Type Open Source)、Catalyst(CGI)、MENTAが動作するのは確認しました。
mod_perlite_blosxom

mod_perlite_catalyst

mod_perlite_mtos

mod_perlite_menta
ただし完璧に動作するという訳でもなく、mod_perliteは内部でperlスクリプトをrun_fileするという簡素な仕様が故に、perl内でシグナルハンドラを書き換えられてしまうと、勝手に終了されてしまうという問題があります。デフォルトだと標準出力されるように$SIG{__DIE__}、$SIG{__WARN__}を書き換えてあるのですが、高性能なWAFはだいたいそのWAF専用のエラー画面を持っていて、内部でシグナルハンドラを上書きしている事があります。この場合、run_fileしてるだけなのでhttpサーバごとダウンするという、とてもおちゃめな動きになります。

とりあえずPOSTが動くようになってWindowsでも動かせる様になったので残る大きな問題は、この$SIG問題だと認識しています。
この辺はおそらくtieを使えばクリア出来るんじゃないかと思ってます。
Posted at by



2009/09/14


今日、子供の自転車の「こま(補助輪)」を取った。
※「こま」って関西オンリーぽい

そろそろこまなしで乗れる様にしなきゃいけないし、本人も望んでいた。
フラフラと倒れそうになりながら、私に支えられながら、なんとか家まで帰り付いた。

家に帰ってから、「そういえば、プログラマって職業でもこういった時期ってあるんだろうか。どんな時期がこまを取る時期なんだろ。」って考えた。
先輩から与えられた仕事だけをコツコツとこなした頃から、自分で自分がやらなきゃいけない仕事を見つけ出せる頃までにあるのか...

はたまた、顧客から言われた仕様を信じて開発していた頃から、顧客とヒアリングして仕様を煮詰められる様になれる頃にあるのか...


別にこまが付いたままでも何処までだって行けるだろうし、速度も出せるだろう。
実際、ウチの子もこまが付いたままでも結構なスピードが出せる。
しかしながら、自分で「もっと上手く乗りたい」って望んだからこそ「こまを取りたい」という気持ちが沸いて来たんだろうな。


コケても泣かないウチの子に、少しだけ成長を垣間見た。
Posted at by



2009/09/07


個人的には一番使っていて無いとちょっと不便に感じる自作のグリモンといえば「Google Reader Full Feed」なのですが、最近メインのブラウザをGoogle Chromeに変えた事もあり、使えずにちょっぴり不便になってました。
しかしながらGoogle Chromeに移植するとなれば簡単には行かないだろう事が分かっていたので移植するのを躊躇していました。先日、はてなブックマーク数を表示するGoogle Chrome Extensionも作った事だし、少しは知識もついたので、ようやく重い腰をあげて作ってみました。
最初は移植を考えてましたが、結構元にしているLDR FullFeedのコードがまばらになっていてメンテナンス性も悪かったので、今回は元のコードを捨てて1から作り直しました。とはいっても中で使っている部品などはConstellationさんの物や、os0xさんの物を使わせて頂いています。感謝

画面キャプチャは以下みたいな感じです。
chrome-grff
操作感はFirefox版とほとんど同じになってます。
今のところ、残課題はSITEINFOをGearsを使ってキャッシュする事くらいだと思ってますが、もしよければ使ってみた感想など頂けると助かります。
ソースは例のごとく、githubに置いてあります。
mattn's chrome-grff at master - GitHub

google reader full feed for chrome

http://github.com/mattn/chrome-grff/tree/master
インストールは以下のリンクから...
chrome-grff.crx
追記
キーは g じゃなく、z ですので、お気をつけて。
Posted at by




この文章はperl.jonallen.info に掲載されていた記事の和訳です。意訳が含まれている可能性があります。間違いがあればご連絡下さい。
by Jon Allen (JJ) - posted on Wednesday, 26 August 2009

ここ2、3年にわたって、Perlでの開発はCatalystやDBIx::Class、Moose等のエキサイティングな新技術により変わってました。

しかしながら、これらや他のツールに共通して言える事が1つあります - それらはこれらがPerl本体の配布物ではなくCPANの一部という事です。共有ホスティングサーバなど信頼されている環境においては、ユーザはルート権限なしでCPANモジュールをシステムにインストールする事が難しいでしょう。 ただ幸い、単純解があります - それが local::lib です。


local::lib の紹介

local::lib は CPAN ディストリビューションをホームディレクトリににインストールできる様にあらゆる設定を行うPerlモジュールです。これはルート権限が必要ない事を意味していて、システムのPerlや他のユーザに干渉せずに確実にインストール出来る事を意味しています。

もちろん、local::lib それ自信も root 権限なしにインストール出来る為、システム管理者を苦しめる必要性は全くありません。


インストールウォークスルー

クリーンインストールされた Ubuntu 9.04 で local::lib 使用するデモンストレーションを行います。

まず最初にhttp://search.cpan.org/dist/local-libで最新版をダウンロードしデスクトップへ .tar.gz を保存します。

search.cpan.org   Downloading local::lib

次に、Terminalセッションを開いて、以下のコマンドを入力してアーカイブを解凍します:

cd Desktop
tar -zxf local-lib-1.004004.tar.gz
cd local-lib-1.004004

"bootstrap"にする事を local::lib に教えてあげる必要があります。これでホームディレクトリにperl5というフォルダが作成され local::lib をそこにインストールする様 toolchain に命令します。

perl Makefile.PL --bootstrap
Bootstrapping local::lib

インストールが自動的に構成される様に一度か二度尋ねられます。デフォルト値('yes')で良いのでただ単にEnterキーを叩きましょう。

Automatic CPAN configuration   CPAN config complete

設定が終了したら以下のコマンドを入力し local::lib をビルド、テスト、インストールして下さい:

make
make test
make install

Installing local::lib

もうちょっとです。残りは1工程です。インストールされたモジュールがどこに保存されるかを Perl に教えてあげる為に幾つかの環境変数を設定します。

echo 'eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >>~/.bashrc

Setting environment variables

全て完了です。実行していたターミナルウィンドウを全て閉じて、新しい設定が反映される様に、新しいTerminalセッションを開きましょう。


local::lib のテスト

ちゃんと動くか簡単なモジュールをインストールしてみましょう。新しいターミナルウィンドウを開き、CPAN シェルを起動します:

perl -MCPAN -eshell

CPAN プロンプトが開き、以下を入力します

install Acme::Time::Baby
Installing Acme::Time::Baby

モジュールをダウンロードしてビルド、テスト、インストールします - マニュアルを見ることもないでしょう。exit をタイプして CPAN シェルから抜けたら簡単なワンライナーでインストールされたモジュールをテスト出来ます:

perl -MAcme::Time::Baby -E 'say babytime'

Acme::Time::Baby installed!

CPAN を恐れるな

さて動作確認です。Catalyst をインストールしましょう。

Catalyst は多くのモジュールに依存した巨大なモジュールですが CPAN.pm でセットアップするので問題はありません。しかしながら初期状態では、CPAN シェルが依存を見付ける度にインストールすべきかどうか尋ねて来て、とても退屈になります。最初ですから自動的に依存に従う様 CPAN.pm に教えてあげます。

perl -MCPAN -eshell
o conf prerequisites_policy follow
o conf build_requires_install_policy yes
o conf commit

Setting CPAN.pm preferences

注意 それでも時折、モジュールがそれら自身の構成に依存した質問をして来るので何が起きるか目を光らせておくべきでしょう。

インストールを開始する準備が整いました。

install Catalyst::Devel

CPAN.pm が全ての Catalyst 依存モジュールをビルドしてくれます。少々時間が経って、以下のメッセージが出ていればインストール成功です:

FLORA/Catalyst-Devel-1.19.tar.gz
/usr/bin/make install -- OK

Catalyst installed!

ちゃんと動作しているかを検証するので CPAN シェルを終了して新しい Catalyst アプリケーションを作成しましょう。

~/perl5/bin/catalyst.pl MyApp
cd MyApp
perl Makefile.PL

Starting a new Catalyst application   Starting a new Catalyst application

注意 catalyst.pl スクリプトは ~/perl5/bin ディレクトリにあります - ここが local::lib の標準的なスクリプトインストール先です。

Catalystアプリケーションはスタンドアロンの開発用サーバを含んでいるので、簡単にアプリケーションを実行できます:

script/myapp_server.pl -r -d

Running the Catalyst development server   Catalyst server running

これで http://localhost:3000をブラウズすれば、光り輝くCatalystのテストページが表示されます!

The Catalyst test page!

Notes

  • いくつかの Perl モジュールは速度を向上やC言語のライブラリ関数と強調する為にC言語で書かれています。CPANの全てを使用するにはCコンパイラをマシンにインストールする必要があります。Linuxや他のUNIXシステムで大概Cコンパイラが既にインストールされていますが(which ccをタイプしてみましょう)、そうでない場合でもベンダは GCC パッケージを提供しているはずです。Macでは Xcode developer tools (OS XをDVDからインストール)をインストールすると必要な物は全て揃うでしょう。

  • 私がテストで使用しているシステム Ubuntu 9.04 は Perl 5 version 10.0 を含んでいます。以下のコマンドで Perl がどのバージョンかをチェックできます。

    perl -v

    Checking your Perl version

    もし Perl 5 のリリースよりも古い物、すなわち v5.8.9 以前のバージョンをお使いであれば、システム管理者かOSベンダーにアップグレードを求めるべきしょう。 Perl5 version 10.0 では smart matching、named regex captures、a switch statement や state variables といった多くの新機能が導入されています - http://perldoc.perl.org/perl5100delta.html を参照してと全ての新機能を確認して下さい。

  • インストール済みのモジュールを全て削除してやり直すのであれば、ローカルの ~/.cpan と ~/perl5 ディレクトリを削除するだけです:

    rm -rf ~/.cpan
    rm -rf ~/perl5

    そして ~/.bashrc を編集し以下の行を削除します:

    eval $(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)

    以上に示した手順で local::lib がインストール出来ます。

Posted at by



2009/09/03


こんな拡張が欲しい人なんて半ば病気ですよ。

夜になるとエアコン無しに過ごせる涼しい季節になって来ました。皆さん如何お過ごしでしょうか。

最近Google Chromeを使っているのですが、AutoPagerizeのGoogle Chrome拡張を入れてみて感動し、被はてなブックマーク数を画面下に表示する拡張が欲しくなったので、つい勢いで作ってしまいました。
はいはい。病気病気

スクリーンショットはこんな感じ
chrome-hbcount

os0xさんの記事をふんだんに参考にしながらなんとか作り上げました(os0x++)。
以下その作成手順。

まず、manifest.jsonを用意しました。
manifest.json
{
  "name": "hatena bookmark counter",
  "description": "hatena bookmark counter for google chrome. hatena bookmark is social bookmark web service. see 'http://b.hatena.ne.jp/'. this extension show image of numbers which how many users bookmarked the URL.",
  "version": "0.0.1",
  "icons": { "normal": "hbcount.gif" },
  "permissions": [ "tabs" ],
  "toolstrips": [ "hbcount.html" ],
  "content_scripts": [
    {
      "js": [ "hbcount.js" ],
      "matches": [ "http://*/*" ]
    }
  ]
}
今回の拡張は、ページがロードされた瞬間に発動するjavascriptと、Google Chrome起動中に画面下で常駐するためのtoolstripsと呼ばれるHTMLコンテンツを使いました。
まずユーザスクリプト側。
hbcount.js
if (window == top{
  var port = chrome.extension.connect();
  port.onMessage.addListener(function(data) {
    location.href = data.url;
  });
  port.postMessage({url: location.href});
}
ユーザスクリプト側からはtoolstripのコントロールに対してアクセス出来ませんのでpostMessageでメッセージを送信し被はてなブックマーク数アイコンを更新します。なおtoolstripにあるアイコンをクリックすると、はてなブックマークエントリページを開く様になっているのですが、toolstrip側から現在開いているタブのコンテンツにはアクセスする事も出来たのですが、少し危険を感じたのでpostMessageを使いtoolstrip側からユーザスクリプト側にURLを返信してlocation.hrefでの画面遷移をさせています。
真ん中のイベント待ちはその為の物です。なお、topかどうかを確認しているのは現在のURL以外でもこの拡張が走ってしまうのを防止している小細工です。例えばiframeなんかでアフィが表示されているとそれに対してもこの拡張が実行されてしまいます。

次に本体であるtoolstripのソースは以下の通り。
hbcount.html
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<script type="text/javascript">
window.onload = function() {
  var hbcount = document.getElementById("hbcount");
  chrome.extension.onConnect.addListener(function(port) {
    function update(url) {
      hbcount.src = 'hbcount.gif';
      hbcount.onclick = null;
      if (url.match(/^chrome:\/\//)) return;
      url = url.replace(/#/g, '%23');
      hbcount.src = 'http://b.hatena.ne.jp/entry/image/' + url;
      hbcount.onclick = function() { port.postMessage({'url': 'http://b.hatena.ne.jp/entry/' + url}); }
    }
    port.onMessage.addListener(function(data) { update(data.url); });
    chrome.tabs.onSelectionChanged.addListener(function(id, props) {
      chrome.tabs.get(id, function(tab) { update(tab.url); });
    });
  });
};
</script>
<img src="hbcount.gif" id="hbcount" style="cursor: pointer;" title="はてなブックマーク"/>
ユーザスクリプトからメッセージを受け取りupdate関数にて更新します。アイコンクリック時のメッセージ返信を行う処理もここにあります。
その下にある処理ではタブが切り替わったタイミングで現在の被ブックマーク数を更新する処理になります。この場合、いくらmanifest.jsonでhttp://のみと宣言していてもタブに対するイベント登録を行うのでchrome://といったURLも対象になってしまいます。update関数でURLを避けているのはその為です。

さて、ここから話がそれ始めます。
Google Chromeの拡張はchromeブラウザ自身でパッキングする事が出来ます。ただchromeへのパスをいちいち書いてられないのでMakefileを書きました。
Makefile
ifndef CHROME
ifneq ($(windir),)

# Windows
DEST = "$(shell pwd)\chrome-hbcount"
CHROME = "$(USERPROFILE)/Local Settings/Application Data/Google/Chrome/Application/chrome.exe"

else

# Other Platform: Linux? Mac?
DEST = $(shell pwd)/chrome-hbcount

CHROME = $(shell which crxmake)
ifeq ($(CHROME),)
CHROME = $(shell which google-chrome)
endif
ifeq ($(CHROME),)
CHROME = $(shell which chromium-browser)
endif
ifeq ($(CHROME),)
CHROME = chrome
endif
endif
endif

SRCS = hbcount.gif hbcount.html hbcount.js manifest.json

all : chrome-hbcount.crx

first : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST)

chrome-hbcount.crx : $(SRCS)
    @-rm -r $(DEST)
    @mkdir $(DEST)
    @cp $(SRCS) $(DEST)/.
    $(CHROME) --pack-extension=$(DEST) --pack-extension-key=chrome-hbcount.pem

clean:
    @-rm *.crx
    @-rm -r $(DEST)
初回だけmake first、以降はmakeでビルドです。
WindowsでもLinuxでもMacでも使える様に、かつgoogle-chromeが入っていない場合にはchromium-browserを使う様になっています。ちなみにcrxmakeというのはConstellationさんが書いたchrome拡張のパッケージングツールです(Constellation++)。chromeを起動する事なくパッケージング出来ます。なおchrome-hbcountという部分を書き換えれば、他の拡張でも使えるかもしれません。

さらに拡張を書いていると拡張のバージョンがちょくちょくあがります。
Google Chrome(chromium-browser)の拡張にはupdate_urlという設定項目があり、決まった形のXMLファイルを用意しておけば自動アップデート出来る様になる予定があるそうなのですが、拡張を書き換える度にいちいちupdate.xmlを書き換えてアップロードするも面倒くさそうだったので、手順を自動化出来る様にしてみました。簡単には
  • manifest.jsonからバージョンを抜き出す
  • 抜き出したバージョンでupdate.xmlを更新する
  • githubのダウンロードページにあるファイルを一旦削除する
  • 拡張モジュール(crx)とupdate.xmlをアップロードする
という一連の流れをPerlスクリプトにしてみました。
dist-upload.pl
#!/usr/bin/perl

use strict;
use warnings;

use JSON;
use Perl6::Slurp;
use WWW::Mechanize;
use Net::GitHub::Upload;
use Config::Pit;

my $config = pit_get('github-upload', require => {
    'login' => 'your login id on github.com',
    'password' => 'your password on github.com',
});

my $manifest = from_json(slurp 'manifest.json');
my $id = $manifest->{id};
my $version = $manifest->{version};

open my $fh, '>update.xml';
print $fh <<EOF;
<gupdate xmlns="http://www.google.com/update2/response" protocol="2.0">
    <app appid="$id">
        <updatecheck codebase="http://cloud.github.com/downloads/mattn/chrome-hbcount/chrome-hbcount.crx" version="$version" />
    </app>
</gupdate>
EOF
close $fh;

my $mech = WWW::Mechanize->new;
$mech->get('https://github.com/login');
$mech->submit_form(
    form_number => 2,
    fields      => {
        login => $config->{login},
        password => $config->{password},
});

$mech->get('http://github.com/mattn/chrome-hbcount/downloads');
for my $form (@{$mech->forms}) {
    if ($form->action =~ /^http:\/\/github.com\/mattn\/chrome-hbcount\/downloads\//) {
        print "deleting ".$form->action."\n";
        $mech->request($form->click);
    }
}

chomp(my $user  = `git config github.user`);
chomp(my $token = `git config github.token`);
my $gh = Net::GitHub::Upload->new(
    login => $user,
    token => $token,
);
print "uploading chrome-hbcount.crx\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'chrome-hbcount.crx' );
print "uploading update.xml\n";
$gh->upload( repos => 'mattn/chrome-hbcount', file  => 'update.xml' );
こちらもmattnとかchrome-hbcountという部分を書き換えれば、他の拡張で使えるかもしれません。ちなみにNet::GitHub::Uploadはtypesterさんのモジュールです(typester++)。
これで、あとは
  • 拡張を書き換える
  • manifest.jsonのバージョンを上げる
  • make
  • dist-upload.pl
という単純な手順で皆様に最新の拡張を自動ダウンロードして頂ける...予定です。まだupdate.xmlを使った自動アップデート機能は動いてないらしいです。


さて、はてなブックマークカウンタ拡張に話を戻して...
コードおよび拡張ファイルはgithubに置いてあります。Downloadのページからでもダウンロード出来ますが、以下のリンクでインストール出来る様になっています。
hatena bookmark counter for google chrome
リポジトリは以下のリンク先にあります。
mattn's chrome-hbcount at master - GitHu

hatena bookmark counter for google chrome

http://github.com/mattn/chrome-hbcount/tree/master
ライセンスはBSDにしましたので、どうぞお好きに使って下さい。流用してdelicious拡張なんてのもいいですね。
Posted at by



2009/09/02


以前、Growl For Windows紹介記事gntp-sendという単純なCのプログラム(パスワードハッシュのみサポート)を書きました。その後、GNTPプロトコルの仕様に合わせパスワードハッシュ以外にもAES/DES/3DESな暗号化通信もサポートしたPerlモジュール、Growl::GNTPを書きました。

C/C++言語から暗号モジュールを扱う上でcryptライブラリはライセンス制約がありますが、public domainなCrypto++を使えばな制約もなくなります。
久々boostを触ってみようとリハビリがてらboost::asioとcrypto++を使って、暗号化通信をサポートしたGNTP Growlプログラムを書いてみた。

crypto++には、ハッシュアルゴリズムや暗号アルゴリズムがごった煮で含まれており、フィルタとして使ったりCBCモードで使ったりと、かなり便利になっています。
例えば乱数を得たいのであれば
CryptoPP::SecByteBlock salt(8);
CryptoPP::AutoSeededRandomPool rng;
rng.GenerateBlock(salt.begin(), salt.size());
こんな感じに、またMD5 Digestのhex文字列を取得したいのであれば
CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
CryptoPP::Weak1::MD5 hash;
hash.Update((byte*)password_.c_str(), password_.size());
hash.Update(salt.begin(), salt.size());
hash.Final(passtext);
CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());
といった感じに。さらにCBCモードで文字列を暗号化したいならば
CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
  encryptor(passtext.begin(), iv.size(), iv.begin());

std::string cipher_text;
CryptoPP::StringSource(text, true,
  new CryptoPP::StreamTransformationFilter(encryptor,
  new CryptoPP::StringSink(cipher_text)
  ) // StreamTransformationFilter
); // StringSource
こんな風に書くことも出来る。これだけそろえば出来たも同前。
boost::asioの便利なiostreamを使って
asio::ip::tcp::iostream sock(hostname, port);
sock << "こんにちわこんにちわ!";
綺麗な書き方も出来る。ちなみにboost::asioを使えばWindowsやUNIX上での特有なコードが現れる事がないので、同じソースでWindowsやLinuxでもコンパイル出来てしまうので非常にありがたいですね。

以下、全体のソースです。ヘッダファイルになってます。
#ifndef gntp_h
#define gntp_h

#define CRYPTOPP_ENABLE_NAMESPACE_WEAK 1
#include <sstream>
#include <iostream>
#include <string>
#include <cryptopp/osrng.h>
#include <cryptopp/files.h>
#include <cryptopp/hex.h>
#include <cryptopp/md5.h>
#include <cryptopp/des.h>
#include <cryptopp/aes.h>
#include <cryptopp/filters.h>
#include <cryptopp/modes.h>

#include <asio.hpp>

class gntp {
private:
  static inline std::string to_hex(CryptoPP::SecByteBlock& in) {
    std::string out;
    CryptoPP::HexEncoder hex( NULL, true, 2, "" );
    hex.Attach(new CryptoPP::StringSink(out));
    hex.PutMessageEnd(in.begin(), in.size());
    return out;
  }

  void send(const char* method, std::stringstream& stm) {
    asio::ip::tcp::iostream sock(hostname_, port_);
    if (!sock) return;

    // initialize salt and iv
    CryptoPP::SecByteBlock salt(8), iv(8);
    rng.GenerateBlock(salt.begin(), salt.size());
    rng.GenerateBlock(iv.begin(), iv.size());

    if (!password_.empty()) {
      // get digest of password+salt hex encoded
      CryptoPP::SecByteBlock passtext(CryptoPP::Weak1::MD5::DIGESTSIZE);
      CryptoPP::Weak1::MD5 hash;
      hash.Update((byte*)password_.c_str(), password_.size());
      hash.Update(salt.begin(), salt.size());
      hash.Final(passtext);
      CryptoPP::SecByteBlock digest(CryptoPP::Weak1::MD5::DIGESTSIZE);
      hash.CalculateDigest(digest.begin(), passtext.begin(), passtext.size());

      // initialize crypt
      CryptoPP::CBC_Mode<CryptoPP::DES>::Encryption
        encryptor(passtext.begin(), iv.size(), iv.begin());

      std::string cipher_text;
      CryptoPP::StringSource(stm.str(), true,
        new CryptoPP::StreamTransformationFilter(encryptor,
        new CryptoPP::StringSink(cipher_text)
        ) // StreamTransformationFilter
      ); // StringSource

      sock << "GNTP/1.0 "
        << method
        << " DES:" << to_hex(iv)
        << " MD5:" << to_hex(digest) << "." << to_hex(salt)
        << "\r\n"
        << cipher_text << "\r\n\r\n";
    } else {
      sock << "GNTP/1.0 "
        << method
        << " NONE\r\n"
        << stm.str() << "\r\n";
    }

    while (1) {
      std::string line;
      if (!std::getline(sock, line)) {
        break;
      }
      //std::cout << "[" << line << "]" << std::endl;
      if (line.find("GNTP/1.0 -ERROR") == 0)
        throw "failed to register notification";
      if (line == "\r") break;
    }
  }

  std::string application_;
  std::string hostname_;
  std::string port_;
  std::string password_;
  CryptoPP::AutoSeededRandomPool rng;
public:
  gntp(std::string application = "gntp-send", std::string password = "",
      std::string hostname = "localhost", std::string port = "23053") :
    application_(application),
    password_(password),
    hostname_(hostname),
    port_(port) { }

  void regist(const char* name) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notifications-Count: 1\r\n";
    stm << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    stm << "Notification-Display-Name: " << name << "\r\n";
    stm << "Notification-Enabled: True\r\n";
    stm << "\r\n";
    send("REGISTER", stm);
  }

  void notify(const char* name, const char* title, const char* text, const char* icon = NULL) {
    std::stringstream stm;
    stm << "Application-Name: " << application_ << "\r\n";
    stm << "Notification-Name: " << name << "\r\n";
    if (icon) stm << "Notification-Icon: " << icon << "\r\n";
    stm << "Notification-Title: " << title << "\r\n";
    stm << "Notification-Text: " << text << "\r\n";
    stm << "\r\n";
    send("NOTIFY", stm);
  }
};

#endif
使い方は非常に簡単。
#include "gntp.h"

int main(void) {
  gntp client("my growl application", "my-password-is-secret");
  client.regist("my-event");
  client.notify("my-event", "タイトル", "本文", "http://mattn.kaoriya.net/images/logo.png");
  return 0;
}
これだけで暗号化通信をサポートしたGNTP Growl通信が出来てしまいます。ちなみにDESな部分をAESに変えればそのままAES暗号通信する事が出来ます。CBC素晴らしい。

本当はboost 1.40.0に含まれる新しい機能を触るつもりだったのですが、まったく触れてもいませんね...苦笑

追記 githubにソースあげておいた。
mattn's gntppp at master - GitHub

GNTP++: gntp client library writen in C++

http://github.com/mattn/gntppp/tree/master
Posted at by



2009/08/26


vimを使っていて人のスクリプトの一部が気に入らない場合、直接書き換える事もするのですが、最近はGLVS(GetLatestVimScripts)を使う事の方が多く、せっかく書き換えたスクリプトを新しいアップデートで上書きされたりして悲しい事になったりします。書き換えて違うファイル名で保存する...なんてのも方法かもしれませんが、いっそvimscriptの中のある関数だけ書き換えられればいいんじゃないか...と思って書き換える方法を考えてみました。
まず、vimにはグローバルスコープ、スクリプトスコープ、ローカルスコープとあり、スクリプトスコープとは1スクリプトファイル内で実行される関数と変数群に位置します。
通常、スクリプトスコープ内の関数には<SID>というスクリプトID識別が付与され、ファイル単位でユニークに格納されています。
ただ実際には識別子が付いているだけで、外部から参照も出来ますしfunction()で関数リファレンスを取る事も出来ます。スクリプトファイルに対する<SID>は :scriptnames
で一覧出来ますから、この出力をredirで盗み取ってしまえばいいのです。
ちなみにウチのubuntuで:scriptnamesを実行した結果は以下の通り。
  1: /usr/share/vim/vimrc
  2: /usr/share/vim/vim72/debian.vim
  3: /usr/share/vim/vim72/syntax/syntax.vim
  4: /usr/share/vim/vim72/syntax/synload.vim
  5: /usr/share/vim/vim72/syntax/syncolor.vim
  6: /usr/share/vim/vim72/filetype.vim
  7: /home/mattn/.vimrc
  8: /usr/share/vim/vim72/syntax/nosyntax.vim
  9: /usr/share/vim/vim72/ftplugin.vim
 10: /usr/share/vim/vim72/indent.vim
 11: /home/mattn/.vim/plugin/autodate.vim
 12: /home/mattn/.vim/plugin/calendar.vim
 13: /home/mattn/.vim/plugin/codepad.vim
...
この左数字部分がIDです。これをsplitとmapを使い、スクリプトファイル名から<SID>を得られるハッシュマップ(vim語ではDictionary)に変換します。
function! GetScriptID(fname)
  let snlist = ''
  redir => snlist
  silent! scriptnames
  redir END
  let smap = {}
  let mx = '^\s*\(\d\+\):\s*\(.*\)$'
  for line in split(snlist, "\n")
    let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
  endfor
  return smap[tolower(a:fname)]
endfunction
ちなみに、tolowerしてるのはほぼWindows用で、ちょっとした大文字小文字の違いで<SID>が取れなくなって悲しくならない為のおまじないです。
次に書き換えるべき関数リファレンスを取得します。<SID>内で定義される関数リファレンスは <SNR>SID_foo という識別になります。これをfunction()関数に渡してあげれば関数リファレンスが取得出来ます。
これだけでも、実はスクリプトスコープ内の関数を外部から呼び出せてウマーなのですが、ここから本題。
関数を書き換えるにはfunction!と「!」付きで宣言すれば良いのですが引数が決められません。書き換え関数の引数を外部から指定して貰っても良いのですが面倒ですよね。書き換え前の関数引数と書き換え後の関数引数が同じである事は書き換えた側の責任でもありますし、call()関数を使えば引数を配列として呼び出す事も出来ます。そこで以下の様なトリックを使いました。
function! funcA(...)
  return call('funcB', a:000)
endfunction
a:000とは可変個引数を宣言した際に使える引数リストです。受け取った引数リストをcall()関数に渡しています。後はこれを動的に実行してやれば書き換え関数の完成です。全体のコードだと以下の様になりました。
function! GetScriptID(fname)
  let snlist = ''
  redir => snlist
  silent! scriptnames
  redir END
  let smap = {}
  let mx = '^\s*\(\d\+\):\s*\(.*\)$'
  for line in split(snlist, "\n")
    let smap[tolower(substitute(line, mx, '\2', ''))] = substitute(line, mx, '\1', '')
  endfor
  return smap[tolower(a:fname)]
endfunction

function! GetFunc(fname, funcname)
  let sid = GetScriptID(a:fname)
  return function("<SNR>".sid."_".a:funcname)
endfunction

function! HookFunc(funcA, funcB)
  if type(a:funcA) == 2
    let funcA = substitute(string(a:funcA), "^function('\\(.*\\)')$", '\1', '')
  else
    let funcA = a:funcA
  endif
  if type(a:funcB) == 2
    let funcB = substitute(string(a:funcB), "^function('\\(.*\\)')$", '\1', '')
  else
    let funcB = a:funcB
  endif
  let oldfunc = ''
  redir => oldfunc
  silent! exec "function ".funcA
  redir END
  let g:hoge = oldfunc
  exec "function! ".funcA."(...)\nreturn call('" . funcB . "', a:000)\nendfunction"
endfunction
さて、このスクリプトの使い方。例えば引数で与えられた文字列XXXを使い「Hello: XXX」を表示するスクリプト内関数s:funcAがあったとして、これを「GoodNight: XXX」と表示するスクリプト内関数s:funcBに書き換えたいとします。それぞれ関数リファレンスを得て書き換えたいタイミングでHookFuncを呼べば、それ以降は書き換えられた関数が実行されます。
so hookfunc.vim

function! s:foo(text)
  echo "Hello, " . a:text
endfunction

function! s:bar(text)
  echo "GoodNight, " . a:text
endfunction

call s:foo("World")
" => Hello, World
"
call s:bar("World")
" => GoodNight, World

" foo を bar に書き換える
call HookFunc(GetFunc(expand("%:p"), "foo"), GetFunc(expand("%:p"), "bar"))

call s:foo("World")
" => GoodNight, World
ちなみにfuncAとfuncBは違うスクリプトファイルに宣言されていても問題ありません。
イメージはコード内のコメントで分かって頂けると思います。時と場合によっては使えそうな機能ですね。

まぁ、ほとんど使い道ないでしょうが...苦笑
Posted at by



2009/08/25


ネタ的にはZIGOROuさんかhasegawaさんのネタっぽいが...
@if(0)==(0) ECHO OFF
CScript.exe //NoLogo //E:JScript "%~f0" %*
GOTO :EOF
@end

function wsock_ConnectionRequest(reqId) {    
    if (socket.State != 0/* closed */) socket.Close();
    socket.Accept(reqId);
}

function wsock_DataArrival(bytesTotal) {    
    var data = script.Run('GetData', socket, bytesTotal);
    socket.SendData([
        "HTTP/1.1 200 OK",
        "Connection: closed",
        "Content-Type: text/html;",
        "",
        "Hello World! " + new Date(),
        ""
    ].join("\n"));
    // 相手が閉じてくれないので閉じたいけど待たないとレスポンスが無くなる
    WScript.Sleep(1000);
    socket.Close();
    socket.Listen();
}

var socket = WScript.CreateObject('MSWinsock.Winsock', 'wsock_');
var script = WScript.CreateObject('ScriptControl');
script.language = 'VBScript';
script.AddObject('WScript', WScript);
script.AddCode([
    'Function GetData(socket, bytesTotal):',
    '  Dim data:',
    '  socket.GetData data, vbString, bytesTotal:',
    '  GetData = data:',
    'End Function'
].join(''));

socket.Bind(8080);
socket.Listen();
while (socket.State != 9/* error */{ 
    WScript.Sleep(100);
} 

// vim:set ft=javascript:
GetDataがByRefなので、ScriptControlを使ってます。
Posted at by



2009/08/24


すぎゃーんさんのirssi全裸プラグインをベースに、kazuhoさんのpicojsonを使い、freenodeのwebchat機能をAPI的に使って、全裸発言リプライするIRC botを作ってみた。
コンパイルは以下の通り # g++ zenra-bot.cc -lmecab -lcurl
Windows(mingw32)でも動作します。

実行は第一引数にnick名、第二引数にチャネル名を指定します。
# ./zenra-bot zenra-bot #zenra-room 常駐している間は、誰かの発言をZENRIZE(全裸的変換)してつぶやきます。
例えば
会社に行く
と誰かが発言すれば
全裸で会社に行く
とつぶやいてくれます。
とても有用ですね!

よろしければどうぞ。

Posted at by



2009/08/21


テストも兼ねて...

追記
これだけだとなんなので...。
RSSもしくはAtomにrel="hub" href="http://pubsubhubbub.appspot.com"のlink要素を追加し、このpluginのhub_urlにRSSもしくはAtomのURLを指定すれば動きます。
Posted at by



2009/08/19


だいたいに10分くらいだと思う。
まずtinytinyhttpd(tthttpd)とmercurialは既に入っている事として

1. リポジトリを作る

# cd /path/to/repo
# hg init

2. hgwebdir.cgiを用意する

mercurialに含まれるhgwebdir.cgiを配置する。
# cd /path/to/web
# cp /path/to/hgwebdir.cgi .
mercurialのソースパッケージに入ってる

3. hgweb.configを用意する

hgwebdir.cgi用の設定ファイルを書く。
[paths]
repo = /path/to/repo

4. hgrcを作る

.hg/hgrcを作る(もしくは修正する)。 [web]
name = repo
description = my private repo
contact = mattn@example.com
push_ssl = false
allow_push = mattn iratqq hasegawa
allow_pull = *
allow_read = *
allow_archive = bz2, gz, zip
encoding = utf-8

5. パスワードファイルを書く

mattn:ggrks
iratqq:kkrkr
hasegawa:xss
プレインテキストなので要注意

6. tthttpdの設定ファイルを書く

Windowsならば [global]
port=8080
root=c:/path/to/repo
indexpages=hgwebdir.cgi

[mime/types]
cgi=@c:/python26/python.exe

[authentication]
/=POST,Admin!,c:/path/to/passwd
Unixならば [global]
port=8080
root=/path/to/repo
indexpages=hgwebdir.cgi
spawn_executable=on

[authentication]
/=POST,Admin!,/path/to/passwd
こんな感じ。

7. tthttpdを起動する

設定ファイルを指定して起動
# tthttpd -vvv -c repo.conf
tthttpd-hgweb1
うごいた。

8. clone/pushしてみる

# hg clone http://localhost:8080/hgwebdir.cgi/repo
destination directory: repo
no changes found
updating working directory
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
# cat > README

^D

# hg add README
# hg commit -m "first import"
# hg push
pushing to http://localhost:8080/hgwebdir.cgi/repo
searching for changes
http authorization required
realm: Admin!
user: mattn
password:
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files

tthttpd-hgweb2
うごいた。
最新版のtinytinyhttpdでしか動きません。

tthttpd.exeを含んだリポジトリフォルダごと持って歩けば、どこでもmercurialサーバになるね。
Posted at by



2009/08/18


色々と修正を施して、sinatraのdispatch.cgiも動くようになっています。またBASIC認証をサポートしましたので、tDiaryのupdate.rbだけに認証をかけられる様になっています。実際には、ターゲットURL、メソッド(GET/POSTなど)、ユーザ・パスワード一覧という設定が可能になったので、複数ユーザを扱う事も出来ます。
ただ現状、プレインテキストのみ対応ですので、これについては今後なんとかして行きたいと思っています。

本当ならば.htaccessを読み込ませたいのですが、.htaccessはcrypt/md5を使って暗号化されており、libcryptを使うとなれば依存が増えると同時に、ライセンス的にもグレーになってきますので、少し悩んでいます。どこかにPublic Domainで書かれたlibcrypt知りませんか?
Posted at by



2009/08/17


ずいぶん前からXMLRPCをC++で扱うのに小さいライブラリないかなーと思ってました。


書いたといっても結構前からあったのですが、いらん所を削ぎ落として簡素なXMLRPCライブラリとして仕立て上げました。
簡素とは言えど、一般的な物ならば色々動きます。
例えば、ブログに一気に、しかも機械的にポストしたい場合とか

おれは今すぐXMLRPCしたいんだ!そしてどうしてもC++でXMLRPCしたいんだー!

って事ないですか?MovablebTypeをインストールしたけど、XMLRPCするのにC++コンパイラは入ってるけど、PerlやRubyやPythonは入ってない...とか悲しすぎます。
今回紹介する"tinyxmlrpc"はそんな、小さい様で大きな問題を解決出来るかもしれないライブラリです。
mattn's tinyxmlrpc at master - GitHub

tiny xmlrpc library written in C++

http://github.com/mattn/tinyxmlrpc/tree/master
今githubが不調でpushが反映されていません。リポジトリからのcloneは出来る様です。
ソースはC++で書いてます。WindowsとUNIX(Linuxでだけ動作確認)で動作します。

機能としては、STLのvectorとかmapとかに親和性高くなる様になっています。内部ではlibxmlでパースしていてcurlで通信しています。
小ささをウリにしていますが、libxmlとcurlのリンクは必須です。
サンプルコードを以下に示します。
#include "tinyxmlrpc.h"
#include <iostream>

class vargs {
private:
    tinyxmlrpc::value::Array args_;
public:
    vargs& operator <<(tinyxmlrpc::value arg) {
        args_.push_back(arg);
        return *this;
    }
    tinyxmlrpc::value::Array& list() {
        return this->args_;
    }
    vargs& first() {
        this->args_.clear();
        return *this;
    }
};

int main(int argc, char* argv[]) {
#ifdef _WIN32
    WSAData wsadata;
    WSAStartup(MAKEWORD(2,0), &wsadata);
#endif

    if (argc != 4) return -1;

    std::string endpoint = argv[1];
    std::string user = argv[2];
    std::string pass = argv[3];

    try {
        tinyxmlrpc::value::Array req;
        tinyxmlrpc::value res;
        tinyxmlrpc::value::Struct entry;
        vargs args;

        res = tinyxmlrpc::call(endpoint,
                "metaWeblog.getRecentPosts",
                (args.first() << "1" << user << pass << 3 << true).list());
        if (!tinyxmlrpc::failed(res)) {
            for(int n = 0; n < res.size(); n++) {
                std::vector<std::string> members = res[n].listMembers();
                std::vector<std::string>::const_iterator it;
                std::cout << "{" << std::endl;
                for(it = members.begin(); it != members.end(); it++) {
                    std::string val = res[n][*it].to_str();
                    std::cout << "  " << it->c_str() << "=" << val.c_str() << std::endl;
                }
                std::cout << "}" << std::endl;
            }
        } else {
            std::cerr << res << std::endl;
        }

        entry["title"] = "hasegawaにいじめられた";
        entry["description"] = "今日は、wassrでhasegawaにいじめられた。悲しかった。";
        entry["dateCreated"] = "";
        res = tinyxmlrpc::call(
                endpoint,
                "metaWeblog.newPost",
                (args.first() << "1" << user << pass << entry << 1).list());
        if (!tinyxmlrpc::failed(res)) {
            std::cout << "result:" << res << std::endl;
        } else 
            std::cerr << res << std::endl;
    } catch(tinyxmlrpc::value::Exception& e) {
        std::cerr << e.message << std::endl;
    }

    return 0;
}

これを使ってコマンドラインから引数として、MetaWeblogをサポートしているブログのエンドポイントURLユーザパスワードを指定すると、「hasegawaにいじめられた」というエントリがポストされます。
タイトルや本文に、特に意味はありません。すごいですね!


ライセンスはまだ決めてませんが、今のところBSDを予定しています。使ってみて下さい。
コードはgithubにあるので、patchウェルカムです。
今後は、この記事の為だけに仕上げた疑いのある汚いコードを綺麗にしていくつもりです。

参考文献:C++で軽量Webサーバ書いた。
Posted at by




ReverseHttp面白いですね。
ReverseHttp

Tunnel HTTP over HTTP, in a structured, controllable, securable way. Let programs claim part of URL space, and serve HTTP, all by using an ordinary HTTP client library.

http://www.reversehttp.net/
ただ勘違いされやすいのが「何がReverseなの」という部分。通常ブラウザからリクエストが送信され、それに対する応答がサーバから返されます。ReverseHttpはサーバで何かアクションが起きた場合に、ブラウザ側がその通知を受信する...なんて事が出来るプロトコルです。仕組みはcometというlong pollに似た仕組みで、サイトのdemoを観るとなんなく理解出来るかと思います。
例えば何が出来るのか...

ローカルPC内で動作するファイアウォール内のwebアプリを外部に公開する

rubyにhookoutというライブラリがあり、これを使用するとrackアプリがさも外部に公開されているかの様に振舞う事が出来ます。
paulj's hookout at master - GitHub

Expose Ruby applications to the web via ReverseHTTP

http://github.com/paulj/hookout/tree/master
グローバルIPが無くても、webアプリを公開出来るなんて素晴らしい!
なお、ReverseHttpはプロトコルですのでhookout以外にも同様のソフトウェアはあります。例えばmiyagawaさんが書いたAnyEvent::ReverseHttpに含まれるeg/proxy.plを使うとローカルPC内のwebアプリを外部に公開する事が出来ます。

外部で起きたアクションをローカルPCに通知させる

例えば、はてなブックマークで自分のサイトがブックマークされた瞬間にデスクトップPCが反応したらどうしますか?
スターを付けに行きませんか(笑)?ReverseHttpを使えば出来るのです。


今日はこの「はてなブックマーク通知」をやってみたいと思います。
使う材料は以下の通り。
  • hookout : 上記で紹介したrackアプリを公開するライブラリ
  • sinatra : ruby製webアプリケーションフレームワーク
  • ruby_gntp : snakaさん作のruby用Growl For Windowsインタフェース
こんだけ。
上記のサイトからhookoutを取得してインストールし、以下のsinatraアプリケーションを作成します。
my-hatebu-growler.rb
require 'rubygems'
require 'sinatra'
require 'ruby_gntp'
  
growl = GNTP.new
growl.register({
  :app_name => "はてブ",
  :notifies => [{
   :name     => "hatenabookmark",
    :enabled  => true,
  }]
})

post '/' do
  return "ng" if params[:status] !~ /add|update/
  user = params[:username]
  text = "#{params[:comment]}\r\r#{params[:title]}\r#{params[:url]}"
  icon = "http://www.hatena.ne.jp/users/#{user[0,2]}/#{user}/profile.gif"
  p params[:status]
  growl.notify({
    :name  => "hatenabookmark",
    :title => user,
    :text  => text,
    :icon  => icon,
  })
  'ok'
end
config.ru
require 'my-hatebu-growler'
set :run, false
run Sinatra::Application
これを以下の様に起動します。
hookout -a http:/www.reversehttp.net/reversehttp -n my-hatebu-growler-application -R config.ru start

my-hatebu-growler-applicationの部分は適当な物に変えて下さい。

起動すると以下の様に出力されます。
Bound to location http://my-hatebu-growler-application.www.reversehttp.net/
このURLを、はてなブックマークにwebhook登録します。
hatebu-webhook
あとは、じっとブクマされるのを待ちます。










hatebu-growler
デタ━━━゚(∀)゚━━━!!

秋の夜長に、こんなツールお一つどうでしょうか。


追記1
HTTP::Engine::Interface::ReverseHTTPもあるよとmiyagwawaさんに教えてもらいました。
hookout for HTTP::Engineらしいです。

追記2
例では分かり易くする為にwebhook APIのキー認証を省いていますが、本当はちゃんと判定する必要があります。

追記3
PerlでHTTP::Engine::Interface::ReverseHTTPを使ってみた。ネットワークGrowlにはアイコンが使える仕組みがないのが残念。
Posted at by



2009/08/16


furyu-teiさんの記事でXSLTの場合はendpointを変えないといけない事が分かった。
AmazonのProduct Advertising API認証プロキシ(REST版・GAE用)ソース

XSLTを使用する場合(Styleオプション指定時)、http://webservices.amazon.co.jp/onca/xmlやhttp://ecs.amazonaws.jp/onca/xmlで指定すると認証エラーに。専用のエンドポイント(http://xml-jp.amznxslt.com/onca/xml)の指定が必要らしい。

http://d.hatena.ne.jp/furyu-tei/20090703/paproxy
お陰様で動くようになりました。
XSLとjQuery/HTMLだけで作る、amazon最速検索
Posted at by



2009/08/05


先日「jjencodeをApacheのmod_ext_filterに仕込む」という記事でhasegawaさんのjjencodeをgoogle chromeなんかで使われているjavascriptエンジン"v8"で動かしてみたのですが、あまりに遅いですし、一回javascriptエンジンが走ってしまうという事がボトルネックに繋がっているんだ...という勝手な推測の元、「jjencodeをv8に依存しない形でc++に移植しよう」と思い始めたのがこの記事をポストする30分前。今出来上がりました。テストしながら書いてます。
まず、オリジナルのまま関数で移植しました。ふつーのC++のコードです。
#include <sstream>
#include <iostream>
#include <string>

using namespace std;

string jjencode(string gv, string& text) {
  stringstream r, s;
  const char* b[] = { "___", "__$", "_$_", "_$$", "$__", "$_$", "$$_", "$$$", "$___", "$__$", "$_$_", "$_$$", "$$__", "$$_$", "$$$_", "$$$$" };

  for (int i = 0; i < text.size(); i++) {
    char n = text.at( i );
    if (n == 0x22 || n == 0x5c) {
      s << "\\\\\\" << dec << n;
    } else if ((0x20 <= n && n <= 0x2f) || (0x3A <= n == 0x40) || (0x5b <= n && n <= 0x60) || (0x7b <= n && n <= 0x7f)) {
      s << dec << n;
    } else if ((0x30 <= n && n <= 0x39) || (0x61 <= n && n <= 0x66)) {
      if (!s.str().empty()) r << "\"" << s.str() << "\"+";
      r << gv << "." << b[ n < 0x40 ? n - 0x30 : n - 0x57 ] << "+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x6c) { // 'l'
      if (!s.str().empty()) r << "\"" << s.str() << "\"+";
      r << "(![]+\"\")[" << gv << "._$_]+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x6f) { // 'o'
      if (!s.str().empty()) r << "\"" << s.str() << "\"+";
      r << gv << "._$+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x74) { // 'u'
      if( !s.str().empty() ) r << "\"" << s.str() << "\"+";
      r << gv << ".__+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x75) { // 'u'
      if( !s.str().empty() ) r << "\"" << s.str() << "\"+";
      r << gv << "._+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n < 128) {
      if (!s.str().empty()) r << "\"" << s.str();
      else r << "\"";
      r << "\\\\\"+";
      stringstream ss;
      string ns;
      ss << oct << int(n) && ss >> ns;
      string s08i = "01234567";
      for (string::iterator it = ns.begin(); it != ns.end(); it++) {
        if (s08i.find(*it) != string::npos)
          r << gv << "." << b[ *it-'0' ] << "+";
        else
          r << *it;
      }
      s.str("");
      s.clear(stringstream::goodbit);
    }else{
      if (!s.str().empty()) r << "\"" << s;
      else r << "\"";
      r << "\\\\\"+" << gv << "._+";
      stringstream ss;
      string ns;
      ss << hex << int(n) && ss >> ns;
      string s16i = "0123456789";
      string s16a = "abcdef";
      for (string::iterator it = ns.begin(); it != ns.end(); it++) {
        if (s16i.find(tolower(*it)) != string::npos)
          r << gv << "." << b[ *it-'0' ] << "+";
        else
          if (s16a.find(tolower(*it)) != string::npos)
            r << gv << "." << b[ tolower(*it)-'a'+10 ] << "+";
          else
            r << *it;
      }
      s.str("");
      s.clear(stringstream::goodbit);
    }
  }
  if (!s.str().empty()) r << "\"" << s.str() << "\"+";

  s.str("");
  s.clear(stringstream::goodbit);
  s <<
    gv << "=~[];" <<
    gv << "={___:++" << gv +",$$$$:(![]+\"\")["+gv+"],__$:++"+gv+",$_$_:(![]+\"\")["+gv+"],_$_:++" <<
    gv+",$_$$:({}+\"\")["+gv+"],$$_$:("+gv+"["+gv+"]+\"\")["+gv+"],_$$:++"+gv+",$$$_:(!\"\"+\"\")[" <<
    gv+"],$__:++"+gv+",$_$:++"+gv+",$$__:({}+\"\")["+gv+"],$$_:++"+gv+",$$$:++"+gv+",$___:++"+gv+",$__$:++"+gv+"};" <<
    gv+".$_=" <<
    "("+gv+".$_="+gv+"+\"\")["+gv+".$_$]+" <<
    "("+gv+"._$="+gv+".$_["+gv+".__$])+" <<
    "("+gv+".$$=("+gv+".$+\"\")["+gv+".__$])+" <<
    "((!"+gv+")+\"\")["+gv+"._$$]+" <<
    "("+gv+".__="+gv+".$_["+gv+".$$_])+" <<
    "("+gv+".$=(!\"\"+\"\")["+gv+".__$])+" <<
    "("+gv+"._=(!\"\"+\"\")["+gv+"._$_])+" <<
    gv+".$_["+gv+".$_$]+" <<
    gv+".__+" <<
    gv+"._$+" <<
    gv+".$;" <<
    gv+".$$=" <<
    gv+".$+" <<
    "(!\"\"+\"\")["+gv+"._$$]+" <<
    gv+".__+" <<
    gv+"._+" <<
    gv+".$+" <<
    gv+".$$;" <<
    gv+".$=("+gv+".___)["+gv+".$_]["+gv+".$_];" <<
    gv+".$("+gv+".$("+gv+".$$+\"\\\"\"+" << r.str() << "\"\\\"\")())();";

  return s.str();
}

int main(void) {
  string line, content;
  while(!cin.eof()) {
    getline(cin, line);
    content += line;
  }
  cout << jjencode("$", content) << endl;
}
これで前回失敗したjqueryで試してみました。結果は...




30秒以内では戻ってこず断念。
あれーと思って、「そうか。一回結果を格納しちゃってるのがまずいのか!そうなら逐次出力すればいい!」とまた勝手な推測の元、stringstreamに溜め込まずcoutに吐き出す様修正しました。
#include <sstream>
#include <iostream>
#include <string>

using namespace std;

int main(void) {
  const char* b[] = { "___", "__$", "_$_", "_$$", "$__", "$_$", "$$_", "$$$", "$___", "$__$", "$_$_", "$_$$", "$$__", "$$_$", "$$$_", "$$$$" };
  string gv = "$";
  cout <<
    gv << "=~[];" <<
    gv << "={___:++" << gv +",$$$$:(![]+\"\")["+gv+"],__$:++"+gv+",$_$_:(![]+\"\")["+gv+"],_$_:++" <<
    gv+",$_$$:({}+\"\")["+gv+"],$$_$:("+gv+"["+gv+"]+\"\")["+gv+"],_$$:++"+gv+",$$$_:(!\"\"+\"\")[" <<
    gv+"],$__:++"+gv+",$_$:++"+gv+",$$__:({}+\"\")["+gv+"],$$_:++"+gv+",$$$:++"+gv+",$___:++"+gv+",$__$:++"+gv+"};" <<
    gv+".$_=" <<
    "("+gv+".$_="+gv+"+\"\")["+gv+".$_$]+" <<
    "("+gv+"._$="+gv+".$_["+gv+".__$])+" <<
    "("+gv+".$$=("+gv+".$+\"\")["+gv+".__$])+" <<
    "((!"+gv+")+\"\")["+gv+"._$$]+" <<
    "("+gv+".__="+gv+".$_["+gv+".$$_])+" <<
    "("+gv+".$=(!\"\"+\"\")["+gv+".__$])+" <<
    "("+gv+"._=(!\"\"+\"\")["+gv+"._$_])+" <<
    gv+".$_["+gv+".$_$]+" <<
    gv+".__+" <<
    gv+"._$+" <<
    gv+".$;" <<
    gv+".$$=" <<
    gv+".$+" <<
    "(!\"\"+\"\")["+gv+"._$$]+" <<
    gv+".__+" <<
    gv+"._+" <<
    gv+".$+" <<
    gv+".$$;" <<
    gv+".$=("+gv+".___)["+gv+".$_]["+gv+".$_];" <<
    gv+".$("+gv+".$("+gv+".$$+\"\\\"\"+";
  cout.flush();

  stringstream s;
  while(!cin.eof()) {
    char n;
    cin.get(n);

    if (n == 0x22 || n == 0x5c) {
      s << "\\\\\\" << dec << n;
    } else if ((0x20 <= n && n <= 0x2f) || (0x3A <= n == 0x40) || (0x5b <= n && n <= 0x60) || (0x7b <= n && n <= 0x7f)) {
      s << dec << n;
    } else if ((0x30 <= n && n <= 0x39) || (0x61 <= n && n <= 0x66)) {
      if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
      cout << gv << "." << b[ n < 0x40 ? n - 0x30 : n - 0x57 ] << "+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x6c) { // 'l'
      if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
      cout << "(![]+\"\")[" << gv << "._$_]+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x6f) { // 'o'
      if (!s.str().empty()) cout << "\"" << s.str() << "\"+";
      cout << gv << "._$+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x74) { // 'u'
      if( !s.str().empty() ) cout << "\"" << s.str() << "\"+";
      cout << gv << ".__+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n == 0x75) { // 'u'
      if( !s.str().empty() ) cout << "\"" << s.str() << "\"+";
      cout << gv << "._+";
      s.str("");
      s.clear(stringstream::goodbit);
    } else if (n < 128) {
      if (!s.str().empty()) cout << "\"" << s.str();
      else cout << "\"";
      cout << "\\\\\"+";
      stringstream ss;
      string ns;
      ss << oct << int(n) && ss >> ns;
      string s08i = "01234567";
      for (string::iterator it = ns.begin(); it != ns.end(); it++) {
        if (s08i.find(*it) != string::npos)
          cout << gv << "." << b[ *it-'0' ] << "+";
        else
          cout << *it;
      }
      s.str("");
      s.clear(stringstream::goodbit);
    }else{
      if (!s.str().empty()) cout << "\"" << s;
      else cout << "\"";
      cout << "\\\\\"+" << gv << "._+";
      stringstream ss;
      string ns;
      ss << hex << int(n) && ss >> ns;
      string s16i = "0123456789";
      string s16a = "abcdef";
      for (string::iterator it = ns.begin(); it != ns.end(); it++) {
        if (s16i.find(tolower(*it)) != string::npos)
          cout << gv << "." << b[ *it-'0' ] << "+";
        else
          if (s16a.find(tolower(*it)) != string::npos)
            cout << gv << "." << b[ tolower(*it)-'a'+10 ] << "+";
          else
            cout << *it;
      }
      s.str("");
      s.clear(stringstream::goodbit);
    }
    cout.flush();
  }
  if (!s.str().empty()) cout << "\"" << s.str() << "\"+";

  cout << "\"\\\"\")())();" << endl;
}
これで前回失敗したjqueryで試してみました。結果は...




またもや30秒以内では戻ってこず断念。

おかしいな...と思ってmod_ext_filterを通さず実行したらjqueryのファイル全体でも1.5秒程で終了しているので、どうやらmod_ext_filter側が蓄積してしまっているのが原因だと分かりました。 ... orz

まーそうやわなー。あたりまえやなー。勝手な推測しすぎです。

ただ、一度作ったjavascriptファイルをこのツールで変換して静的ファイルとして置く事は出来るはずですので、利用価値がない...とまでは言えないかもしれません。なお、jQueryの場合は$を潰すので変数gvを$以外のものにしないと動きません。
Posted at by




そもそもvimでOOはしんどい。
function! Class_Prototype() dict
  return self
endfunction

function! Class_Override(...) dict
  if a:0 == 0|throw "Invalid Parameter"|endif
  let class = copy(self)
  let class.__NAME__ = a:1
  if type(a:2) == type(class.New)
    let class.New = a:2
  else
    let class.New = self.New
  endif
  let class.Super = self
  return class
endfunction

function! Class_New(...) dict
  let instance = copy(self)
  call remove(instance, "New")
  call remove(instance, "Override")
  let instance.Super = self
  return instance
endfunction

function! Class_ToString() dict
    return self.__NAME__
endfunction

let Object = {
 \ "__NAME__" : "Object",
 \ "Prototype"function("Class_Prototype"),
 \ "Override"function("Class_Override"),
 \ "Super"{},
 \ "New"function("Class_New"),
 \ "ToString"function("Class_ToString")}


function! Human_Sing() dict
  echo self.perfix . "は" . self.name . "。" . self.title . "\n"
  return self
endfunction
function! Human_New(...) dict
  let instance = copy(self)
  let instance.perfix = a:1
  let instance.name = a:2
  let instance.title = a:3
  let instance.Sing = function("Human_Sing")
  return instance
endfunction
let Human = Object.Override("Human", function("Human_New"))

function! Gian_Boxing(who) dict
  echo a:who . "のくせに生意気だぞ!!!\n"
  return self
endfunction
let Gian = Human.Override("Gian", {})
let Gian.Boxing = function("Gian_Boxing")
let Dekisugi = Human.Override("Dekisugi", {})

silent! unlet gian
silent! unlet dekisugi
let gian = Gian.New("俺", "ジャイアン", "ガキ大将")
let dekisugi = Dekisugi.New("僕", "出来杉", "優等生")

call dekisugi.Sing()
call gian.Sing().Boxing("のび太").Boxing("スネ夫")

僕は出来杉。優等生
俺はジャイアン。ガキ大将
のび太のくせに生意気だぞ!!!
スネ夫のくせに生意気だぞ!!!
つかわねー!

参考文献
JSでthisをそのまま返す関数を作っておくと便利?
PHP で引数をそのまま返す関数を作っておくと便利
Posted at by



2009/08/04


smegheadさんがtthttpdのpatchを書いてくれました。感謝!

その後、デフォルトページがCGIの場合に上手く動かないバグを修正しました。
お礼とは言っては何ですが(Windowsなんかイラネかもしれませんが)、smegheadさんが作っておられるC言語で書かれたBTS「starbug1」をWindowsにポーティングしてみました。
以下patch
diff -u starbug1-1.3.01.orig/hook.c starbug1-1.3.01/hook.c
--- starbug1-1.3.01.orig/hook.c 2009-07-12 20:37:55.000000000 +0900
+++ starbug1-1.3.01/hook.c  2009-08-04 10:11:19.125000000 +0900
@@ -4,7 +4,11 @@
 #include <cgic.h>
 #include <dirent.h>
 #include <sys/stat.h>
+#ifndef _WIN32
 #include <dlfcn.h>
+#else
+#include <windows.h>
+#endif
 #include "data.h"
 #include "alloc.h"
 #include "util.h"
@@ -12,6 +16,24 @@
 #include "hook_data.h"
 #include "simple_string.h"
 
+#ifdef _WIN32
+#define dlopen(x,y) (void*)LoadLibrary(x)
+#define dlsym(x,y) (void*)GetProcAddress((HMODULE)x,y)
+#define dlclose(x) FreeLibrary((HMODULE)x)
+const char* dlerror() {
+    static char szMsgBuf[256];
+    FormatMessage(
+            FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+            NULL,
+            GetLastError(),
+            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+            szMsgBuf,
+            sizeof szMsgBuf,
+            NULL);
+    return szMsgBuf;
+}
+#endif
+
 static void put_env_a(char* name, char* value, char* buf)
 {
     sprintf(buf, "%s=%s", name, value);
diff -u starbug1-1.3.01.orig/util.c starbug1-1.3.01/util.c
--- starbug1-1.3.01.orig/util.c 2009-07-25 22:38:03.000000000 +0900
+++ starbug1-1.3.01/util.c  2009-08-04 11:39:35.078125000 +0900
@@ -698,9 +698,21 @@
     char locale_utf8[DEFAULT_LENGTH];
     sprintf(locale_utf8, "%s.UTF-8", locale);
     d("locale: %s\n", locale_utf8);
+#ifndef _WIN32
     setenv("LANG", locale_utf8, 1); /* FreeBSD(さくらインターネット)でsetlocaleが動作しない場合があったため、環境変数を書き換える。 */
+#else
+    {
+        char envstr[256];
+        strcpy(envstr, "LANG=");
+        strcat(envstr, locale_utf8);
+        putenv(envstr);
+    }
+#endif
     d("setlocale: %s\n", setlocale(LC_ALL, locale_utf8));
     d("bindtextdomain: %s\n", bindtextdomain("starbug1", "locale"));
     d("textdomain: %s\n", textdomain("starbug1"));
+#ifdef _WIN32
+    bind_textdomain_codeset("starbug1", "utf-8");
+#endif
 }
 /* vim: set ts=4 sw=4 sts=4 expandtab fenc=utf-8: */
ビルドに必要なツール類はgnuwin32なんかで取得して下さい。
見事起動出来ました。
starbug1-win32
ありがとうございました。
Posted at by



2009/08/03





以下はてなスターの引用に使う時の単語集
このド変態めが

ウホッ!

誰コレ!

なんでamachang無いの?

俺も入れろ

Posted at by



2009/08/01


普段はあまりブラウザを立ち上げてなくて、vimでコーディングしてる時なんかに調べたい事があればw3mを使っています。
ただ良いコンテンツを見つけて、「うむ。はてブしたい!」と思った時にイチイチFirefox起動するのも面倒臭かったので、w3mから直接ブックマーク出来る様にした。
まずは、perlからはてなブックマークする仕組み。これはAtom APIを使った単純な物。 #!/usr/bin/perl
use warnings;
use strict;

use Config::Pit;
use XML::Atom::Client;
use XML::Atom::Entry;
use XML::Atom::Link;

my $url = shift;
print "\n$url\n";
print "はてなブックマーク: ";
my $summary = <STDIN>;
my $config  = pit_get( "b.hatena.ne.jp", require => {
  "username" => "your username in b.hatena.ne.jp",
  "password" => "your password in b.hatena.ne.jp",
});

my $entry = XML::Atom::Entry->new;
my $link  = XML::Atom::Link->new;
$link->rel('related');
$link->type('text/html');
$link->href($url);
$entry->add_link($link);
$entry->summary($summary);

my $client = XML::Atom::Client->new;
$client->username( $config->{username} );
$client->password( $config->{password} );
my $edit_uri = $client->createEntry( "http://b.hatena.ne.jp/atom/post", $entry )
  or warn $client->errstr;
Config::Pitを使ってる所と、はてブコメントを入力している部分がちょっと違うだけ。
このスクリプト "hb.pl" を $HOME/.w3m/hb.pl として置き、$HOME/.w3m/keymap に以下の行を追加する。 keymap m EXTERN "/usr/bin/env perl ~/.w3m/hb.pl '%s'"
これでw3mから"m"をタイプすれば、コメント入力が表示され http://mattn.kaoriya.net/
はてなブックマーク: [これはすごい]よーわからんけどね!
確定させればブックマーク出来る。中止は CTRL-C ね。

快適快適。
Posted at by



2009/07/30


こまったねー。
例えば、divにclickとdblclickを両方割り当てた場合、「トンッ、トントン!」とクリックとダブルクリックを発生させた場合
  • Firefox/Google Chromeだと「トントン」で2クリックが発生
  • IEだと「トントン」では1クリックしか発生しない
なんて動きになってる。以下検証に使ったコード。
<html>
<head>
<script type="text/javascript">
(function() {
  var reg = function(e, t, f) {
    if (window.attachEvent)
      e.attachEvent('on'+t, f);
    else
      e.addEventListener(t, f, false);
  };

  reg(window, 'load', function(e) {
    var c1 = 0, c2 = 0;
    var panel = document.getElementById('click-panel');

    var updateStatus = function() {
      panel.innerHTML = "click:" + c1 + " dblclick:" + c2;
    };

    reg(panel, 'click', function() { c1++; updateStatus() });
    reg(panel, 'dblclick', function() { c2++; updateStatus() });
    reg(panel, 'mouseout', function() { c1 = c2 = 0; updateStatus() });
    updateStatus();
  });
})();
</script>
</head>
<body>
<div id="click-panel" style="background: blue; width: 600; height: 300"></div>
<body>
</html>
例えば、図を描きたい場合にクリックで多角形を描き、ダブルクリックで完了なんてユーザインタフェースは良くある話。
上の例で言えば
  • Firefox/Google Chromeは「click:3 dblclick:1」
  • IEだとは「click:2 dblclick:1」
なにそれ。前回クリックした時の(new Date).getTime()覚えておいて一定間隔以上でクリック発生...なんてコード、簡単だけど書きたくないよ。(ノД`)ウワーン
Posted at by




書いたといっても結構前からあったのですが、いらん所を削ぎ落として軽量Webサーバとして仕立て上げました。
軽量とは言えど、CGIを使って結構色々動きます。
例えば、ソースアーカイブを解凍したらCGIがあって、apacheから見える場所にコピーして...とか面倒くさかったりしますよね。

おれは今すぐWebサーバを起動したいんだ!そして今いるディレクトリのファイルをWebサーバからサーブしたいんだー!

って事ないですか?blogソフトウェアをダウンロードして今すぐ試したいけど、apacheインストールされてなかった...とか悲しすぎます。
今回紹介する"tinytinyhttpd"(tthttpd)はそんな、小さい様で大きな問題を解決出来るかもしれないソフトウェアです。
mattn's tinytinyhttpd at master - GitHub

tiny tiny httpd

http://github.com/mattn/tinytinyhttpd/tree/master
リンク先に、動いてる様子のキャプチャがあります。
ソースはC++で書いてます。WindowsとUNIX(Linuxでだけ動作確認)で動作します。

機能としては、マルチスレッドサーバ、カスタマイズ、ディレクトリスティング、CGI起動が可能です。
小ささをウリにしているのでインストールとか、レジストリとか、面倒くさいものは要りません。exeファイルだけあれば起動します。
一応、設定ファイルの記述も可能で [global]
port=8080
root=c:/temp/mtos
indexpages=index.html,index.php
charset=cp932

[mime/types]
cgi=@c:/strawberry/perl/bin/perl.exe
php=@c:/progra~1/php/php-cgi.exe
という設定ファイルを"-c"オプションで指定する事も出来ます。デフォルトではポート8080番でカレントディレクトリをドキュメントルートとし、phpとperlが適当なパスでCGI起動される様になっています(パスが違う場合は設定ファイルから変更出来ますし、拡張子rbに対してrubyを追加する事も出来ます)。
私が試した限りですが、以下のソフトウェアの動作が確認出来ています。
  • MTOS(Movable Type Open Source) (perl)
  • WordPress (php)
  • blogn Plus (php)
  • tDiary (ruby)
  • PukiWiki (php)
  • NucreusCMS (php)
  • blosxom (perl)
などなど。CGIで動くならば大体動きます。

ライセンスはBSDライセンスとします。使ってみて下さい。
コードはgithubにあるので、patchウェルカムです。
今後は、コードのブラッシュアップと、拡張なんかを考えて行きたいなーと思ってます。
Posted at by



2009/07/23


miyagawaさんのAnyEvent::Twitter::Streamを使って「NullPointerException」をtrackして「Ga!!」を返信するスクリプトを書いてみた。trackにマルチバイトが使えないのが残念。「がっ!!」にしても良いけど、出来れば欧米の方達にも反応したい。
#!/usr/bin/perl
use strict;
use warnings;
use Config::Pit;
use AnyEvent::Twitter::Stream;
use Net::Twitter::Lite;

binmode STDOUT, ":utf8";

my $config = pit_get("nullpo-ga", require => {
        "username" => "your username on twitter.com",
        "password" => "your password on twitter.com"
    });

my $nt = Net::Twitter::Lite->new(
    username => $config->{username},
    password => $config->{password},
);

my $done = AnyEvent->condvar;
my $streamer = AnyEvent::Twitter::Stream->new(
    username => $config->{username},
    password => $config->{password},
    method   => "track",
    ( track => 'NullPointerException' ),
    on_tweet => sub {
        my $tweet = shift;
        print "$tweet->{user}{screen_name}: $tweet->{text}\n";
        eval { $nt->update("\@$tweet->{user}{screen_name} Ga!!") };
    },
    on_error => sub {
        my $error = shift;
        warn "ERROR: $error";
        $done->send;
    },
    on_eof   => sub {
        $done->send;
    },
);

$done->recv;


追記
miyagawaさんから返信のupdateでブロックしちゃうのでAnyEvent::Twitter使えば良いと教えて貰いました。
#!/usr/bin/perl
use strict;
use warnings;
use Config::Pit;
use AnyEvent::Twitter;
use AnyEvent::Twitter::Stream;

binmode STDOUT, ":utf8";

my $config = pit_get("nullpo-ga", require => {
        "username" => "your username on twitter.com",
        "password" => "your password on twitter.com"
    }
);

my $done = AnyEvent->condvar;

my $twitty = AnyEvent::Twitter->new(
    username => $config->{username},
    password => $config->{password},
);

my $streamer = AnyEvent::Twitter::Stream->new(
    username => $config->{username},
    password => $config->{password},
    method   => "track",
    ( track => 'NullPointerException' ),
    on_tweet => sub {
        my $tweet = shift;
        print "$tweet->{user}{screen_name}: $tweet->{text}\n";
        $twitty->update_status("\@$tweet->{user}{screen_name} Ga!!", sub {
                my ($twitty, $status, $js_status, $error) = @_;
                if (defined $error) {
                    warn "ERROR: $error";
                }
            }
        );
    },
    on_error => sub {
        my $error = shift;
        warn "ERROR: $error";
        $done->send;
    },
    on_eof   => sub {
        $done->send;
    },
);

$done->recv;
Posted at by




MOONGIFTでKeepNoteってのを知った。
MOONGIFT: » Evernoteのような個人用スクラップブック「KeepNote」:オープンソースを毎日紹介

今回紹介するオープンソース・ソフトウェアはKeepNote、マルチプラットフォームで動作するメモアプリケーションだ。

http://www.moongift.jp/2009/07/keepnote/
試してみた。WYSIWYGエディタでなかなか良い。
keepnote
MOONGIFTでは

GTKとあって、Windows上での動作がちょっと不安定な気もする。マルチプラットフォームで動作するメモ環境に興味のある方は試してみよう。

と書かれていたけど、少し手を加えるだけで結構使い勝手が良くなった。
以下、WindowsでKeepNoteを使う場合のフリカケ。

日付表示でロケールを使わない

そのままだと、一覧に表示される作成日付が「%p」というAM/PMをロケールに直した物が使われるが、utf-8なGTKにも関わらずWindowsなのでシフトJISが使われてしまう。設定画面で、AM/PM付き12時間表記「%I:%M %p」から24時間表記「%H:%M」に直す。
ちなみに、もしソースからコンパイルされたのであれば g_locale_to_utf8 という関数で utf-8 に直すようなpatchを書くといい。

入力モジュールを追加する

KeepNoteのWindows版にはimmoduleというGTK上での入力機構モジュールが同根されていない。入力出来ない訳ではないが OverTheSpot な入力は出来ない。そこでここからGTKのランタイムを持ってきて中にある lib/gtk-2.0/2.10.0/immodules/ をKeepNoteにコピーする。これで完了。

この2つだけでもグンと使いやすくなった。

このKeepNote、ページ自身はHTMLで出力されるのでノート出力先フォルダをDropboxにしておけば、もうEvernoteですね。
Linux版もあるので、試してみよう。
Posted at by



2009/07/21


jjencode素晴らしいですね。記号だけでjavascriptが実行出来てしまいます。
何かに使えないかなーと考えて、apacheのmod_ext_filterを使ってjavascriptの難読サーブをやってみようかと思います。
javascriptエンジンとして使うのは、v8。本当は全部Cで書けば速いのでしょうがメンドクサイのでやりません。
以下フィルタのソース
crypt-js.cc
#include <v8.h>
#include <string>
#include <iostream>

static std::string&
replace_string(std::string& str, const std::string from, const std::string dest) {
    std::string::size_type n, nb = 0;
    while((n = str.find(from, nb)) != std::string::npos) {
        str.replace(n, from.size(), dest);
        nb = n + dest.size();
    }
    return str;
}

v8::Handle<v8::Value> Print(const v8::Arguments& args) {
  bool first = true;
  for (int i = 0; i < args.Length(); i++) {
    v8::HandleScope handle_scope;
    if (first) {
      first = false;
    } else {
      printf(" ");
    }
    v8::String::Utf8Value str(args[i]);
    const char* cstr = *str ? *str : "";
    printf("%s", cstr);
  }
  printf("\n");
  return v8::Undefined();
}

int main(int argc, char* argv[]) {
  v8::V8::SetFlagsFromCommandLine(&argc, argv, true);
  v8::HandleScope handle_scope;
  v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
  global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print));
  v8::Handle<v8::Context> context = v8::Context::New(NULL, global);
  v8::Context::Scope context_scope(context);

  std::string line, content;
  while(!std::cin.eof()) {
    std::getline(std::cin, line);
    content += line;
  }
  replace_string(content, "\\", "\\\\");
  replace_string(content, "'", "\\'");
  line = "var text = '";
  line += content;
  line += "';";
  line +=
    "var r=\"\";"
    "var n;"
    "var t;"
    "var b=[ \"___\", \"__$\", \"_$_\", \"_$$\", \"$__\", \"$_$\", \"$$_\", \"$$$\", \"$___\", \"$__$\", \"$_$_\", \"$_$$\", \"$$__\", \"$$_$\", \"$$$_\", \"$$$$\", ];"
    "for( var i = 0; i < text.length; i++ ){"
    "    n = text.charCodeAt( i );"
    "    if( n < 128 ){"
    "        r += \"\\\"\\\\\\\\\\\"+\" + n.toString( 8 ).replace( /[0-7]/g, function(c){ return \"$.\"+b[ c ]+\"+\" } );"
    "    }else{"
    "        r += \"\\\"\\\\\\\\\\\"+$._+\" + n.toString(16).replace( /[0-9a-f]/gi, function(c){ return \"$.\"+b[parseInt(c,16)]+\"+\"} );"
    "    }"
    "}"
    "print("
    "\"$=~[];$={___:++$,$$$$:(![]+\\\"\\\")[$],__$:++$,$_$_:(![]+\\\"\\\")[$],_$_:++$,$_$$:({}+\\\"\\\")[$],$$_$:($[$]+\\\"\\\")[$],_$$:++$,$$$_:(!\\\"\\\"+\\\"\\\")[$],$__:++$,$_$:++$,$$__:({}+\\\"\\\")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};\"+"
    "\"$.$_=\"+"
    "\"($.$_=$+\\\"\\\")[$.$_$]+\"+"
    "\"($._$=$.$_[$.__$])+\"+"
    "\"($.$$=($.$+\\\"\\\")[$.__$])+\"+"
    "\"((!$)+\\\"\\\")[$._$$]+\"+"
    "\"($.__=$.$_[$.$$_])+\"+"
    "\"($.$=(!\\\"\\\"+\\\"\\\")[$.__$])+\"+"
    "\"($._=(!\\\"\\\"+\\\"\\\")[$._$_])+\"+"
    "\"$.$_[$.$_$]+\"+"
    "\"$.__+\"+"
    "\"$._$+\"+"
    "\"$.$;\"+"
    "\"$.$$=\"+"
    "\"$.$+\"+"
    "\"(!\\\"\\\"+\\\"\\\")[$._$$]+\"+"
    "\"$.__+\"+"
    "\"$._+\"+"
    "\"$.$+\"+"
    "\"$.$$;\"+"
    "\"$.$=($.___)[$.$_][$.$_];\"+"
    "\"$.$($.$($.$$+\\\"\\\\\\\"\\\"+\" + r + \"\\\"\\\\\\\"\\\")())();\");";
  v8::Handle<v8::String> source = v8::String::New(line.c_str(), line.size());
  v8::TryCatch try_catch;
  v8::Handle<v8::Script> script = v8::Script::Compile(source, v8::String::New(""));
  v8::Handle<v8::Value> result = script->Run();
  return 0;
}

エスケープして変数textに入れて、残りはjjencodeそのままです。
これをhttpd.confで以下の様に設定します。 LoadModule ext_filter_module modules/mod_ext_filter.so
ExtFilterDefine crypt_js mode=output intype=application/x-javascript cmd="/path/to/crypt-js"
出来ました。簡単ですね。
これで以下のjavascriptをダウンロードしてみます。
window.alert('hasegawa!');
コマンドラインから...
curl http://localhost:8080/filter/foo.js
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.__$+$.$_$+$.__$+"\\"+$.__$+$.$_$+$.$$_+"\\"+$.__$+$.$__+$.$__+"\\"+$.__$+$.$_$+$.$$$+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.$_$+$.$$_+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$_$+$.$__+"\\"+$.__$+$.$__+$.$_$+"\\"+$.__$+$.$$_+$._$_+"\\"+$.__$+$.$$_+$.$__+"\\"+$.$_$+$.___+"\\"+$.$__+$.$$$+"\\"+$.__$+$.$_$+$.___+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$$_+$._$$+"\\"+$.__$+$.$__+$.$_$+"\\"+$.__$+$.$__+$.$$$+"\\"+$.__$+$.$__+$.__$+"\\"+$.__$+$.$$_+$.$$$+"\\"+$.__$+$.$__+$.__$+"\\"+$.$__+$.__$+"\\"+$.$__+$.$$$+"\\"+$.$_$+$.__$+"\\"+$.$$$+$._$$+"\"")())();
おぉ!
HTMLから実行してみました。
hasegawa-crypt-js
おぉ!

これいいね!とか思ってjQueryでやってみたら、30秒以上は戻って来ませんでした... orz
Posted at by




別にEditor's Warを起こそうって訳では無いです。
だたvimが弱々しく読める文章だったので補足させて頂きたい。
Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。

Ctr キーは1ストロークには入りません!

普段 Emacs を使うとき、どう考えても「修飾キー + 文字キーの組み合わせは一発操作」という感覚でしかありません。

「Ctr + Alt + s」とか、修飾キーが複数であっても、同時押しであれば、僕の中では1ストロークです。

http://d.hatena.ne.jp/tomoya/20090715/1247590638
慣れればそうかもしれませんが、モディファイアキーを使うには両手、もしくは指が2本要ります。片手で済むならばもう片方の手で鼻をほじる事だって出来るんです。なんてすばらしい。

Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。

一発で何んでもできるっていうところが重要で、とりあえず、気に入ったコマンドはキーバインドを割り当てます*1。これ常識。

http://d.hatena.ne.jp/tomoya/20090715/1247590638
vimもしますよ。人によってはストロークではなくてモディファイアキーを使った物にマップする人もいます。

Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。

何ならキーバインド定義用のマイナーモードとか作って、オンオフを切り替えることで、キーバインドを自由に入れ替えて使う事もできます。

http://d.hatena.ne.jp/tomoya/20090715/1247590638
それもvimで出来ますし、無ければ作れます。

Emacs の人と Vim の人のキーストロークの数え方の違い。 - 日々、とんは語る。

そういった、キー操作カスタマイズの柔軟性が Emacs の利点であり、Vim の場合のアレコレする場合はコマンドを打ち込まないとアレコレできないところに、僕は不満を感じるのかもしれないなぁと、ふと思った。

http://d.hatena.ne.jp/tomoya/20090715/1247590638
vimも設定次第ではいくらでも出来ます。キー登録用のスクリプトもやろうと思えば出来ます。
emacsでも次回起動からも有効にしようと思えば .emacs に書くかと思いますが、間違ってますか?

ただしvimmerはしません。なぜなら...

コマンドを打ち込むのが好きだから。

自分だけのvimrcを作る為に、惜しげなく時間を費やす人達なのです。DropBoxなんかのストレージサービスやgithubなんかにvimrcを置いてしまう人達なんです。
ブログなんかでvimrcを晒しちゃう人達なんです。


えっそれemacsユーザも同じ?変態ですね!

mattn the vimmer!
Posted at by



2009/07/16


2009/07/16 追記
AWS認証制限に対応しました。本文中はそのままですが、デモには非公開キーを使用してアクセスするCGIに変更しています。


時代は便利になった物です。
MOONGIFT: » XMLをJSONにするXSLT「xml2json.xslt」:オープンソースを毎日紹介

xml2json.xsltを見ていたらma.la氏のAmazon最速検索を思い出した。あちらはAmazon AWS専用になるだろうが、xml2json.xsltはそれをもっと汎用的なものにしたと考えられるだろう。

http://www.moongift.jp/2009/04/xml2json-xslt/
javascriptとXSLTを使うならば、AWSサーバから見えるサーバを用意しないといけないのですがYahoo! YQLを使えばそれも要りません。
YQLといえばPipesに毛の生えた様な物だと思う方もいらっしゃるかもしれませんが、解析した結果をXMLやJSONで返す事ができJSONならばDOMツリーをJSONで表現してくれます。
例えばgithubで自分が公開したりforkしたりしているプロジェクトの一覧を作る場合、何を使って作りますか?Pipes?CGI?YQLならば簡単です。

続きを読む...

Posted at by



2009/07/15


amachangの記事glooxなんて物があるのを知った。
Gloox で XMPP を書いてみた - IT戦記

けっこうシンプルに書ける

http://d.hatena.ne.jp/amachang/20090601/1243852022

gloox - A portable high-level Jabber/XMPP library for C++

gloox is a rock-solid, full-featured Jabber/XMPP client library, written in C++. It makes writing spec-compliant clients easy and allows for hassle-free integration of Jabber/XMPP functionality into existing applications. gloox is released under the GNU GPL. Commercial licensing and support are available.

http://camaya.net/
確かに簡単そう。って事でWindowsだけれど試した。ソース見ると、なんと既にWindows対応出来ている。すばらしい。ただVisual Studio用のプロジェクトファイルが付いているだけだったので、mingw32からビルドする際には少し小細工が必要。
以下、その手順。
# wget http://camaya.net/download/gloox-1.0-beta7.tar.bz2
# tar xjvf gloox-1.0-beta7.tar.bz2
# cd gloox-1.0-beta7
# vim config.h.win

comment out HAVE_WINDNS_H
// #define HAVE_WINDNS_H 1

# cd src
# gcc -I. -c *.cpp
# ar cr libgloox.a *.o
これでmingw32用のlibgloox.aが出来上がる。そしてソース。glooxを使っておしゃべりするプログラムなので鶏っぽく"glookoo"(ぐるっくー)と名づけた。
必要な物はreadlineとpthread。readlineは出回っている物には--enable-multibyte付きでコンパイルされた物が見当たらなかったので、rubyのmingw32バイナリ等でも使われているココから使わせて頂く。
またpthreadはココからダウンロードして # make clean GCE-inlined
でビルドした物を使う。以下コード。
#include <gloox/client.h>
#include <gloox/connectionlistener.h>
#include <gloox/messagesessionhandler.h>
#include <gloox/messageeventhandler.h>
#include <gloox/messagehandler.h>
#include <gloox/message.h>
#include <gloox/messagesession.h>
#include <pthread.h>
#include <readline/readline.h>

#ifdef _WIN32
#include <windows.h>
#endif

static char* str_to_utf8_alloc(const char* str) {
#ifdef _WIN32
    size_t in_len = strlen(str);
    wchar_t* wcsdata;
    char* mbsdata;
    size_t mbssize, wcssize;

    wcssize = MultiByteToWideChar(GetACP(), 0, str, in_len,  NULL, 0);
    wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
    wcssize = MultiByteToWideChar(GetACP(), 0, str, in_len, wcsdata, wcssize + 1);
    wcsdata[wcssize] = 0;

    mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
    mbsdata = (char*) malloc((mbssize + 1));
    mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
    mbsdata[mbssize] = 0;
    free(wcsdata);
    return mbsdata;
#else
    return strdup(str);
#endif
}

static char* utf8_to_str_alloc(const char* utf8) {
#ifdef _WIN32
    size_t in_len = strlen(utf8);
    wchar_t* wcsdata;
    char* mbsdata;
    size_t mbssize, wcssize;

    wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len,  NULL, 0);
    wcsdata = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t));
    wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, in_len, wcsdata, wcssize + 1);
    wcsdata[wcssize] = 0;

    mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, NULL, 0, NULL, NULL);
    mbsdata = (char*) malloc((mbssize + 1));
    mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsdata, -1, mbsdata, mbssize, NULL, NULL);
    mbsdata[mbssize] = 0;
    free(wcsdata);
    return mbsdata;
#else
    return strdup(utf8);
#endif
}

class Glookoo : public gloox::ConnectionListener, gloox::MessageHandler
{
public:
    Glookoo(const char* server, const char* jid, const char* passwd, const char* user)
            : server_(server), jid_(jid), passwd_(passwd), user_(user) {
        client_ = NULL;
        session_ = NULL;
    }

    virtual ~Glookoo() {
        delete client_;
    }

    virtual void onConnect() {
        displayMessage("=== connected ===");
        session_ = new gloox::MessageSession(client_, gloox::JID(user_));
        session_->registerMessageHandler(this);
    }

    virtual void handleMessage(const gloox::Message& msg, gloox::MessageSession *session) {
        char* message = utf8_to_str_alloc(msg.body().c_str());
        displayMessage(message);
        free(message);
    }

    virtual void onDisconnect(gloox::ConnectionError reason) {
        displayMessage("=== disconnected ===");
        delete session_;
        session_ = NULL;
    }

    virtual bool onTLSConnect(const gloox::CertInfo &info) {
        return true;
    }

    void run() {
        client_ = new gloox::Client(gloox::JID(jid_), passwd_);
        client_->registerConnectionListener(this);
        client_->setServer(server_);
        client_->connect();
    }

    void sendMessage(const char* message) {
        if (session_) {
            char* send_message = str_to_utf8_alloc(message);
            session_->send(send_message, "");
            free(send_message);
        }
    }
    
    void displayMessage(const char* message) {
        int point = rl_point;
        rl_beg_of_line(0, 0);
        rl_redisplay();
        printf("%s\n", message);
        rl_point = point;
        rl_refresh_line(0, 0);
    }

private:
    gloox::Client* client_;
    gloox::MessageSession* session_;
    const char* server_;
    const char* jid_;
    const char* passwd_;
    const char* user_;
};

Glookoo* glookoo = NULL;

void* input_func(void* arg) {
    pthread_detach(pthread_self());
    while (true) {
        rl_callback_read_char();
    }
    return 0;
}

void* gloox_func(void* arg) {
    glookoo->run();
}

void input_handler(void) {
    char* message = rl_copy_text(0, rl_end);
    if (glookoo) glookoo->sendMessage(message);
    free(message);
}

int  opterr = 1;
int  optind = 1;
int  optopt;
char *optarg;
 
int getopt(int argc, char** argv, char* opts) {
    static int sp = 1;
    register int c;
    register char *cp;

    if(sp == 1) {
        if(optind >= argc ||
                argv[optind][0] != '-' || argv[optind][1] == '\0')
            return(EOF);
        else if(strcmp(argv[optind], "--") == 0) {
            optind++;
            return(EOF);
        }
    }
    optopt = c = argv[optind][sp];
    if(c == ':' || (cp=strchr(opts, c)) == NULL) {
        if(argv[optind][++sp] == '\0') {
            optind++;
            sp = 1;
        }
        return('?');
    }
    if(*++cp == ':') {
        if(argv[optind][sp+1] != '\0')
            optarg = &argv[optind++][sp+1];
        else if(++optind >= argc) {
            sp = 1;
            return('?');
        } else
            optarg = argv[optind++];
        sp = 1;
    } else {
        if(argv[optind][++sp] == '\0') {
            sp = 1;
            optind++;
        }
        optarg = NULL;
    }
    return(c);
}

int main(int argc, char* argv[]) {
    pthread_t pt_input, pt_gloox;
    char* server = "talk.google.com";
    char* jid = NULL;
    char* passwd = NULL;
    char* user = "wassr-bot@wassr.jp";
    int c;

    opterr = 0;
    while ((c = getopt(argc, argv, "j:p:s:u:") != -1)) {
        switch (optopt) {
        case 'j': jid = optarg; break;
        case 'p': passwd = optarg; break;
        case 's': server = optarg; break;
        case 'u': user = optarg; break;
        case '?': break;
        default:
            argc = 0;
            break;
        }
        optarg = NULL;
    }
    
    if (!jid || !passwd) {
        fprintf (stderr, "Usage: glookoo [-s server] [-j jid] [-p passwd] [-u user]\n");
        exit (1);
    }
    
    rl_callback_handler_install("#> ", (void(*)(char*))input_handler);

    glookoo = new Glookoo(server, jid, passwd, user);
    pthread_create(&pt_input, NULL, &input_func, glookoo);
    pthread_create(&pt_gloox, NULL, &gloox_func, glookoo);
    pthread_join(pt_gloox, NULL);
    pthread_join(pt_input, NULL);
    rl_deprep_terminal();
    return 0;
}
相変わらずコメントもなく適当ですね。
mingw32でのビルド手順は以下の通り。
gcc -Ic:/mingw/include/pthread -o glookoo.exe -I. glookoo.cxx libgloox.a c:/mingw/lib/readline.lib -lstdc++ -lws2_32 -lcrypt32 -lsecur32 -lpthreadGCE2
使い方はこんな感じ
Usage: glookoo [-s server] [-j jid] [-p passwd] [-u user]
デフォルトでサーバはGoogle Talk、ユーザはワッサーのボットになっています。
glookoo
マルチスレッドで入力とGlooxを処理していて入力途中の文字列を消されないようにしてあります。
今回はバイナリも置いておきます。
glookoo-0.0.1.tar.gz
よろしければどうぞ。ソースは後でgithubにでも上げておきます。
Posted at by




javascript:(function(e){e[0].value='your-name';e[1].value='#room-1,#room-2,#room-3';e[5].click()})(document.getElementsByTagName('form')[0].elements);
javascriptで動的にform/input要素を作っているのでname属性もid属性も無い。なのでこんなjs。
これでwebhatを開いてブックマークレット一発でログイン。
your-name, room-1, room-2, room-3 等を修正してお使い下さい。
Posted at by



2009/07/11


またさらに動かなくなってしまいました。
(再)ブラウザを全く使わずにustream.tvを楽しむ方法

その後、ustream.tvで何か変更があり、そのままでは使えなくなってしまったのですが、もういっかいチャレンジしたら見れる事が分かりました。

http://mattn.kaoriya.net/web/ustream/20090622220622.htm
今度の変更は、paramに&amp;(アンパサンド)が入ったのと、IE対応による重複したparam要素です。
でも抽出しなきゃ行けない値は変わりません。
#!/bin/bash

FLASHPLAYER=/usr/bin/flashplayer

if [ "x$1" == "x" ]; then
  echo "usage: `basename $0` [channel]"
  exit
fi

TMP=`mktemp /tmp/ustplayer.XXXXXX`
curl -s "http://www.ustream.tv/channel/$1" |\
    /bin/grep "<param name=\"\(flashvars\|movie\)\"" |\
    /bin/sed -e 's/&amp;/\&/g' -e 's/^\s*//g' |\
    /usr/bin/sort |\
    /usr/bin/uniq > $TMP
URL=`cat $TMP | sed -ne 's/^\s*<param name="movie" value=\"\([^\"]\+\)\".*$/\1/p'`
URL=$URL\&`cat $TMP | sed -ne 's/^\s*<param name="flashvars" value=\"\([^\"]\+\)\".*/\1/p'`
rm $TMP

if [ "x$URL" == "x" ]; then
  echo "currently offline?"
  exit
fi
echo playing $URL
$FLASHPLAYER "$URL"
負けません。
もうそろそろ面倒くさくなってきたので、githubに置いて更新して行きます。
mattn's ustplayer at master - GitHub

ustream.tv player using standalone flashplayer

http://github.com/mattn/ustplayer/tree/master
Posted at by



2009/07/09


XML::Simpleだと格納される結果が決まっており、例えば <statuses>
    <status>
        <id>4773580</id>
        <text>kazuhoさんがやってくれました!</text>
        <user>
            <screen_name>mattn</screen_name>
        </user>
    </status>
    <status>
        <id>4773581</id>
        <text>今日のnickは○○提供です。</text>
        <user>
            <screen_name>kazuho</screen_name>
        </user>
    </status>
</statuses>
こんなXMLを以下の様な形にしたい場合がある場合に少し不便だったりします。 ---
id: 4773580
text: kazuhoさんがやってくれました!
screen_name: mattn
---
id: 4773581
text: 今日のnickは○○提供です。
screen_name: kazuho
XML::Simpleを使うと、arrayノード一つにID要素があると勝手にノード扱いになったり、不必要なノードへのアクセスが必要になったりします。以下XML::Simpleのパース結果
---
status:
  4773580:
    text: kazuhoさんがやってくれました!
    user:
      screen_name: mattn
  4773581:
    text: 今日のnickは○○提供です。
    user:
      screen_name: kazuho
こんな場合にはXML::CuteQueriesを使うと便利です。
Paul Miller / XML-CuteQueries - search.cpan.org

A cute little query language for converting XML to Perl

http://search.cpan.org/dist/XML-CuteQueries/
上の例であれば以下のコードで望み通りの形式でパース出来てしまいます。
use strict;
use warnings;
use LWP::Simple;
use XML::CuteQueries;

my $cq = XML::CuteQueries->new;
$cq->parse(get "http://api.wassr.jp/statuses/public_timeline.xml");
my @statuses   = $cq->cute_query("/statuses/*" => {'*' => '', 'user/*' => ''});
use YAML;
warn Dump @statuses;
PODを見ていただければ分かりますが、XPathでクエリ式を書きそれに対するデータシェイプを指定します。例であれば"/statuses/*"にあるノードすべて"*"はデータシェイプのルートに、また"/statuses/*"にある"user/*"(user内の全て)もデータシェイプ内のルートに置くという指定になります。
これ、XML版のWeb::Scraperって感じですかね。便利だわー。

Posted at by



2009/07/07


kazuhoさんがやってくれました。
ずいぶん前からjsonをC++でパース(SAXじゃなくてDOM)するのに小さいライブラリないかなーと思ってました。個人的にはjson-cというのを使ってたのですが、幾らか気に入らない所があったりビルドが少し手間だったりしていました。STLしか使わなくてvectorとかmapで表現されるツリー構造な物が欲しいなぁって思ってたんです。
とあるIRCで昨日、kazuhoさんと「ほしいですよねー」という話から始まって、githubにあるjsonxxとかも物色しながら「いいのないねー」とか言ってたらkazuhoさんが「もすこし綺麗に書けそう」って言い出して朝から本格的に書き始めてついさっき出来上がりました。速いw
名前はpicojson
とても小さく、実装コードだと300数十ステップ程です。しかもヘッダファイルだけなので管理が楽です。

試しにwassrのpublicタイムラインをパースしてみました。
コードはこんな感じ。
curlのコードではなく、jsonのパース部分を見てください。
#include <curl/curl.h>
#include "../picojson.h"

typedef struct {
  char* data;   // response data from server
  size_t size;  // response size of data
} MEMFILE;

MEMFILE*
memfopen() {
  MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
  mf->data = NULL;
  mf->size = 0;
  return mf;
}

void
memfclose(MEMFILE* mf) {
  if (mf->data) free(mf->data);
  free(mf);
}

size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
  MEMFILE* mf = (MEMFILE*) stream;
  int block = size * nmemb;
  if (!mf->data)
    mf->data = (char*) malloc(block);
  else
    mf->data = (char*) realloc(mf->data, mf->size + block);
  if (mf->data) {
    memcpy(mf->data + mf->size, ptr, block);
    mf->size += block;
  }
  return block;
}

char*
memfstrdup(MEMFILE* mf) {
  char* buf = (char*)malloc(mf->size + 1);
  memcpy(buf, mf->data, mf->size);
  buf[mf->size] = 0;
  return buf;
}

using namespace std;
using namespace picojson;

int
main(int argc, char* argv[]) {
  char error[256];

  MEMFILE* mf = memfopen();
  CURL* curl = curl_easy_init();
  curl_easy_setopt(curl, CURLOPT_URL, "http://api.wassr.jp/statuses/public_timeline.json");
  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, &error);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
  if (curl_easy_perform(curl) != CURLE_OK) {
    cerr << error << endl;
  } else {
    value v;
    string err;
    parse(v, mf->data, mf->data + mf->size, &err);
    if (err.empty()) {
      array arr = v.get<array>();
      array::iterator it;
      for (it = arr.begin(); it != arr.end(); it++) {
        object obj = it->get<object>();
        cout << obj["user_login_id"].to_str() << ": " << obj["text"].to_str() << endl;
      }
    } else {
      cerr << err << endl;
    }
  }
  curl_easy_cleanup(curl);
  memfclose(mf);

  return 0;
}
こんなに短いコードでアプリが作れる!
STLに慣れた人ならイメージ沸くかと思います。すばらしい!
こういうのが欲しかったんです。
ただまだ出来上がったばっかりですしバグはあるかもしれません。また高機能にするつもりもないでしょうから使用目的を選ぶのが先決かと思います。
ライセンスはBSDとの事なので、バイナリ配布も可能です。

share - Revision 34226: /lang/cplusplus/picojson/trunk

picojson

http://svn.coderepos.org/share/lang/cplusplus/picojson/trunk/
ありがたや、ありがたや。

追記
kazuhoさんも記事を書いてますんでそちらも...
Kazuho@Cybozu Labs: 今更 C++ で JSON パーサ「picojson」を書いたわけ
http://developer.cybozu.co.jp/kazuho/2009/07/c-json-picojson.html
Posted at by



2009/07/05


最後までC言語で書き直そうかと迷いましたが、結局Perlのままになりました。
スクリーンショットは以下
nhk-news
しくみは、起動時にオートディスカバリでフィード一覧をコンボボックスに展開、コンボボックスが選ばれたらフィードを取得してエントリ一覧をリストに展開、リストがダブルクリックされたらHTMLをスクレイピングしてGtk2::Ex::MPlayerEmbedで再生という単純なもの。プレーヤ部分のツールチップにニュース本文が表示されます。
本当は、fltk2でmplayer埋め込みプレーヤを2年ほど前に作ってあったのでそれを使っても良かったのですが、いかんせんfltkがマイナー過ぎるのでやめました。
mattn's nhk-news-pl at master - GitHub

NHK News Movie Player

http://github.com/mattn/nhk-news-pl/tree/master
ライセンスとか難しい物はありません。ソースは上のリンクにあるのでパクるとか、お好み焼きに付けるとか、勝手にして下さい。
問題ありそうならキャプチャは取り下げる予定です。
Posted at by



2009/06/26


Luaでもやろうと思えば、色んな事が出来ます。先日書いた「LuaでTwitterるわ」を良い例に、Luaでも拡張さえ利用すれば何でも出来るんです。
で、今日は何をしようかと考えて...
Luaでmigemoを使用して、本当に「Lua」で「るわ」が検索出来るかどうかやってみようと思います。
#もう結果が見えていますが、やるんです...
まず、Luaで高度な処理を行うには、拡張モジュールを作る必要があります。
MigemoはC/Migemoを基礎ライブラリとして、ラッパI/Fとして作成します。
まずC/Migemoを、香り屋からsvnで取得して、ビルドします。
#今回は、Windowsでコンパイルしています。
C:¥temp> svn co http://cvs.kaoriya.net/svn/CMigemo/trunk cmigemo
C:¥temp> cd cmigemo
C:¥temp¥cmigemo> cp config.mak config.mk.orig
C:¥temp¥cmigemo> vim config.mak
C:¥temp¥cmigemo> diff config.mk.org config.mk
38,39c38,39
< FILTER_CP932  = qkc -q -u -s
< FILTER_EUCJP  = qkc -q -u -e
---
> #FILTER_CP932 = qkc -q -u -s
> #FILTER_EUCJP = qkc -q -u -e
41,42c41,42
< #FILTER_CP932 = nkf -s
< #FILTER_EUCJP = nkf -e
---
> FILTER_CP932  = nkf -s
> FILTER_EUCJP  = nkf -e

C:¥temp¥cmigemo> vcvars32
Setting environment for using Microsoft Visual C++ tools.
C:¥temp¥cmigemo> nmake msvc
C:¥temp¥cmigemo> nmake msvc-dict

次に、今回作成したluamigemoをビルドします。 C:¥Lua¥luamigemo> nmake 後は以下のコードを実行するまで local migemo = require("migemo")
local rex_pcre = require("rex_pcre")

local mcx = migemo.open("C:/temp/cmigemo/dict/migemo-dict")

--migemo.set(
--  mcx,
--  migemo.DICTID_ROMA2HIRA,
--  "C:/temp/cmigemo/dict/roma2hira.dat")

migemo.set(mcx, migemo.OPINDEX_OR, "|");
migemo.set(mcx, migemo.OPINDEX_NEST_IN, "\\(");
migemo.set(mcx, migemo.OPINDEX_NEST_OUT, "\\)");

function test(subject, query)
  local pattern = migemo.query(mcx, query)
  local match = rex_pcre.match(subject, pattern)
  if match then
    print(subject.." is matched with "..query)
  else
    print(subject.." is not matched with "..query)
  end
end

test("まっつん", "mattun")
test("マッツン", "mattun")
test("マッツン", "mattn")
test("幹事", "kanji")
test("私はマッツンです", "watashiHaMattunDesu")
test("るわ", "lua")

今回は、migemoで取得した正規表現パタンを食わせる為にLrexlibという拡張を使用し、検証しています。
実行結果は まっつん is matched with mattun
マッツン is matched with mattun
マッツン is not matched with mattn
幹事 is matched with kanji
私はマッツンです is matched with watashiHaMattunDesu
るわ is not matched with lua
となり、結果「Lua」では「るわ」は検索出来ない事が分かりました。

ダウンロード:
Posted at by



2009/06/24


これはgithubじゃなくても使えるかも
Gitはトランスポート層が選択出来るのは知っていたけど、まさかこんな書き方出来るとは思ってなかった。
以前、こんな記事書きましたが
github.comへのSSH接続にはホスト名"ssh.github.com"、ポート"443"に接続する様に設定します。※このssh.github.comが味噌です。

github.comへのSSH接続にはホスト名"ssh.github.com"、ポート"443"に接続する様に設定します。 ※このssh.github.comが味噌です。

http://mattn.kaoriya.net/software/20081029172540.htm
これ以下の1行で出来る事が分かりました。
# git clone ssh://git@ssh.github.com:443/my-name/my-repo.git
ssh.github.comに443ポートでSSHプロトコルを喋るよ!っていう指定になります。これでcloneしたワーキングツリーでは、以前書いた記事の様にpullはpublic clone、pushはowner cloneというやり方ではなくowner cloneといてpullしている為 .ssh/config ファイルを修正したり # git remote add origin git@github.com:my-name/my-repo.git
する事もなくいきなりpush出来るになります。
こりゃいいわ。

Posted at by




Freenodeのwebchatインタフェースを叩いて通信し、携帯電話からでもIRC出来るCGI書いた。
欲しい人なんかいるんかなーとか思いながら、作ったのでメッセージの送受信しか出来ません。 nick登録もなし、nickの自動割り当てもなしです。
私はこんだけあったら十分なので...。コードはこの辺にあります。
mattn's freenode-mobile-gateway at master - GitHub

IRC gateway for freenode writen in perl.

http://github.com/mattn/freenode-mobile-gateway/tree/master
SoftBank携帯ですが、一応動いてます。欲しい機能があればgithubでforkして下さい。
Posted at by



2009/06/23


以前こんな記事を書いた事がありました。
Big Sky :: ブラウザを全く使わずにustream.tvを楽しむ方法

映像/音声ですが、Linux版に用意されているスタンドアローン版flashplayerを使っています。ネット上にあるFLVもコマンドラインから起動して閲覧出来ます。

http://mattn.kaoriya.net/web/ustream/20071018005843.htm
その後、ustream.tvで何か変更があり、そのままでは使えなくなってしまったのですが、もういっかいチャレンジしたら見れる事が分かりました。
答えは簡単でobjectタグのパラメータ値を全て足せば再生出来るURLになるという事。
たとえばさっきまで見ていたVimMの動画。
<object
    id="viewer"
    name="viewer"
    width="480"
    height="386"
    allowfullscreen="true"
    codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
ここから
    <param name="movie" value="http://cdn1.ustream.tv/swf/4/viewer.137.swf?varnish=true" />
    <param name="flashvars" value="loc=/&cid=19118&channelid=19118&share=false&group=channel19118&imu=medrect&autoResize=false&localid=4398457344a3f82bac4140&varnish=true&vrsl=c.4.158&" />
ここまで
</object>
これを結合して出来上がったURLにflashplayerスタンドアローン版でアクセスすれば良いのです。
これをshellスクリプトにすると以下の様になりました。
#!/bin/bash

GFLASHPLAYER=/usr/bin/flashplayer
#GFLASHPLAYER=mplayer

if [ "x$1" == "x" ]; then
  echo "usage: `basename $0` [channel]"
  exit
fi

TMP=`mktemp /tmp/XXXXXX`
curl -s "http://www.ustream.tv/channel/$1" | grep "<param" > $TMP
URL=''
while read -r line; do
    if [ ! -z $URL ]; then
        URL="$URL&"
    fi
    URL="$URL`echo \"$line\" | sed  's/^.* value=\"\([^\"]\+\)\".*$/\1/'`"
done < $TMP
rm $TMP

if [ "x$URL" == "x" ]; then
  echo "currently offline?"
  exit
fi
echo playing $URL
$GFLASHPLAYER "$URL"
これでまたブラウザが楽になりました。
Posted at by




アナウンスしてなかったですが、先日書いた「VimからFastLadderを扱えるスクリプトFastLadder.vim書いた。」ですが、LivedoorReaderに対応させています。 let g:fastladder_server = 'http://reader.livedoor.com'
とvimrcに書いておくと :FastLadder
でLivedoorReaderのフィードが見れます。
今後はFastLadderやLivedoorReaderの様にフォルダ単位の閲覧が出来る様に改良して行く予定です。
Posted at by



2009/06/22


VCだとビルド出来るらしいけど、strawberry perlとかだとコンパイル出来ない。
まずCoro側のpatch。
diff -ur Coro-5.132.orig/Coro/libcoro/coro.c Coro-5.132/Coro/libcoro/coro.c
--- Coro-5.132.orig/Coro/libcoro/coro.c 2008-11-19 11:50:13.000000000 +0900
+++ Coro-5.132/Coro/libcoro/coro.c  2009-06-19 15:01:16.140625000 +0900
@@ -228,6 +228,9 @@
   #if __CYGWIN__
     ctx->env[7] = (long)((char *)sptr + ssize) - sizeof (long);
     ctx->env[8] = (long)coro_init;
+  #elif defined(__MINGW32__)
+    ctx->env[4] = (int)((unsigned char *)sptr + ssize);
+    ctx->env[5] = (long)coro_init;
   #elif defined(_M_IX86)
     ((_JUMP_BUFFER *)&ctx->env)->Eip   = (long)coro_init;
     ((_JUMP_BUFFER *)&ctx->env)->Esp   = (long)STACK_ADJUST_PTR (sptr, ssize) - sizeof (long);
mingw32のjumpbufは4番目がEipで5番目がEspだったはず。
一応手元で動いてます。
あとこのままでも駄目で、ExtUtils::MakeMakerがgccの時に付けてしまう--image-baseオプションがまずい。
アドレスの作り方が Coro::Event であれば Event という文字列に対して上位4バイト、下位4バイトで分けてunpackとかやってござる。
    if ($GCC) {
        my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
        $dllname =~ /(....)(.{0,4})/;
        my $baseaddr = unpack("n", $1 ^ $2);
        $otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
    }
なので、Coro::Event が使っている Event というモジュールと --image-base がバッティングして起動時にメモリロケーション不正参照のエラーが起きる。
こちらについては、ExtUtils::MakeMakerに含まれる MM_Win32.pm を修正する。最近の gcc は勝手に --image-base 作ってくれるので、そちらに任せる。
--- MM_Win32.pm.orig    2009-06-22 17:48:43.906250000 +0900
+++ MM_Win32.pm 2009-06-22 17:49:05.703125000 +0900
@@ -307,12 +307,12 @@
 # we try to overcome non-relocateable-DLL problems by generating
 #    a (hopefully unique) image-base from the dll's name
 # -- BKS, 10-19-1999
-    if ($GCC) {
-       my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
-       $dllname =~ /(....)(.{0,4})/;
-       my $baseaddr = unpack("n", $1 ^ $2);
-       $otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
-    }
+#    if ($GCC) {
+#      my $dllname = $self->{BASEEXT} . "." . $self->{DLEXT};
+#      $dllname =~ /(....)(.{0,4})/;
+#      my $baseaddr = unpack("n", $1 ^ $2);
+#      $otherldflags .= sprintf("-Wl,--image-base,0x%x0000 ", $baseaddr);
+#    }
 
     push(@m,'
 # This section creates the dynamically loadable $(INST_DYNAMIC)
SYNOPSISの例でもちゃんと動きます。

あとはバグ報告から上手く修正されれば...
Posted at by




IRCのWebChatと言えば、Mibbit.comが有名ですが、Freenode自身がqwebircという、TwistedMootoolsを使ったIRCチャットサーバアプリを持っている事に気づきました。
freenode Web IRC (qwebirc)

Connect to freenode IRC

http://webchat.freenode.net/
Webページへの埋め込みもウィザード付きで簡単です。チャネル#Vim-users.jpであればこんな感じの埋め込みでいけます。

続きを読む...

Posted at by



2009/06/18


WindowsではGrowl For Windowsとそれが使っているプロトコルGNTPにより、Windowsでもアイコンを使ったGrowlアプリケーションの開発が可能になりました。その一つにmiyagawaさんが作ったgithub growlerのGNTP版でもあるyet another github growlerというのも作りました。
これでMac, Windowsでのgithub growlerがある事になるのですが、Linuxにありません。Linuxにはアイコンが表示できてGNTPプロトコルを喋るGrowlシステムがありません。
そこで以前から使っていた、Growlネットワークプロトコル(Growlネットワークプロトコルはアイコンが出せません)をサポートしているmumblesというGrowlシステムを調べて見たところ、内部ではpythonでDBusによるプロセス間通信を行っている事が分かりました。
mumbles-project.org

a plugin driven, modern notification system for Gnome

http://www.mumbles-project.org/
さらにそのDBusインタフェース上ではアイコン表示をサポートしていた為、これは!と思いGitHubのGrowlアプリケーションを作ってみました。
まず、DBusで通信する為のプラグインを作成します。
DBusでメソッドが呼ばれると、MumblesPluginクラスに渡されるMumblesNotifyオブジェクトのalertメソッドを呼び出します。
全体のソースは以下の様になります。
from MumblesPlugin import *
import dbus
import gnomevfs
import os
import urllib

class GithubMumbles(MumblesPlugin):
  plugin_name = 'GithubMumbles'
  dbus_interface = 'com.github.DBus'
  dbus_path = '/com/github/DBus'
  icons = {'github' : 'github.png'}
  __url = None

  def __init__(self, mumbles_notify, session_bus):
    self.signal_config = {
      'Notify': self.Notify,
      'NotifyNum': self.NotifyNum
    }
    MumblesPlugin.__init__(self, mumbles_notify, session_bus)
    self.add_click_handler(self.onClick)

  def NotifyNum(self, num):
    self.__url = 'http://github.com/'
    icon = self.get_icon('github')
    title = 'Github'
    msg = str(num)+' new messages!'
    self.mumbles_notify.alert(self.plugin_name, title, msg, icon)

  def Notify(self, link, author, text):
    self.__url = link
    path = os.path.join(PLUGIN_DIR_USER, 'icons', 'github-%s' % author)
    if os.path.exists(path):
      self.icons[author] = 'github-%s' % author
      icon = self.get_icon(author)
    else:
      icon = self.get_icon('github')
    self.mumbles_notify.alert(self.plugin_name, author, text, icon)

  def onClick(self, widget, event, plugin_name):
    if event.button == 3:
      self.mumbles_notify.close(widget.window)
    else:
      self.open_url(self.__url)

  def open_url(self, url):
    mime_type = gnomevfs.get_mime_type(url)
    application = gnomevfs.mime_get_default_application(mime_type)
    os.system(application[2] + ' "' + url + '" &')
インタフェースはリンク、作者、本文のみとしました。これをegg形式にビルドしてmumblesのpluginフォルダに置くと、上記のインタフェース呼び出しによりGrowlが表示されます。
MumblesPluginにはget_iconメソッドが用意されており、これにはアイコン名称を渡す事になります。実際にはplugin/iconsというフォルダにある名称のファイルが使用されるので、今回の仕組としてはgithubフィードのチェッカースクリプトでアイコンをplugin/iconsフォルダに格納させ、それを使用してプラグイン側が使用するという形になっています。プラグイン側でアイコンを取って来ても良いのですがアイコンをダウンロードしている最中はGrowlが固まってしまう為、今回の様な作りとなっています。
次にチェッカースクリプトですが以下の様なコードになります。 #!/usr/bin/env python

UPDATE_INTERVAL=1000 # 10 minutes
MAX_NOTIFICATIONS = 40
DEBUG = True
##################################################

import os
import sys
import time
import getopt
import rfc822
import calendar
import urllib
import feedparser
import dbus
import dbus.service
from dbus.mainloop.glib import DBusGMainLoop
import gobject
from BeautifulSoup import BeautifulSoup
from pit import Pit

GITHUB_DBUS_INTERFACE = 'com.github.DBus'
GITHUB_DBUS_PATH = '/com/github/DBus'

config = Pit.get('github.com', {'require': {
    'user'  : 'user id on github.com',
    'token' : 'user token on github.com'
    }})

class Usage(Exception):
  def __init__(self, msg=None):
    app = sys.argv[0]
    if msg != 'help':
        self.msg = app+': Invalid options. Try --help for usage details.'
    else:
      self.msg = app+": DBus notifications on new github messages.\n"

class GithubCheck(dbus.service.Object):
  def __init__(self):
    session_bus = dbus.SessionBus()
    bus_name = dbus.service.BusName(GITHUB_DBUS_INTERFACE, bus=session_bus)
    dbus.service.Object.__init__(self, bus_name, GITHUB_DBUS_PATH)

    self.interval = UPDATE_INTERVAL
    self.notifyLimit = MAX_NOTIFICATIONS
    self.debug = DEBUG

    self.lastCheck = None
    self.minInterval = 60000 # 1 minute min refresh interval

    if self.interval < self.minInterval:
      print "Warning: Cannot check github more often than once a minute! Using default of 1 minute."
      self.interval = self.minInterval
    self._check()

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='sss')
  def Notify(self, link, author, text):
    pass

  @dbus.service.signal(dbus_interface=GITHUB_DBUS_INTERFACE, signature='i')
  def NotifyNum(self, num):
    pass

  def _check(self):
    if self.debug:
      if self.lastCheck:
        print "checking feed (newer than %s):" %(self.lastCheck)
      else:
        print "checking feed:"
    try:
      items = feedparser.parse("http://github.com/%s.private.atom/?token=%s" % (config['user'], config['token']))['entries']
    except Exception, e:
      items = []

    if self.lastCheck:
      lastCheck = calendar.timegm(time.localtime(calendar.timegm(rfc822.parsedate(self.lastCheck))))
      for item in items:
        if calendar.timegm(item.published_parsed) < lastCheck:
          items.remove(item)

    self.lastCheck = rfc822.formatdate()
    num_notifications = len(items)

    if num_notifications > MAX_NOTIFICATIONS:
      if self.debug:
        print "%s new entries\n" %(num_notifications)
      self.NotifyNum(num_notifications)
    elif num_notifications < 0:
      if self.debug:
        print "no new entries\n"
    else:
      for item in items:
        path = os.path.join(os.path.expanduser('~'), '.mumbles', 'plugins', 'icons', 'github-%s' % item['author'])
        if not os.path.exists(path):
          html = urllib.urlopen('http://github.com/%s' % item['author']).read()
          soup = BeautifulSoup(html)
          img = soup.findAll('div', {'class':'identity'})[0].find('img')['src']
          img = img.replace("?s=50&", "?s=30&");
          urllib.urlretrieve(img, path)
        self.Notify(item['link'], item['author'], item['title'])
        time.sleep(6)
    gobject.timeout_add(self.interval,self._check)

if __name__ == '__main__':
  DBusGMainLoop(set_as_default=True)
  try:
    try:
      opts, args = getopt.getopt(
        sys.argv[1:], "hp", ["help"])
    except getopt.GetoptError:
      raise Usage()

    for o, a in opts:
      if o in ("-h", "--help"):
        raise Usage('help')
      else:
        raise Usage()
  except Usage, err:
    print >> sys.stderr, err.msg
    sys.exit(2)

  t = GithubCheck()
  try:
    loop = gobject.MainLoop()
    loop.run()
  except KeyboardInterrupt:
    print "githubcheck shut down..."
  except Exception, ex:
    print "Exception in githubcheck: %s" %(ex)
だらだらとしたコードですが、大体分かってもらえるかと思います。pitを使っているので初回起動のみユーザとトークンをエディタで入力する必要があります。トークンはGitHubのダッシュボードにあるRSSアイコンのリンク先URLに含まれています。

実行してしばらくすると以下の様な画面が表示されます。
mumbles-github-growler
これで快適になりました。
github上で全てのソースを公開しています。
mattn's mumbles-github-growler at master - GitHub

github growler using mumbles plugin and checker script.

http://github.com/mattn/mumbles-github-growler/tree/master
よろしければどうぞ。
Posted at by




require "rubygems"
require "dl/import"
module Lib_MSVCRT extend DL::Importable
  LC_CTYPE = 2
  dlload "msvcrt.dll"
  extern "char* setlocale(int, char*)"
end
Lib_MSVCRT::setlocale(Lib_MSVCRT::LC_CTYPE, "")
Posted at by



2009/06/17


使っているreadline.dllはここから持って来ているんだけど、まずこれがマルチバイトありでビルドされていない。なのでマルチバイト文字を入力した後、バックスペース押すと1バイト単位でしか戻らない。
今日はそれを解決してみる。readline-4.3-2-src.zipを持ってきて # unzip readline-4.3-2-src.zip
# cd readline-4.3-2¥win32¥shlib
# copy config.h config.h.orig
# vim config.h
diff -u config.h.orig config.h
--- config.h.orig   2003-04-14 16:04:46.000000000 +0900
+++ config.h    2009-06-17 17:04:07.234375000 +0900
@@ -128,11 +128,9 @@
 /* Define if you have the <varargs.h> header file.  */
 //#define HAVE_VARARGS_H 1
 
-/*
 #define HAVE_WCTYPE_H 1
 #define HAVE_WCHAR_H 1
 #define HAVE_MBSRTOWCS 1
-*/
 /* config.h.bot */
 /* modify settings or make new ones based on what autoconf tells us. */
 
# copy config.h ..
# mingw32-make -f GNUmakefile
とするとreadline.dllが出来上がるのでそれをパスの通った場所に置く。古いreadline.dllをバックアップしておいて入れ替えるのもOK。
次に、id:Constellationさんの記事にある以下のソースをlocale.cとして保存する。
#include <locale.h>
#include "ruby.h"

static VALUE mLocale;

static VALUE
locale_setlocale(obj)
{
  setlocale(LC_CTYPE, "");
#ifdef LC_MESSAGES
  setlocale(LC_MESSAGES, "");
#endif
  return Qnil;
}

void
Init_locale()
{
  mLocale = rb_define_module("Locale");
  rb_define_module_function(mLocale, "setlocale", locale_setlocale, 0);
}
そしてコンパイル # gcc -Ic:/ruby/lib/ruby/1.8/i386-mswin32 -shared -o locale.so locale.c libreadline.a c:/ruby/lib/msvcrt-ruby18.lib ワーニングが出るけど気にしない
出来上がったlocale.soを C:¥ruby¥lib¥ruby¥1.8¥locale.so として配置し、ホームディレクトリ(HOME環境変数を設定していないならばUSERPROFILE変数の位置)に".irbrc"というファイルを作って以下の様に書く。
begin
  require"locale"
  Locale.setlocale
rescue
end
require 'rubygems'
require 'utility_belt'
require 'win32console'
"utility_belt"はこの記事を、win32consoleはエスケープシーケンスを色付けして表示して貰うために...
後はirbを起動すれば、マルチバイト文字でバックスペースしても正しく1文字消えてくれます。

Constellation++

これでようやく、termtterでも正しく日本語が打てる。
なお、Constellationさんの記事にも書かれていますがtime.rb周りで弊害が出る可能性があるので、気を付けて。termtterの場合なら"~/termtter/config"にLocale.setlocaleを書いた方が良いかもしれない。
Posted at by




ついカッとなってやった。今も後悔してない。
昨日作ったGoogleReader.vimがだんだん安定して来て少し飽きたのでFastLadder版を作ってみました。
FastLadder.vim - vimscript for fastladder : vim online

This is vimscript for fastladder (http://fastladder.com/)

FastLadder.vim - vimscript for fastladder : vim online
開発はこの辺でやってます。
mattn's fastladder-vim at master - GitHub

This is vimscript for fastladder

http://github.com/mattn/fastladder-vim/tree/master
起動は :FastLadder
で <c-n> と <c-p> で上下移動、リターンキーで閲覧、qで終了です。一応 <c-i> でブラウザ起動ですがキーは変えるかもしれません。
vimrcとかに g:fastladder_user と g:fastladder_passwd を設定してあれば curl だけで動作します。
画面はこんな感じ。
fastladder-vim1
詳しくは"?"をタイプしてヘルプを見て下さい。
ちなみにg:fastladder_serverを"http://localhost"に設定してローカルでお楽しみ頂く事も出来ますが、LivedoorReaderには対応していません。

mattn the vimmer!
Posted at by



2009/06/15


ついカッとなってやった。今も後悔してない。
Peepの改造としてはじめたcpeepですが、今cpeepが実装している機能くらいならばvimで出来るやん...と思って勢いで書いてみました。
GoogleReader.vim - vimscript for googlereader : vim online

This is vimscript for googlereader (www.google.com/reader/)

http://www.vim.org/scripts/script.php?script_id=2678
開発はこの辺でやってます。
mattn's googlereader-vim at master - GitHub

This is vimscript for googlereader

http://github.com/mattn/googlereader-vim/tree/master
起動は :GoogleReader
で <c-n> と <c-p> で上下移動、リターンキーで閲覧、qで終了です。一応 <c-i> でブラウザ起動ですがキーは変えるかもしれません。
vimrcとかに g:googlereader_email と g:googlereader_passwd を設定してあれば curl だけで動作します。
画面はこんな感じ。
googlereader-vim1
googlereader-vim2
作りかけなので、まだまだです。
えっ?cpeep?何でしたっけそれ...

正直言うと、これがキッカケだったりする。
はてなブックマーク - pekepekesamuraiのブックマーク

だれかEmacsのpeep-mode作ってくれないだろうか。と期待している

http://b.hatena.ne.jp/pekepekesamurai/20090615#bookmark-13902255

mattn the vimmer!
Posted at by



2009/06/14


先日書いた、「コマンドラインからGoogle Readerが使えるPeepを試した」でLinuxでは快適になった。ただWindowsでは動かない。まずWindowsのpython2.6にはcursesのバイナリモジュールが含まれていなかった。これは「Python2.6にはcursesのバイナリが含まれていないので作る」で解決したのだけど、元になっているpdcursesがWindows上ではまともに動かなくて、例えばマルチバイト文字を含んだ複数行をscrl(スクロール)すると、正しくマルチバイト文字の幅を取れていないのか下段の行が上段の行にゴミとなって重なったりした。
Peep自身を触っても良かったのだが修正非量も多い。常に残念だったのでCで書く事にした。APIの呼び出しまわりはPeepを参考にさせて頂き、libxml2やcurl、pdcurses(linuxではncurses)で書き直す事にした。ただWindows上でcursesの動作が変なのは変わらない話なので、scrlを使わない事にした。どうやったかというとスクロールの度にclear()を呼んで全行書き直し。試してみた所、それ程操作感も悪く無かった。また色属性についてもACS_REVERSEを使うと幅計算が間違っているのか1行の幅で反転してくれなかったので、A_BOLDを使う事にした。現状、WindowsでもLinuxでも動作するようになっているけど、C++のstlをふんだんに使っててメモリ効率を何も考えてないソースになっています。
mattn's cpeep at master - GitHub

terminal front-end for Google Reader writen in C

http://github.com/mattn/cpeep/tree/master
cpeep1
cpeep2
まだまだ、全く作りかけなので閲覧しか出来ません。
j,kスクロールと、oで閲覧、qで閉じる、vでブラウザ(現状Windowsは通常使うブラウザ、それ意外はfirefox限定)です。
ちなみにPeepはHTMLをテキストに変換するのにw3mを内部で呼び出していたけど、cpeepでは自前でウンチャラカンチャラやってます。
まだまだ、これからです。

ちなみに今気付いたのですが、PeepのAuthorさんからPeepのcommit bitを付与して頂いている様です。ありがとうございます。何か協力出来る事があればcommitさせて頂きます。

追記
なんかの間違いだった様です。すみません。
Posted at by



2009/06/13


追記
パッチがいるのを忘れていました。
一番下にあります。

バッチファイル。実行すると直接バイナリ配置するので気をつけて。バッチの先頭にあるパスを環境に合わせ修正して下さい。
pythonのソースはオフィシャルから「Python-2.6.2.tar.bz2」を、pdcursesもオフィシャルから「pdcurs34.zip」をダウンロードし"C:¥TEMP¥"に展開しておきます。
@echo off
setlocal

set PYTHON26=c:\Python26

set PYSOURCE=c:\temp\Python-2.6.2
set PDSOURCE=c:\temp\pdcurses

if "%1" == "msvc" goto msvc
if "%1" == "gcc" goto gcc
goto usage

:msvc
set FLAGS=/nologo /LD -DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2
echo building _curses.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\_curses.pyd
echo building _curses_panel.pyd
cl %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c /link %PYTHON26%\libs\python26.lib %PDSOURCE%\win32\pdcurses.lib %PDSOURCE%\win32\panel.lib /out:%PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:gcc
set FLAGS=-DHAVE_CURSES_RESIZE_TERM -DNCURSES_MOUSE_VERSION=2 -Wl,--enable-auto-import -Wl,--export-all -s -shared
echo building _curses.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_cursesmodule.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses.pyd
echo building _curses_panel.pyd
gcc %FLAGS% -I%PYTHON26%\include -I%PDSOURCE% %PYSOURCE%\Modules\_curses_panel.c %PYTHON26%\libs\libpython26.a %PDSOURCE%\win32\pdcurses.a %PDSOURCE%\win32\panel.a -o %PYTHON26%\lib\curses\_curses_panel.pyd
goto end

:usage
echo pycurses_build.bat [msvc or gcc]

:end
endlocal
以下ソースにあてるパッチです。Windows上のsetuptermは必ずエラーで返すコードになっているので呼ばないようにしています。
--- Modules/_cursesmodule.c.orig    2009-06-13 01:30:02.000000000 +0900
+++ Modules/_cursesmodule.c 2009-06-13 01:30:23.000000000 +0900
@@ -2036,6 +2036,7 @@
        }
    }
 
+#ifndef _WIN32
    if (setupterm(termstr,fd,&err) == ERR) {
        char* s = "setupterm: unknown error";
        
@@ -2048,6 +2049,7 @@
        PyErr_SetString(PyCursesError,s);
        return NULL;
    }
+#endif
 
    initialised_setupterm = TRUE;
 
Posted at by



2009/06/11


Tagtterをリリースして、初期は「CPU Quota」連発してしまい見苦しい状態でした。ごめんなさい。
さて、一時はどうなるかと思いましたが現在は「CPU Quota」も表示される事無く動いています。
今日はこの状況を乗り切る際に行った「Google App Engine」のパフォーマンスチューニングtipsを3つほどご紹介。

webapp.WSGIApplicationのdebugフラグはFalseにすべし

いきなり当たり前で申し訳ないですが、実はこのフラグがTrueかFalseかというだけで「CPU Quota」が出る頻度が極度に異なります。
実際に同じ症状の方もいらっしゃる様なので、おそらく間違いありません。
また公開する様なシステムでは、パスワード等も管理される事になるかと思いますが、debug=Trueだとスタックトレースに変数の値が表示されてしまい無茶苦茶危険です。気を付けましょう。
なお、Tagtterではユーザのパスワードは一切保存していません。

時間の掛かる既知な処理はキャッシュする

これはGoogle App Engineだけに言える話ではないですが、Tagtterではユーザの存在確認の為にtwitter.comへアクセスしています。
但し、タグが追加される度、voteされる度にこれを行ってしまうとtwitter.comにも負荷を掛ける事になります。
そこでTagtterではTagtter内に存在するユーザに対してはtwitter.comへのアクセスを行わないようになっています。
確かにGoogle App Engineの処理能力は良いのですが、いかんせん制限が厳しかったりします。余談になりますが現状Tagtterのデータ全件をXMLエクスポートする処理は「CPU Quota」で動きません。現在はデータの種別毎にバックアップしています。

Templateにはクラスオブジェクトは渡さない

Django Templateには、db.Modelのインスタンスを渡せて非常に便利なのですが、このDBインスタンスをTemplateの中から扱うと非常に遅くなります。
またdb.Modelに持ったgeneratorをTemplateから使うと更にパフォーマンスが落ちます。
例えばTagtterではユーザに対して付けられたタグをManyToManyで管理しており、モデルの一部を抜粋すると class TagtterTag(db.Model):
  name = db.StringProperty(required=True)

  ... snip ...

class TagtterMembership(db.Model):
  user = db.ReferenceProperty(TagtterUser)
  tag = db.ReferenceProperty(TagtterTag)

  ... ship ...

class TagtterUser(db.Model):
  name = db.StringProperty(required=True)

  ... snip ...

  def tags(self):
    return (x.tag for x in self.tagttermembership_set)

この様なコードになるのですが、user.tagsをTemplate側から呼び出すとかなりパフォーマンスダウンになります。また、Google App EngineではCPU占有度により「CPU Quota」させる仕組みがあるのですが、実はこれはTemplate内の呼び出しにも適応される為 application → template → model procedure という処理を行うと、1処理(1値参照)あたりのCPU使用回数が増える事になります。
つまり上記Tagtterのtagsページでは全てのタグが表示されますがこれを     template_values = {
      'tags' : TagtterTag.all(),
    }
この様にしてしまうとTemplate側からModelのメソッドを呼び出す事になり、現状のデータ件数だと100%エラーが発生してしまいます。
Google App EngineではCPU占有状態が5秒程続くとエラーになる様で、このブリッジ状態だけでもパフォーマンスダウンに繋がるって事になります。
これを回避する為には、applicationにてTemplateで使う値を全て保持してしまう事が最も効果的でした。
つまり全てdictとして渡してあげるのです。変にTemplate側で「タグの数が0でない物を表示」とするのではなく     all_tags = []
    for tag in TagtterTag.all():
      if tag.size():
        size = tag.size()
        all_tags.append({
          'name' : tag.name,
          'title' : tag.title,
          'size' : size,
          'fontsize' : font_size(size),
        })
    ... ship ...

    template_values = {
      ... ship ...

      'tags' : all_tags,

      ... ship ...
    }
この様にTemplate側からdb.Modelメソッドを呼び出させないようにするのです。 これで、現状エラーも出なくなりました。要するに極力db.Modelインスタンスにアクセスしない様にするってのが味噌ですね。
こんな品祖貧祖なパフォーマンスチューニングですが、皆さんの何かしらのお役に立てば幸いです。

他にもパフォーマンスチューニングの方法は幾らでもあるかと思います。Tagtterで言えばクライアント側の処理は殆どパフォーマンスチューニングされていないのが現状です。
不定期ですが時間を見付けてやって行きたいと思います。

何かご要望などあれば、Feature Requestまで。
Posted at by




タイトルは半分釣りです。

はてなブックマーク - ブック・マークパンサー

prismがいいんじゃないかな

http://b.hatena.ne.jp/mattn/20090611#bookmark-13920618

Mozilla Prismでtumblr Dashboard専用ブラウザを作る - otsune's SnakeOil - subtech

(これは http://otsune.tumblr.com/post/121569443/mozilla-prism-tumblr-dashboard をはてな記法に修正した再録記事です)

http://subtech.g.hatena.ne.jp/otsune/20090611/prismTumblr
って事で作ってみた。
otsuneさんところに書いてある手順通りPrismをダウンロードして起動後にURL:「http://localhost:10010/」、Name:「Remedie」を入れただけ。
remedie-prism1

で、起動
remedie-prism2
Prism.exeが内部でxul-runnerを起動している部分のコードを見たけど、起動時に最大化する仕組みは無いみたい。でもナビゲーションなんかが無い分、可視領域が増えてウマー
Posted at by



2009/06/10


これは良いね。
MOONGIFT: » GoogleリーダーのCUIフロントエンド「Peep」:オープンソースを毎日紹介

Googleリーダーは出始めた頃はAjaxを使った高速な操作性が可能で、とても便利なRSSリーダーだった。だが今ではソーシャル的な機能も増えており「読む」という機能に特化していない。速度だってlivedoorリーダーの方が速いだろう。そんなGoogleリーダーを再度便利にしてくれる、それがPeepだ。

http://www.moongift.jp/2009/06/peep/
ryuji's peep at master - GitHub

This software is a TUI front-end for Google Reader

http://github.com/ryuji/peep/tree/master
待ってたよ。こんなの。
peep
peep-view
ブラウザを起動するとfirefoxになっちゃうので環境変数BROWSERにw3mを設定すれば端末内で暮らせてウマー!
Posted at by



2009/06/03


どっちかって言うと前向きな発言が好きかな。私は

Web進化論も読んでないし、私本人Web屋でもない。ただ後ろ向きな話が多くて滅入る。
実験的なサービスを出したり、模索したりするのは良い事。ただ新しくサービスを作ると既存ユーザから「あーだこーだ」言われる事もあるだろう。
そんな中最近感じているのは、はてなは今後既存ユーザを無視したサービスを作って行くべきなんじゃないかと。

物理的にも切り離して良いと思う。新しくサービス作って、はてなのアカウントは引き継がず。さらに言うなら、既存ユーザに告知もせず後から「へぇ、これはてなが作ってたんだ」なんてのも良いと思う。
ラボなんかで出てくるサービス見てても既存ユーザありきで盛り上がってるだけだし、おおよそ検討が付いてしまうサービスは数日で飽きてしまい、いつもの辛口ユーザに叩かれてブクマ炎上し、まだ使ってもなかったユーザに悪い印象垂れ流し状態になるまで半月も掛からないだろう。

じゃぁ何を作ればいいか。とても漠然として難しいお題だろうけど、コンテンツはサービスが作るんじゃなく、ユーザが作る物。それをどう引き出すかでサービスの良し悪しが決まるはず。
これはWebが進化したとしても変わらないよね?
例えば、馴れ合った既存ユーザを新しいサービスに持ち込んだとしても、新規サービス開始時に見られる「誰だこれ」「面白いじゃんこの人」とか全く無いだろうし、新規サービスにありがちな「サービス規約を無視したユーザと、自警団」みたいな物は既存ユーザは既にやってしまっていて、ある意味面白みがない。既存ユーザは新しいサービスが出たとしても、その中で一定の認知度を獲得してしまうと納得してしまって使わなくなってしまうんだよね。アカウントの早取り合戦や新規ユーザ同士でルール作って行ったりしてサービスの色が付いて行くんじゃないかな。新規のサービスで盛り上がるまでを体験した事のある人なら分かるよね?

別に自分の所で全部作らなくてもいいと思う。Googleだって色んなサービスは別の会社が作ってた物を買い取ってGoogleブランドにしてたりするし、「はてなだってやっちゃえ!」って思うな。なんていうかびっくり出来ないんだよな。
言ってしまうと既存ユーザに見きられる程度の行動範囲しか取れてないんじゃないかと。

Web屋でも無い私に言われる筋合いもないだろうし、内情知らない部外者が何を言ってるんだと言われるかもしれない。

だとしてもやっぱり思うのは、既存ユーザに見られながら作られる新規サービスには高揚感が無い。


んー。オチも結論も無い話になるけど、言って置きたいのは、はてないっそはてなアカウントを切り離したサービスを作ってみるのが面白いんじゃないかなと思うなー。
Posted at by



2009/06/01


まずは一言。

起動はえーーーーーーー!
気付いたらLinux版のバイナリ配布してるじゃないですか。 知らなかった。
Index of /buildbot/snapshots/chromium-rel-linux
http://build.chromium.org/buildbot/snapshots/chromium-rel-linux/
一番下にある最新のディレクトリからchrome-linux.zipをダウンロードして解凍。中にあるchromeを実行します。色々と入れていたせいか何も追加する事無く起動しました。

キター
chrome-linux
まだ設定画面等はTODO contentだったりしますが、遊ぶには十分。
期待大ですね。これから遊びます。
Posted at by



2009/05/22


YQLを使うと色んなネットワークリソースをさもAPIを扱うかの様に操作でき、幾らでも新しい可能性が生まれて来ます。YQLには初期の状態でYahoo!で扱える色んなテーブル(flickrやdelicious等)が用意されており show tables
と入力することでそのテーブル一覧が表示されます。
yql-community-tables1
また右側のサイドバーにあるテーブル一覧で「Show Comminity Tables」をクリックするとユーザコミュニティが作成した便利なテーブルも扱う事が出来ます。
yql-community-tables2
これらのComminity Tablesはgithubで開発されており、日々新しいデータテーブルが作成されています。
実はこのユーザテーブルは、ネットワーク上にXMLを配置する事が出来る人ならば誰でも作れます。
今日はこのユーザテーブルを自作する手順をご紹介します。

ユーザテーブルはユーザテーブル群を纏めるenvファイルと、実際のクエリを記述するファイルとで構成され、YQLからenvパラメータを使って参照する事が出来ます。例えばユーザコミュニティのテーブルであればenvファイルのURLを指定して以下の様にアクセスされます。
http://developer.yahoo.com/yql/console/?env=http://datatables.org/alltables.env
このenvパラメータで指定されたURLにアクセスしてみて頂けると分かると思いますが形式は以下の様なuse文の羅列になっています。
use 'http://www.datatables.org/amazon/amazon.ecs.xml' as amazon.ecs;
use 'http://www.datatables.org/auth/auth.basic.xml' as auth.basic;
use 'http://www.datatables.org/bitly/bit.ly.shorten.xml' as bit.ly.shorten;
use 'http://www.datatables.org/data/data.html.cssselect.xml' as data.html.cssselect;
このenvファイルと、実際のXMLファイルを用意すれば自分専用のAPIを作る事が出来ます。
今日はサンプルとして、はてなのユーザプロフィールをスクレイピングしてXML(もしくはJSON)を返すAPIを作ってみたいと思います。
まず配置場所としては、Google Page Creatorを使用しました。Google Page CreatorならばGoogleのアカウントさえ作れば誰でも無料でXMLを配置する事が出来ます。
はてなプロフィールを扱うAPIなのでファイル名はhatena.profile.xmlとし、alltables.envファイル use 'http://mattn.jp.googlepages.com/hatena.profile.xml' as hatena.profile;
としました。
次に実際のAPI部ですが、形式は以下の様になります。
<?xml version="1.0" encoding="UTF-8"?>
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
  <meta>
    <author>mattn</author>
  </meta>
  <bindings>
    <select produces="XML" itemPath="">
      <urls>
        <url>http://www.hatena.ne.jp/{id}/profile</url>
      </urls>
      <inputs>
        <key id="id" type="xs:string" paramType="variable" required="true"/>
      </inputs>
      <execute/>
    </select>
  </bindings>
</table>
YQLではXMLの中に書かれたexecuteノードにjavascriptを記述する事ができ、response.objectというオブジェクト変数に値を格納する事で独自のフォーマットを作り出す事が出来ます。
またそこで使うことが出来るjavascriptにはE4X(ECMAScript for XML)が採用されており、XMLを扱いやすくなっています。
今回のはてなプロフィールAPIではhttp://www.hatena.ne.jp/mattn/で表示されるページを組み込み関数y.queryを使ってスクレイピングします。
以下javascript部のコード
var contents = y.query("select * from html where url=@url and xpath=@xpath", {
  url: "http://www.hatena.ne.jp/" + id + "/",
  xpath: "'//dl[@class=\"profile\"]/dd'"
});
var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
var profile =
<profile>
    <user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
    <nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
    <shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
    <longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
    <medals/>
    <addresses/>
    <services/>
</profile>;
for each(var n in contents.results..div.(@['class']=="medals").img) {
    profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
}
for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
    profile.addresses.address +=
    <address>
        <name>{n.th.*.text().toString()}</name>
        <value>{n.td.*.text().toString()}</value>
    </address>;
}
for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
    profile.services.service +=
    <service>
        <name>{n.img.@['title'].toString()}</name>
        <url>{n.@['href'].toString()}</url>
    </service>
}
response.object = profile;
y.queryにはYQLから使うことが出来るselect文を使用する事ができ、xpathを使って絞り込む事も出来ます。ただ今回は一つのAPIで
  • アカウントID
  • ニックネーム
  • 短い紹介
  • 自己紹介
  • アドレス情報
  • 使っているサービス一覧
  • 市民メダル情報
を一度に扱うので、個別にxpathでクエリを投げるのでは無く、一部を除いたページ全体を取得しておいて、E4XのDOM操作でxpathライクな処理を書いています。
例えばE4Xでは、DOMオブジェクトの階層スキップや条件指定検索を contents.results..div.(@['id']=="hatena-body")..dd[2];
等といった書き方をする事が出来ます。つまり無理にxpathで絞り込む必要は無いという事です。
おそらくサーバ側ではrhinoあたりを使っているのかもしれませんね。

XML全体のコードは以下の様になりました。
<?xml version="1.0" encoding="UTF-8"?>
<table xmlns="http://query.yahooapis.com/v1/schema/table.xsd">
  <meta>
    <author>mattn</author>
  </meta>
  <bindings>
    <select produces="XML" itemPath="">
      <urls>
        <url>http://www.hatena.ne.jp/{id}/profile</url>
      </urls>
      <inputs>
        <key id="id" type="xs:string" paramType="variable" required="true"/>
      </inputs>
      <execute><![CDATA[
      var contents = y.query("select * from html where url=@url and xpath=@xpath", {
        url: "http://www.hatena.ne.jp/" + id + "/",
        xpath: "'//dl[@class=\"profile\"]/dd'"
      });
      var shortdesc = contents.results..div.(@['id']=="hatena-body")..dd[2];
      var longdesc = contents.results..div.(@['id']=="hatena-body")..dd[3];
      var profile =
      <profile>
          <user>{contents.results..div.(@['id']=="hatena-body")..dd[0].p.text().toString()}</user>
          <nickname>{contents.results..div.(@['id']=="hatena-body")..dd[1].p.text().toString()}</nickname>
          <shortdesc>{shortdesc ? shortdesc.*.toString() : ''}</shortdesc>
          <longdesc>{longdesc ? longdesc.*.toString() : ''}</longdesc>
          <medals/>
          <addresses/>
          <services/>
      </profile>;
      for each(var n in contents.results..div.(@['class']=="medals").img) {
          profile.medals.medial += <medal>{n.@['title'].toString()}</medal>;
      }
      for each(var n in contents.results..table.(@['class']=="profile addresslist")..tr) {
          profile.addresses.address +=
          <address>
              <name>{n.th.*.text().toString()}</name>
              <value>{n.td.*.text().toString()}</value>
          </address>;
      }
      for each(var n in contents.results..ul.(@['class']=="hatena-fotolife floatlist")[0]..a) {
          profile.services.service +=
          <service>
              <name>{n.img.@['title'].toString()}</name>
              <url>{n.@['href'].toString()}</url>
          </service>
      }
      response.object = profile;
      ]]></execute>
    </select>
  </bindings>
</table>
あとはこれをYQLから指定してやれば良い事になります。
実際にはここで試す事が出来ます。

さてこれでAPIが出来上がったので、APIを使ったサンプルを書いてみましょう。
$(function() {
    $('#hatena-profile').submit(function() {
        $('#hatena-profile-ajaxloader').attr('src', 'http://mattn.kaoriya.net/images/ajax-loader.gif').show();
        var query = "select * from hatena.profile where id = '" + $('#hatena-id').val() + "'";
        var envurl = "http://mattn.jp.googlepages.com/alltables.env";
        $.getJSON('http://query.yahooapis.com/v1/public/yql?q=' + encodeURIComponent(query) + '&format=json&env=' + encodeURIComponent(envurl) + '&callback=?', function(data) {
            $('#hatena-profile-ajaxloader').hide();
            $('#hatena-profile-user').text(data.query.results.profile.user);
            $('#hatena-profile-nickname').text(data.query.results.profile.nickname);
            $('#hatena-profile-medals').html(data.query.results.profile.medals.medal.join('<br />'));
            $.each(data.query.results.profile.addresses.address, function() {
                var html = $('#hatena-profile-addresses').html();
                $('#hatena-profile-addresses').html(html + this.name + ':' + this.value + '<br />');
            });
            $.each(data.query.results.profile.services.service, function() {
                var html = $('#hatena-profile-services').html();
                $('#hatena-profile-services').html(html + this.name + ':' + this.url + '<br />');
            });
            $('#hatena-profile-info').show('slow');
        });
        return false;
    });
});
簡単にプロフィール情報を表示するだけの物です。
以下実行例です。

続きを読む...

Posted at by



2009/04/16


はてなブックマークをdeliciousに同期する場合、どうやってますか?
  • 同時ポストツール?
  • Plagger?
  • まさか、手作業?
同時ポストツールの場合、確かにその場で同時にポスト出来て便利ですね。でも携帯ではてなブックマークから登録した場合、同期されませんよね。 Plaggerだと、cronで動き続けるPCが要りますよね。家に24時間稼動可能なPC無いよ!なんて人いるかもしれません。
手作業?問題外!

先日、Google App Engineにcronが導入されました。
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™

Cron support: schedule tasks like report generation or DB clean-up at an interval of your choosing.

http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
つまり...
  1. cronでイベント発生
  2. はてなブックマークからRSS取得
  3. 内部のデータベースから既にポスト済みでないか確認
  4. deliciousにポストする
  5. モテモテ
って事で作ってみました。
mattn's hatenabookmark-meets-delicious at master - GitHub

post hatenabookmark to delicious periodically using google app engine

http://github.com/mattn/hatenabookmark-meets-delicious/tree/master
Google App Engineのアカウントを作成後アプリケーションを作成します。
app.yamlの application: your_app_name
version: 1
runtime: python
api_version: 1

handlers:
  - url: /tasks/sbm-sync
    script: sbm-sync.py

  - url: /
    script: sbm-sync.py

  - url: /favicon.ico
    static_files: static/images/favicon.ico
    upload: static/images/favicon.ico
    mime_type: image/x-icon

  - url: /static
    static_dir: static
your_app_nameの部分を作成したアプリケーションIDに書き換えて下さい。次にmy-config.yaml.exampleをmy-config.yamlにコピーし hatena_user : your_hatena_user
delicious_user : your_delicious_user
delicious_pass : your_delicious_password
timezone: JST
timeoffset: 9
あなたの、はてなブックマークIDとdeliciousユーザID/パスワードに書き換えて下さい。
あとはサーバにアップロードすれば10分毎に、はてなブックマークのRSSを取得してdeliciousに同期されます。
gae-sbm-sync

よろしければ使ってみて下さい。ソースはgithubにあるのでpatch welcomeです。
Posted at by




SQLite便利!
SQLite3におけるREGEXP演算子 - anon_193の日記

SQLite では、load_extension 関数を用いて、外部の拡張モジュールをロードすることが出来る。拡張モジュールは、いわばユーザ関数ライブラリで、SQLite3 ODBC Driver には標準で BLOB二次元マッピング拡張(sqlite3_mod_blobtoxy.dll)、外部データ取込・出力拡張(sqlite3_mod_impexp.dll)、全文検索拡張(sqlite3_mod_fts3.dll) が付属している。これらと同様にして、正規表現マッチングを行う regexp ユーザ関数を持つ拡張モジュールを制作し、ロードすれば、お目当ての REGEXP 演算子が使えるわけだ。

http://d.hatena.ne.jp/anon_193/20090114/1231935112
sqlite3_mod_regexp.cxx
#include <boost/regex.hpp>
#include <sqlite3ext.h>
extern "C" {
    SQLITE_EXTENSION_INIT1
    static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
        if (argc >= 2) {
            const char *target  = (const char *)sqlite3_value_text(argv[1]);
            const char *pattern = (const char *)sqlite3_value_text(argv[0]);
            try {
                boost::regex ereg(pattern, boost::regex_constants::perl);
                sqlite3_result_int(context, boost::regex_search(target, ereg));
            } catch (boost::regex_error &e) {
                sqlite3_result_error(context, e.what(), 0);
            }
        }
    }
    __declspec(dllexport) int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
        SQLITE_EXTENSION_INIT2(api);
        return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, (void*)db, regexp_func, NULL, NULL);
    }
}
VCでコンパイルしました。
# cl /EHsc -Isrc -I. -I "c:\boost_1_35_0" sqlite3_mod_regexp.cxx /LD "C:\boost_1_35_0\libs\regex\build\vc80\libboost_regex-vc80-mt-s-1_35.lib"
サンプルデータ
foo.sql
CREATE TABLE foo(id integer primary key, value text);
INSERT INTO "foo" VALUES(1,'abc');
INSERT INTO "foo" VALUES(2,'def');
INSERT INTO "foo" VALUES(3,'あいうえお');
INSERT INTO "foo" VALUES(4,'かきくけこ');
INSERT INTO "foo" VALUES(5,'さしすせそ');
utf-8で保存して下さい
# cat foo.sql | sqlite3 foo.db
そしてPerlのコード
use strict;
use warnings;
use utf8;
use YAML;
use DBIx::Simple;

my $db = DBIx::Simple->connect("dbi:SQLite:dbname=c:/foo.db", "", "")
    or die DBIx::Simple->error;

$db->func(1, "enable_load_extension");

my $result = $db->query("select load_extension('/sqlite3_mod_regexp.dll')")
    or die DBIx::Simple->error;
warn Dump $db->query("select * from foo where value regexp '^[あか]'")->hashes;
dbのパスとdllのパスは指定して下さい
実行すると...
---
id: 3
value: あいうえお
---
id: 4
value: かきくけこ
スゲーーーー便利!
Posted at by



2009/04/14


JSONって大体のサーバレスポンスだと、改行もなくエディタで開いてちまちま改行入れて内容確認...なんて事やってる人もいるかと思います。
結構前に作った物ですが、もしかしたら便利と思ってくれる人もいるんじゃないか...と思ったの晒しておきます。
mattn's gtkjsonviewer at master - GitHub

A simple json viewer written in GTK.

http://github.com/mattn/gtkjsonviewer/tree/master
早い話が、JSONをGUIで見ようという話です。中身は簡単なpythonのコードで、GTKを使っています。
# curl http://twitter.com/public_timeline/?format=json | gtkjsonview.py
の様に使います。Windowsの場合はpython.exeがちゃんと標準入力を読み取ってくれないので # curl http://twitter.com/statuses/user_timeline/mattn_jp.json | python gtkjsonview.py
とする必要があります。起動すると以下の様な画面が表示されます。
gtkjsonview
不正なJSON(例えば最終カンマあり)等の場合はパースエラーが出ます。
Posted at by



2009/04/10


少し前からですがリポジトリをgoogle codeかgithubに移しています。
その際少しだけ触って、以前まではデータ更新時にフローティングウィンドウを出し、そこにローディングアニメーションを表示していましたが、ローディング中に親画面を移動されるとカッコ悪いのでボタンの横でクルクル回る様に修正しました。
gtktwitter-20090410
気が向いたら機能アップするかもしれません。
以上、GtkTwitter近況でした。
Posted at by




jQueryでマウスホバーでリンクに影をつけるカッコいいプラグインを見つけました。
nakajima's jquery-glow at master - GitHub

Make your elements glow. Ooooh.

http://github.com/nakajima/jquery-glow/tree/master
以下実行例

続きを読む...

Posted at by



2009/04/08


きたー!
Google App Engine Blog: Seriously this time, the new language on App Engine: Java™

Today, we're very excited to announce the availability of a new programming language for Google App Engine. Please welcome the Java runtime!

http://googleappengine.blogspot.com/2009/04/seriously-this-time-new-language-on-app.html
で、最初はEarly Lockというサーバを更新出来ない状態だったけど何故かさっき「Lock解除したよ」のメールが来た。

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    </head>
    <body>
        Hello JSP!<br />
    </body>
</html>

ここで動いてます。

ちなみに、上の記事によるとGAEでCronが動かせる様になった。こっちの方が嬉しい人いるんじゃないかな?
Posted at by



2009/04/06


Growl For Windowsのコミットビットを貰ったので、国際化の為のresxファイルを書いた。
growl4windows-japanese
次のリリースあたりには降りてくるはず。
その他の活動としてはC言語で書いたGNTP送信プログラムgntp-sendを書いた。
mattn's gntp-send at master - GitHub
あと、perlモジュールGNTP::Growlは暗号化送信出来る様になった。現状AESとDESに対応しています。3DESはまだ未対応です。
mattn's perl-gntp-growl at master - GitHub
ちなみに、GrowlのソースコードにはブランチですがGNTPなコードが入っているっぽいので、もしかしたら上記のツール類でMacにGrowl出来る日が来るのではないか...と期待しています。
Posted at by




そうだった。そうだった。
Posted at by




追記
vimperatorrcに追加するスクリプトを少し修正する必要があります。詳しくはココを参照。

こうするといいよ。

add to your .vimperatorrc javascript if (typeof hBookmark) liberator.loadScript('chrome://hatenabookmark/content/vimperator/plugin/hatenabookmark.js', {__proto__: this});
javascript if (typeof hBookmark != 'undefined') liberator.loadScript('chrome://hatenabookmark/content/vimperator/plugin/hatenabookmark.js', {__proto__: this});

and try :hbsearch はてな
:hbtabsearch はてな
id:hazime2914さんに間違いを指摘して頂きました。正しくはhbtagsearchではなくhbtabsearchです。
はてなブックマーク Firefox 拡張のベータテストを開始します - はてなブックマーク日記 - 機能変更、お知らせなど
Posted at by



2009/04/03


こうすればいいのか...
デザイン設定画面のスタイルシートに
.hatena-star-star-image { background-image: url('http://mattn.kaoriya.net/images/unko.gif'); }
こんな感じになります。
Posted at by



2009/03/31


貧乏なので買えません。
カラースターショップ - はてな

ソース
// ==UserScript==
// @name           poor man's hatena color star
// @namespace      http://mattn.kaoriya.net/
// @description    poor man's hatena color star
// @include        http://*
// @include        https://*
// ==/UserScript==

(function() {
    // extend version of $X
    // $X(exp);
    // $X(exp, context);
    // $X(exp, type);
    // $X(exp, context, type);
    function $X (exp, context, type /* want type */{
        if (typeof context == "function"{
            type    = context;
            context = null;
        }
        if (!context) context = document;
        exp = (context.ownerDocument || context).createExpression(exp, function (prefix) {
            return document.createNSResolver((context.ownerDocument === null ? context
                                                                             : context.ownerDocument).documentElement)
                           .lookupNamespaceURI(prefix) ||
                   document.contentType === "application/xhtml+xml" ? "http://www.w3.org/1999/xhtml" : "";
        });

        switch (type) {
            case String:  return exp.evaluate(context, XPathResult.STRING_TYPE, null).stringValue;
            case Number:  return exp.evaluate(context, XPathResult.NUMBER_TYPE, null).numberValue;
            case Boolean: return exp.evaluate(context, XPathResult.BOOLEAN_TYPE, null).booleanValue;
            case Array:
                var result = exp.evaluate(context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
                for (var ret = [], i = 0, len = result.snapshotLength; i < len; i++) {
                    ret.push(result.snapshotItem(i));
                }
                return ret;
            case undefined:
                var result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
                switch (result.resultType) {
                    case XPathResult.STRING_TYPE : return result.stringValue;
                    case XPathResult.NUMBER_TYPE : return result.numberValue;
                    case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
                    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
                        // not ensure the order.
                        var ret = [], i = null;
                        while ((i = result.iterateNext())) ret.push(i);
                        return ret;
                }
                return null;
            default: throw(TypeError("$X: specified type is not valid type."));
        }
    }

    var stars = ['star-red.gif', 'star-green.gif', 'star.gif#', 'star-blue.gif'];
    setInterval(function() {
        $X('//img[@src="http://s.hatena.ne.jp/images/star.gif"]').forEach(function(n) {
            n.src = "http://s.hatena.ne.jp/images/" + stars[parseInt(Math.random() * 4)];
        });
    }, 1000);
})();
インストール
poor-mans-hatena-color-star.user.js
Posted at by



2009/03/23


書いた。 use strict;
use warnings;
use lib qw/lib/;
use GNTP::Growl;

my $growl = GNTP::Growl->new(AppName => "my perl app");
$growl->register([
    { Name => "foo", },
    { Name => "bar", },
]);

$growl->notify(
    Event => "foo",
    Title => "おうっふー おうっふー",
    Message => "大事な事なので\n2回言いました",
    Icon => "http://mattn.kaoriya.net/images/logo.png",
);
こんなソースで
perl-gntp-growl
こんな物が動く。
開発はこの辺で...
mattn's perl-gntp-growl at master - GitHub
Posted at by




TopHatenarやってみた。
tophatenar-bigsky
gigazineもdqnplusも遠いですなぁ。。。
ちなみにdankogaiさんも遥か彼方へ...
tophatenar-dankogai

購読者数・ブックマーク数相関グラフ

購読者数推移グラフ

ブックマーク数推移グラフ

Posted at by



2009/03/18


twitterのOAuthベータが開始されたので、さっそく軽量Web Application Framework「MENTA」でOAuthする物を作ってみた。
構成も簡単で │  .htaccess
│  menta.cgi
├─app
│  ├─controller
│  │      callback.pl
│  │      request.pl
│  ├─data
│  └─static
├─bin
├─lib
└─plugins
        session.pl
こんな感じ。トップページからrequest.plに飛び、OAuthのcallbackとしてcallback.plがキックされる。
要のrequest.plは以下の様な感じ use MENTA::Controller;
use URI;
use OAuth::Lite::Consumer;

sub run {
    my $consumer = OAuth::Lite::Consumer->new(
        consumer_key       => config()->{application}->{consumer_key},
        consumer_secret    => config()->{application}->{consumer_secret},
        site               => 'http://twitter.com/',
        request_token_path => 'http://twitter.com/oauth/request_token',
        access_token_path  => 'http://twitter.com/oauth/access_token',
        authorize_path     => 'http://twitter.com/oauth/authorize',
    );

    my $request_token = $consumer->get_request_token();
    my $uri           = URI->new( $consumer->{authorize_path} );
    $uri->query(
        $consumer->gen_auth_query(
            "GET", "http://twitter.com/", $request_token
        )
    );
    redirect( $uri->as_string );
}

そしてcallback.plはこんな感じ
use utf8;
use MENTA::Controller;
use OAuth::Lite::Consumer;
use JSON;

sub run {
    my $consumer = OAuth::Lite::Consumer->new(
        consumer_key       => config()->{application}->{consumer_key},
        consumer_secret    => config()->{application}->{consumer_secret},
        site               => 'http://twitter.com/',
        request_token_path => 'http://twitter.com/oauth/request_token',
        access_token_path  => 'http://twitter.com/oauth/access_token',
        authorize_path     => 'http://twitter.com/oauth/authorize',
    );

    my $access_token = $consumer->get_access_token( token => param('oauth_token') );
    my $res = $consumer->request(
        method => 'POST',
        url    => q{http://twitter.com/statuses/update.json},
        token  => $access_token,
        params =>
          { status => 'おうっふー', token => $access_token },
    );
    if ( $res->is_success ) {
        my $status = from_json( $res->content );
        redirect( "http://twitter.com/"
              . $status->{user}->{screen_name}
              . "/status/"
              . $status->{id} );
    }
    else {
        redirect("http://twitter.com/");
    }
}
ちなみにこのアプリケーションを"Accept"にするとtwitteのステータスラインに「おうっふー」がポストされます。自分で作る方はtwitterのアプリケーション登録画面から得られるconsumer_keyとconsumer_secretをmenta.cgiのapplication項目に設定するのをお忘れなく。
簡単ですね!
Posted at by




vimはファイルを開く際、iconvを使用してfileencodingsの順に変換を試し、正しく変換出来た物をfileencodingとして使用する仕組みになっています。
つまり化けたファイルだと、どのfileencodingsにもマッチせずencodingで指定されたエンコーディングでファイルが開かれステータス行に「変換失敗」と表示される事になります。
しかしながらそのファイルのエンコーディングが分かっている場合もあります。vimでは++encオプションを指定する事で指定のfileencodingでファイルを開く事が出来ます。
:e ++enc=utf-8 foo.txt
この場合、vimはiconvで変換出来なかった文字を?という文字で置き換えてしまいます。
# echo あいうえお | iconv -f char -t utf-8 > utf8.txt
# echo あいうえお | iconv -f char -t cp932 > sjis.txt
# cat utf8.txt sjis.txt > bad.txt
# vim
cp932の場合は文字集合の区画が広い為、vimではutf-8が構成するバイト列をcp932の範囲内として開いてしまいます。 :e ++enc=cp932 bad.txt
縺ゅ>縺?縺医♀
あいうえお
またutf-8の場合はcp932の区画は範囲外になります。つまり :e ++enc=utf-8 bad.txt
あいうえお
??????????
この様に?に変換されてしまう事になります。ステータス行には何行目で変換が失敗したかを警告するメッセージも表示されますが、元のテキストは失われます。
こういったファイルを開くのにフィルタを使用する人もいるかもしれません。
:%!nkf -Ws
これでも良いのですが、nkfが変換出来ない文字は削られてしまいます。
実はvimの:eコマンドには++badオプションという物があり、この?の挙動を変更出来る様になっています。
                                                                *++bad*
The argument of "++bad=" specifies what happens with characters that can't be
converted and illegal bytes.  It can be one of three things:
    ++bad=X      A single-byte character that replaces each bad character.
    ++bad=keep   Keep bad characters without conversion.  Note that this may
                 result in illegal bytes in your text!
    ++bad=drop   Remove the bad characters.

The default is like "++bad=?": Replace each bad character with a question
mark.
例えば++badオプションに%を指定すると :e ++enc=utf-8 bad.txt
あいうえお
%%%%%%%%%%
となるのです。現在の所1文字の場合はその文字で置き換える様になっています。また++badオプションにkeepを指定すると、何も変換せずに開いてくれます。
:e ++enc=utf-8 ++bad=keep bad.txt
あいうえお
<82><a0><82><a2><82><a4><82><a8><82><a6>
さらにdropを指定する事で不正な文字を削除してファイルを開く事も出来ます。
あいうえお

vimでファイルが開けなくなってしまった方、一度++badオプションを試してみては如何でしょうか?
Posted at by



2009/03/13


ruby版C版なでしこ版と書いたので最後(きっと)にvim版


Posted at by



2009/03/11


解説は...いらんでしょ。
母艦は「dan the shell」。

!変数宣言が必要
ルートメニューとはメインメニュー

ファイルメニューとはメニュー
実行とは、メニュー。実行のテキストは「実行」
区切り1とは、メニュー。区切り1のテキストは「-」
閉じるとは、メニュー。閉じるのテキストは「閉じる」

「実行
区切り1
閉じる」を反復
 「ファイルメニューの追加({それ}を)」をナデシコする

「ファイルメニュー」を反復
 「ルートメニューの追加({それ}を)」をナデシコする

入力メモとはメモ。
入力メモについて
  X=0
    Y=0
    W=母艦のクライアントW
    H=母艦のクライアントHを2で割る
結果メモとはメモ。
結果メモについて
  X=0
    Y=母艦のクライアントHを2で割る
    W=母艦のクライアントW
    H=母艦のクライアントHを2で割る

母艦のサイズ変更した時は~
 入力メモについて
  X=0
    Y=0
    W=母艦のクライアントW
    H=母艦のクライアントHを2で割る
 結果メモについて
  X=0
    Y=母艦のクライアントHを2で割る
    W=母艦のクライアントW
    H=母艦のクライアントHを2で割る

閉じるのクリックした時は~
 「閉じますか?」と二択
  もし、それがはいならば、終わり

実行のクリックした時は~
  URLは「http://api.dan.co.jp/perleval.cgi?c=callback&s=」
  それは入力メモのテキスト
  それをURLエンコード
  「{改行}」を「%0A」に置換
  URLにそれを追加
  URLをHTTPデータ取得して、結果に代入
  結果を「^callback\((.*)\);$」で正規表現マッチ
  結果は抽出文字列[0]
  結果をJSONデコード
  結果はそれ
  結果メモのテキストは結果@「result」

参考文献: 404 Blog Not Found:Ajax - perlを実行するAPI
Posted at by




readline使ってコマンドライン提供、curlで通信、結果をjson-cでパースまで作った。燃え尽きた。
追記
shebangから使えるようにした。
#!/usr/bin/dansh
以下コード // for MSVC: cl -I.. /Tp dansh.cpp curl.lib readline.lib ..\Release\json.lib
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <io.h>
namespace json {
#include "json.h"
}
#define READLINE_STATIC
#include <readline/readline.h>
#include <curl/curl.h>
#define API_URL "http://api.dan.co.jp/perleval.cgi?c=callback&s="

typedef struct {
    char* data;     // response data from server
    size_t size;    // response size of data
} MEMFILE;

MEMFILE*
memfopen() {
    MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
    mf->data = NULL;
    mf->size = 0;
    return mf;
}

void
memfclose(MEMFILE* mf) {
    if (mf->data) free(mf->data);
    free(mf);
}

size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
    MEMFILE* mf = (MEMFILE*) stream;
    int block = size * nmemb;
    if (!mf->data)
        mf->data = (char*) malloc(block);
    else
        mf->data = (char*) realloc(mf->data, mf->size + block);
    if (mf->data) {
        memcpy(mf->data + mf->size, ptr, block);
        mf->size += block;
    }
    return block;
}

char*
memfstrdup(MEMFILE* mf) {
    char* buf = (char*) malloc(mf->size + 1);
    memcpy(buf, mf->data, mf->size);
    buf[mf->size] = 0;
    return buf;
}

char*
url_encode_alloc(const char* str, int force_encode) {
    const char* hex = "0123456789abcdef";

    char* buf = NULL;
    unsigned char* pbuf = NULL;
    int len = 0;

    if (!str) return NULL;
    len = strlen(str)*3;
    buf = (char*) malloc(len+1);
    memset(buf, 0, len+1);
    pbuf = (unsigned char*)buf;
    while(*str) {
        unsigned char c = (unsigned char)*str;
        if (c == ' ')
            *pbuf++ = '+';
        else if (c & 0x80 || force_encode) {
            *pbuf++ = '%';
            *pbuf++ = hex[c >> 4];
            *pbuf++ = hex[c & 0x0f];
        } else
            *pbuf++ = c;
        str++;
    }
    return buf;
}

void do_dan(const char* line) {
    char* source = url_encode_alloc(line, TRUE);
    char* url = (char*) malloc(strlen(API_URL) + strlen(source) + 1);
    strcpy(url, API_URL);
    strcat(url, source);

    MEMFILE* mf = memfopen();
    CURL* curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
    CURLcode res = curl_easy_perform(curl);
    free(url);
    if (res == CURLE_OK) {
        char* data = memfstrdup(mf);
        memfclose(mf);

        // remove callback function
        char* ptr;
        ptr = strrchr(data, ')');
        if (ptr) *ptr = 0;
        ptr = strchr(data, '(');
        if (ptr) *ptr++ = 0;
        else ptr = data;

        json::json_object *obj = json::json_tokener_parse(ptr);
        if (!is_error(obj)) {
            json::json_object *result = json::json_object_object_get(obj, "result");
            if (!is_error(result)) {
                printf("%s\n", json::json_object_to_json_string(result));
                json_object_put(result);
            }
        }
        free(data);
    }
    curl_easy_cleanup(curl);
}

int main(int argc, char **argv)
{
    char* line = NULL;

    //json::mc_set_debug(1);
    if (argc == 2) {
        FILE* fp = fopen(argv[1], "rb");
        if (!fp) {
            perror("can't open file");
            exit(-1);
        }
        char buf[BUFSIZ];
        while (fgets(buf, sizeof(buf), fp)) {
            if (!line) {
                if (strncmp(buf, "#!", 2))
                    line = strdup(buf);
            } else {
                line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
                strcat(line, buf);
            }
        }
        fclose(fp);
        do_dan(line);
        free(line);
    }
    if (!isatty(fileno(stdin))) {
        char buf[BUFSIZ];
        while (fgets(buf, sizeof(buf), stdin)) {
            if (!line) {
                if (strncmp(buf, "#!", 2))
                    line = strdup(buf);
            } else {
                line = (char*) realloc(line, strlen(line) + strlen(buf) + 1);
                strcat(line, buf);
            }
        }
        do_dan(line);
        free(line);
    } else {
        while (line = readline("dan> ")) {
            do_dan(line);
            free(line);
        }
    }
    return 0;
}

参考文献: 404 Blog Not Found:Ajax - perlを実行するAPI
Posted at by



2009/03/05


結果から言うと実に使いにくい!
PyAWS - A Python wrapper for Amazon Web Service

PyAWS is a Python wrapper for the latest Amazon Web Service. It is designed to pave the way for Python developers to interactivate AWS. This project is forked from the code base of pyamazon. The Amazone E-Commerce Services is supported.

http://pyaws.sourceforge.net/
配列なら配列で、存在したり存在しない場合があるプロパティならばそれ用のアクセサを作ってほしい...
Djangoテンプレートで配列かどうかを判断してループで回してってのはつらいです。
あと、Django Template Engineの最新版でないと {% for key, value in values %}
こういう書き方が出来ないので、結局明示的な名前の付いたdictに作り変える必要があった。まぁこれはpyawsのせいではないけれど。
aws.py
#!-*- coding:utf-8 -*-
import os
import cgi
import wsgiref.handlers
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.api import urlfetch
import logging
import yaml
from pyaws import ecs

def to_dict_array(array):
  ret = []
  for item in array:
    ret.append({'key': item.keys()[0], 'value':item.values()[0] })
  return ret

class MainPage(webapp.RequestHandler):
  config = yaml.safe_load(open(os.path.join(os.path.dirname(__file__), 'aws.yaml'), 'r'))
  def get(self):
    kind = self.request.get('kind')
    keyword = self.request.get('keyword')
    template_values = {}
    template_values = {
      'asoid'    : self.config['asoid'],
      'kinds'    : to_dict_array(self.config['kinds']),
      'kind'     : kind,
      'keyword'  : keyword,
      'books'    : [],
    }
    if keyword:
      asoid = self.config['asoid']
      devkey = self.config['devkey']
      kinds = self.config['kinds']
      ecs.setLicenseKey(devkey)
      ecs.setLocale(self.config['locale'])
      books = ecs.ItemSearch(keyword, SearchIndex=kind, ResponseGroup='Medium,Offers')
      count = 0
      max = 5
      for book in books:
        if 'Author' in book.__dict__ and not isinstance(book.Author, list):
          book.Author = [book.Author]
        if 'Offer' in book.Offers.__dict__:
          if isinstance(book.Offers.Offer, list):
            book.Availability = book.Offers.Offer[0].OfferListing.Availability
          elif 'Availability' in book.Offers.Offer.OfferListing.__dict__:
            book.Availability = book.Offers.Offer.OfferListing.Availability
        template_values['books'].append(book)
        count += 1
        if count > max: break
    path = os.path.join(os.path.dirname(__file__), 'aws.html')
    self.response.out.write(template.render(path, template_values))

def main():
  application = webapp.WSGIApplication([('/pyaws/', MainPage)], debug=True)
  wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
  main()
aws.html
<html>
<head>
<title>AWS商品検索</title>
<style tyle="text/css"><!--
body {
    font-family: 'メイリオ', 'Osaka'
}
#content {
    margin-left: 50px;
}
#error {
    color: red;
}
.awsxom {
    background: #eeeeee;
    padding: 0.5em;
}
--></style>
</head>
<body>
    <h1>AWS商品検索</h1><img src="http://b.hatena.ne.jp/entry/image/http://mattn.appspot.com/pyaws/" title="はてなブックマーク" />
    <div id="content">
        <p align="right"><a href="/">目次</a></p>
        <form method="get">
            <label for="kind">種類</label>
            <select id="kind" name="kind" value="{{ kind }}">
                {% for item in kinds %}<option value="{{ item.key }}"{% ifequal item.key kind %} selected="selected"{% endifequal %}>{{ item.value }}</option>
                {% endfor %}
            </select>
            <label for="keyword">キーワード</label><input id="keyword" name="keyword" type="text" value="{{ keyword|escape }}" />
            <input type="submit" />
        </form>
        <hr />
        {% for book in books %}
        <div class="awsxom">
            <a href="http://www.amazon.co.jp/exec/obidos/ASIN/{{ book.ASIN }}/ref=nosim/{{ asoid }}">
                <img src="{{ book.SmallImage.URL }}" align="left" hspace="5" border="0" alt="{{ book.Title }}" class="image" />
                <strong>{{ book.Title }}</strong></a><br />
            {{ book.Author|join:", " }}<br />
            {{ book.Manufacturer}} / {{ book.ListPrice.FormattedPrice }} ({% if book.PublicationDate %}{{ book.PublicationDate }}{% else %}{{ book.ReleaseDate }}{% endif %})<br />
            &nbsp;<br />
            発送可能時間:{{ book.Availability }}<br />
            <br clear="all" />
        </div><br />
        {% endfor %}
    </div>
</body>
</html>
あと、設定用のyaml
aws.yaml
asoid : xxxxxxxxx
devkey : 1XXXXXXXXXXXXXXXXXXX
locale: jp
kinds:
 - Blended: Blended すべての商品
 - Books: 本
 - Classical: クラシック音楽
 - DVD: DVD
 - Electronics: エレクトロニクス
 - ForeignBooks: 洋書
 - Hobbies: ホビー
 - Kitchen: ホーム&キッチン
 - Music: 音楽
 - MusicTracks: 曲名
 - Software: ソフトウェア
 - SportingGoods: スポーツ
 - Toys: おもちゃ
 - VHS: VHSビデオ
 - Video: DVD&ビデオ
 - VideoGames: ゲーム
 - HealthPersonalCare: ヘルス&ビューティー
これ、AuthorとかAvailabilityなんかのアクセサが最初から決まった型で扱えればコードは2/3くらいになりそう。
いっそBeautifulSoupとかで作った方が作り手側としては納得が行くのかも。
動いてる物は以下
AWS商品検索
追記
パッチを付けるのを忘れてました。
pyaws.diff
--- pyaws/ecs.py.orig   Mon Apr 09 07:38:57 2007
+++ pyaws/ecs.py    Wed May 07 18:12:21 2008
@@ -19,7 +19,8 @@
 
 
 import os, urllib, string, inspect
-from xml.dom import minidom
+from google.appengine.api import urlfetch
+import pxdom
 
 __author__ = "Kun Xi < kunxi@kunxi.org >"
 __version__ = "0.2.0"
@@ -164,10 +165,7 @@
    """Send the query url and return the DOM
    
    Exception is raised if there is errors"""
-   u = urllib.FancyURLopener(HTTP_PROXY)
-   usock = u.open(url)
-   dom = minidom.parse(usock)
-   usock.close()
+   dom = pxdom.parseString(urlfetch.fetch(url).content)
 
    errors = dom.getElementsByTagName('Error')
    if errors:
@@ -282,7 +280,7 @@
    if(plugins == None):
        plugins = {}
 
-   childElements = [e for e in element.childNodes if isinstance(e, minidom.Element)]
+   childElements = [e for e in element.childNodes if isinstance(e, pxdom.Element)]
 
    if childElements:
        for child in childElements:
@@ -291,7 +289,7 @@
                if type(getattr(rc, key)) <> type([]):
                    setattr(rc, key, [getattr(rc, key)])
                setattr(rc, key, getattr(rc, key) + [unmarshal(child, plugins)])
-           elif isinstance(child, minidom.Element):
+           elif isinstance(child, pxdom.Element):
                if plugins.has_key('isPivoted') and plugins['isPivoted'](child.tagName):
                        unmarshal(child, plugins, rc)
                elif plugins.has_key('isBypassed') and plugins['isBypassed'](child.tagName):
@@ -303,7 +301,7 @@
                else:
                    setattr(rc, key, unmarshal(child, plugins))
    else:
-       rc = "".join([e.data for e in element.childNodes if isinstance(e, minidom.Text)])
+       rc = "".join([e.data for e in element.childNodes if isinstance(e, pxdom.Text)])
    return rc
 
 
pxdomはDOM Level 3をpythonで実装しているすばらしいライブラリです。ココから取得して下さい。
Posted at by



2009/03/04


Perlでジョブキューサーバと言えばTheSchwartzかGearmanといった所ですが、TheSchwartzみたいに不揮発なデータを扱わないのであればGearmanも有用かと思います。
Gearmanのクライアントライブラリ実装には
Gearman - Language Support (Client APIs)
http://www.danga.com/gearman/

Gearman - Client/Worker APIs

The C client/worker API can be found in the same package as the C server (BSD license):

The Perl API can be found as the Gearman::Client and Gearman::Worker modules in CPAN.

The PHP API can be found as Net_Gearman on PEAR.

The Python API can be found on PyPI as “gearman”, and it can be installed with "easy_install gearman".

These are a set of MySQL UDFs that depend on the Gearman server and library in C.

http://www.gearman.org/doku.php?id=download#client_worker_apis
と数種類ありますが、サーバ実装としては
Gearman Job Server (gearmand)
  • Gearman - Gearman server and library (0.3)
  • Gearwoman - Another server written in C
  • Gearman::Server - Gearman::Server - function call "router" and load balancer
http://www.gearman.org/doku.php?id=download#job_server_gearmand
の様に、CによるものとPerlによるものしかありません。Perl版でも良いのですがC版の方が少しでも速いのではないか...という事でgearmandのC版をWindowsにポーティングする事にしました。
ただ、オリジナルのソースは結構UNIX臭く作ってあり差分も大きくなりそうだったので、別の方が実装したGearwomanをベースにWindowsポーティングしてみました。
以下パッチ diff --git a/client.c b/client.c
index ba21f1d..e4154b9 100644
--- a/client.c
+++ b/client.c
@@ -10,7 +10,11 @@ See LICENSE and COPYING for license details.
 #include <stdio.h>
 #include <string.h>
 #include <errno.h>
+#ifndef _WIN32
 #include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
 
 #include <assert.h>
 
diff --git a/common.h b/common.h
index 0a56ad5..204a9d2 100644
--- a/common.h
+++ b/common.h
@@ -12,6 +12,10 @@ See LICENSE and COPYING for license details.
 #define MSG_NOSIGNAL 0
 #endif
 
+#if defined(_WIN32)
+#define MSG_NOSIGNAL 0
+#endif
+
 #ifndef max
 #define max(a,b) (a<b?a:b)
 #define min(a,b) (a<b?a:b)
diff --git a/gearmand.c b/gearmand.c
index 16e21ac..edfe2b0 100644
--- a/gearmand.c
+++ b/gearmand.c
@@ -13,6 +13,7 @@ See LICENSE and COPYING for license details.
 #include <unistd.h>
 #include <fcntl.h>
 #include <time.h>
+#ifndef _WIN32
 #include <netinet/in.h>
 #include <getopt.h>
 #include <arpa/inet.h>
@@ -21,6 +22,12 @@ See LICENSE and COPYING for license details.
 #include <sys/signal.h>
 #include <sys/types.h>
 #include <sys/stat.h>
+#else
+#include <getopt.h>
+#include <signal.h>
+#include <ws2tcpip.h>
+#define in_addr_t unsigned long
+#endif
 
 #include <event.h>
 #include <glib.h>
@@ -48,7 +55,8 @@ GHashTable *g_workers   = NULL; /* maps functions -> list of worker clients (GPt
 int g_foreground = 1;
 char *g_logfilename = "gearmand.log";
 char *g_bind = "0.0.0.0";
-int g_port = 4730;
+/* int g_port = 4730; */
+int g_port = 7003;
 char g_handle_base[MAX_HANDLE_LEN];
 
 void work_fail(Job *job);
@@ -72,7 +80,14 @@ int listen_on(in_addr_t addr, int port)
     int i = 1;
     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&i, sizeof(i));
     // setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&i, sizeof(i));
+#ifndef _WIN32
     fcntl(sock, F_SETFL, O_NONBLOCK);
+#else
+    {
+        unsigned long flags = 1;
+        ioctlsocket(sock, FIONBIO, &flags);
+    }
+#endif
 
     sin.sin_addr.s_addr = addr;
     sin.sin_port        = htons(port);
@@ -822,7 +837,14 @@ void listener_cb(int fd, short events, void *arg)
 
     int s = accept(fd, (struct sockaddr *)&sin, &addrlen);
    
+#ifndef _WIN32
     fcntl(s, F_SETFL, O_NONBLOCK);
+#else
+    {
+        unsigned long flags = 1;
+        ioctlsocket(s, FIONBIO, &flags);
+    }
+#endif
 
     Client *cli = client_new();
     cli->state = CLIENT_STATE_CONNECTED;
@@ -871,8 +893,12 @@ void logger(const gchar *domain, GLogLevelFlags level, const gchar *message, gpo
 {
     struct tm dt;
     time_t tme = time(NULL);
-    localtime_r(&tme, &dt);
     char str[64], *lvl = "OTHER";
+#ifndef _WIN32
+    localtime_r(&tme, &dt);
+#else
+    memcpy(&dt, localtime(&tme), sizeof(dt));
+#endif
 
     strftime(str, 64, "%F %T", &dt);
     switch(level) {
@@ -954,6 +980,7 @@ void signal_cb(int fd, short event, void *arg)
 
 void detach()
 {
+#ifndef _WIN32
     if (fork() != 0)
         exit(0);
 
@@ -977,6 +1004,10 @@ void detach()
     open("/dev/null", O_RDWR, 0);   /* 0 stdin */
     dup2(0, 1);  /* 1 stdout */
     dup2(0, 2);  /* 2 stderr */
+#else
+    perror("daemon mode is disabled on win32 ...");
+    exit(0);
+#endif
 }
 
 /****************************************************************************
@@ -986,6 +1017,11 @@ int main(int argc, char *argv[])
     int nsockets = 0;
     struct event listeners[10];
 
+#ifdef _WIN32
+    WSADATA wsaData;
+    WSAStartup(MAKEWORD(2, 0), &wsaData);
+#endif
+
     parseargs(argc, argv);
 
     if (g_foreground == 0) {
@@ -1009,8 +1045,11 @@ int main(int argc, char *argv[])
     event_init();
     //printf("%s %s\n", event_get_version(), event_get_method());
 
+#ifndef _WIN32
     signal(SIGPIPE, SIG_IGN);
+#endif
 
+#ifndef _WIN32
     struct event sig_int, sig_hup, sig_term;/*, sig_pipe;*/
     if (g_foreground) {
         event_set(&sig_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_int);
@@ -1025,6 +1064,17 @@ int main(int argc, char *argv[])
     event_add(&sig_term, NULL);
     /*event_set(&sig_pipe, SIGPIPE, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_pipe);
     event_add(&sig_pipe, NULL);*/
+#else
+    struct event sig_int, sig_term;/*, sig_pipe;*/
+    if (g_foreground) {
+        event_set(&sig_int, SIGINT, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_int);
+        event_add(&sig_int, NULL);
+    } else {
+        signal(SIGINT, SIG_IGN);
+    }
+    event_set(&sig_term, SIGTERM, EV_SIGNAL|EV_PERSIST, signal_cb, &sig_term);
+    event_add(&sig_term, NULL);
+#endif
 
     int s = listen_on(inet_addr(g_bind), g_port);
     if (s == -1) {
diff --git a/job.c b/job.c
index 812226b..9a28043 100644
--- a/job.c
+++ b/job.c
@@ -9,7 +9,11 @@ See LICENSE and COPYING for license details.
 #include <stdio.h>
 #include <string.h>
 #include <errno.h>
+#ifndef _WIN32
 #include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
 
 #include <assert.h>
 
diff --git a/util.c b/util.c
index a482011..6135c9a 100644
--- a/util.c
+++ b/util.c
@@ -9,7 +9,11 @@ See LICENSE and COPYING for license details.
 #include <stdio.h>
 #include <string.h>
 #include <errno.h>
+#ifndef _WIN32
 #include <sys/socket.h>
+#else
+#include <winsock2.h>
+#endif
 
 #include <assert.h>
 
ここでひとつ問題が起こりました。libeventです。libeventのWindowsポーティングとしてはmemcached for Win32が有名で、その配布物にlibeventのソースとバイナリがあります。これをmingw32からリンクしようかと思ったのですが、どうやらWindowsのバッファオーバーフローチェックライブラリbufferoverflow.libをリンクしているらしく__security_cookieが無いよとエラーが出ました。
しかしながら諦めず、libeventのオフィシャルからlibevent-1.4.9-stableをダウンロードし、以下の様にlibeventをWindowsに移植しました。
% cat > config.h
#define VERSION "1.4.9"
#define HAVE_STRTOLL 1
^D

% gcc -c -DHAVE_CONFIG_H -Icompat -Iwin32-code -I. event.c log.c signal.c evutil.c WIN32-Code/win32.c
% ar cr libevent.a event.o log.o signal.o evutil.o win32.o
% cp libevent.a c:/mingw/lib/.
% mkdir c:/mingw/include/libevent
% cp ev*.h c:/mingw/include/libevent/.
あとはgearwomanのMakefileです。
--- Makefile    2009-03-04 09:42:20.000000000 +0900
+++ Makefile.w32    2009-03-04 15:52:29.765625000 +0900
@@ -3,7 +3,7 @@
 CC = gcc
 LD = gcc -o
 AR = ar
-LDFLAGS = `pkg-config --libs gthread-2.0` -levent
+LDFLAGS = `pkg-config --libs gthread-2.0` -levent -lws2_32
 CFLAGS = -Wall
 
 # # Debug
@@ -12,10 +12,10 @@
 # OPTIMIZATIONS =
 
 # Production (NDEBUG = NO DEBUG / remove asserts)
-CPPFLAGS += -Wall `pkg-config --cflags glib-2.0` -DNDEBUG
+CPPFLAGS += -Wall `pkg-config --cflags glib-2.0` -DNDEBUG -Ic:/mingw/include/libevent
 OPTIMIZATIONS = -O2 -funroll-loops -finline-functions
 
-BIN =   gearmand
+BIN =   gearmand.exe
 
 OBJ =   gearmand.o      \
         client.o        \
拡張子と、libeventへのパス指定だけです。
この状態でmingw32-makeすると、Windowsネイティブ(cygwin未使用)なgearmand.exeが出来上がります。

さて実際にはどれくらい速いのか...
Perl版をポート番号7003で、Native版をポート番号7004で動作させ use strict;
use Gearman::Worker;

my $gw = Gearman::Worker->new;
$gw->job_servers('127.0.0.1:7004');
#$gw->job_servers('127.0.0.1:7003');
$gw->register_function(
    'sum' => sub {
        my ( $lhs, $rhs ) = split /,/, shift->arg;
        $lhs + $rhs;

    }
);
$gw->work while 1;
ワーカーとしては数値を足すだけの物をそれぞれのポートで起動しました。
クライアントは接続、ジョブ登録500回、処理実行という流れを30回行うベンチマークで検証してみました。
use strict;
use Benchmark;
use Gearman::Client;



timethese(
    30,
    {
        'perl gearmand'   => '&test("127.0.0.1:7003");',
        'native gearmand' => '&test("127.0.0.1:7004");',
    }
);

sub test {
    my $server = shift;

    my $gc = Gearman::Client->new;
    $gc->job_servers($server);
    my $ts = $gc->new_task_set;
    for my $i ( 1 .. 500 ) {
        $ts->add_task(
            "sum" => "3,4",
            {
                on_complete => sub {
                    #print "$i:" . ${ $_[0] } . "\n";
                  }
            }
        );
    }
    $ts->wait;
}
実行結果は Benchmark: timing 30 iterations of native gearmand, perl gearmand...
native gearmand: 19 wallclock secs (11.88 usr +  1.08 sys = 12.95 CPU) @  2.32/s (n=30)
perl gearmand: 30 wallclock secs (13.25 usr +  1.17 sys = 14.42 CPU) @  2.08/s (n=30)
の様になり、若干ですがネイティブ版の方が速い様です。

クライアント・ワーカーライブラリにもC言語の物を使うのであれば、クライアント・ワーカー・サーバ全てでPerlを必要とする事なくGearmanが使える様になりますね。

まぁWindowsでネイティブなGearman使いたいなんて変態は私しかいないかもしれませんが、ご参考になれば...
Posted at by



2009/02/26


IO::Lambdaを見てて、おーいいねー、と思ってサンプル動かしたらエラー出た。
追ってくと、IOなんて根底のモジュールに原因がある事が分かった。原因っていうかエラーが出るように仕込んであった。
以下パッチ作ってオフィシャルにメールした。
--- IO.xs.orig  2006-03-26 11:27:13.000000000 +0900
+++ IO.xs   2009-02-24 20:16:34.921875000 +0900
@@ -121,7 +121,12 @@
     }
     return RETVAL;
 #else
+#  ifdef WIN32
+   unsigned long flags = block;
+   return ioctl(PerlIO_fileno(f), FIONBIO, &flags);
+#  else
     return -1;
+#  endif
 #endif
 }
 
もしかしたらFAQなpatchで蹴られるだろうけど...

追記 2009/02/26
patchがマージされました。
Posted at by



2009/02/24


memcachedサーバへのアクセスモジュールは数ありますが、一般的にはlibmemcachedが使われる事が多いと思います。
Perlにおいても
  • Cache::Memcached
  • Cache::Memcached::Fast
  • Cache::Memcached::libmemcached
  • Memcached::libmemcached
と数種類存在し、一般的に使用されるlibmemcachedのラッパインタフェースであるCache::Memcached::libmemcachedが使われる事が多い様に思います。
libmemcachedに関しては、以前Windowsへのポーティングを行い、オフィシャルへのパッチ送付も行いました。
成果物としてはcodereposに置いてあります。オフィシャルからもリンクを張って頂けるようになりました。
さらにCache::Memcached::Fastについても、以前Windowsへのポーティングを行い、Cache::Memcached::Fast version 0.13に取り込まれました。
つまり特殊な事をせずにWindowsから利用出来る高速なPerlのmemcachedクライアントライブラリとしてはCache::Memcached::Fastになります。
ちなみこのCache::Memcached::Fast、実はlibmemcachedと比較しても格段に速く、tokuhiromさんが取ったベンチマークでも素晴らしい結果を叩き出してくれています。

さて今日は、このCache::Memcached::Fastが内部で使用しているXSクライアントモジュールを使用して、高速にmemcachedにアクセスする物を作ってみたいと思います。
このCache::Memcached::FastのXSコードは、Perlに依存した部分とPerlに依存していない部分で分けられており、その後者は一般的なC言語のソースから呼び出しが可能になっています。
なぜこのCache::Memcached::Fastが速いかと言うと、libmemcachedの様に逐次送信を行っているのではなくwritev(2)を使った一括送信を行っているからです。またCache::Memcached::Fastはselect(2)ではなくpoll(2)を使っている為、FD_SETの設定を毎回行わなくて良いというのも微量ではありますが影響しているのではないかと思っています。
このクライアントモジュールは、libmemcachedの様に単一取得(memcached_get)や複数取得(memcached_mget)のAPI呼び出し時に逐次送受信されるのではなく、client_prepare_get/client_prepare_setを使った前準備方式を使っています。これにより複数の問い合わせに対しても内部では一括送信してくれ、一括で受信してくれます。libmemcachedは複数の問い合わせに対してそれぞれ結果待ちをし、全ての問い合わせが完了した時点で制御が戻ります。これについてはMEMCACHED_BEHAVIOR_NO_BLOCKを使用する事でよく似た動作をする事が出来ます。
このクライアントモジュールを使用した実際のサンプルコードは以下の様になります。
/* fast memcached client using client module of Cache::Memcached::Client.
 *   compile   : gcc -o a.exe foo.c libclient.a
 *   for win32 : gcc -o a.exe foo.c libclient.a -lws2_32
 */

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"

static void* alloc_value(value_size_type value_size, void **opaque) {
    *opaque = (char *) malloc(value_size + 1);
    memset(*opaque, 0, value_size + 1);
    return (void *) *opaque;
}

static void free_value(void *opaque) {
    free(opaque);
}

static void result_store(void *arg, void *opaque, int key_index, void *meta) {
    char* res = (char*) opaque;
    if (res) *(char**)arg = strdup(res);
    else     *(char**)arg = strdup("");
}

int main(int argc, char* argv[]) {
    struct client* c = NULL;
    const char* host = "127.0.0.1";
    const char* port = "11211";
    const char* key = "foo";
    const char* value = "bar";
    struct result_object object;
    char* result = NULL;
    int ret;

    /* initialize client module of Cache::Memcached::Fast */
    c = client_init();

    /* add memcached server */
    client_add_server(c, host, strlen(host), port, strlen(port), 1.0, 1);

    /*--- set function --------------------------------*/
    /* initialize staff context for set */
    object.alloc = NULL;
    object.store = result_store;
    object.free = NULL;
    object.arg = NULL;
    client_reset(c, &object, 1);

    /* prepare for set function */
    printf("setting value of '%s' as '%s'\n", key, value);
    client_prepare_set(c, CMD_SET, 0, key, strlen(key), 0, 0, value, strlen(value));

    /* execute set function */
    client_execute(c);
    /*-------------------------------------------------*/

    /*--- get function --------------------------------*/
    /* initialize staff context for get */
    object.alloc = alloc_value;
    object.store = result_store;
    object.free = free_value;
    object.arg = &result;
    client_reset(c, &object, 0);

    /* prepare for get function */
    printf("getting value of '%s'\n", key);
    client_prepare_get(c, CMD_GET, 0, key, strlen(key));

    /* execute set function */
    client_execute(c);
    /*-------------------------------------------------*/

    printf("result value of '%s' is '%s'\n", key, result);

    free(result);

    return 0;
}
少し変わったコードになりますが、メモリの確保から開放まで自分でハンドリングでき、自前の構造を使った処理も行えるかと思います。
なお、libmemcachedとCache::Memcached::Fastのクライアントモジュールでget/setを繰り返すベンチマークを取ってみました。
まずはlibmemcachedのコード
#include <winsock2.h>
#include <memcached.h>
#include <stdio.h>

#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT 11211
#define KEY "foo"
#define VALUE "bar"

int main(void) {
    memcached_return rc;
    memcached_st *memc;
    char* value;
    int value_length = 0;
    int flags = 0;
    int n;

    memc = memcached_create(NULL);
    memcached_server_add(memc, SERVER_NAME, SERVER_PORT);
    for (n = 0; n < 30000; n++) {
        memcached_set(memc, KEY, strlen(KEY), VALUE, strlen(VALUE), 0, 0);
        value = memcached_get(memc, KEY, strlen(KEY), &value_length, &flags, &rc);
    }
    memcached_free(memc);
    return 0;
}
次にCache::Memcached::Fast(CMF)のクライアントモジュールのコード
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"

#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT "11211"
#define KEY "foo"
#define VALUE "bar"

static void* alloc_value(value_size_type value_size, void **opaque) {
    *opaque = (char *) malloc(value_size + 1);
    memset(*opaque, 0, value_size + 1);
    return (void *) *opaque;
}

static void free_value(void *opaque) {
    free(opaque);
}

static void result_store(void *arg, void *opaque, int key_index, void *meta) {
    char* res = (char*) opaque;
    if (res) *(char**)arg = strdup(res);
    else     *(char**)arg = strdup("");
}

int main(int argc, char* argv[]) {
    struct client* c = NULL;
    struct result_object object_set, object_get;
    char* result = NULL;
    int n;

    c = client_init();
    client_add_server(c, SERVER_NAME, strlen(SERVER_NAME), SERVER_PORT, strlen(SERVER_PORT), 1.0, 1);

    object_set.alloc = NULL;
    object_set.store = result_store;
    object_set.free = NULL;
    object_set.arg = NULL;
    object_get.alloc = alloc_value;
    object_get.store = result_store;
    object_get.free = free_value;
    object_get.arg = &result;

    for (n = 0; n < 30000; n++) {
        client_reset(c, &object_set, 1);
        client_prepare_set(c, CMD_SET, 0, KEY, strlen(KEY), 0, 0, VALUE, strlen(VALUE));
        client_execute(c);
        client_reset(c, &object_get, 0);
        client_prepare_get(c, CMD_GET, 0, KEY, strlen(KEY));
        client_execute(c);
    }

    free(result);

    return 0;
}
計測結果は

libmemcached: 6.953125

CMF: 5.984375

となりました。get/setのループだけなのにCMFは速いですね。
Perlに依存していないので、通常アプリケーションでも問題なく使えるかと思います。
memcachedと通信するC言語で作ったプログラムのパフォーマンスが悪いと思われたならば、一度試して見られてはどうでしょうか?
Posted at by




なんかlibmemcachedはMLにパッチ送っても反応無いし、Cache::Memcached::Fastの方が速いという噂なので、Cache::Memcached::FastをWindowsに移植してみた。
http://svn.coderepos.org/share/lang/perl/Cache-Memcached-Fast-0.12/
オリジナルからの差分は svn diff -r19958
で取得して下さい。
リポジトリを作成し直しました。最新取得しなおして下さい。
チェックアウトしてそのままビルドして頂いてもいいです。libmemcachedはgccとVisual Studioをサポートしましたが、今回はgcc限定にしました。
よろしければ、どうぞ。
Posted at by



2009/02/23


WassrのAPIには、imageパラメータによる画像アップロードがあるのですが、これに困ってる人がいたので、人助け。
Wassr [お気軽メッセージングハブ・ワッサー]

質問です。Firefox の Live HTTP Headers で確認すると Web のフォームから画像をアップロードすると素のバイナリデータが送信されています。API を利用した POST でも素のバイナリデータで送信しなければならないのでしょうか?

http://wassr.jp/channel/wassr_api/messages/hNHcDCCtja

試しにvimperatorから出来ないか挑戦してたらハマってハマって...色々試してたんですが、どうも画像だけがアップロードされない。
送信データを以下の様な関数で保存して目で確認してみたけど、あってる。
function writeToFile (filePath, content) {
    try {
        netscape.security.PrivilegeManager.enablePrivilege ('UniversalXPConnect');
        var file = Components.classes["@mozilla.org/file/local;1"].createInstance (Components.interfaces.nsILocalFile);
        file.initWithPath (filePath);
        if (! file.exists ()) file.create (0, 0664);
        var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance (Components.interfaces.nsIFileOutputStream);
        out.init (file, 0x20 | 0x02, 00004, null);
        out.write(content, content.length);
        out.close();
    } catch (e) {
        throw e;
    }
}
で、最後の最後は自前でCGIを立ててデータを確認してみた所、なんとデータが欠けてる!!

もしかしてsendじゃ送れないんじゃないか?と思ったらXMLHttpRequestにsendAsBinaryなんてメソッドがあった。
XMLHttpRequest - MDC
This method was added in Firefox 3.

A variant of the send() method that sends binary data.

void sendAsBinary(
  in DOMString body
);
Parameters

body

The request body as a DOM string. This data is converted to a string of single-byte characters by truncation (removing the high-order byte of each character).

https://developer.mozilla.org/en/XMLHttpRequest#sendAsBinary%28%29
はよ言えや!

見事送信する事が出来ました。
function createMultipartData(data, boundary) {
    var ret = [], boundary = "BOUNDARY-" + (boundary||'')+Math.floor(0x1000000 + Math.random() * 0xffffff).toString(16);
    for (var item in data) {
        if (!data.hasOwnProperty(item)) continue;
        var obj = data[item];
        if (obj.filename) {
            ret.push(
                '--' + boundary,
                'Content-Type: '+obj.contentType,
                'Content-Length: '+obj.content.length,
                'Content-Disposition: form-data; name="'+item+'"; filename="'+obj.filename+'"',
                'Content-Transfer-Encoding: binary',
                '',
                obj.content
            );
        } else {
            ret.push(
                '--' + boundary,
                'Content-Disposition: form-data; name="'+item+'"',
                '',
                String(obj)
            );
        }
    }
    ret.push('--' + boundary + '--', '');
    return {
        contentType : "multipart/form-data; boundary=" + boundary,
        content: ret.join("\n")
    };
}


function getFileContents(aFilePath) {
    var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
    file.initWithPath(aFilePath);
    var fstream = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream);
    fstream.init(file, 1, 1, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
    var bstream = Components.classes['@mozilla.org/network/buffered-input-stream;1'].createInstance(Components.interfaces.nsIBufferedInputStream);
    bstream.init(fstream, 4096);
    var binary = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
    binary.setInputStream(fstream);

    var data = "", size = 0;
    while ((size = binary.available()) > 0) {
        data += binary.readBytes(size);
    }
    return data;
}

var sendData = createMultipartData({
    status: "image upload from vimperator!",
    source: "vimperator/wassr.js",
    image: {
        filename: "example.jpg",
        contentType: "application/octet-stream",
        content: getFileContents("C:\\example.png")
    }
});

var req = new XMLHttpRequest();
req.open("POST", "http://api.wassr.jp/statuses/update.json");
req.setRequestHeader("Content-Type", sendData.contentType);
req.setRequestHeader("Content-Length", sendData.content.length);
req.sendAsBinary(sendData.content);
こんなんでどうでしょうか?
てかFirefox2の時、どうしてたの?苦笑
Posted at by



2009/02/18


先ほど試していたtw2ですが、少しやる気になってoutputzの設定もすませて動かしてみました。
ただ、中で使っているreadlineはimportした瞬間にbuilt-inのraw_input()を乗っ取ってしまい、かつ完全にマルチバイトに対応しきれていない様で、入力後の文字が化け化けになってしまいました。
おそらく、入力をutf-8で想定した作りになっているんだと思いますが、これはおそらく解決までに時間が掛かりそうな気がしています。
なのでpatchを書いてa2cさんにpull requestしました。
まれにutf-8しかない文字をtwitter/wassrに書き込んでくれる人がいるので、良いテストになりました。
raw_input()の変わりに、sys.stdin.readline()を使うようにしてあります。
diff --git a/tw2.py b/tw2.py
index fb04179..98fe90a 100755
--- a/tw2.py
+++ b/tw2.py
@@ -73,6 +73,9 @@ if __name__ == "__main__":
   friends = set()
   friendsTimeLine = []
 
+  def _T(text):
+    return text.encode('mbcs', 'ignore')
+
   def complete(text, status):
     results = [x for x in friends if x.startswith(text)] + [None]
     return results[status]
@@ -90,13 +93,13 @@ if __name__ == "__main__":
     # get twitter friends Replies
     print '\twassr replies\t'
     for data in reversed(wassr.getReplies()):
-      print '%-12s : %s' % (data['user_login_id'] ,data['text'])
+      print _T('%-12s : %s' % (data['user_login_id'] ,data['text']))
       friends.add(data['user_login_id'])
  
     # get twitter friends Replies        
     print '\n\twassr replies\t'
     for data in reversed(twitter.GetReplies()):
-      print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+      print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
       friends.add(data.GetUser().GetScreenName())
     return 0
 
@@ -105,7 +108,7 @@ if __name__ == "__main__":
     # Get FriendsTimeLine
     print ' -----  wassr Friends Time Line  -----'
     for data in reversed(wassr. getTimeline()):
-      print "%-12s: %s" % (data['user_login_id'] ,data['text'])
+      print _T("%-12s: %s" % (data['user_login_id'] ,data['text']))
       twit = "wr[]%-12s: %s" % (data['user_login_id'] ,data['text'])
       if twit in friendsTimeLine:
         pass
@@ -114,7 +117,7 @@ if __name__ == "__main__":
         friendsTimeLine.append(twit)
     print '\n  -----  twitter Friends Time Line  -----'
     for data in reversed(twitter.GetFriendsTimeline()):
-      print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+      print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
       # append log
       twit  = "tw[%s]%-12s: %s" % (data.GetCreatedAt(), data.GetUser().GetScreenName(), data.GetText())
       if twit in friendsTimeLine:
@@ -127,7 +130,8 @@ if __name__ == "__main__":
 
   prompt = '\n cmd: Friendstimeline[f] eXit[xx] \n> '
   while True:
-    input = raw_input(prompt).split(" ")
+    print prompt,
+    input = sys.stdin.readline().decode('mbcs').encode('utf-8').strip().split(" ")
     if input[0] != '':
       if len(input) == 1:
         if len(input[0]) > 2:
Macで動かなかったら御免なさい。
Posted at by




a2cさんが作ったtwitter client「tw2」を動かして見ようと思ったのですが、エラーも出るしpitが起動するエディタに何もテンプレートが出力されないのでコードを追ってみた。
調べた結果tw2ではなくpit側に原因がある事が分かった。
Bug #325139 in web.py: “web.profiler does not work on Windows”

tempfile.NamedTemporaryFile() to generate a temp file. On Windows the file cannot be opened a second time. Using tempfile.mkstemp() instead works but you lose the guaranteed cleanup of the temp file.

https://bugs.edge.launchpad.net/webpy/+bug/325139
どうやらNamedTemporaryFile()を使っていて、かつNamedTemporaryFile()はclose()時にファイルを削除してくれるのだけれど、内部でmkstempを使いfdを貰っているのにそのfdをclose()してくれていないものだからWindowsのハンドルが残りっぱなしになりwrite()もflush()もremove()出来ないと言う物だった。
Windowsでpit.pyを使ったプログラムを起動された事がある方は、一度テンポラリディレクトリの中を綺麗にした方が良いかもしれません。
    (fd, name) = _mkstemp_inner(dir, prefix, suffix, flags)
    file = _os.fdopen(fd, mode, bufsize)
    return _TemporaryFileWrapper(file, name)
つまり import os, yaml, tempfile

t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
result = open(path).read()
t.close()
print result
の様にclose()前だと、fdが開放されていないので"Permission Denited"、また import os, yaml, tempfile

t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
t.close()
result = open(path).read()
print result
の様にclose()後だと、ファイルは消えてしまっているのでopen()出来ない...という事。なにそれ苦笑

上記リンクにある通り、NamedTemporaryFile()の変わりにmkstempを使い、かつremove()前にclose()してやれば動いた。
--- pit.py.orig 2008-06-28 16:58:42.000000000 +0900
+++ pit.py  2009-02-18 17:22:30.734375000 +0900
@@ -18,13 +18,21 @@
         else:
             if not os.environ.has_key('EDITOR'):
                 return {}
-            t = tempfile.NamedTemporaryFile()
             c = yaml.dump(opts['config'] if opts.has_key('config') else Pit.get(name) ,default_flow_style=False)
+
+            fd, path = tempfile.mkstemp()
+            os.close(fd)
+            t = open(path, "w+b")
             t.write(c)
             t.flush()
-            path = os.path.abspath(t.name)
+
             Popen([os.environ['EDITOR'],path]).communicate()
-            result = open(path).read()
+
+            t = open(path)
+            result = t.read()
+            t.close()
+            os.remove(path)
+
             if result == c:
                 print 'No Changes'
                 return profile[name]
@@ -44,7 +52,7 @@
         if opts.has_key('require'):
             flg = False
             keys = set(opts['require'].keys()) - set(ret.keys())
-            if keys:
+            if len(keys):
                 for key in keys:
                     ret[key] = opts['require'][key]
                 ret = Pit.set(name,{'config' : ret})
で、tw2については、outputzのIDがいるらしく面倒臭くなったのでヤメ。また今度試します。

ところでこのパッチ、NamedTemporaryFile()を修正するパッチにすべきか、はたまたpit.pyを修正してyoshioriさんに送ればよいのか...どうしましょうねぇ
Posted at by



2009/02/16


さらに朗報は続く
Google App Engine Blog: The sky's (almost) the limit! "High CPU" is no more.

We're very excited today to announce that we've raised limits on several App Engine operations:

  • No more "High CPU Requests"! App Engine Apps were once allowed no more than 2 CPU-intensive requests per minute. We've made some adjustments to the way we handle requests, and have eliminated this limitation altogether. To learn more about how this works and the implications for your app, see our documentation.
  • Response deadline raised to 30 seconds. The amount of time an App Engine app can take to respond to an incoming request has been raised from 10 to 30 seconds! There are limits on the number of simultaneous active requests an application can process at any given moment--see our docs to learn more.
  • Size limits on code files, static files, and requests/responses raised to 10MB! App Engine apps can now receive requests and send responses of up to 10MB in size, and users can upload 10MB code and static files as well. Note that API requests (e.g. memcache.set(), db.put()) are still limited to 1MB in size.
http://googleappengine.blogspot.com/2009/02/skys-almost-limit-high-cpu-is-no-more.html
Google App Engineに1分あたり2CPU以上負荷を掛ける様なリクエストが許される事になりました。
またレスポンスのデッドラインが10秒から30秒にまで引き上げられました。
さらにコード、静的ファイル、および要求/応答におけるサイズ制限が10MBに引き上げられました(但しmemcache.set()、db.put()はまだ1MBに制限されています)。

これで例えば、デカいクエリを投げる様な処理でもエラーで落ちるって事が少なくなるんじゃないですかね。
もうすぐすると、Google App Engineでタイマ発動処理をサポートするという噂もあるので、よりインタラクティブな処理が作れるんじゃないですかね。
Posted at by



2009/02/14


Luaも動的にメンバが追加出来る言語ですので、データ構造を動的に作り上げる事が出来ます。今日はなんとなく、「LuaでWebScraper作ったら、どんなんになるんだろう...」と思いつきで...
用意するのは
  • LuaからXPathを操作出来るltxml
  • LuaからSocketを操作出来るLuaSocket
あくまでサンプルですので、CSSセレクタも使えなければ最新のWebScraperの様に相対/絶対URL展開や、フィルタ等はサポートしていません。
またresultも動作させていない為、結果が全て戻ります。
さらにltxmlが内部で使っているTinyXML/TinyXPathの仕様からか、XPathの途中に「//」をめり込ます事が出来ませんでした。
まずLua版WebScraperのソース。

luascraper.lua
local http = require("socket.http")
local xml = require("xml")

-- return process structure
function process(t)
  return {name="process",process={xpath=t[1], name=t[2], scraper=t[3]}}
end
-- not supported
function result(p)
  return p
end
-- return scraper structure
function scraper(self)
  self.name = "scraper"
  -- scrape method
  function self.scrape(url, ctx)
    -- create http session and parse HTML
    if ctx == nil then
      local chunk = {}
      local b, c = http.request {
        method = "GET",
        url = url,
        sink = ltn12.sink.table(chunk)
      }
      if not c == 200 then
        return nil
      end
      local html = table.concat(chunk)
      ctx = {op = self, doc = xml.parse(html)}
    end
    -- scraping...
    self.res = {}
    if not ctx.doc then
      return nil
    end
    for k,v in pairs(self) do
      if (type(v) == "table" and v.name == "process") then
        if (type(v.process.scraper) == "table") then
          for k1,v1 in pairs(ctx.doc:select(v.process.xpath)) do
            local newctx = {top=ctx.top, doc=v1}
            self.res[#(self.res)+1] = v.process.scraper.scrape(url, newctx)
          end
        else
          local node = ctx.doc:select(v.process.xpath)
          if node then
            local attr = v.process.scraper
            if attr == "TEXT" then
              self.res[v.process.name] = node[1]:text()
            elseif string.sub(attr, 1, 1) == "@" then
              attr = string.sub(attr, 2)
              self.res[v.process.name] = node[1]:attribute(attr)
            end
          end
        end
      end
    end
    return self.res
  end
  return self
end
案外ボリューム無く書けました。
このモジュールを使って私のtwitter/with_friendsの発言リストをスクレイピングするLuaスクリプトがコレ

twitter_scraper.lua
require "luascraper"

local s = scraper {
  process {'//tr[@class="hentry"]',
    'status', scraper {
      process {'//td[@class="content"]/strong/a', 'nick', 'TEXT'},
      process {'//td[contains(@class,"author")]/a/img', 'name', '@alt'},
      process {'//td[contains(@class,"author")]/a/img', 'image', '@src'},
      process {'//span[contains(@class,"entry-title")]', 'description', 'TEXT'},
      process {'//a[@rel="bookmark"]', 'url', '@href'},
    }},
  result = 'friends';
}

function dump(r)
  print('-')
  for k,v in pairs(r) do
    if type(v) == "table" then
      dump(v)
    else
      print(k,v)
    end
  end
end
r = s.scrape('http://twitter.com/mattn_jp/with_friends/')
dump(r)
そして結果がコレ
image   http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name    Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371889282
description fooo.nameのおかげで自分が入り忘れてるウェブサービスがわかるようになった
nick    negipo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name    ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371888702
description 今見るとシローアマダはかっこ悪いけどアイナサハリンは(ry
nick    mobcov
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33598712/wt_normal.png
name     暴君
url http://twitter.com/VoQn/statuses/371888452
description 授業終わった。やっとご飯食べられる
nick    VoQn
-
image   http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name    Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371887582
description はてなperlグループに入った
nick    negipo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34078402/cat_888_normal.gif
name    (c)yabkoji
url http://twitter.com/yabkoji/statuses/371887022
description 激スイマー襲来中。
nick    yabkoji
-
image   http://s3.amazonaws.com/twitter_production/profile_images/22560082/nobita_normal.jpg
name    kimidora
url http://twitter.com/kimidora/statuses/371886072
description ねむいねむいねむいねむいねむいねむいねむいねむいねむいねむい。。。。。。。。。。ぐぅ *Tw*
nick    kimidora
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371885732
description うぅ、ムラサメで部長とカブった。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371885022
description はてなグループって排他制御あるのかしら。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name    ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371884382
description TextMateアップデートキターーーーー!!
nick    mobcov
-
image   http://s3.amazonaws.com/twitter_production/profile_images/32257122/basara_normal.jpg
name    japo
url http://twitter.com/japo/statuses/371884052
description ハマーン様万歳だろJK
nick    japo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/19383642/ore_normal.jpg
name    Sugano Yoshihisa(E)
url http://twitter.com/koshian/statuses/371883862
description 「あのPerlモジュールなんてったっけ?」とぐぐったら、自分のはてなブックマークがひっかかって見事に発見
nick    koshian
-
image   http://s3.amazonaws.com/twitter_production/profile_images/22022432/plagger_logo_purple_normal.png
name    plagger.org
url http://twitter.com/plagger/statuses/371883772
description Safariの『Webクリップ』を試してみる。 - sta la sta
nick    plagger
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371882682
description うわ、 GFF でペーネロペー出てるのか。超欲しい。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name    Miki@7500
url http://twitter.com/7500/statuses/371881092
description 午後の部にとりかかりまっしゅ!夕方までしゃいなら?ノシ
nick    7500
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371879692
description 初代からν→ F91 まで + アレックスと 08 小隊とデンドロビウムは把握してる。その後はさっぱりだな。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/15059862/al3x_normal.jpg
name    Alex Payne
url http://twitter.com/al3x/statuses/371879502
description Oh, how I wanted to be rid of MacPorts. Curse you, Image/Rmagick.
nick    al3x
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34297902/kamon_normal.png
name    wrongbee
url http://twitter.com/wrongbee/statuses/371878872
description ペーネロペーの画像検索結果
nick    wrongbee
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name    Miki@7500
url http://twitter.com/7500/statuses/371878362
description @
nick    7500
-
image   http://s3.amazonaws.com/twitter_production/profile_images/15927972/otsune_hanaji_purple_normal.jpg
name    ??uns?o ??n??s??
url http://twitter.com/otsune/statuses/371877452
description Pukkaのすばらしい所はdel.icio.usのアカウント切り替えが一瞬でプルダウンメニューで選べる所だ。bookmarkletなんかでソーシャルブックマークしてたらログアウト・ログインの時間分だけ人生を無駄にする lang:ja
nick    otsune
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371876992
description 閃光のハサウェイはガンダムサイドストーリーでは一番好きだ。
nick    smokeymonkey
-
-
Luaでもやれない事はない!

追記
result処理、相対/絶対URL展開処理を加えて、CodeReposに入れておきました。
Posted at by



2009/02/12


hexdigestでなくdigestですね。:-)
mod_access_tokenを入れてみた - まめ畑

ここまでは良かったのですが、Signatureを生成するスクリプトを作ろうと思ったのですが、Rubyで上手くいかない・・・。

http://d.hatena.ne.jp/con_mame/20090209
あとはbase64に付く改行とターミネータをカットすればOKですね。
require 'openssl'
require 'base64'
require 'uri'
require 'time'

access_key = 'foo'
secret = 'bar'
url = 'http://localhost:8080/access_token/example.jpg'
exp = (Time.new+300).to_i.to_s
uri = URI.parse(url)
plain = "GET#{uri.path}#{exp}#{access_key}"
hmac = OpenSSL::HMAC::digest(OpenSSL::Digest::SHA1.new, secret, plain)
sig = Base64.encode64(hmac).chomp.sub(/=*$/, '')
puts "#{url}?Signature=#{URI.escape(sig)}&AccessKey=#{URI.escape(access_key)}&Expires=#{URI.escape(exp.to_s)}"
結果はこんな感じ。
http://localhost:8080/access_token/example.jpg?Signature=Rt2cKnnOYuDiE/XYKTLTaOuY4Wg&AccessKey=foo&Expires=1234417692 ちなみにBase64.b64encodeはエンコード結果を表示してしまうのでencode64が良さそうですね。
  def b64encode(bin, len = 60)
    encode64(bin).scan(/.{1,#{len}}/) do
      print $&, "\n"
    end
  end 
Posted at by



2009/02/10


これは朗報。
Google App Engine Blog: SDK version 1.1.9 Released
  • You can now use the Python standard libraries urllib, urllib2 or httplib to make HTTP requests. This has been a frequent request on our issue tracker.
  • We've been working on a set of tools that will make the process of uploading and downloading data from App Engine applications easier. Today we're excited to announce an early release of our new bulk uploading client. You can try it out here. Let us know what you think in our Google Group!
  • Several updates to our datastore, including the automatic generation of single property indexes and the addition of IN and != operators to db.Query. See the Datastore API docs for more details.
  • A bunch of additional bugfixes and enhancements, listed in our Release Notes.
http://googleappengine.blogspot.com/2009/02/sdk-version-119-released.html
試しに以下の様なコードを書いて import urllib2

f = urllib2.urlopen('http://www.google.com/')
print "Content-Type: text/plain;"
print
print f.read()

実行させてみた。

おー動いてる。これで今までGoogle App Engineのurlfetch API様にパッチを当ててきた物が要らなくなる。
Posted at by



2009/02/09


livedoor 製品で mod_access_token というのが出たみたいです。
livedoor ラボ「EDGE」 開発日誌 : 「mod_access_token」の配布開始と「EDGE src」公開のお知らせ - livedoor Blog(ブログ)

ウェブサイト上の画像やファイルに有効期限を指定して、ユーザーに一時的なダウンロードを許可する、ライブドアで独自開発したApacheモジュールです。このモジュールをApache Webサーバに組み込むことにより、画像やファイルをウェブ上で公開するときに有効期限をつけることができるようになり、Webアプリケーションと組み合わせる事で公開範囲の制御を行なう事が可能になります。

http://blog.livedoor.jp/edge_labs/archives/717201.html
modaccesstoken - Google Code

mod_access_token provides access token based secure downloading.

http://code.google.com/p/modaccesstoken/
ソースコード見たら依存が浅かったのでWindowsでビルドしてみた。
Makefile.w32
APACHE_ROOT=C:\Program Files\Apache Software Foundation\Apache2.2
CFLAGS=/I"$(APACHE_ROOT)\include" /DWIN32 /nologo
LDFLAGS=/LIBPATH:"$(APACHE_ROOT)\lib"
LIBS= libapr-1.lib libaprutil-1.lib libhttpd.lib

all : mod_access_token.so

mod_access_token.so : mod_access_token.obj
    link /nologo /DLL /OUT:$@ /EXPORT:access_token_module mod_access_token.obj $(LDFLAGS) $(LIBS)

mod_access_token.obj : mod_access_token.c
    cl -c $(CFLAGS) mod_access_token.c
なぜかmingw32では実行時にエラーが出たのであきらめました。VC6では可変長マクロが使えないので最終的にはVC8でしか試せませんでした。

ビルドしたモジュールを C:\Program Files\Apache Software Foundation\Apache2.2\modules\ に置き、httpd.confへLoadModuleを追加。対象のフォルダに以下の様に設定(.htaccess)します。
.htaccess
AccessTokenCheck On
AccessTokenAccessKey foo
AccessTokenSecret bar
このAccessTokenAccessKey(foo)が公開鍵、AccessTokenSecret(bar)が秘密鍵になります。
認証はREADMEに書かれている通り
download.pl
use strict;
use URI;
use Digest::HMAC_SHA1;

my $access_key = 'foo';
my $secret = shift || die('specify secret key!');
my $exp = time + 300; # 5minutes
my $url = 'http://localhost:8080/access_token/example.jpg';
my $uri = URI->new( $url );
my $plain = sprintf '%s%s%s%s', 'GET', $uri->path, $exp, $access_key;
my $hmac = Digest::HMAC_SHA1->new( $secret );
$hmac->add( $plain );
my $sig = $hmac->b64digest;
$uri->query_form({
    Signature => $sig,
    AccessKey => $access_key,
    Expires => $exp,
});
printf "%s\n", $uri->as_string;
といった感じ。キーが間違ってるとDECLINED(Forbidden)になります。
内部はSHA1による認証処理になってます。

誰ですか!「WindowsなんてマイナーなOSのことは知りません」とか言ってるの!(謎)

追記
パッチを当てないと動かなかったのを忘れてました。
Index: mod_access_token.c
===================================================================
--- mod_access_token.c  (revision 3)
+++ mod_access_token.c  (working copy)
@@ -2,10 +2,12 @@
 #include "httpd.h"
 #include "http_config.h"
 #include "http_protocol.h"
+#include "http_request.h"
 #include "http_log.h"
 #include "ap_config.h"
 #include "apr_sha1.h"
 #include "apr_strings.h"
+#include "apr_base64.h"
 #include "apr_lib.h"
 
 #define ACCESS_KEY_NAME "AccessKey"
Posted at by



2009/01/27


よく検索エンジンなんかでtwitterの発言が引っかかった時、@付きの会話があり話の前後を辿ってみたりする事があります。
非常にめんどくさいですね。

例えば先日あったkuさんとの会話の一部が見つかったとしましょう。
http://twitter.com/ku/status/1136652052
twitter-20090127
http://twitter.com/ku/status/1136652052
この発言から会話の前後を辿るには、時刻が記述されたリンクをポチポチとクリックして行くしかなく、とても大変な手間になります。

これを簡単に知る方法はないか...あります。

twitterは検索機能を持っているのですが、この結果に会話の前後を辿る機能があるのです。試しに「mattn_jp ku」という検索語で検索し、結果にある「Show Conversation」という部分をクリックしてみましょう。
twitter-search-result-20090127
綺麗に会話のツリーが出ますね。この「Show Conversation」は、search.twitter.com の http://search.twitter.com/search/thread/{ステータスID} からのHTML出力結果をインライン表示している処理になっています。
つまり、上記の
http://twitter.com/ku/status/1136652052
から会話のツリーを得るには
http://search.twitter.com/search/thread/1136652052
にアクセスすれば良い事が分かります。
但しインライン用に用意されたHTMLなのでCSSも付きませんし、ドメインが twitter.com ではなく、search.twitter.com なので twitter が使っている jQuery で get メソッド呼び出しする事も出来ません。
ここはいたし方無くGrasemonkeyと行きましょう。twitterのタイムラインのHTMLにはmicroformatと共に返信があるかどうかを示すクラス属性値「reply」が付いています。これが付いているステータスのみ「Show Conversation」を付け、クリックしたらインラインで会話を表示する物を作ってみました。
twitter-show-conversation
タイムラインに「Show Conversation」と付いているリンクをクリックすると、前後の会話が表示されます。何気に便利かもしれません。
ソースはこの辺においておきます。
宜しければどうぞ。
Posted at by



2009/01/21


このサイトでは、jQuery Lightbox Plugin は"balupton edition"を入れているのですが、jQueryのバージョンを1.3に上げた途端、エラーが出る様になってしまいました。
調べた所、jQuery1.3ではSizzleという新しいセレクタが採用されており、以下の様な属性フィルタが動かない事が分かりました。
$('[@rel*=foo]')
さらに調べた所、どうやら"@"を付けている事自体が間違っているらしく"@"を取って $('[rel*=foo]')
としてやれば動く様になりました。なぜオリジナルの"@"付きのまま動いていたのかは分かりませんが...
本体への反映は以下
--- jquery.lightbox.js.orig 2008-09-12 02:46:50.000000000 +0900
+++ jquery.lightbox.js  2009-01-21 21:15:37.312500000 +0900
@@ -827,7 +827,7 @@
            var groups_n = 0;
            var orig_rel = this.rel;
            // Create the groups
-           $.each($('[@rel*='+orig_rel+']'), function(index, obj){
+           $.each($('[rel*='+orig_rel+']'), function(index, obj){
                // Get the group
                var rel = $(obj).attr('rel');
                // Are we really a group
ま、balupton edition使ってる人少ないかも知れませんが...
Posted at by



2009/01/20


Greasemonkeyから扱いやすくなった。
Gist for Greasemonkey - GitHub

We’re now appending the gist name at the end of its raw url. That means it’s dead-simple to serve greasemonkey (or greasekit) scripts directly from gist.github.com. I was able to write my first script and install it in less than five minutes: ...

http://github.com/blog/302-gist-for-greasemonkey
今までのraw形式URLの後に、ファイル名が付与される様になった。
これにより
gist の GM を簡単にインストールする方法 - 8時40分が超えられない - subtech

あー、いちいち raw format 叩かなくても
http://gist.github.com/00000
なスクリプトなら
http://gist.github.com/00000.txt?.user.js
とするだけでおkなのか。

http://subtech.g.hatena.ne.jp/secondlife/20090116/1232083225
といったhackを使わずGreasemonkeyスクリプトを直接インストール出来る様になる。
embedならばview rawから直接インストール出来る様になった。

gist++
Posted at by



2009/01/16


今日、とある場所でkanaさんからvimに"syn-include"なんて物があるのを教えて貰いました。

:he syn-include
9. Including syntax files                               *:syn-include* *E397*

It is often useful for one language's syntax file to include a syntax file for
a related language.  Depending on the exact relationship, this can be done in
two different ways:

        - If top-level syntax items in the included syntax file are to be
          allowed at the top level in the including syntax, you can simply use
          the |:runtime| command: >

  " In cpp.vim:
  :runtime! syntax/c.vim
  :unlet b:current_syntax

<       - If top-level syntax items in the included syntax file are to be
          contained within a region in the including syntax, you can use the
          ":syntax include" command:

:sy[ntax] include [@{grouplist-name}] {file-name}

          All syntax items declared in the included file will have the
          "contained" flag added.  In addition, if a group list is specified,
          all top-level syntax items in the included file will be added to
          that list. >

   " In perl.vim:
   :syntax include @Pod <sfile>:p:h/pod.vim
   :syntax region perlPOD start="^=head" end="^=cut" contains=@Pod
<
          When {file-name} is an absolute path (starts with "/", "c:", "$VAR"
          or "<sfile>") that file is sourced.  When it is a relative path
          (e.g., "syntax/pod.vim") the file is searched for in 'runtimepath'.
          All matching files are loaded.  Using a relative path is
          recommended, because it allows a user to replace the included file
          with his own version, without replacing the file that does the ":syn
          include".
知らんかった...
これを使えばPerlのPodだけ別のsyntaxファイルから適用出来るといった物。
これは凄い!

つまりはregionだけ決められれば、その部分に言語毎のsyntaxが適用出来る事になる。試しに、はてなのスーパーPre記法に色を付けられる様にしてみた。
ベースはmotemenさんのhatena-vim
motemen's hatena-vim at master - GitHub

Vim scripts for posting/updating hatena diary/group

http://github.com/motemen/hatena-vim/tree/master
このsyntax/hatena.vimの最下行に以下のコードを貼り付ける。
" append to syntax/hatena.vim
function SyntaxSuperPre()
  let lnum = 1
  let lmax = line("$")
  let mx = '^>|\(.*\)|$'
  while lnum <= lmax
    let curline = getline(lnum)
    if curline =~ mx
      let lang = substitute(curline, mx, '\1', '')
      exec 'runtime! syntax/'.lang.'.vim'
      unlet b:current_syntax
      let syntaxfile = fnameescape(substitute(globpath(&rtp, 'syntax/'.lang.'.vim'), '[\r\n].*$', '', ''))
      if len(syntaxfile)
        exec 'syntax include @inline_'.lang.' '.syntaxfile
        exec 'syn region hatenaSuperPre matchgroup=hatenaBlockDelimiter start=+^>|'.lang.'|$+ end=+^||<$+ contains=@inline_'.lang
      endif
    end
    let lnum = lnum + 1
  endwhile
  " workaround for perl
  syn cluster inline_perl remove=perlFunctionName
endfunction
call SyntaxSuperPre()
適当なので使わないで下さい
perlFunctionNameはregionが広すぎるので無効にしてます
すると...
hatena super pre on vim
おーーー!出ました。

続きはこの辺でやっていきます。出来上がったらmotemenさんにmergeして貰うのも良いかも。
ちなみに、filetype適用時にロードしているので、pre記法の言語を編集途中で変更したり、新しくスーパーPre記法を追加したりするとsyntaxが適用されなくなります。ま、これからですな。

syn-include++
Posted at by



2009/01/14


追記
事情が変わった!(髭男爵)
変わってなかった...orz
詳細は下記。

おもしろい。
static - 素人がプログラミングを勉強するブログ
var counter = function () {
  var static = /(^o^)/;
  return ('i' in static)? ++static.i:
    static.i = 0;
};

console.log(counter()); // 0
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
http://d.hatena.ne.jp/javascripter/20090113/1231863436
正規表現リテラルがコンパイル時に生成され、静的保持される特性を利用したカウンタ。

でも使わない。気持ちワルイ!苦笑
皆、思いつくだろうけど私はやっぱりこう書く。
var counter = (function() {
  var static = 0;
  return function() {return static++};
})();

ちなみにベンチを取ってみた。
if (typeof console == 'undefined') console = {log:print};

var counter1 = function () {
  var static = /(^o^)/;
  return ('i' in static)? ++static.i:
    static.i = 0;
};

var counter2 = (function() {
  var static = 0;
  return function() {return static++};
})();

var start;
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter1();
console.log(new Date().getTime() - start)

start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter2();
console.log(new Date().getTime() - start)
Windows XP、P4 3GHz CPU、1G Mem。

tracemonkey : JavaScript-C 1.8.0 pre-release 1 2007-10-03
6515
5500

v8 : V8 version 0.3.4 (internal)
1558
383

ま、やっぱりね...というところ。
結論
V8はえーーー!

追記 os0xさんから
var counter1 = function () { var static = /(^o^)/; return ++static.i || (static.i=0); }; こうすれば差は縮まる http://la.ma.la/blog/diary_200705301141.htm
とブックマークコメントを貰った。
counter3としてベンチに追加した所、結果が変わった
if (typeof console == 'undefined'{
  if (typeof print == 'function') console = {log:print};
  else if (typeof WScript != 'undefined') console = {log:function(s){WScript.StdOut.WriteLine(s)}};
  else console = {log:alert};
}

var counter1 = function () {
  var static = /(^o^)/;
  return ('i' in static)? ++static.i:
    static.i = 0;
};

var counter2 = (function() {
  var static = 0;
  return function() {return static++};
})();

var counter3 = function () {
  var static = /(^o^)/; return ++static.i || (static.i=0);
};

var start;
start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter1();
console.log("counter1:" + (new Date().getTime() - start))

start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter2();
console.log("counter2:" + (new Date().getTime() - start))

start = new Date().getTime();
for(var n = 0; n < 10000000; n++) counter3();
console.log("counter3:" + (new Date().getTime() - start))
さらにベンチにwindows scripting hostも足してみた。

tracemonkey : JavaScript-C 1.8.0 pre-release 1 2007-10-03
counter1:1624
counter2:381
counter3:529

v8 : V8 version 0.3.4 (internal)
counter1:1610
counter2:403
counter3:665

cscript : Microsoft (R) Windows Script Host Version 5.6
counter1:107282
counter3:44016
counter2:51297

おぉぉぉぉぉぉぉぉぉぉ!!!!なんと
正規表現オブジェクトの方が速いではないか!!!
詳細はos0xさんか何方かが書いてくれるとして...
失礼しました!!!
上の結果が正しいです。

結論
windows scripting hostおせーーー!

Posted at by




追記

POSIX では明確にソケットの最大値とはうたってはいないものの、Linux の実装を見ても最大値と扱う方が良い様です。また Winsock では select(2) の第一引数は無視されるようです。

C言語でソケットを使うプログラミングを行う際、ソケットディスクリプタがシグナル状態かを調べる方法としてselect(2)があります。
使い方は int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock, &rfds);

r = select(1, &rfds, NULL, NULL, NULL);
といった感じ。ここでselect(2)の第一引数に渡している値は、ディスクリプタ集合rfdsの内、いくつ検証するかを指す値。つまりrfdsに対してFD_ZERO/FD_CLRしてからFD_SETした回数となります。
ちなみに戻り値は、ディスクリプタ集合の内どれだけシグナル状態かの数が返ります。つまり r = select(num_fds, &rfds, &wfds, &efds, NULL);
の場合、rfds/wfds/efdsの内、シグナル状態であるディスクリプタの総数が返ります。

昔のC言語で書かれたソースを見ると、よく int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock, &rfds);

r = select(FD_SETSIZE, &rfds, NULL, NULL, NULL);
FD_SETSIZEを使って書かれた記述を見ます。これはrfdsが構造体であり、そのメンバに持つディスクリプタ格納配列fd_arrayFD_SETSIZEでサイズ定義されている事を利用している為です。
これを無駄と考える人がいた為か、こういう記述も未だに良く見かけます。
int r;
fd_set rfds;

FD_ZERO(&rfds);
FD_SET(sock1, &rfds);
FD_SET(sock2, &rfds);
int check_sock = max(sock1, sock2);

r = select(max(sock1, sock2), &rfds, NULL, NULL, NULL);
確かにFD_SETSIZEの理屈から言えば納得が行く話かも知れませんが、FD_SETSIZEの定義とはfd_arrayの個数でありディスクリプタの値が取り得る値の最大値ではありません。ディスクリプタが順列で生成されるなんて仕様もありません。
つまり、間違いです。

にも関わらず、ディスクリプタの最大値はFD_SETSIZEだと思わせる記述が出回ってしまったのだと思います。
Manpage of SELECT_TUT

nfds: 全ての集合に含まれるファイルディスクリプタのうち、値が最大のものに 1 を足した整数である。すなわち、ファイルディスクリプタを集合に加える作業の途中で、全てのファイルディスクリプタを見て最大値を求め、それに 1 を加えて nfds として select に渡さないといけない、ということだ。

http://www.linux.or.jp/JM/html/LDP_man-pages/man2/select_tut.2.html
おそらく初期のディスクリプタの実装が配列の添え字であった為、こういう記述となり残っていったのだと思う。

例えばWindowsで言えば、socket(7)関数で返るディスクリプタの値はFD_SETSIZEに収まらない値で返ります。またMinGW(Minimalist GNU for Windows)のwinsock.hで定義されているFD_SETSIZEは64と中途半端な値になっています。
それはなぜか...
適当だからです。

通常、1プロセスが扱えるディスクリプタの数が設定されるべきですが、それが明確に定義すべきでない環境では意味のない値になったのだと思います。
しかしながら、多数のディスクリプタ(ファイルもソケットも)を扱うプログラムならば64個使い切ってしまう事はあり得りえるでしょうね。
ではどうすれば良いか。FD_SETSIZEfd_arrayの個数を定義するマクロであり、FD_ZERO/FD_CLR/FD_SETでそれを操作する再に用いられる閾値であり、ループ回数なのです。さらにFD_ZERO/FD_CLR/FD_SETFD_SETSIZEと同じくマクロなのです。 #define FD_CLR(fd,set) do { u_int __i;\
for (__i = 0; __i < ((fd_set *)(set))->fd_count ; __i++) {\
       if (((fd_set *)(set))->fd_array[__i] == (fd)) {\
       while (__i < ((fd_set *)(set))->fd_count-1) {\
               ((fd_set*)(set))->fd_array[__i] = ((fd_set*)(set))->fd_array[__i+1];\
               __i++;\
       }\
       ((fd_set*)(set))->fd_count--;\
       break;\
       }\
}\
} while (0)
しかもFD_SETSIZEの定義は#ifdefにより使い手側が変更出来る様になっています。もし通常よりも多くディスクリプタを扱いたいならばコンパイル時にFD_SETSIZEを定義してやれば良いのです。
この事は、MSDNにも書いてあります。
select Function (Windows) http://msdn.microsoft.com/en-us/library/ms740141.aspx

Four macros are defined in the header file Winsock2.h for manipulating and checking the descriptor sets. The variable FD_SETSIZE determines the maximum number of descriptors in a set. (The default value of FD_SETSIZE is 64, which can be modified by defining FD_SETSIZE to another value before including Winsock2.h.)

ディスクリプタ集合をを操作/チェックするためにヘッダーファイルWinsock2.hに4つのマクロが定義されています。FD_SETSIZEはディスクリプタ集合の最大個数を記述子の最大数を決定します。 (FD_SETSIZEの初期値は64です。これはwinsock2.hをインクルードする前に別の値で変更する事が出来ます。)


但し、このFD_SETSIZEが少ない値のままコンパイルされたライブラリと、多く設定した値のライブラリを併用すると場合によっては誤動作する可能性があるので注意が必要です。まぁこれはUNIXでも同じ話ですね。

だらだら書きましたが、何を言いたいかというと

UNIXで開発していて、将来的にWindowsにも移植するかもしれないソフトウェアならば、ディスクリプタの値がディスクリプタ集合の最大個数である...といった様なコーディングは辞めましょう。

という事です。
Posted at by



2009/01/07


つい勢いでやった。今は反省している。
やる夫で学ぶCooking&AAstory 第0章 http://d.hatena.ne.jp/lionfan/20090105#1231170925
pygtk-aaview
#! /usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os
import gtk
import pango
import urllib
from BeautifulSoup import BeautifulSoup

win = gtk.Window()
win.connect('destroy', gtk.main_quit)
win.set_default_size(800, 600)
win.set_title('やる夫で学ぶCooking&AAstory 第0章')

table = gtk.Table(1, 2, False)
win.add(table)

swin = gtk.ScrolledWindow()
swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

l = gtk.Label('')
l.set_alignment(0.5, 0.5)
fonts = ['Mona 10', 'MS UI Gothic 10']
for fn in fonts:
  try:
    font = pango.FontDescription(fn)
    if font:
      l.modify_font(font)
      break
  except:
    pass
swin.add_with_viewport(l)

table.attach(swin,
  # X direction           Y direction
  0, 1,                   0, 1,
  gtk.EXPAND | gtk.FILL,  gtk.EXPAND | gtk.FILL,
  0,                      0)

b = gtk.Button('次へ')
def button_clicked(widget):
  if len(ascii_arts) == 0:
    gtk.main_quit()
    return
  elif len(ascii_arts) == 1:
    b.set_label('閉じる')
  text = ''.join(ascii_arts.pop(0).findAll(text=True))
  l.set_text(text)

b.connect('clicked', button_clicked)

table.attach(b,
  # X direction           Y direction
  0, 1,                   1, 2,
  gtk.EXPAND | gtk.FILL,  0,
  0,                      0)

html = urllib.urlopen('http://d.hatena.ne.jp/lionfan/20090105#1231170925').read().decode('euc-jp', 'ignore')
soup = BeautifulSoup(html, convertEntities='html')
ascii_arts = soup.findAll('div', { 'class' : 'ascii-art'})
b.emit('clicked')

win.show_all()

try:
  gtk.main()
except KeyboardInterrupt:
  gtk.main_quit()
追記
第1章も作った...というかタイトルとURL変えただけだが...
#! /usr/bin/python
# -*- coding: utf-8 -*-
import sys
import os
import gtk
import pango
import urllib
from BeautifulSoup import BeautifulSoup

win = gtk.Window()
win.connect('destroy', gtk.main_quit)
win.set_default_size(800, 600)
win.set_title('やる夫で学ぶCooking&AAstory 第1章')

table = gtk.Table(1, 2, False)
win.add(table)

swin = gtk.ScrolledWindow()
swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)

l = gtk.Label('')
l.set_alignment(0.5, 0.5)
fonts = ['Mona 10', 'MS UI Gothic 10']
for fn in fonts:
  try:
    font = pango.FontDescription(fn)
    if font:
      l.modify_font(font)
      break
  except:
    pass
swin.add_with_viewport(l)

table.attach(swin,
  # X direction           Y direction
  0, 1,                   0, 1,
  gtk.EXPAND | gtk.FILL,  gtk.EXPAND | gtk.FILL,
  0,                      0)

b = gtk.Button('次へ')
def button_clicked(widget):
  if len(ascii_arts) == 0:
    gtk.main_quit()
    return
  elif len(ascii_arts) == 1:
    b.set_label('閉じる')
  text = ''.join(ascii_arts.pop(0).findAll(text=True))
  l.set_text(text)

b.connect('clicked', button_clicked)

table.attach(b,
  # X direction           Y direction
  0, 1,                   1, 2,
  gtk.EXPAND | gtk.FILL,  0,
  0,                      0)

html = urllib.urlopen('http://d.hatena.ne.jp/lionfan/20090106#1231249409').read().decode('euc-jp', 'ignore')
soup = BeautifulSoup(html, convertEntities='html')
ascii_arts = soup.findAll('div', { 'class' : 'ascii-art'})
b.emit('clicked')

win.show_all()

try:
  gtk.main()
except KeyboardInterrupt:
  gtk.main_quit()
Posted at by




im.kayac.comに送信するperlスクリプト。
コードだけ。
#!/usr/bin/perl
use strict;
use warnings;

use Encode;
use LWP::UserAgent;
use HTTP::Request::Common qw(POST);
use Config::Pit;
use Digest::SHA1 qw(sha1_hex);
use JSON;

if ($^O eq 'MSWin32') {
    eval {
        require Win32::API;
        Win32::API->Import('kernel32', 'UINT GetACP()');
        Encode::from_to($ARGV[0], 'cp'.GetACP(), 'utf-8');
    };
}

my $config = pit_get("im.kayac.com", require => {
        "username" => "your username on im.kayac.com",
        "password" => "your password(or secretkey) on im.kayac.com",
        "authtype" => "auth type on im.kayac.com: none/password/secretkey",
    });

my %data = ( message => shift );
die "should be specify message" unless $data{message};
$data{password} = $config->{password} if $config->{authtype} eq "password";
$data{sig} = sha1_hex($data{message} . $config->{password}) if $config->{authtype} eq "secretkey";

my $ua = LWP::UserAgent->new;
my $res = from_json($ua->post(
    "http://im.kayac.com/api/post/$config->{username}", \%data,
    "Content-Type" => "application/x-www-form-urlencoded"
)->decoded_content);

print $res->{result}.$res->{error};
Posted at by



2009/01/06


WindowsでGitを使う場合、msysGitというMSYS(Minimal SYStem)ポーティングされたGitを使うのですが、これにはssh-askpassが付いてきません。以前書いた「Big Sky :: Windowsでもssh-agentとssh-addを使ってパスフレーズ入力を省略する。」でご紹介した方法は、開いたコマンドプロンプト内でしか有効になりません。これは新しく起動したコマンドプロンプトに、ssh-agentから引き渡されるSSH_AGENT_PID/SSH_AUTH_SOCKといった環境変数が引き継がれていないのが原因です。これを解決する方法のひとつとして、win-ssh-askpassという物があります。
win-ssh-askpass 1.05 (GANAware)

GUI ssh-agent for cygwin.

http://www.ganaware.jp/archives/2006/04/winsshaskpass_1.html
win-ssh-askpass バージョンアップについてあれこれ (GANAware)

ずいぶん久しぶりに win-ssh-askpass をバージョンアップしました。

http://www.ganaware.jp/archives/2006/04/winsshaskpass_2.html
このwin-ssh-askpassは、ssh-agent起動時に得た環境変数情報をWindowsのユーザ環境変数に登録してくれ、次回起動するコマンドプロンプトでもパスフレーズを聞かれなくするという物です。UNIXだとx11-ssh-askpassとか、ssh-askpass-gnomeというGUIバージョンも存在しますが、これのWindows版という所ですね。
但し、win-ssh-agentはcygwinを使っており、cygwin配下でunix domain socketを使った通信を行っています。unix domain socketを使わず実装されているmsysGitでは動かないのです。他に、puttyのpagent/plinkを使った方法もありますが私が使いたい条件
  • OpenSSH for windows
  • msysGit付属のbashではなく、コマンドプロンプト
  • cygwinは使いたくない
には適応出来なかったりします。
という事で、win-ssh-askpass-1.05のcygwin依存を無くすパッチを書いてみました。
気を付ける所は
  • setenv/putenvの置き換え
  • fork/execからsystemへ
だけです。
以下パッチ diff -u win-ssh-askpass-1.05.orig/agent.cpp win-ssh-askpass-1.05/agent.cpp
--- win-ssh-askpass-1.05.orig/agent.cpp 2006-04-02 10:42:24.000000000 +0900
+++ win-ssh-askpass-1.05/agent.cpp  2009-01-06 17:21:35.953125000 +0900
@@ -2,8 +2,10 @@
 #include <stdlib.h>
 #include <string.h>
 #include <string>
+#ifdef __CYGWIN__
 #include <sys/cygwin.h>
 #include <sys/wait.h>
+#endif
 #include <unistd.h>
 #include <list>
 #include <stdarg.h>
@@ -56,6 +58,7 @@
 // run ssh-add
 int run_ssh_add(const char *i_identityFile)
 {
+#ifdef __CYGWIN__
   pid_t childPID = fork();
   setenv("SSH_ASKPASS", getAskpassPath().c_str(), 1);
   verbose("export SSH_ASKPASS=%s\n", getAskpassPath().c_str());
@@ -95,6 +98,25 @@
       else
    return -1;
   }
+#else
+  std::string str;
+
+  str = "SSH_ASKPASS=";
+  str += getAskpassPath().c_str();
+  putenv(str.c_str());
+  verbose("export SSH_ASKPASS=%s\n", getAskpassPath().c_str());
+
+  str = "DISPLAY=localhost:0";
+  putenv(str.c_str());
+  verbose("exec ssh-add %s\n", i_identityFile ? i_identityFile : "");
+  if (i_identityFile && !i_identityFile[0])
+    i_identityFile = NULL;
+  str = "ssh-add ";
+  if (i_identityFile) str += i_identityFile;
+  str += " < NUL 2> NUL";
+  verbose(str.c_str());
+  system(str.c_str());
+#endif
 }
 
 // http://sourceware.org/ml/cygwin/2006-02/msg00289.html
@@ -118,6 +140,7 @@
       /* Convert POSIX to Win32 where necessary */
       if (!strcmp(var, "PATH") ||
      !strcmp(var, "LD_LIBRARY_PATH")) {
+#ifdef __CYGWIN__
    winpathlist = (char *)
      malloc(cygwin_posix_to_win32_path_list_buf_size(val) + 1);
    if (winpathlist) {
@@ -125,12 +148,19 @@
      SetEnvironmentVariable(var, winpathlist);
      free(winpathlist);
    }
+#else
+   SetEnvironmentVariable(var, val);
+#endif
       } else if (!strcmp(var, "HOME") ||
         !strcmp(var, "TMPDIR") ||
         !strcmp(var, "TMP") ||
         !strcmp(var, "TEMP")) {
+#ifdef __CYGWIN__
    cygwin_conv_to_win32_path(val, winpath);
    SetEnvironmentVariable(var, winpath);
+#else
+   SetEnvironmentVariable(val, winpath);
+#endif
       } else {
    SetEnvironmentVariable(var, val);
       }
@@ -144,7 +174,11 @@
 void run_program(char **i_argv)
 {
   char win32_pathname[1024];
+#ifdef __CYGWIN__
   cygwin_conv_to_win32_path(i_argv[0], win32_pathname);
+#else
+  strcpy(win32_pathname, i_argv[0]);
+#endif
   setup_win_environ();
  
   if (i_argv[1])
@@ -386,7 +420,7 @@
           NULL, NULL, g_hInst, this);
    
     // show tasktray icon
-    std::memset(&m_ni, 0, sizeof(m_ni));
+    memset(&m_ni, 0, sizeof(m_ni));
     m_ni.cbSize = sizeof(m_ni);
     m_ni.uID    = ID_TaskTrayIcon;
     m_ni.hWnd   = m_hwndTaskTray;
@@ -513,7 +547,13 @@
       verbose("DISPLAY=%s\n", getenv("DISPLAY"));
    
     std::string askpassPath(getAskpassPath());
+#ifdef __CYGWIN__
     setenv("SSH_ASKPASS", askpassPath.c_str(), 1);
+#else
+    std::string str("SSH_ASKPASS=");
+    str += askpassPath;
+    putenv(str.c_str());
+#endif
     writeRegistry(HKEY_CURRENT_USER, "Environment", "SSH_AGENT_PID",
          getenv("SSH_AGENT_PID"));
     verbose("export SSH_AGENT_PID=%s\n", getenv("SSH_AGENT_PID"));
diff -u win-ssh-askpass-1.05.orig/misc.cpp win-ssh-askpass-1.05/misc.cpp
--- win-ssh-askpass-1.05.orig/misc.cpp  2002-09-23 03:32:39.000000000 +0900
+++ win-ssh-askpass-1.05/misc.cpp   2009-01-06 17:18:43.656250000 +0900
@@ -1,5 +1,7 @@
 #include "misc.h"
+#ifdef __CYGWIN__
 #include <sys/cygwin.h>
+#endif
 
 // instance
 HINSTANCE g_hInst = NULL;
@@ -74,9 +76,16 @@
 // get the path of win-ssh-askpass.exe
 std::string getSelfPath()
 {
+#ifdef __CYGWIN__
   char win32_pathname[1024];
   GetModuleFileName(NULL, win32_pathname, NUMBER_OF(win32_pathname));
   char posix_pathname[1024];
   cygwin_conv_to_posix_path(win32_pathname, posix_pathname);
   return posix_pathname;
+#else
+  char win32_pathname[1024], *ptr = win32_pathname;
+  GetModuleFileName(NULL, win32_pathname, NUMBER_OF(win32_pathname));
+  while (*ptr++) if (*ptr == '\\') *ptr = '/';
+  return win32_pathname;
+#endif
 }
これを適応すると、mingw32だけでビルド出来る様になります(mingw32-make)。なお、cygwinでビルドすればこれまで通りのモジュールが出来上がります。
ビルド出来たら、win-ssh-agentを起動します。すると
win-ssh-askpass
の様に入力ダイアログが表示されます。ここにパスフレーズを入力すれば、後は次回から起動するコマンドプロンプトで
win-ssh-askpass-console
と環境変数が引き継がれる事になります。
Big Sky :: Windowsでもssh-agentとssh-addを使ってパスフレーズ入力を省略する。」で書いた、setxがあればwin-ssh-agentを使わなくても美味く行くのですが、コマンドプロンプトが出ないので少しカッコよくなりました。
これをWindowsのスタートアップに入れておけば初回にパスフレーズを入力した後は、sshパスフレーズを聞かれる事無くsshが行える様になります。

win-ssh-agent++

よろしければどうぞ。
というか私くらいしか、必要ないか...
Posted at by