File Coverage

blib/lib/Mojolicious/Plugin/Webtail.pm
Criterion Covered Total %
statement 18 83 21.6
branch 0 20 0.0
condition 0 8 0.0
subroutine 6 13 46.1
pod 1 1 100.0
total 25 125 20.0


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