File Coverage

blib/lib/App/htmlcat.pm
Criterion Covered Total %
statement 30 105 28.5
branch 0 16 0.0
condition 0 11 0.0
subroutine 10 24 41.6
pod 3 3 100.0
total 43 159 27.0


line stmt bran cond sub pod time code
1             package App::htmlcat;
2 1     1   7 use strict;
  1         2  
  1         58  
3 1     1   8 use warnings;
  1         3  
  1         42  
4 1     1   30 use 5.008_001;
  1         5  
  1         48  
5 1     1   1578 use AnyEvent::Handle;
  1         31986  
  1         39  
6 1     1   1182 use HTML::FromANSI::Tiny;
  1         1416  
  1         9  
7 1     1   1098 use HTML::Entities;
  1         8581  
  1         87  
8 1     1   905 use Data::Section::Simple qw(get_data_section);
  1         486  
  1         131  
9 1     1   1925 use IO::Socket::INET;
  1         19916  
  1         9  
10 1     1   1446 use Plack::Runner;
  1         11402  
  1         35  
11 1     1   1150 use Encode;
  1         10559  
  1         1082  
12              
13             our $VERSION = '0.01';
14              
15             sub new {
16 0     0 1   my ($class, @args) = @_;
17              
18             my $self = bless {
19             args => \@args,
20             clients => {},
21             ansi => HTML::FromANSI::Tiny->new(
22             auto_reverse => 1,
23             no_plain_tags => 1,
24 0     0     html_encode => sub { encode_entities($_[0], q("&<>)) },
25 0           ),
26             }, $class;
27              
28             $self->{in} = AnyEvent::Handle->new(
29             fh => \*STDIN,
30             on_eof => sub {
31 0     0     my ($handle) = @_;
32 0           exit 0;
33             },
34             on_error => sub {
35 0     0     my ($handle, $fatal, $message) = @_;
36 0           warn "stdin: $message\n";
37 0           $self->_broadcast($_[0]{rbuf});
38 0           exit 1;
39             }
40 0           );
41              
42 0           return $self;
43             }
44              
45             sub _on_read_cb {
46 0     0     my $self = shift;
47              
48             return sub {
49 0     0     my ($handle) = @_;
50 0           $self->_broadcast($handle->rbuf);
51 0           $handle->rbuf = '';
52 0           };
53             }
54              
55             sub _broadcast {
56 0     0     my ($self, $data) = @_;
57              
58 0           open my $fh, '<', \$data;
59 0           while (defined (my $line = <$fh>)) {
60 0           $line = decode_utf8 $line;
61 0           foreach my $client (values %{ $self->{clients} }){
  0            
62 0           $self->_push_line($client->{handle}, $line);
63             }
64             }
65             }
66              
67             sub _push_line {
68 0     0     my ($self, $handle, $line) = @_;
69 0           $handle->push_write("data:" . Encode::encode("utf-8", scalar $self->{ansi}->html($line) ) );
70 0           $handle->push_write("\n");
71             }
72              
73             sub as_psgi {
74 0     0 1   my $self = shift;
75              
76             return sub {
77 0     0     my $env = shift;
78              
79 0 0         $env->{'psgi.streaming'} or die 'psgi.streaming not supported';
80              
81 0 0         if ($env->{PATH_INFO} eq '/stream') {
    0          
    0          
    0          
82             return sub {
83 0           my $respond = shift;
84              
85 0           my $remote_addr = $env->{REMOTE_ADDR};
86              
87 0           my $writer = $respond->([
88             200, [
89             'Content-Type' => 'text/event-stream; charset=utf-8',
90             'Cache-Control' => 'no-cache'
91             ]
92             ]);
93              
94 0           my $io = $env->{'psgix.io'};
95             my $handle = AnyEvent::Handle->new(
96             fh => $io,
97             on_error => sub {
98 0           my ($handle, $fatal, $message) = @_;
99 0           warn "client [$remote_addr]: $message\n";
100 0           delete $self->{clients}->{ 0+$io };
101 0 0         if (keys %{$self->{clients}} == 0) {
  0            
102 0           $self->{in}->on_read();
103             }
104             }
105 0           );
106              
107 0           $self->{clients}->{ 0+$io } = {
108             handle => $handle,
109             writer => $writer, # keep reference
110             };
111 0           $self->{in}->on_read($self->_on_read_cb);
112 0           };
113             } elsif ($env->{PATH_INFO} eq '/css') {
114 0           return [ 200, [ 'Content-Type' => 'text/css' ], [ $self->{ansi}->css ] ];
115             } elsif ($env->{PATH_INFO} eq '/js') {
116 0           return [ 200, [ 'Content-Type' => 'text/javascript' ], [ get_data_section('js') ] ];
117             } elsif ($env->{PATH_INFO} eq '/') {
118 0           return [ 200, [ 'Content-Type' => 'text/html; charset=utf-8' ], [ get_data_section('html') ] ];
119             } else {
120 0           return [ 404, [], [] ];
121             }
122 0           };
123             }
124              
125             sub run {
126 0     0 1   my $self = shift;
127 0           my $runner = Plack::Runner->new(app => $self->as_psgi);
128 0           $runner->parse_options(
129             '--env' => 'production',
130             '--port' => _empty_port(),
131 0           @{ $self->{args} }
132             );
133              
134 0 0         if (my $exec = { @{$runner->{options}} }->{exec}) {
  0            
135 0           push @{ $runner->{options} }, server_ready => sub {
136 0     0     my ($args) = @_;
137 0   0       my $host = $args->{host} || 'localhost';
138 0   0       my $proto = $args->{proto} || 'http';
139 0           system "$exec $proto://$host:$args->{port}/";
140 0           };
141             } else {
142 0           push @{ $runner->{options} }, server_ready => sub {
143 0     0     my ($args) = @_;
144 0   0       my $host = $args->{host} || 'localhost';
145 0   0       my $proto = $args->{proto} || 'http';
146 0           print STDERR "$0: $proto://$host:$args->{port}/\n";
147 0           };
148             }
149              
150 0           $runner->run;
151             }
152              
153             # from Test::TCP
154             sub _empty_port {
155 0   0 0     my $port = $ENV{HTTPCAT_PORT} || 45192 + int(rand() * 1000);
156              
157 0           while ($port++ < 60000) {
158 0           my $remote = IO::Socket::INET->new(
159             Proto => 'tcp',
160             PeerAddr => '127.0.0.1',
161             PeerPort => $port,
162             );
163              
164 0 0         if ($remote) {
165 0           close $remote;
166             } else {
167 0           return $port;
168             }
169             }
170              
171 0           die 'Could not find empty port';
172             }
173             __DATA__