Given at YAPC::EU 2012
Dancer + WebSocket + AnyEvent + Twiggy
This in *not* a talk about doing a hello world in Dancer, as there are plenty of it. This is a real-life example of using Dancer to address a problem in an elegant and powerful way
At $job, we have cpan mirrors. We want them to stay a bit behind the real CPAN for stability, but we have a tool to update modules from the real CPAN to our mirrors. Cool.
I wanted to have a web interface to trigger it, and monitor the injection. This problem is not a typical one (blog, wiki, CRUD, etc). Here we have a long running operation that shall happen only one at a time, that generates logs to be displayed, with states that need keeping. In this regard, it's interesting to see how Dancer is versatile enough to address these situations with ease.
This talk details how I did that, the technology I used, and the full source code (which is quite short). I used Dancer + WebSocket + AnyEvent + Twiggy + some other stuff.
This talk doesn't require any particular knowledge beyond basic Perl, and very basic web server understanding.
6. But,
• CPAN = moving target
• need to stay stable
• local CPAN mirror
• but we need to catch up,
sometimes
7. • We have a CLI injecter
• We want a web injecter
• Let’s see the process
8. The process
• mirror created with minicpan
• it’s «single threaded»
• managed with CPAN::Mini
• one mirror at a time
• injecting = calling
• one injection at a time
CPAN::Mini::Inject::inject
• one command to perform
• with the mirror’s config file
9. no user no database
but,
no session
a forking process
10. The web interface
• everything on a single page
• input a string, verify
• get corresponding CPAN module
• check there is a newer version
• confirm/cancel
• inject the module in the mirror
• display the process log
• keep track of the log and the status
11. W
Status ire
fra
input me
schedule confirm cancel
logs
12. Technologies
We need We choose
• « instantaneous » update
of the logs • WebSockets
• every user see the same • Event programming
thing
• MetaCPAN::API
• info about the module
13. Dancer in one sentence
• « Dancer allows you to easily create web applications where you
can associate http routes to code, which usually end up sending
back data via a template engine »
• dancer -a plop creates a new application
• templating system:
• a layout, with a [% content %] placeholder
• views, template page, that are injected as content
14. WebSocket
• What are WebSockets ? bidirectional web communication
• The server can push to the client (e.g. add more log lines)
• Dancer::Plugin::WebSocket
• uses Web::Hippie, websocket/comet Plack implementation
• which uses AnyEvent
• so we need an AnyEvent Plack web server : Twiggy
15. forked injection AnyEvent run_cmd
process WebSocket
logs + status javascript
status
server process
logs
server side client side
22. setup the websocket, handle logs
var socket = new WebSocket(
"ws://[% server_host %]:[% server_port %]/_hippie/ws"
);
socket.onmessage = function(e) {
var data = JSON.parse(e.data);
if (data.msg) {
$('#injection_log').append(data.msg);
}
};
function send_msg(message) {
socket.send(JSON.stringify({ msg: message }));
}
23. handle status change
var status_regex = /__STATUS_CHANGED:/;
if (status_regex.test(data.msg)) {
var new_status = data.msg;
new_status
= new_status.replace(/__STATUS_CHANGED:(.*)__/, "$1");
$('#status_text').remove();
$('#status').append(
'<span id="status_text">' + new_status + '</span>'
);
}
24. package My::Mirror::Injector;
use Dancer;
use Dancer::Plugin::FlashMessage; # for status message
use Dancer::Plugin::WebSocket; # for WebSocket write
use AnyEvent::Util; # for run_cmd
my $status = 0;
my $log = ''; # OMG GLOBAL VARIABLES !!!111
my $module_name;
get '/' => &display_page;
sub display_page {
my $uri = request->uri_base; my $scheme = request->scheme;
my $host = $uri =~ m|$scheme://(.*?):|;
template index => {
status => $status,
module_name => $module_name,
server_host => $host,
server_port => request->port,
log => $log
};
}
25. post '/' => sub {
param 'confirm' or $module_name = param 'module_name';
if (defined param 'schedule') {
$status = 1; flash info => "Scheduled injection";
$log = '';
launch_command(
"mirror-inject --autoflush --dryrun --module $module_name"
);
} elsif (defined param 'confirm') {
# Same, but status = 2
# and run command with no --dryrun
} elsif (defined param 'cancel') {
# User clicked on Cancel
$status = 0;
flash info => "Cancelled injection";
}
display_page();
}
26. my $next_status;
sub launch_command {
my ($cmd) = @_;
run_cmd( $cmd ,
'>' => sub {
my ($data) = @_;
if ( defined $data ) {
$data =~ s/__CONFIRM__// and $next_status = 2;
$data =~ s/__FINISHED__// and $next_status = 0;
$log .= $data;
ws_send $data;
} else {
# End of execution
if (defined $next_status) {
$status = $next_status;
$next_status = undef;
ws_send '__STATUS_CHANGED:' . $status . "__n";
}
}});
}