D-7 <altijd in beweging>

Day to day life of a Perl/Go/C/C++/whatever hacker. May include anything from tech, food, and family.

タグ:plack

さきほどの記事に関してtokuhiromに指摘されたのでRPC::XMLにしてみた。基本的にはここに書いてある事と同じ
で自分がちょっと変更した部分は以下の通り:

  1. XMLRPCで prefix.method_nameって呼んでた名残があるので、prefixをとっぱらっちゃう
  2. $q->argsが返すのは RPC::XML::simple_type とかの値なので、これを直してあげないとXMLRPC::Liteで動いてたコードが動かない
  3. @ret = () で RPX::XML::resposeを作ろうとするとundefを返してくるので、その後の $content->as_stringがこける。レガシーコードを期待しているクライアント側との兼ね合いもあるので@retが空だったら [] を渡してあげる

use strict;
use Plack::Builder;
use Plack::Request;
use RPC::XML;
use RPC::XML::ParserFactory 'XML::LibXML';

my $app = sub {
    my $req = Plack::Request->new(@_);
    my $q = RPC::XML::ParserFactory->new()->parse($req->content);
    my $method_name = $q->name;

    # (1) 
    $method_name =~ s/^prefix\.//; 

    my $code = $webapp->can($method_name);
    if (! $code) {
        return [
            404,
            [ "Content-Type" => "text/plain" ],
            [ "RPC method $method_name not found" ]
        ];
    }

    # (2)
    my @ret = $webapp->$code( map { $_->value } @{$q->args} );

    # (3)
    my $content = RPC::XML::response->new( @ret ? @ret : [] );
    return [
        200,
        [ "Content-Type" => "text/xml" ],
        [ $content->as_string ]
    ];
};

builder {
    enable 'ContentLength';
    $app;
};

    このエントリーをはてなブックマークに追加 mixiチェック

諸事情によりレガシーなXMLRPC::LiteベースのプログラムをPlack上にポートすることになったので色々悪戦苦闘してみた。とりあえず普通に動かすのは辛い、ということはなんとなくわかったので色々やった結果、こんな感じのコードを書けばとりあえずディスパッチは動くようになった。もしベターな方法があったら教えて下さい(XMLRPC::Lite以外のベターなライブラリでもOK!)

(追記:指摘を受けたのでRPC::XMLで書き直してみた

use strict;
use Plack::Builder;
use CGI::Emulate::PSGI;
use IO::String;
use XMLRPC::Transport::HTTP;

my $xmlrpc = XMLRPC::Transport::HTTP::CGI->dispatch_to( "MyApp" );

builder {
    my $code = CGI::Emulate::PSGI->handler(sub {
        $xmlrpc->handle;
    });
    sub {
        my $env = shift;
        my $io = $env->{'psgi.input'};
        my $content = do { local $/; <$io> };

        $env->{'psgi.input'} = IO::String->new($content);
        delete $env->{'psgix.io'};
        local %ENV;
        $code->($env);
    };
};
psgi.inputに関してはPOSTデータを読み込むのにSOAP::Lite側で Plack::TempBufferをsysreadしようとしていて、そうすると必ず 0 を返すのですな。open()で作れる疑似ファイルハンドルやなんかでお茶を濁そうとがんばって見たのだけど、結局IO::Stringのようにtieしないと動かないという結論に

なんで local %ENVしてるのかは忘れちゃった。

というわけでこうして動くサーバーは作れたのだけど、なんか他に良い方法あるのかなー
    このエントリーをはてなブックマークに追加 mixiチェック

*追記有り。

今Markdownで紙用の原稿書いているんだけど・・・

Markdownって楽だけど、実際にフォーマッティングがどんな感じになるかを見るには1回パースして処理する必要があるじゃないですか。いちいちMarkdown.pl呼び出すのも面倒くさいからPlackでディレクトリ直下のファイルをパースしてはき出すようにした。

Plackはなにげにこういう使い方が異常に便利な気がする。まさにSwiss Army Knifeって感じ。
use strict;
use File::Spec;
use Text::MultiMarkdown 'markdown';

sub {
    my $env = shift;

    my $file = File::Spec->catfile(
         File::Spec->curdir, $env->{PATH_INFO});
    open my $fh, '<', $file or die "Failed to open $file: $!";

    [ 
        200, 
        [ "Content-Type" => 'text/html; charset=utf-8' ],
        [
            <<EOM,
<html>
<head>
    <style type="text/css">
    <!--
        body {
            font-family: Halvetica, sans-serif;
            padding-left: 2em;
        }
        pre { padding-left: 2em }
    -->
    </style>
</head>
<body>
EOM
            markdown(do { local $/; <$fh> }), 
            <<EOM,
</body>
</html>
EOM
        ]   
    ]   
};

*追記

全く持ってめんどくせぇけど、PHPのほうがいいじゃないか、ってのはすげぇ違うので一応書いておく。


まず誤解を生まないように書くと、CGI環境で動くようなのもText::MultiMarkdownさえ動く環境ならどこでもすぐできますし、Plackがあろうとなかろうとら昔からこれくらいできますね。

でもCGIだとこれをサーバーにアップロードしないといけないじゃない。俺はローカルディスクに入ってるファイルをちょこちょこ見ながら編集したいだけなんだよね。だから書いたの。というわけでPHPだから云々は話が違う。「RoRだったら・・・」だったら分かるけど。


ついでなのでPlack版も組み替えましたよ。別に汚くてもリリースするわけじゃないし、いずれにせよ5分で実用的なものがかけたからいいと思うけどね。ちなみにpsgiじゃなくてもPlack::App::WrapCGI使えばさっきのCGIファイル使えるよ!(自分もそれでテストしたのに忘れてた)

    このエントリーをはてなブックマークに追加 mixiチェック

まだちゃんと使ってないから問題点とかわからないけど、とりあえずStarmanを使うとかで結構スケールできる気がする。そのあたりを自由に変えられるのもPlack++だね。

使用想定としては、これをplackupしておいて、クライアント AnyEvent::HTTPで http_postして非同期で応答待ちする感じ。

package Lyra::Server::Worker;
use Moose;
use Router::Simple;
use JSON::XS;
use namespace::autoclean;

use constant NO_SUCH_WORKER =>
    [ 404, [ "Content-Type" => "application/json" ],
        [ q|{ "status" => 0, "message" => "no such worker" }| ] ];
use constant BAD_PAYLOAD =>
    [ 500, [ "Content-Type" => "application/json" ],
        [ q|{ "status" => 0, "message" => "bad payload" }| ] ];
use constant WORKER_ERROR =>
    [ 500, [ "Content-Type" => "application/json" ],
        [ q|{ "status" => 0, "message" => "worker error" }| ] ];
use constant WORKER_SUCCESS =>
    [ 200, [ "Content-Type" => "application/json" ],
        [ q|{ "status" => 1 }| ] ];

has router => (
    is => 'ro',
    isa => 'Router::Simple',
    default => sub { Router::Simple->new() }
);

sub register {
    my ($self, $path, $worker, $method) = @_;
    $method ||= 'process';
    $self->router->connect( $path, { controller => $worker, action => $method } );
}

sub psgi_app {
    my $self = shift;
    return sub {
        $self->process(@_);
    }
}

sub process {
    my ($self, $env) = @_;

    my $matched = $self->router->match( $env );
    if (! $matched) {
        return NO_SUCH_WORKER;
    }

    my $payload;
    eval {
        $payload = $self->fetch_payload( $env );
    };
    if ($@) {
        warn $@;
        return BAD_PAYLOAD;
    }

    my $response;
    eval {
        my $worker = $matched->{controller};
        my $method = $matched->{action};
        $response = $worker->$method( $payload );
    };
    if ($@) {
        warn $@;
        return WORKER_ERROR;
    }

    if (! $response ) {
        $response = WORKER_SUCCESS;
    }

    return $response;
}

sub fetch_payload {
    my ($self, $env) = @_;

    if ($env->{REQUEST_METHOD} ne 'POST') {
        return;
    }

    my $cl = $env->{ CONTENT_LENGTH };
    my $ct = $env->{ CONTENT_TYPE };

    # $ct should be application/json, but we're not checking this right now

    my $input = $env->{ 'psgi.input' };

    # Just in case if input is read by middleware/apps beforehand
    $input->seek(0, 0);

    my $buffer = '';
    my $spin = 0;
    while ($cl > 0) {
        $input->read(my $chunk, $cl < 8192 ? $cl : 8192);
        my $read = length $chunk;
        $cl -= $read;
        $buffer .= $chunk;
        if ($read == 0 && $spin++ > 2000) {
            Carp::croak "Bad Content-Length: maybe client disconnect? ($cl bytes remaining)";
        }
    }

    decode_json $buffer;
}

__PACKAGE__->meta->make_immutable();

1;
    このエントリーをはてなブックマークに追加 mixiチェック

前のエントリ書いたら宮川さんにオススメ方法を教えてもらった

http://twitter.com/miyagawa/status/10271128928
plackup \
      -L Shotgun
      -MPlack::App::WrapCGI
      -e 'Plack::App::WrapCGI->new(script => "/path/to/cgiscript.cgi")'

だそうです!これなら全ての要件を満たせるもよう!そしてapp.psgiを作る必要ありません。

今これやってみて一つだけ問題点。多分POD入りのCGIは動かない。それと、__DATA__は見てくれるけど、__END__があると動かない。__END__さっき簡単なパッチのpull requestを送っておきました!

なお、-L Shotgunを使うと CGIファイルの中身は実行時までコンパイルされないのでご注意(つまり、plackup -rもいらないってことですね)
    このエントリーをはてなブックマークに追加 mixiチェック

ちょっと案件で簡単なCGIスクリプトをを開発することになった。StarmanとかTwiggyでほとんど全て書いているのに今更CGIとかそういう突っ込みはとりあえずおいておいて・・・環境を作るのも面倒くさいし、簡単にサーバーを立ち上げたり落としたりしたい。

そこでPlackです!

追記:その後もらったコメントによると、こちらのほうがよさそうです







具体的にはplackupとCGI::Emulate::PSGIを使います。

何も考えずにとりあえず開発したいので、以下のようにapp.psgiにコードを書いていく。
use strict;
use CGI::Emulate::PSGI;
return CGI::Emulate::PSGI->handler(sub {
    CGI::initialize_globals(); # 重要
# ここから
#!/usr/bin/env perl
use strict;
use warnings;
use CGI;
use CGI::Carp qw(fatalsToBrowser);

sub main {
    my $q = CGI->new();
    ....
}

main();

# ここまでがCGIのコード
});
あとで本当に単純にコピペしたいのでshebangもそのまま書いちゃうし、インデントもその部分だけファイルに書いたかのように記述していっちゃいます。

これを以下のように-rオプションをつけて起動。app.psgiのCGIコードに変更を加える度にサーバーも再起動して新しいコードを適用してくれる。
plackup -r -a app.psgi


これはいいわー。
    このエントリーをはてなブックマークに追加 mixiチェック

  • 注1:まだ本番にはデプロイしてませんが、確認テストでは使いました(単位テストでは使わない。あくまで本番さながらの形で動いているのを確認したい時だけ)。
  • 注2:以下スクリプトは開発者の労力を減らすためのスクリプトで、万全なデプロイ方法だとか言うわけではありません。
  • 注3:正直シェルスクリプトは素人です。

  • 追記:envdirはどうか、と言われた件。最初それを思い出せなくてcat ENVなんてしてたんだけど、設定値をもらって後からフックしたいところがあるからenvdirではそれを実現できない・・・気がする。ので、今回は以下で。
  • hirose31 さんが美しい スクリプトを提供してくれたので、それをベースにしてgithubにあげておけました。最新版はここです

ここ最近のPlackだとかStarmanとかを使ったWebアプリケーションのバンドル・デプロイについてちょっと固まりつつあるので、書いてみる

まず アプリケーションと、その依存関係。デプロイ側のサーバーにはlocal::libと必要なModule::Install系のモジュール、それにModule::Install::Bundle::LocalLibがインストールされている前提です。アプリケーションの依存関係は全部Makefile.PLに書きます。

use inc::Module::Install;

name 'MyApp';

..... # 必要な事色々 ....

# Plack stuff
requires 'Plack' => '0.9910'; # or 1.00 when the time comes
requires 'Starman';
requires 'Server::Starter';
requires 'Net::Server::SS::PreFork';

bundle_local_lib;

WriteAll;

こんな風にしておく。で、デプロイする際には
make bundle_local_lib
する。するとextlibというディレクトリに依存関係が全部入ります。このextlibは一旦安定稼働するのを確認したら、依存関係のアップグレード時には一旦別のところに待避させるとかコピーするとかしてからアップグレードを行ってみるという事ができる。万が一動かない場合はすぐ元のextlibに戻せば良いってわけだ。

ってなわけで、Plackを含めた全ての依存関係をextlib以下にある。

で、これをdaemontoolsで動かすのでrunファイルが欲しい。作る。今までdaemontoolsで運用してて開発→デプロイで面倒くさいなーと思ってたのがこのrunファイルの細かい設定を変えていくことなんだけど、今回はこれを分離してみた:
#!/bin/sh

# ENVというファイルが このファイルと同じディレクトリか
# pwdにあれば、それを読み込んで、そこから環境変数を広う。
# 例えば後で出てくる PSGI_FILEの場所を明示的に指定したければ
# PSGI_FILE=/path/to/app.psgi とかENVファイルに書いておく。
if [ -f `dirname $0`/ENV ]; then
    ENV_FILE=`dirname $0`/ENV
    export `cat $ENV_FILE`
elif [ -f ENV ]; then
    export `cat ENV`
fi

if [ -z $APP_HOME ]; then
    APP_HOME=`pwd`;
fi
export APP_HOME
if [ -z $CATALYST_HOME ]; then
    CATALYST_HOME=$APP_HOME
    export CATALYST_HOME
fi

if [ -z $CATALYST_CONFIG ]; then
    if [ -f "catalyst.yaml" ]; then
        CATALYST_CONFIG=catalyst.yaml
        export CATALYST_CONFIG
    fi
fi

if [ ! -z $DEBUG ]; then
    DBIC_TRACE=2
    export DBIC_TRACE
fi

if [ -z $PLACK_SERVER ]; then
    PLACK_SERVER=Starman
fi

if [ -z $PSGI_FILE ]; then
    PSGI_FILE=app.psgi
fi

if [ -z $PORT ]; then
    PORT=5000
fi

if [ -z $USER ]; then
    USER=www
fi

if [ -z $PERL ]; then
    PERL=`which perl`
fi

EXTLIB=$APP_HOME/extlib

# start_serverを通して plackupを実行するのだが、こいつらはextlibの中に
# あるので、それを指定して実行する(local::lib設定もつけないと依存関係が
# みつからない状態になってしまう)
exec setuidgid $USER \
    $PERL -Mlocal::lib=$EXTLIB \
    $EXTLIB/bin/start_server --port $PORT -- \
    $PERL -Mlocal::lib=$EXTLIB \
    $EXTLIB/bin/plackup -s $PLACK_SERVER -a $PSGI_FILE -p $PORT 2>&1

こんな感じ。これでENVの中身を変えてsvc -tするとその設定が反映される、と。

自分の場合はPSGIファイルの中にももう少し仕掛けを入れていく必要があったので、ENVから可変な情報をもらっていくようにした:
use strict;
use lib "$ENV{CATALYST_HOME}/lib"; # パスを通す
use local::lib "$ENV{CATALYST_HOME}/extlib"; # ついでにlocal::libのパスも通す

use MyApp;
use Plack::Builder;

MyApp->setup_engine('PSGI');

builder {
    # リバースプロキシ使う場合はenableしちゃう
    if ($ENV{USE_PROXY}) {
        enable "Plack::Middleware::ReverseProxy";
    }
    return sub { MyApp->run(@_) };
};
これをレポジトリに入れておき、デプロイするときにはdaemontoolsあたりからsymlinkしておいて動かす。開発時にもrunファイルを実行すれば本番さながらの感じでサーバーが立ち上がる(ただし、実際にはテストは全然違う方法で動かしてる)

誰かの役に立つなら幸い。なんか突っ込みがあればお願いします。
    このエントリーをはてなブックマークに追加 mixiチェック

ミニブログサービスで先行するTwitterと差別化を図った上で新たなサービスを出すのは面倒くさいので、id:miyagawa氏のコードを丸々パクった上でTwitter支援ツール、Hamakiを昨日の夜からちょこちょこと開発しました(本当はTwitter以上に色々できるんですが、そのあたりはまださわってない)

もう自分がやりたい事はだいたいできるようになったのでとりあえず公開です。

まず前提としてTwitterを使ってていくつか不満な点があったわけです:

  • Twitterで特定のアプリの発言がちょっぴり気になるので自分のTLに表示したくない
  • かといって上記アプリの発言している人たちの他の発言は読みたいので、それらの人をremoveするのもしたくない。
  • あと、全般的にtwitterをリロードするような事をしていると非常に効率が悪い
  • かといって、今のところアプリを気に入ったアプリも存在しないし、第一おれは色々カスタマイズしたいんじゃ、ゴルァ。
ってことで、Plackベースの非同期エンジンTatsumakiを使用したHamakiです。こんな感じで見れます。

hamaki.png

ほしかった機能としては、まず自動ロード。一回ページを読み込んでしまえば、その後は置いておけばJSが勝手に新規発言をロードしてページをリフレッシュしてくれます。

これはAnyEvent::Twitter::StreamでTwitter Stream APIを使うことによって実現してます。ちょっと仕組み上、普通のTwitter TLとは表示内容が違ってきますが、僕はこっちのほうが好きだったりします。

あと、フィルタリングができます。例えば、発言内容によって更新を無視したりできるわけです。

ちなみに今回使った設定はこれですね。ごめんね、radiotube。
で、最後に、元々のパクったソースコードにはなかった機能、Twitterに自分の新発言を送信する部分を追加したので、このUIだけでTwitter的な事はほとんどできてしまうことになります。まぁ、DMとか@自分とかはみれないけど、そんなんたまにしかみないし・・・

設定ファイルを書いたらあとは./script/hamaki.pl --configfile=config.yamlってするだけ!サーバーが立ち上がるのでそこにブラウザで /chat/twitterとかにアクセスすれば上記画面がでて勝手に更新してくれますお。

とまぁ色々書きましたが、実際の構造のほとんどはid:miyagawa氏のものですね。元々Tatsumakiに入っているデモ用のスクリプトdemo.plが素晴らしかったので、僕はこれをががっとファイルごとにわけて整理して、スコープが違ってもデータを共有できるようにして、Twitterにポストする機能と、それを実現化するためのTatsumakiフレームワークの拡張を子クラスで行った程度です。なので、miyagawa++, Tatsumaki++, Plack++ なのでした。
    このエントリーをはてなブックマークに追加 mixiチェック

PSGIだPlackだって某IRCチャンネルでお祭りをやっている最中金策に走っていたので、まださわっていなかったのですが、とりあえず練習でさわってみました。

http://github.com/lestrrat/GitPlack

結論からまず言います:WAFを作りたいんじゃなければとりあえずCatalystとか使っておけ。

私の見解は以下の通り:

PSGI/Plackは統一された、しかも非同期エンジンを念頭に置いて考えられた仕様・実装なのでその点は素晴らしい。だけど、Plackはフレームワークではないし、とりあえずアプリを作る分には我々凡人は普通にフレームワークを使っておけばいい。・・・という感じ。

結果的には2時間くらいでWAFもどきとgitの情報を表示するアプリを作れたし、こういう仕様があるのは素晴らしい事です。

細かいところだけど難点はリスタートサーバーが動かないところくらいかなぁ。でもこれもそのうちなおるんじゃなかろうか。

追記: 10/21の時点でgit レポジトリ版はplackup -r とすることで自動的に再起動がちゃんと動くようになっています
    このエントリーをはてなブックマークに追加 mixiチェック

このページのトップヘ