Object Oriented Programming in Perl

advertisement
Writing Pluggable Software
Tatsuhiko Miyagawa
miyagawa@gmail.com
Six Apart, Ltd. / Shibuya Perl Mongers
YAPC::Asia 2007 Tokyo
For non-JP attendees …
If you find \ in the code,
Replace that with backslash.
(This is MS' fault)
Tatsuhiko Miyagawa
Plaggable
Software
Tatsuhiko Miyagawa
Plaggable
Software
Tatsuhiko Miyagawa
Pluggable
Software
Tatsuhiko Miyagawa
Agenda
Tatsuhiko Miyagawa
#1
How to make
your app pluggable
Tatsuhiko Miyagawa
#2
TMTOWTDP
There's More Than One Way To Deploy Plugins
Pros/Cons by examples
Tatsuhiko Miyagawa
First-of-all:
Why pluggable?
Tatsuhiko Miyagawa
Benefits
Tatsuhiko Miyagawa
#1
Keep the app design
and code simple
Tatsuhiko Miyagawa
#2
Let the app users
customize the behavior
(without hacking the internals)
Tatsuhiko Miyagawa
#3
It's fun to write plugins
for most hackers
(see: Plagger and Kwiki)
Tatsuhiko Miyagawa
"Can your app do XXX?"
"Yes, by plugins."
Tatsuhiko Miyagawa
"Your app has a bug in YYY"
"No, it's the bug in plugin YYY,
Not my fault."
(Chain Of Responsibilities)
Tatsuhiko Miyagawa
Good Enough
Reasons, huh?
Tatsuhiko Miyagawa
#1
Make your app pluggable
Tatsuhiko Miyagawa
Example
Tatsuhiko Miyagawa
ack (App::Ack)
Tatsuhiko Miyagawa
grep –r for programmers
Tatsuhiko Miyagawa
Ack is a "full-stack"
software now.
Tatsuhiko Miyagawa
By "full-stack" I mean:
Easy install
No configuration
No way to extend
Tatsuhiko Miyagawa
Specifically:
These are hardcoded
Ignored directories
Filenames and types
Tatsuhiko Miyagawa
Ignored Directories
@ignore_dirs = qw( blib CVS RCS SCCS .svn
_darcs .git );
Tatsuhiko Miyagawa
Filenames and languages mapping
%mappings = (
asm
binary
cc
cpp
csharp
…
perl
…
);
Tatsuhiko Miyagawa
=>
=>
=>
=>
=>
[qw(
…,
[qw(
[qw(
[qw(
s S )],
c h xs )],
cpp m h C H )],
cs )],
=> [qw( pl pm pod tt ttml t )],
What if making
these pluggable?
Tatsuhiko Miyagawa
DISCLAIMER
Tatsuhiko Miyagawa
Don't get me wrong Andy,
I love ack the way it is…
Just thought it can be
a very good example for the tutorial.
Tatsuhiko Miyagawa
Quickstart:
Class::Trigger
Module::Pluggable
© Six Apart Ltd. Employees
Tatsuhiko Miyagawa
Class::Trigger SYNOPSIS
package Foo;
use Class::Trigger;
sub foo {
my $self = shift;
$self->call_trigger('before_foo');
# some code ...
$self->call_trigger('after_foo');
}
package main;
Foo->add_trigger(before_foo => \&sub1);
Foo->add_trigger(after_foo => \&sub2);
Tatsuhiko Miyagawa
Class::Trigger
Helps you to implement
Observer Pattern.
(Rails calls this Observer)
Tatsuhiko Miyagawa
Module::Pluggable SYNOPSIS
package MyClass;
use Module::Pluggable;
use MyClass;
my $mc = MyClass->new();
# returns the names of all plugins installed under
MyClass::Plugin::*
my @plugins = $mc->plugins();
package MyClass::Plugin::Foo;
sub new { … }
1;
Tatsuhiko Miyagawa
Setup plugins in App::Ack
package App::Ack;
use Class::Trigger;
use Module::Pluggable require => 1;
__PACKAGE__->plugins;
Tatsuhiko Miyagawa
Setup plugins in App::Ack
package App::Ack;
use Class::Trigger;
use Module::Pluggable require => 1;
__PACKAGE__->plugins; # "requires" modules
Tatsuhiko Miyagawa
Ignored Directories (Before)
@ignore_dirs = qw( blib CVS RCS SCCS .svn
_darcs .git );
Tatsuhiko Miyagawa
Ignored Directories (After)
# lib/App/Ack.pm
__PACKAGE__->call_trigger('ignore_dirs.add',
\@ignore_dirs);
Tatsuhiko Miyagawa
Ignored Directories (plugins)
# lib/App/Ack/Plugin/IgnorePerlBuildDir.pm
package App::Ack::Plugin::IgnorePerlBuildDir;
App::Ack->add_trigger(
"ignore_dirs.add" => sub {
my($class, $ignore_dirs) = @_;
push @$ignore_dirs, qw( blib );
},
);
1;
Tatsuhiko Miyagawa
Ignored Directories (plugins)
# lib/App/Ack/Plugin/IgnoreSourceControlDir.pm
package App::Ack::Plugin::IgnoreSourcdeControlDir;
App::Ack->add_trigger(
"ignore_dirs.add" => sub {
my($class, $ignore_dirs) = @_;
push @$ignore_dirs, qw( CVS RCS .svn
_darcs .git );
},
);
1;
Tatsuhiko Miyagawa
Filenames and languages (before)
%mappings = (
asm
binary
cc
cpp
csharp
…
perl
…
);
Tatsuhiko Miyagawa
=>
=>
=>
=>
=>
[qw(
…,
[qw(
[qw(
[qw(
s S )],
c h xs )],
cpp m h C H )],
cs )],
=> [qw( pl pm pod tt ttml t )],
Filenames and languages (after)
# lib/App/Ack.pm
__PACKAGE__->call_trigger('mappings.add',
\%mappings);
Tatsuhiko Miyagawa
Filenames and languages (plugins)
package App::Ack::Plugin::MappingCFamily;
use strict;
App::Ack->add_trigger(
"mappings.add" => sub {
my($class, $mappings) = @_;
$mappings->{asm} = [qw( s S )];
$mappings->{cc} = [qw( c h xs )];
$mappings->{cpp} = [qw( cpp m h C H )];
$mappings->{csharp} = [qw( cs )];
$mappings->{css} = [qw( css )];
},
);
1;
Tatsuhiko Miyagawa
Works great
with few lines of code!
Tatsuhiko Miyagawa
Now it's time to add
Some useful stuff.
Tatsuhiko Miyagawa
Example Plugin:
Content Filter
Tatsuhiko Miyagawa
sub _search {
my $fh = shift;
my $is_binary = shift;
my $filename = shift;
my $regex = shift;
my %opt = @_;
if ($is_binary) {
my $new_fh;
App::Ack->call_trigger('filter.binary', $filename, \$new_fh);
if ($new_fh) {
return _search($new_fh, 0, $filename, $regex, @_);
}
}
Tatsuhiko Miyagawa
Example:
Search PDF content
with ack
Tatsuhiko Miyagawa
PDF filter plugin
package App::Ack::Plugin::ExtractContentPDF;
use strict;
use CAM::PDF;
use File::Temp;
App::Ack->add_trigger(
'mappings.add' => sub {
my($class, $mappings) = @_;
$mappings->{pdf} = [qw(pdf)];
},
);
Tatsuhiko Miyagawa
PDF filter plugin (cont.)
App::Ack->add_trigger(
'filter.binary' => sub {
my($class, $filename, $fh_ref) = @_;
if ($filename =~ /\.pdf$/) {
my $fh = File::Temp::tempfile;
my $doc = CAM::PDF->new($file);
my $text;
for my $page (1..$doc->numPages){
$text .= $doc->getPageText($page);
}
print $fh $text;
seek $$fh, 0, 0;
$$fh_ref = $fh;
}
},
);
Tatsuhiko Miyagawa
PDF search
> ack --type=pdf Audrey
yapcasia2007-pugs.pdf:3:Audrey Tang
Tatsuhiko Miyagawa
Homework
Use File::Extract
To handle arbitrary media files
Tatsuhiko Miyagawa
Homework 2:
Search non UTF-8 files
(hint: use Encode::Guess)
You'll need another hook.
Tatsuhiko Miyagawa
Summary
Class::Trigger
+ Module::Pluggable
= Pluggable app easy
Tatsuhiko Miyagawa
#2
TMTOWTDP
There's More Than One Way To Deploy Plugins
Tatsuhiko Miyagawa
Module::Pluggable
+ Class::Trigger
= Simple and Nice
but has limitations
Tatsuhiko Miyagawa
In Reality,
we need more control
over how plugins behave
Tatsuhiko Miyagawa
1)
The order of
plugin executions
Tatsuhiko Miyagawa
2)
Per user configurations
for plugins
Tatsuhiko Miyagawa
3)
Temporarily
Disable plugins
Should be easy
Tatsuhiko Miyagawa
4)
How to install
& upgrade plugins
Tatsuhiko Miyagawa
5)
Let plugins
have storage area
Tatsuhiko Miyagawa
Etc, etc.
Tatsuhiko Miyagawa
Examples:
Kwiki
Plagger
qpsmtpd
Movable Type
Tatsuhiko Miyagawa
I won't talk about
Catalyst plugins
(and other framework thingy)
Tatsuhiko Miyagawa
Because they're
NOT
"plug-ins"
Tatsuhiko Miyagawa
Install plugins
And now you write
MORE CODE
Tatsuhiko Miyagawa
95% of Catalyst plugins
Are NOT "plugins"
But "components"
95% of these statistics is made up by the speakers.
Tatsuhiko Miyagawa
Kwiki 1.0
Tatsuhiko Miyagawa
Kwiki Plugin code
package Kwiki::URLBL;
use Kwiki::Plugin -Base;
use Kwiki::Installer -base;
const class_id
=> 'urlbl';
const class_title => 'URL Blacklist DNS';
const config_file => 'urlbl.yaml';
sub register {
require URI::Find;
my $registry = shift;
$registry->add(hook => 'edit:save', pre => 'urlbl_hook');
$registry->add(action => 'blacklisted_url');
}
Tatsuhiko Miyagawa
Kwiki Plugin (cont.)
sub urlbl_hook {
my $hook = pop;
my $old_page = $self->hub->pages->new_page($self->pages>current->id);
my $this
= $self->hub->urlbl;
my @old_urls = $this->get_urls($old_page->content);
my @urls
= $this->get_urls($self->cgi->page_content);
my @new_urls = $this->get_new_urls(\@old_urls, \@urls);
if (@new_urls && $this->is_blocked(\@new_urls)) {
$hook->cancel();
return $self->redirect("action=blacklisted_url");
}
}
Tatsuhiko Miyagawa
Magic implemented
in Spoon(::Hooks)
Tatsuhiko Miyagawa
"Install" Kwiki Plugins
# order doesn't matter here (according to Ingy)
Kwiki::Display
Kwiki::Edit
Kwiki::Theme::Basic
Kwiki::Toolbar
Kwiki::Status
Kwiki::Widgets
# Comment out (or entirely remove) to disable
# Kwiki::UnnecessaryStuff
Tatsuhiko Miyagawa
Kwiki plugin config
# in Kwiki::URLBL plugin
__config/urlbl.yaml__
urlbl_dns: sc.surbl.org, bsb.spamlookup.net, rbl.bulkfeeds.jp
# config.yaml
urlbl_dns: myowndns.example.org
Tatsuhiko Miyagawa
Kwiki plugins are CPAN modules
Tatsuhiko Miyagawa
Install and Upgrade plugins
cpan> install Kwiki::SomeStuff
Tatsuhiko Miyagawa
Using CPAN as a repository
Pros #1:
reuse most of current
CPAN infrastructure.
Tatsuhiko Miyagawa
Using CPAN as a repository
Pros #2:
Increasing # of modules
= good motivation
for Perl hackers
Tatsuhiko Miyagawa
Cons #1:
Installing CPAN deps
could be a mess
(especially for Win32)
Tatsuhiko Miyagawa
Cons #2:
Whenever Ingy releases
new Kwiki, lots of plugins
just break.
Tatsuhiko Miyagawa
Kwiki plugin storage
return if
grep {$page->id}
@{$self->config->cached_display_ignore};
my $html = io->catfile(
$self->plugin_directory,$page->id
)->utf8;
Tatsuhiko Miyagawa
Kwiki 2.0
Tatsuhiko Miyagawa
Same as Kwiki 1.0
Tatsuhiko Miyagawa
Except:
plugins are now
in SVN repository
Tatsuhiko Miyagawa
Tatsuhiko Miyagawa
Plagger plugin
package Plagger::Plugin::Publish::iCal;
use strict;
use base qw( Plagger::Plugin );
use
use
use
use
Data::ICal;
Data::ICal::Entry::Event;
DateTime::Duration;
DateTime::Format::ICal;
sub register {
my($self, $context) = @_;
$context->register_hook(
$self,
'publish.feed' => \&publish_feed,
'plugin.init ' => \&plugin_init,
);
}
Tatsuhiko Miyagawa
Plagger plugin (cont)
sub plugin_init {
my($self, $context) = @_;
my $dir = $self->conf->{dir};
unless (-e $dir && -d _) {
mkdir $dir, 0755
or $context->error("Failed to mkdir $dir: $!");
}
}
Tatsuhiko Miyagawa
Plagger plugin storage
$self->conf->{invindex} ||=
$self->cache->path_to('invindex');
Tatsuhiko Miyagawa
Plagger plugin config
# The order matters in config.yaml
# if they're in the same hooks
plugins:
- module: Subscription::Config
config:
feed:
- http://www.example.com/
- module: Filter::DegradeYouTube
config:
dev_id: XYZXYZ
- module: Publish::Gmail
disable: 1
Tatsuhiko Miyagawa
Plugins Install & Upgrade
>
#
>
>
notest cpan Plagger
or …
svn co http://…/plagger/trunk plagger
svn update
Tatsuhiko Miyagawa
Plagger impl.
ripped off by
many apps now
Tatsuhiko Miyagawa
qpsmtpd
Tatsuhiko Miyagawa
mod_perl for SMTP
Runs with tcpserver, forkserver
or Danga::Socket standalone
Tatsuhiko Miyagawa
Plugins: Flat files
rock:/home/miyagawa/svn/qpsmtpd> ls -F plugins
async/
greylisting
auth/
hosts_allow
check_badmailfrom
http_config
check_badmailfromto
ident/
check_badrcptto
logging/
check_badrcptto_patterns
milter
check_basicheaders
parse_addr_withhelo
check_earlytalker
queue/
check_loop
quit_fortune
check_norelay
rcpt_ok
check_relay
relay_only
check_spamhelo
require_resolvable_fromhost
content_log
rhsbl
count_unrecognized_commands sender_permitted_from
dns_whitelist_soft
spamassassin
dnsbl
tls
domainkeys
tls_cert*
dont_require_anglebrackets
virus/
Tatsuhiko Miyagawa
qpsmtpd plugin
sub hook_mail {
my ($self, $transaction, $sender, %param) = @_;
my @badmailfrom = $self->qp->config("badmailfrom")
or return (DECLINED);
for my $bad (@badmailfrom) {
my $reason = $bad;
$bad =~ s/^\s*(\S+).*/$1/;
next unless $bad;
$transaction->notes('badmailfrom', $reason) …
}
return (DECLINED);
}
Tatsuhiko Miyagawa
Actually qpsmtpd
Plugins are "compiled"
to modules
Tatsuhiko Miyagawa
my $eval = join("\n",
"package $package;",
'use Qpsmtpd::Constants;',
"require Qpsmtpd::Plugin;",
'use vars qw(@ISA);',
'use strict;',
'@ISA = qw(Qpsmtpd::Plugin);',
($test_mode ? 'use Test::More;' : ''),
"sub plugin_name { qq[$plugin] }",
$line,
$sub,
"\n", # last line comment without newline?
);
$eval =~ m/(.*)/s;
$eval = $1;
eval $eval;
die "eval $@" if $@;
Tatsuhiko Miyagawa
qpsmtpd plugin config
rock:/home/miyagawa/svn/qpsmtpd> ls config.sample/
config.sample:
IP
logging
require_resolvable_fromhost
badhelo
loglevel
rhsbl_zones
badrcptto_patterns
plugins
size_threshold
dnsbl_zones
rcpthosts
tls_before_auth
invalid_resolvable_fromhost relayclients
tls_ciphers
Tatsuhiko Miyagawa
config/plugins
# content filters
virus/klez_filter
# rejects mails with a SA score higher than 2
spamassassin reject_threshold 20
Tatsuhiko Miyagawa
config/badhelo
# these domains never uses their domain when greeting
us, so reject transactions
aol.com
yahoo.com
Tatsuhiko Miyagawa
Install & Upgrade plugins
Just use subversion
Tatsuhiko Miyagawa
Tatsuhiko Miyagawa
MT plugins are
flat-files
(or scripts that call
modules)
Tatsuhiko Miyagawa
MT plugin code
package MT::Plugin::BanASCII;
our $Method = "deny";
use MT;
use MT::Plugin;
my $plugin = MT::Plugin->new({
name => "BanASCII v$VERSION",
description => "Deny or moderate ASCII or Latin-1
comment",
});
MT->add_plugin($plugin);
MT->add_callback('CommentFilter', 2, $plugin, \&handler);
Tatsuhiko Miyagawa
MT plugin code (cont)
sub init_app {
my $plugin = shift;
$plugin->SUPER::init_app(@_);
my($app) = @_;
return unless $app->isa('MT::App::CMS');
$app->add_itemset_action({
type => 'comment',
key => 'spam_submission_comment',
label => 'Report SPAM Comment(s)',
code => sub {
$plugin->submit_spams_action('MT::Comment', @_) },
}
);
Tatsuhiko Miyagawa
Tatsuhiko Miyagawa
Tatsuhiko Miyagawa
MT plugin storage
require MT::PluginData;
my $data = MT::PluginData->load({
plugin => 'sidebar-manager',
key
=> $blog_id },
);
unless ($data) {
$data = MT::PluginData->new;
$data->plugin('sidebar-manager');
$data->key($blog_id);
}
$data->data( \$modulesets );
$data->save or die $data->errstr;
Tatsuhiko Miyagawa
Order control
MT->add_callback('CMSPostEntrySave', 9, $rightfields,
\&CMSPostEntrySave);
MT->add_callback('CMSPreSave_entry', 9, $rightfields,
\&CMSPreSave_entry);
MT::Entry->add_callback('pre_remove', 9, $rightfields,
\&entry_pre_remove);
Defined in plugins.
No Control on users end
Tatsuhiko Miyagawa
Conclusion
Flat-files
vs.
Modules
Tatsuhiko Miyagawa
Flat-files:
☺ Easy to install (Just grab it)
☻ Hard to upgrade
OK for simple plugins
Tatsuhiko Miyagawa
Modules:
☺ Full-access to Perl OO goodness
☺ Avoid duplicate efforts of CPAN
☻ Might be hard to resolve deps.
Subversion to the rescue
(could be a barrier for newbies)
Tatsuhiko Miyagawa
Nice-to-haves:
Order control
Temporarily disable plugins
Per plugin config
Per plugin storage
Tatsuhiko Miyagawa
Resources
Class::Trigger
http://search.cpan.org/dist/Class-Trigger/
Module::Pluggable
http://search.cpan.org/dist/Module-Pluggable/
Ask Bjorn Hansen: Build Easily Extensible Perl Programs
http://conferences.oreillynet.com/cs/os2005/view/e_sess/6806
qpsmtpd
http://smtpd.develooper.com/
MT plugins
http://www.sixapart.com/pronet/plugins/
Kwiki
http://www.kwiki.org/
Plagger
http://plagger.org/
Tatsuhiko Miyagawa
Download