Plaggerのソースコード読む(4)

Plaggerのソースコード読む(2)の続き。


今回はPluginのロードについて。


まずload_pluginsメソッド(Plagger.pmです)。

    $self->load_plugins(@{ $config->{plugins} || [] });

$config->{plugins}の中身は、(2)の変数をDumpしたのを見たらわかると思う。
一応書くと、

'plugins' => [
               {
                'config' => {
                             'feed' => [
                                         {
                                          'url' => 'http://d.hatena.ne.jp/tayaya/rss'
                                         }
                                       ]
                             },
                'module' => 'Subscription::Config'
               },
            ・
            ・
            ・
             [

こんな感じで、各Pluginの設定が入ってる。
ちなみにGlobalな設定は$self->conf (Plugin側からは$context->conf) でアクセスできます。


load_pluginsの中を見てみると、

    my $plugin_path = $self->conf->{plugin_path} || [];
       $plugin_path = [ $plugin_path ] unless ref $plugin_path;

Globalのplugin_path(外部Pluginのパス)をとってくる。
配列になってるとこからわかるように、plugin_pathは複数設定できるということですね。


次。

    for my $path (@$plugin_path) {
    ・
    ・
    ・

このforループでは、plugin_pathで設定したディレクトリ内に
PlaggerのPluginファイルがあるかどうかを調べ、
Pluginファイルがあれば$self->plugins_pathに($self->conf->plugin_pathじゃないですよ)
そのパスを設定、ということをやっています。


ここでは
File::SpecやFile::Find::Ruleモジュール、ファイルテスト演算子など、
パスやファイルの扱い方に関して結構勉強になると思うので、
興味のある方はみてみると良いかもしれません。


外部Pluginのパスを設定し終わったら、いよいよconfig.yamlに書いたPluginをロードしていきます。

    for my $plugin (@plugins) {
        $self->load_plugin($plugin) unless $plugin->{disable};
    }

@pluginsはload_pluginsを呼ぶときに渡された @{$config->plugins} です。


ではload_pluginの中身。

    my $module = delete $config->{module};
    $module =~ s/^Plagger::Plugin:://;
    $module = "Plagger::Plugin::$module";

$moduleに「Plagger::Plugin::*」と、正式名称(?)を代入しています。
$module =~ s/^Plagger::Plugin:://; がなぜ書いてあるかよくわからないのですが、
昔はconfig.yamlには

  - module: Plagger::Plugin::Subscription::Config

みたいに書くようにしてたのかな?(古いconfig.yamlと互換性もたせるため?)


とりあえず続き。

    if ($module->isa('Plagger::Plugin')) {
        $self->log(debug => "$module is loaded elsewhere ... maybe .t script?");
    } elsif (my $path = $self->plugins_path->{$module}) {
        eval { require $path } or die $@;
    } else {
        $module->require or die $@;
    }

ここがPluginをロードしてる所です。
まず、

    if ($module->isa('Plagger::Plugin')) {

Pluginがすでにロードされているかどうかを調べ、
ロードされていなければ

    } elsif (my $path = $self->plugins_path->{$module}) {
        eval { require $path } or die $@;

外部Pluginのpathをrequire。
どちらでもなければ

$module->require or die $@;

普通にrequireします。


とまあさらっと書いたんですが、
2番目のrequire $pathの部分。
$pathには 'C:\plugin\Sample.pm' みたいにパスが入ってます。
ということは下のようになるわけで、

require 'C:\plugin\Sample.pm';

こんな風にパスでもrequireできるんですね。
初めて知りました。へえー。


最後です。

    my $plugin = $module->new($config);
    $plugin->cache( Plagger::CacheProxy->new($plugin, $self->cache) );
    $plugin->register($self);

    push @{$self->{plugins}}, $plugin;

Pluginをnewし、
cache、registerを呼び出します。


そして
@{$self->{plugins}}にpushして終了です。


registerは重要なので、次回にでも書いていきたいと思います。

メモ

うーん、書く時間がない。


とりあえずPlaggerのフェーズごとの挙動が知りたい人は
Plagger.pmの、
run、run_hook、do_run_with_feedsあたりを見ればいいんじゃないかな、と思います。


特にrun_hookはPluginの中のメソッドが実行されるとこなので、
( $action->{callback}->($plugin, $self, $args)のとこ )
じっくり読んでいけばきっと

                 ハ_ハ  
               ('(゚∀゚∩ わかるよ!
                ヽ  〈 
                 ヽヽ_)

Plaggerのソースコード読む(3)

眠いので走り書き。
rule: expressionの仕組み。


下記yamlの場合。
(タイトルが qw/perる 日誌/ なfeedだけBreakEntriesToFeedsするyaml)

- module: Filter::BreakEntriesToFeeds
  rule:
     expression: $args->{feed}->title eq 'qw/perる 日誌/'

まず、ruleが適用される場所は、Plagger.pmのrun_hookの中のここ。

if ( $plugin->rule->dispatch($plugin, $hook, $args) ) {

$pluginにはPlagger::Plugin::BreakEntriesToFeedsオブジェクトが入ってて、ruleはアクセサ。
$plugin->ruleでPlagger::Rulesオブジェクトが返ってくる。


Rulesオブジェクト作ってるのはPlagger::Pluginのinitメソッド。

sub init {
    my $self = shift;

    if (my $rule = $self->{rule}) {
        $rule = [ $rule ] if ref($rule) eq 'HASH';
        my $op = $self->{rule_op};
        $self->{rule} = Plagger::Rules->new($op, @$rule);

$self->{rule}には expression => $args->{feed}->title eq 'qw/perる 日誌/' が入ってる。


Plagger::Rules->new。
中でPlagger::Rule->newしてる。

    bless {
        op => uc($op),
        rules => [ map Plagger::Rule->new($_), @rules ],
    }, $class;

Plagger::Rule->new。

sub new {
    my($class, $config) = @_;

    if (my $exp = $config->{expression}) {
        $config->{module} = 'Expression';
    }

    my $module = delete $config->{module};
    $module = "Plagger::Rule::$module";
    $module->require or die $@;

    my $self = bless {%$config}, $module;

ここでexpression登場。
Plagger::Rule::Expressionをrequireして、
{%$config}とPlagger::Rule::Expressionをblessして返す。
%$configは expression => $args->{feed}->title eq 'qw/perる 日誌/' ですよ。


ここで最初に戻ってみる。

if ( $plugin->rule->dispatch($plugin, $hook, $args) ) {

$plugin->rule->dispatchでPlagger::Rules->dispatchが呼び出される。
その中身。

    for my $rule (@{ $self->{rules} }) {
        push @bool, ($rule->dispatch($args) ? 1 : 0);
    }

$self->{rules}の中身は[Plagger::Rule::Expressionオブジェクト]。
$argsはPlagger::Feedオブジェクト。
$rule->dispatchでPlagger::Rule::Expression->dispatchを呼び出す。


やっとたどりついた。
Plagger::Rule::Expression->dispatch。

sub dispatch {
    my($self, $args) = @_;
    my $status = eval $self->{expression};
    if ($@) {
        Plagger->context->log(error => "Expression error: $@ with '$self->{expression}'");
    }
    $status;
}

eval $self->{expression}; で、

$args->{feed}->title eq 'qw/perる 日誌/' 

が実行されます。


ここでfeedのタイトルと qw/perる 日誌/ が同じなら真が返る。




まあそのあとごにょごにょしたり(ANDとかの処理)して
真が返ったときのみBreakEntriesToFeedsが適用される、といった感じです。



※ちょいと気になったので注意書き。
rule: expressionでは正規表現とか条件文だけじゃなく、好きなコード書けます。
例えば、

- module: Filter::BreakEntriesToFeeds
  rule:
     expression: system 'shutdown -s'

みたいなyamlだとPC終了させられる(shutdown -sはWindowsのshutdownコマンド)。
人のconfig.yamlコピペして使うときは注意したほうが良いかも。

rule: expression

昨日のエントリですが、ブクマでツッコミいただきました。

Rule: expression: $args->{feed}->title で、やれそうな気も

やってみました。

plugins:
  - module: Subscription::Config
    config:
      feed: 
        - url: http://d.hatena.ne.jp/tayaya/rss
  - module: Filter::BreakEntriesToFeeds
    rule:
       expression: $args->{feed}->title ne 'qw/Perる 日誌/'
  - module: Publish::Gmail

あっさり成功。


どうやらrule: expressionというのは
条件にマッチしたものだけをFilterするようにしてくれるみたい。
上の例だと、feedのタイトルが qw/Perる 日誌/ に一致しないものを
BreakEntriesToFeedsする、ということ。
とても便利。


昨日やってたことは無駄だったんだなあ。
あー恥ずかし。
でもひとつ勉強になりました。
ソース読んでどんな仕組みか眺めてみることにします。


id:otsuneさん、どうもありがとうございました。

Filter::BreakEntriesToFeeds

で特定のfeedだけスキップするようにしてみた。


※追記
rule: expressionで簡単にできます。
このエントリは読む価値なし。




config.yaml

- module: Filter::BreakEntriesToFeeds
  config:
    skip:
      - url: http://d.hatena.ne.jp/tayaya/

Filter::BreakEntriesToFeedsソース

sub break {
    my($self, $context, $args) = @_;

    if ( my $skip_items = $self->conf->{skip} ) {
        $skip_items = [ $skip_items ] unless ref $skip_items;

        my $url = $args->{feed}->link;
        if ( grep { $_->{url} eq $url } @$skip_items ) {
            $context->log(debug => "Skip break entries : $url");
            return;
        }
    }
・
・
・

Alpha Geek Trackerとか、
量が多いのは分割しちゃうとGmailで読み難いので。




※失敗メモ
 あーBloglines2Gmailで試したらうまくいかんかった・・・
 Subscription::Configなら成功したんだけど。
 あしたにでも直す。
 $args->{feed}->link?


 どうやら$args->{feed}->titleで弾いたほうがよさげ。




でけたー

- module: Filter::BreakEntriesToFeeds
  config:
    skip:
      - title: qw/Perる 日誌/
sub break {
    my($self, $context, $args) = @_;

    if ( my $skip_items = $self->conf->{skip} ) {
        $skip_items = [ $skip_items ] unless ref $skip_items;

        my $title = $args->{feed}->title;
        if ( grep { $_->{title} eq $title } @$skip_items ) {
            $context->log(debug => "Skip break entries : $title");
            return;
        }
    }
・
・
・

Subscription::Bloglinesの既読処理

今日もいつものようにBloglines2Gmail♪
・・・
Plaggerが落ちましたorz


んで、Bloglinesにログインしてみると予想通り全部既読になってました。


Bloglinesにはmark_readというオプションがあるんですが、
これを設定してると(デフォルトではオンになってます)、
更新feedを取得するとその分を既読にしてくれます。


その既読にする処理はsubscription.loadフェーズで行われます。
なので、subscription.loadフェーズの後に
Plaggerが落ちたり他のPluginでdieしたりすると、
Gmailに転送できてないけどBloglines側は全部既読になってしまうわけですね。


しばし考えて、
既読にする処理をplugin.finalizeフェーズに持っていけばいいんじゃね?と思ったのでやってみました。
以下Subscription::Bloglinesのソース。


・register_hookにplugin.finalizeを追加。

$context->register_hook(
    $self,
    'subscription.load' => \&notifier,
    'plugin.finalize'   => \&mark_read,

・既読にするよ処理を追加。

sub mark_read {
    my ($self, $context) =@_;
    
    my $mark_read = $self->conf->{mark_read};
       $mark_read = 1 unless defined $mark_read;
    
    return unless $mark_read;

    my $count = $self->{bloglines}->notify();
    $context->log(debug => "mark $count unread items as read");    

    eval { $self->{bloglines}->getitems(0, $mark_read) };
}

・subscription.loadフェーズの既読処理をコメントアウト
 (147〜151行目付近)

#} elsif ($mark_read) {
#    # no error found with XML ... call the API again to mark read
#    eval {
#        @updates = $self->{bloglines}->getitems(0, $mark_read);
#    };


こんな感じなんですがちょっと問題もあって、
Subscription::Bloglinesはfeedを取得しに行くときに、

mark_readオフでfeedを取得→エラーがなければmark_readオンでもう一度feedを取得

getitems(0,0)      →getitems(0,$mark_read)

という風にやってます。


なので、
subscription.loadフェーズとplugin.finalizeフェーズの間にものすごく時間がかかった場合、
その間にBloglines側で新しいfeedが更新されちゃったりして、
まだ読んでないfeedまで既読になってしまう可能性があるかも??
う〜ん。


あと例外処理どうしよう。