Plack::APP::CGIBinの利用

宮川さんのPlack Handbookを購入しました。ネットでも色々調べながらPSGIについて色々試して行きたいと思っています。既存のCGIをそのままPSGIに変換して動作させるには、Plack::APP::CGIBinを利用するのが最も簡単のようなので、まずはその方法を使って、webkoza.comの訪問者数をグラフ表示するCGIをそのままPSGI化してみました。
GD::Graphについての記事でありませんので、このモジュールの使い方についてはあまり触れていません。知りたい方は下記あたりを参考にしてください。
 Perl でグラフを作ろう (GD::Graph)
なお、Plack関連モジュールは全てインストール済みであることを前提としています。

1.訪問者数グラフ表示CGIの紹介

(1)目的
このCGIは、webkoza.comの各ページで使用している「イメージ出力型の昨日今日セッションカウンター(GD::Stingを使用,本記事最下部でも実装してあります)」が残した各ページのアクセスログファイルを参照して、月毎に日の訪問者数推移をグラフ表示させる目的で作りました。特徴は「Agent」別の人数を積み上げ型の色分け棒グラフにしているところです。ここでその動き見る事ができます。
サーバーサイドでイメージ生成を全て行っているので、PSGI化した場合の効果が顕著に出ることを期待しました。実際に上がった速度はMovableTypeの管理画面と同様に1.7-2.0倍ぐらいでした。

(2)コード概要
コードとしては下記のような感じです。
**********************
#!/usr/bin/perl
$|=1;
#************** Main routine **************
use GD; # GD::Stringを利用する為
use GD::Graph::mixed; # <--棒と折れ線混合グラフにするため
use Jcode;
use CGI; # <-- Plack::APP::CGIBinを使用するとCGI.pmを使用したCGIもそのままPSGI化できます。(後述)
my $q = new CGI;
print $q->header(-type=>'image/png',-expires=>'+3d');
#print $q->header(-type=>'text/html',-expires=>'+3d');
#--------グローバル変数セット---------
$UAID = 13;
$EX_LOCK=2; #flockの排他ロックモード
$UNLOCK=8; #flockの解除
# ページ・年・月をクエリーで指定してログファイルを選択できるようにします。
$Pa = $q->param('page');
$Ya = $q->param('year');
$Mo = $q->param('month');
$FilePath = "/*****/imgcnt"; # ディレクトリ伏せています
$LOGFILE = $FilePath . '/' . $Pa . '/img' . $Ya . '_' . $Mo . '.txt';
# グラフオブジェクトに渡す配列の初期化。
# 横軸は常に1-31日、積み上げ対象は複数のAgent別人数
@DayNo = qw(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31);
@DayNinzu = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_iPhone = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_iPod = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_iPad = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_AndroidStd = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_AndroidOpera = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_AndroidFirefox = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_WH = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_BB = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Sym = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_IE9 = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_IE10 = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_IE11 = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_IE8 = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_IEOther = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_GC = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Lunascape = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Safari = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Opera = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_bot = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_NMFirefox = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Au = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Doco = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_Softb = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
@Agent_other = qw(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0);
#---------------------------
&initgd0; # グラフサイズなど基本情報の初期化
my $method = $q->request_method();
if(uc($method) eq 'GET'){
if($q->param('act') eq 'disp_grp'){
disp_grp($q->param('page'));
}else{
err_wr("Call Error1"); # サブルーチン掲載省略しますが、GD::Stringを使用して引数の文字列をイメージで返す。
}
}else{
err_wr("Call Error0");
}
#************** Sub routines **************
#==================
sub disp_grp($){
my($page)=@_;
my($text1,$gr,@gdata);
# print $q->h1({-align=>center}, "Page = $page"),"\n";
$text1 = "Page = $page , $Ya/$Mo"; # グラフのタイトル文字
&set_logdata;# イメージカウンターが記録したログファイルをオープンしてAgent別に毎日の人数を集計、用意した配列変数にデータをセットする
# プロットするデータ配列を生成
@gdata = (\@DayNo,\@Agent_iPhone,\@Agent_iPod,\@Agent_iPad,
\@Agent_AndroidStd,\@Agent_AndroidOpera,\@Agent_AndroidFirefox,
\@Agent_WH,\@Agent_BB,\@Agent_Sym,
\@Agent_IE9,\@Agent_IE10,\@Agent_IE11,\@Agent_IE8,\@Agent_IEOther,
\@Agent_GC,\@Agent_Lunascape,\@Agent_Safari,\@Agent_Opera,\@Agent_bot,\@Agent_NMFirefox,
\@Agent_Au,\@Agent_Doco,\@Agent_Softb,\@Agent_other,\@DayNinzu);

$gr = GD::Graph::mixed -> new(800,600); # 複合グラフオブジェクト生成。毎日の合計人数のみ折れ線グラフ(最後)
$gr->set( title => jcode("Agent別訪問者数:" . $text1)->utf8,
x_label => jcode("[日]")->utf8,
y_label => jcode("人数[人]")->utf8,
types => [ qw(bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars bars lines) ],
dclrs => [ qw(#ff8080 #ff0000 #804040
#ffff80 #ff8040 #804000
#000000 #808080 #c0c0c0
#80ff80 #00ff00 #008000 #808040 #004000
#80ffff #004080 #3737ff #000080 #408080 #8080ff
#ff80ff #ff00ff #ff0080 #8000ff #aa0000) ],
cumulate => 1,
y_tick_number => 5,
y_label_skip =>1,
bar_width => 22
);
$gr->set_legend(iPhone,iPod,iPad,
AndroidStd,AndroidOpera,AndroidFirefox,
WindowsPhone,BlackBerry,Symbian,IE9,IE10,IE11,IE8,IEOther,
GoogleCrome,Lunascape,Safari,Opera,bot,NMFirefox,
Au,Doco,Softb,other,Total);
GD::Text->font_path( "/usr/share/fonts/vlgothic/" );
$gr->set_title_font( "VL-Gothic-Regular", 14 );
$gr->set_legend_font( "VL-Gothic-Regular", 8 );
$gr->set_x_axis_font( "VL-Gothic-Regular", 8 );
$gr->set_x_label_font( "VL-Gothic-Regular", 10 );
$gr->set_y_axis_font( "VL-Gothic-Regular", 8 );
$gr->set_y_label_font( "VL-Gothic-Regular", 8 );
$gimage = $gr->plot( \@gdata ) or die( "Cannot create image" );
binmode STDOUT;
# Convert the image to PNG and print it on standard output
print $gimage->png;
}
#==================
sub set_logdata{
my(@all_lines,$text1,$i);
#------------------ログレコード:タブ区切り----------------
#------ファイルから読み込み---------
if (-e $LOGFILE){
open R,"<$LOGFILE" or die "Cannot Open $LOGFILE :$!"; #読み出し専用
}else{
err_wr("Nothing Logfile");
return;
}
flock R, $EX_LOCK;
@all_lines = readline R;
close R;
for(@all_lines){
$_ = jcode($_)->euc;
@da = split(/\t/,$_);
$i = [split(/日/,[split(/月/,$da[1])]->[1])]->[0] - 1;#
日にち抽出→1引いてindexに変換
++$DayNinzu[$i];
$_ = $da[$UAID]; # User_Agent
# print "$da[11]".':'."$da[12]".':'."$da[13]".'--';
{
++$Agent_iPhone[$i],last if(/iPhone/);
++$Agent_iPod[$i],last if(/iPod/);
++$Agent_iPad[$i],last if(/iPad/);
++$Agent_AndroidStd[$i],last if(/Android.+Safari/);
++$Agent_AndroidOpera[$i],last if(/Android.+Opera/);
++$Agent_AndroidFirefox[$i],last if(/Android.+Firefox/);
++$Agent_WH[$i],last if(/Windows\sPhone/);
++$Agent_BB[$i],last if(/BlackBerry/);
++$Agent_Sym[$i],last if(/Symbian/);
++$Agent_IE9[$i],last if(/MSIE\s9/);
++$Agent_IE10[$i],last if(/MSIE\s10/);
++$Agent_IE11[$i],last if(/Mozilla\/5\.0\s\(Windows\sNT\s6\.3;\sWOW64;\sTrident\/7\.0;\sTouch;\srv:11\.0\)\slike\sGecko/);
++$Agent_IE8[$i],last if(/MSIE\s8/);
++$Agent_IEOther[$i],last if(/MSIE/);
++$Agent_GC[$i],last if(/Chrome/);
++$Agent_Lunascape[$i],last if(/Lunascape/);
++$Agent_Safari[$i],last if(/Safari/);
++$Agent_Opera[$i],last if(/Opera/);
++$Agent_bot[$i],last if(/bot|Bot|slurp|Ask\sJeeves\/Teoma/);
++$Agent_NMFirefox[$i],last if(/Mozilla\/[2-5]|Netscape|Firefox/);
++$Agent_Au[$i],last if(/KDDI/);
++$Agent_Doco[$i],last if(/DoCoMo/);
++$Agent_Softb[$i],last if(/SoftBank|Vodafone/);
++$Agent_other[$i],last;
}
}
}
#==================
sub initgd0{
my($bcolor,$fcolor);
# create a new image
$im = new GD::Image(480,50) || die;
# allocate some colors
$bcolor = $im->colorAllocate(0,0,200);
$fcolor = $im->colorAllocate(0,255,0);
$BaseCref = $im->colorAllocate(255,0,0);
$im->fill(0,0,$fcolor);
$im->rectangle(1,1,480 - 1,50 - 1,$bcolor);
}
**********************

2.plackupコマンドによるCGIディレクトリのPSGI化

(1)plackupコマンドの使用方法
Plack Handbookによると、psgiアプリを起動する前に起動するモジュールを指定できるMオプションと、PSGIアプリを与えるコードを指定できるeオプションを組み合わせて、下記のように指定ディレクトリのCGIプログラムをそのままPSGI化して起動できます。
# plackup -M Plack::App::CGIBin -e 'Plack::App::CGIBin->new(root => "/****/cgi-bin")'
これで指定のディレクトリのCGIプログラムfoo.plには下記のようにアクセスできます。
http://127.0.0.1:5000/

(2)Apacheとの連携
80番ポート以外は開放したくないので、MovableType同様mod_proxyを使うことになります。
httpd.confに下記を追加しました。
***********
<IfModule proxy_module>
ProxyPass /****/cgi-bin/ http://localhost:5001/
ProxyPassReverse /****/cgi-bin/ http://localhost:5001/
</IfModule>
***********
5000番はMovableTypeで使用しているので5001番としました。

(3)PSGIエミュレーションのdaemon化
下記のように、「webkoza_psgi.service」という.serviceファイルを作成
********************
[Unit]
Description=Webkoza PSGI/Plack
After=syslog.target
After=network.target
After=webkoza_mt.service

[Service]
#User=movabletype
#Group=movabletype

Restart=always
SyslogIdentifier=webkoza_psgi
ExecStart=/usr/local/bin/plackup -p 5001 -M Plack::App::CGIBin -e 'Plack::App::CGIBin->new(root => "/****/cgi-bin")'

[Install]
WantedBy=multi-user.target
********************
このファイルを
/usr/lib/systemd/system/
にコピーして下記を実行。全てrootで行いました。
# systemctl enable webkoza_psgi.service
ln -s '/usr/lib/systemd/system/webkoza_psgi.service' '/etc/systemd/system/multi-user.target.wants/webkoza_psgi.service'

サーバー再起動して下記によりPSGIアクセスできていることを確認。
http://www.webkoza.com/****/cgi-bin/**.cgi

(4)P
lack::App::CGIBin の注意事項
少しはまった事を記載しておきます。独立したCGIプログラムをperlで書く場合、mainルーチンであっても癖で必ず変数には「my」で宣言していたのですが、このプログラムを「Plack::App::CGIBin」に渡すと、CGI::Compileでサブルーチンとしてコンパイルすることを行っているらしく、これによりmainと思っていた部分はmainでなくなるため、そこでmyした変数は、自分で定義したサブルーチン内ではスコープされずエラーになります。下記はこの時にブラウザに表示した美しい(?)トレース画面です。このトレース画面はplackupコマンドでは標準で組み込まれているそうです。
せっかくなので色々試してみましたが、アプリ(CGI)内のSyntaxエラーも丁寧に表示してくれるので大変便利なものです。
psgi_errtrace.png