2011/07/28


最近earthquakeという、rubyで書かれていて端末上で動作するtwitterクライアントをWindowsで動かそうと色々弄ってます。作者のjugyoさんにコミットビットも貰ってwork-on-windowsブランチで作業してます。earthquake側の修正はだいたいイケてるはずなんですが、問題はreadlineという行編集ライブラリで問題が発生。
ちゃんと書くと、C言語で書かれたreadlineをwrapしているreadlineモジュールじゃなくて、rubyinstallerに標準添付されたPure RubyScriptなモジュール。中身はUNIXのコードとWindowsのコードが入り乱れていて、rbreadline.rbなんか8686行もある大作。

まぁPure RubyScriptでCの真似事をしようってんだからこうなるよね...って感じ。
ただバイト長とキャラクタ数と、文字幅の扱いが間違ってて、Windows-31JなDBCSなんかではちゃんと動かない。パッチ書いて「問題があるんだよ。気付いて!」ってつもりでpull request送ったらいきなりIssue trackerでcloseされてカチンと来たので「その態度はいかがなもんかと思う」的なコメントをした。そしたら「まずテストを書け」との事だったので書いた。

しかしながら、上記の間違いを直すとしてreadlineの正しい動きをテストするって難しい。関数単体なら出来るけどUIのテストはどの言語でも苦しむ。おまけにreadlineはreadline()メソッドを呼び出してる最中は、テスターが止まってしまう。終えるにはユーザの入力が必要。
じゃぁユーザの入力を横取りしてやんよ!って事で、rl_get_char()をぶんどる事にした。
module RbReadline
  #...

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end
こんな感じにRbReadline.rl_get_char()をMix-inで上書きしてやる。こうすれば、外からキー入力を差し替えられる。 RbReadline.rl_get_char = [""""""""""]
buf = Readline.readline("")
これさえ出来れば、文字入力させて最後のカーソル位置が正しい位置にいる事を確認出来る。カーソルの位置はGetConsoleScreenBufferInfo()で得られるのでWin32API使ってゴリゴリ取った。また、最終的な入力結果が文字化けしていない事を確認する為にReadConsoleOutputCharacter()も使った。
全体のコードは以下の様になった。
# encoding: CP932
require 'test/unit'
require 'rb-readline'
require 'Win32API'

module RbReadline
  @GetStdHandle = Win32API.new("kernel32","GetStdHandle",['L'],'L')
  @hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE)
  @GetConsoleScreenBufferInfo = Win32API.new("kernel32","GetConsoleScreenBufferInfo",['L','P'],'L')
  @ReadConsoleOutputCharacter = Win32API.new("kernel32","ReadConsoleOutputCharacter",['L','P','L','L','P'],'I')

  alias :old_rl_get_char :rl_get_char
  def rl_get_char(*a)
    c = old_rl_get_char(*a)
    c.force_encoding("ASCII-8BIT"if c
    @last_xy = xy
    return (c || EOF)
  end

  def rl_get_char=(val)
    for c in val.reverse
      _rl_unget_char(c)
    end
  end

  def last_xy
    @last_xy
  end

  def last_xy=(val)
    @last_xy = val
  end

  def xy
    csbi = 0.chr * 24
    @GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi)
    [csbi[4,2].unpack('s*').first, csbi[6,4].unpack('s*').first]
  end

  def get_line(l)
    line = 0.chr * 80
    length = 80
    coord = l << 16
    num_read = ' ' * 4
    @ReadConsoleOutputCharacter.Call(@hConsoleHandle,line,length,coord,num_read)
    line.force_encoding("Windows-31J")
  end

  module_function :old_rl_get_char:rl_get_char:"rl_get_char="
end

class TestReadline < Test::Unit::TestCase

  def setup
    Readline::HISTORY << "世界".force_encoding("ISO-8859-1")
    Readline::HISTORY << "abc".force_encoding("ISO-8859-1")
    Readline::HISTORY << "bcdef".force_encoding("ISO-8859-1")
    RbReadline.rl_get_char = []
    RbReadline.last_xy = RbReadline.xy
    puts
  end

  def test_cursor_position_normal
    a = RbReadline.xy
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 2, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_mix
    a = RbReadline.xy
    buf = Readline.readline("$$$")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_single_width
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""b""c""d""e"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_double_width
    a = RbReadline.xy
    RbReadline.rl_get_char = [""""""""""]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 12, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_previous_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_next_history
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340P"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 7, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_history_include_multibyte
    a = RbReadline.xy
    RbReadline.rl_get_char = ["a""\340H""\340H""\340H"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 6, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
  end

  def test_cursor_position_insert_into
    a = RbReadline.xy
    RbReadline.rl_get_char = ["""""""\340K""\340K""a"]
    buf = Readline.readline("")
    b = RbReadline.last_xy
    assert_equal 5, b[0] - a[0]
    assert_equal 0, b[1] - a[1]
    assert_equal "$あaいう"RbReadline.get_line(b[1])
  end
end
僕がpull requestしたpatchもtest_cursor_position_insert_intoはNG出るのでこれを直していこうと思う。
Posted at by



2011/07/20


なんだか楽しそう。
Amon2::Liteでmarkdownその他のリアルタイムプレビュー - すぎゃーんメモ

Amon2::Liteでmarkdownその他のリアルタイムプレビュー Perl Amon2::Lite というモジュールを Amon2 に添付してみました。 - TokuLog 改メ tokuhir...

http://d.hatena.ne.jp/sugyan/20110720/1311146296
こういうの皆で共有したいよねと思ったので最近話題のPaaS、fluxflexにデプロイしてみた。
fluxflex

Easy One-Click Install for Web Applications You can install various OSS in a second just with one-cl...

http://www.fluxflex.com
http://text-converter.fluxflex.com
ちょっとハマった点が、fluxflexにはcpanm等による自動インストール機能が無い事。自前でextlibみたいなのに放り込めばいいんだけど、これがなかなか面倒くさい。適当なcgiを書いてどのモジュールの読み込みで失敗しているか調べた結果でいろいろ添付してます。
正直、要らない物もあがってるかもしれません。
とりあえず動いたので、負荷かけない程度に遊んで下さい。

一応CGIのコード貼っておきます。
#!/usr/bin/perl
use strict;
use warnings;

use lib qw( ../lib );
use Amon2::Lite;
use Encode 'encode_utf8';
use Text::Markdown 'markdown';
use Text::Xatena;
use Pod::Simple::XHTML;
use Plack::Handler::CGI;

$ENV{REQUEST_METHOD} ||= 'GET';
$ENV{SCRIPT_NAME} ||= $0;
$ENV{PATH_INFO} ||= '/';

my $converters = {
    markdown => sub {
        my $text = shift;
        return markdown($text);
    },
    xatena => sub {
        my $text = shift;
        return Text::Xatena->new->format($text);
    },
    pod => sub {
        my $text = shift;
        my $parser = Pod::Simple::XHTML->new;
        $parser->html_header('');
        $parser->html_footer('');
        $parser->output_string(\my $html);
        $parser->parse_string_document($text);
        return $html;
    },
};

get '/' => sub {
    my ($c) = @_;
    return $c->render('index.tt');
};

post '/preview' => sub {
    my ($c) = @_;
    my $converter = $converters->{$c->req->param('format')};
    my $html = $converter ? $converter->($c->req->param('text')) : '';
    return $c->create_response(200, ['Content-Type' => 'text/plain'], [encode_utf8($html)]);
};

Plack::Handler::CGI->new->run(__PACKAGE__->to_app);
あと、頭の方でやってるENVの初期値設定は、これが無いとエラーで動かなかった為。この辺は後でフィードバックしておきます。
dotcloudの時もそうでしたが、__DATA_トークンにあったテンプレートはtmpl/index.ttに配置して動作させています。

おまけで、Text::Xatenaの出力を少し変えて(codeというclass属性をprettyprintに変更)、google code prettifyによる色付け機能を足してあります。
#ちょさん!変更出来る様にして下さい!

SuprePre記法で遊んで下さい!
Posted at by



2011/07/19


なんか呼ばれたけど、勉強しとかなくちゃ答えるにも答えれないだろうなと思ったので、ちょっとくらいは勉強しておこうとAmon2でgyazoを書いてdotcloudにpushしてみた。
まず # amon2-setup.pl --flavor=Lite Gyazo
として雛形を作る。
POSTハンドラを書く。
post '/' => sub {
    my $c = shift;
    my $imagedata = $c->req->param('imagedata');
    $imagedata = read_file($c->req->uploads->{imagedata}->pathbinmode => ':raw'unless $imagedata;
    my $filename = "image/" . md5_hex($imagedata) . ".png";
    write_file($filename, {binmode => ':raw'}, $imagedata);
    my $url = $c->req->base() . $filename;
    return $c->create_response(200, ['Content-Type' => 'text/plain'], [$url]);
};
セッション使わないのでプラグイン読み込み処理をカットして、Plack::Middleware::Staticでimageディレクトリを見える様にした。あとdotcloudで動かす為に__DATA__トークンに書かれているindex.ttをtmpl/index.ttに移した。
app.psgiの全体コードはこんな感じ。
use strict;
use warnings;
use File::Spec;
use File::Basename;
use File::Slurp;
use Digest::MD5 qw( md5_hex );
use lib File::Spec->catdir(dirname(__FILE__), 'extlib''lib''perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;
use Amon2::Lite;

# put your configuration here
sub config {
    +{
    }
}

get '/' => sub {
    my $c = shift;
    return $c->render('index.tt');
};

post '/' => sub {
    my $c = shift;
    my $imagedata = $c->req->param('imagedata');
    $imagedata = read_file($c->req->uploads->{imagedata}->pathbinmode => ':raw'unless $imagedata;
    my $filename = "image/" . md5_hex($imagedata) . ".png";
    write_file($filename, {binmode => ':raw'}, $imagedata);
    my $url = $c->req->base() . $filename;
    return $c->create_response(200, ['Content-Type' => 'text/plain'], [$url]);
};

# for your security
__PACKAGE__->add_trigger(
    AFTER_DISPATCH => sub {
        my ( $c$res ) = @_;
        $res->header'X-Content-Type-Options' => 'nosniff' );
    },
);

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^(?:/static/|/robot\.txt$|/favicon.ico$|/image/)},
        root => File::Spec->catdir(dirname(__FILE__));
    enable 'Plack::Middleware::ReverseProxy';

    __PACKAGE__->to_app();
};
最後にdotcloud.ymlに www:
    type: perl
    requirements:
        File::Slurp
        Digest::MD5
        Amon2
を書いてgitでcommitした後に # dotcloud push mattn すれば、あとはdotcloudが自動で依存物をワンサカワンサカ入れてくれて、動くようになる。(mattnというのはdotcloud createで作った際のapplication。上記wwwはservice)
できあがったサーバはこれ

なんか知らない間にhttp://gyazo.mattn.dotcloud.comみたいなURLで公開出来なくなっちゃったので、ひとまず我慢します。

最後に一言

それAmon2じゃなくてもおk
Posted at by