File Coverage

blib/lib/Mojolicious/Plugin/Webtail.pm
Criterion Covered Total %
statement 21 86 24.4
branch 0 20 0.0
condition 0 8 0.0
subroutine 7 14 50.0
pod 1 1 100.0
total 29 129 22.4


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Webtail;
2              
3 1     1   34934 use strict;
  1         2  
  1         41  
4 1     1   5 use warnings;
  1         2  
  1         48  
5             our $VERSION = '0.06';
6              
7 1     1   1448 use Mojo::Base 'Mojolicious::Plugin';
  1         15306  
  1         7  
8 1     1   3763 use Mojo::Util qw{ slurp };
  1         135010  
  1         205  
9 1     1   13 use Carp ();
  1         2  
  1         15  
10 1     1   6 use Encode ();
  1         1  
  1         25  
11              
12 1     1   5 use constant DEFAULT_TAIL_OPTIONS => '-f -n 0';
  1         1  
  1         1517  
13              
14             has 'template' => <<'TEMPLATE';
15            
16            
17             <%= $file %> - Webtail
18             %= stylesheet begin
19             /* Stolen from https://github.com/r7kamura/webtail */
20             * {
21             margin: 0;
22             padding: 0;
23             }
24             body {
25             margin: 1em 0;
26             color: #ddd;
27             background: #111;
28             }
29             pre {
30             padding: 0 1em;
31             line-height: 1.25;
32             font-family: "Monaco", "Consolas", monospace;
33             }
34             #message {
35             position:fixed;
36             top:1em;
37             right:1em;
38             }
39             % end
40             %= javascript 'https://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.min.js'
41             %= javascript begin
42             $(function() {
43             var autoscroll = true;
44             // press 's' key to toggle autoscroll
45             $(window).keydown(function(e) { if (e.keyCode == 83 ) autoscroll = (autoscroll) ? false : true });
46              
47             var ws = new (WebSocket || MozWebSocket)('<%= $ws_url %>');
48             var timer_id;
49             ws.onopen = function() {
50             console.log('Connection opened');
51             timer_id = setInterval(
52             function() {
53             console.log('Connection keepalive');
54             ws.send('keepalive');
55             },
56             1000 * 240
57             );
58             };
59             ws.onmessage = function(msg) {
60             if (msg.data == '\n' && $('pre:last').text() == '\n') return;
61             $('
').text(msg.data).appendTo('body'); 
62             if (autoscroll) $('html, body').scrollTop($(document).height());
63              
64             % if ($webtailrc) {
65             // webtailrc
66             <%== $webtailrc %>
67             % }
68             };
69             ws.onclose = function() {
70             console.warn('Connection closed');
71             clearInterval(timer_id);
72             };
73             ws.onerror = function(msg) {
74             console.error(msg.data);
75             };
76             });
77             % end
78            
79            
press 's' to toggle autoscroll
80            
81             TEMPLATE
82              
83             has 'file';
84             has 'webtailrc';
85             has 'tail_opts' => sub { DEFAULT_TAIL_OPTIONS };
86              
87             has '_tail_stream';
88             has '_clients' => sub { +{} };
89              
90             sub DESTROY {
91 0     0     my $self = shift;
92 0 0         $self->_tail_stream->close if $self->_tail_stream;
93             }
94              
95             sub _prepare_stream {
96 0     0     my ( $self, $app ) = @_;
97              
98 0 0         return if ( $self->_tail_stream );
99              
100 0           my ( $fh, $pid );
101 0           my $read_from = 'STDIN';
102 0 0         if ( $self->file ) {
103 0           require Text::ParseWords;
104 0           my @opts = Text::ParseWords::shellwords( $self->tail_opts );
105 0           my @cmd = ('tail', @opts, $self->file);
106 0 0         $pid = open( $fh, '-|', @cmd ) or Carp::croak "fork failed: $!";
107 0           $read_from = join ' ', @cmd;
108             }
109             else {
110 0           $fh = *STDIN;
111             }
112 0           $app->log->debug("reading from: $read_from");
113              
114 0           my $stream = Mojo::IOLoop::Stream->new($fh)->timeout(0);
115 0           my $stream_id = Mojo::IOLoop->stream($stream);
116             $stream->on( read => sub {
117 0     0     my ($stream, $chunk) = @_;
118 0           for my $key (keys %{ $self->_clients }) {
  0            
119 0           my $tx = $self->_clients->{$key};
120 0 0         next unless $tx->is_websocket;
121 0           $tx->send( Encode::decode_utf8($chunk) );
122 0           $app->log->debug( sprintf('sent %s', $key ) );
123             }
124 0           } );
125             $stream->on( error => sub {
126 0     0     $app->log->error( sprintf('error %s', $_[1] ) );
127 0           Mojo::IOLoop->remove($stream_id);
128 0           $self->_tail_stream(undef);
129 0           });
130             $stream->on( close => sub {
131 0     0     $app->log->debug('close tail stream');
132 0 0         if ($pid) {
133 0 0         kill 'TERM', $pid if ( kill 0, $pid );
134 0           waitpid( $pid, 0 );
135             };
136 0           Mojo::IOLoop->remove($stream_id);
137 0           $self->_tail_stream(undef);
138 0           });
139              
140 0           $self->_tail_stream($stream);
141 0           $app->log->debug( sprintf('connected tail stream %s', $stream_id ) );
142             }
143              
144             sub register {
145 0     0 1   my $plugin = shift;
146 0           my ( $app, $args ) = @_;
147              
148 0   0       $plugin->file( $args->{file} || '' );
149 0   0       $plugin->webtailrc( $args->{webtailrc} || '' );
150 0   0       $plugin->tail_opts( $args->{tail_opts} || DEFAULT_TAIL_OPTIONS );
151              
152             $app->hook(
153             before_dispatch => sub {
154 0     0     my $c = shift;
155 0           my $path = $c->req->url->path;
156              
157 0 0         return unless ($c->req->url->path =~ m|^/webtail/?$|);
158              
159 0 0         if ( $c->tx->is_websocket ) {
160 0           $plugin->_prepare_stream($app);
161 0           my $tx = $c->tx;
162 0           $plugin->_clients->{"$tx"} = $tx;
163 0           $c->app->log->debug( sprintf('connected %s', "$tx" ) );
164             Mojo::IOLoop->stream( $tx->connection )->timeout(300)->on( timeout => sub {
165 0           $c->finish;
166 0           delete $plugin->_clients->{"$tx"};
167 0           $c->app->log->debug( sprintf('timeout %s', $tx ) );
168 0           });
169             $c->on( message => sub {
170 0           $c->app->log->debug( sprintf('message "%s" from %s', $_[1], $tx ) );
171 0           } );
172             $c->on( finish => sub {
173 0           delete $plugin->_clients->{"$tx"};
174 0           $c->app->log->debug( sprintf('finish %s', $tx ) );
175 0           } );
176 0           $c->res->headers->content_type('text/event-stream');
177 0           return;
178             }
179              
180 0           my $ws_url = $c->req->url->to_abs->scheme('ws')->to_string;
181 0 0 0       $c->render(
182             inline => $plugin->template,
183             ws_url => $ws_url,
184             webtailrc => ( $plugin->webtailrc ) ? slurp( $plugin->webtailrc ) : '',
185             file => $args->{file} || 'STDIN',
186             );
187             },
188 0           );
189 0           return $app;
190             }
191              
192             1;
193             __END__