package Mojolicious::Plugin::Webtail; use Mojo::Base 'Mojolicious::Plugin'; our $VERSION = '0.02'; use Mojo::Util qw{ slurp }; use Carp (); use Encode (); has 'template' => <<'TEMPLATE'; <%= $file %> - Webtail %= stylesheet begin /* Stolen from https://github.com/r7kamura/webtail */ * { margin: 0; padding: 0; } body { margin: 1em 0; color: #ddd; background: #111; } pre { padding: 0 1em; line-height: 1.25; font-family: "Monaco", "Consolas", monospace; } #message { position:fixed; top:1em; right:1em; } % end %= javascript 'https://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js' %= javascript begin $(function() { var autoscroll = true; // press 's' key to toggle autoscroll $(window).keydown(function(e) { if (e.keyCode == 83 ) autoscroll = (autoscroll) ? false : true }); var ws = new (WebSocket || MozWebSocket)('<%= $ws_url %>'); var timer_id; ws.onopen = function() { console.log('Connection opened'); timer_id = setInterval( function() { console.log('Connection keepalive'); ws.send('keepalive'); }, 1000 * 240 ); }; ws.onmessage = function(msg) { if (msg.data == '\n' && $('pre:last').text() == '\n') return; $('
').text(msg.data).appendTo('body');
      if (autoscroll) $('html, body').scrollTop($(document).height());

      % if ($webtailrc) {
      // webtailrc
      <%== $webtailrc %>
      % }
    };
    ws.onclose = function() {
      console.warn('Connection closed');
      clearInterval(timer_id);
    };
    ws.onerror = function(msg) {
      console.error(msg.data);
    };
  });
  % end

press 's' to toggle autoscroll
TEMPLATE has 'file'; has 'webtailrc'; has '_tail'; has '_clients' => sub { +{} }; sub DESTROY { my $self = shift; $self->_tail->close if $self->_tail; } sub _prepare_stream { my ( $self, $app ) = @_; return if ( $self->_tail ); my ( $fh, $pid ); if ( $self->file ) { $pid = open( $fh, '-|', 'tail', '-F', '-n', '0', $self->file ) or Carp::croak "fork failed: $!"; } else { $fh = *STDIN; } $app->log->debug( sprintf("tailing file: %s", $self->file || 'STDIN') ); my $stream = Mojo::IOLoop::Stream->new($fh)->timeout(0); my $stream_id = Mojo::IOLoop->stream($stream); $stream->on( read => sub { my ($stream, $chunk) = @_; for my $key (keys %{ $self->_clients }) { my $tx = $self->_clients->{$key}; next unless $tx->is_websocket; $tx->send( Encode::decode_utf8($chunk) ); $app->log->debug( sprintf('sent %s', $key ) ); } } ); $stream->on( error => sub { $app->log->error( sprintf('error %s', $_[1] ) ); Mojo::IOLoop->remove($stream_id); $self->_tail(undef); }); $stream->on( close => sub { $app->log->debug('close tail stream'); if ($pid) { kill 'TERM', $pid if ( kill 0, $pid ); waitpid( $pid, 0 ); }; Mojo::IOLoop->remove($stream_id); $self->_tail(undef); }); $self->_tail($stream); $app->log->debug( sprintf('connected tail stream %s', $stream_id ) ); } sub register { my $plugin = shift; my ( $app, $args ) = @_; $plugin->file( $args->{file} || '' ); $plugin->webtailrc( $args->{webtailrc} || '' ); $app->hook( before_dispatch => sub { my $c = shift; my $path = $c->req->url->path; return unless ($c->req->url->path =~ m|^/webtail/?$|); if ( $c->tx->is_websocket ) { $plugin->_prepare_stream($app); my $tx = $c->tx; $plugin->_clients->{"$tx"} = $tx; $c->app->log->debug( sprintf('connected %s', "$tx" ) ); Mojo::IOLoop->stream( $tx->connection )->timeout(300)->on( timeout => sub { $c->finish; delete $plugin->_clients->{"$tx"}; $c->app->log->debug( sprintf('timeout %s', $tx ) ); }); $c->on( message => sub { $c->app->log->debug( sprintf('message "%s" from %s', $_[1], $tx ) ); } ); $c->on( finish => sub { delete $plugin->_clients->{"$tx"}; $c->app->log->debug( sprintf('finish %s', $tx ) ); } ); $c->res->headers->content_type('text/event-stream'); return; } my $ws_url = $c->req->url->to_abs->scheme('ws')->to_string; $c->render( inline => $plugin->template, ws_url => $ws_url, webtailrc => ( $plugin->webtailrc ) ? slurp( $plugin->webtailrc ) : '', file => $args->{file} || 'STDIN', ); }, ); return $app; } 1; __END__ =head1 NAME Mojolicious::Plugin::Webtail - display tail to your browser =head1 SYNOPSIS use Mojolicious::Lite; plugin( 'Webtail', file => "/path/to/logfile", webtailrc => '/path/to/webtail.rc' ); app->start; or > perl -Mojo -e 'a->plugin("Webtail", file => "/path/to/logfile", webtailrc => "/path/to/webtail.rc")->start' daemon or > tail -f /path/to/logfile | perl -Mojo -e 'a->plugin("Webtail", webtailrc => "/path/to/webtail.rc")->start' daemon and access "http://host:port/webtail" in your web browser. =head1 DESCRIPTION Mojolicious::Plugin::Webtail is display tail to your browser by WebSocket. =head1 METHODS L inherits all methods from :. =head1 OPTIONS L supports the following options. =head2 C displays the contents of C or, by default, its C. =head2 C define your custom callback in C file. the code in C file is executed when a new line is inserted. =head1 AUTHOR hayajo Ehayajo@cpan.orgE =head1 SEE ALSO L =head1 LICENSE This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut