File Coverage

blib/lib/Pinto/Server/Responder/Action.pm
Criterion Covered Total %
statement 119 133 89.4
branch 20 26 76.9
condition 6 11 54.5
subroutine 24 26 92.3
pod 1 4 25.0
total 170 200 85.0


line stmt bran cond sub pod time code
1             # ABSTRACT: Responder for action requests
2              
3             package Pinto::Server::Responder::Action;
4              
5 12     12   5574 use Moose;
  12         36  
  12         86  
6              
7 12     12   69590 use Carp;
  12         26  
  12         594  
8 12     12   62 use JSON;
  12         24  
  12         84  
9 12     12   4144 use IO::Pipe;
  12         11170  
  12         124  
10 12     12   304 use IO::Select;
  12         24  
  12         372  
11 12     12   60 use Try::Tiny;
  12         36  
  12         474  
12 12     12   62 use File::Temp;
  12         26  
  12         730  
13 12     12   60 use File::Copy;
  12         34  
  12         462  
14 12     12   2650 use Proc::Fork;
  12         17132  
  12         62  
15 12     12   1946 use Path::Class;
  12         28  
  12         516  
16 12     12   2808 use Proc::Terminator;
  12         152932  
  12         688  
17 12     12   826 use Plack::Response;
  12         1974  
  12         108  
18 12     12   298 use HTTP::Status qw(:constants);
  12         24  
  12         3602  
19              
20 12     12   74 use Pinto;
  12         26  
  12         136  
21 12     12   226 use Pinto::Result;
  12         26  
  12         64  
22 12     12   4486 use Pinto::Chrome::Net;
  12         48  
  12         124  
23 12     12   524 use Pinto::Constants qw(:protocol);
  12         26  
  12         868  
24              
25             #-------------------------------------------------------------------------------
26              
27             our $VERSION = '0.13'; # VERSION
28              
29             #-------------------------------------------------------------------------------
30              
31             extends qw(Pinto::Server::Responder);
32              
33             #-------------------------------------------------------------------------------
34              
35             sub respond {
36 59     59 1 203 my ($self) = @_;
37              
38 59         202 my $error_response = $self->check_protocol_version;
39 59 100       756 return $error_response if $error_response;
40              
41             # path_info always has a leading slash, e.g. /action/list
42 56         1597 my ( undef, undef, $action_name ) = split '/', $self->request->path_info;
43              
44 56         631 my %params = %{ $self->request->parameters }; # Copying
  56         1384  
45 56 50       30322 my $chrome_args = $params{chrome} ? decode_json( $params{chrome} ) : {};
46 56 50       368 my $pinto_args = $params{pinto} ? decode_json( $params{pinto} ) : {};
47 56 100       681 my $action_args = $params{action} ? decode_json( $params{action} ) : {};
48              
49 56         1935 for my $upload_name ( $self->request->uploads->keys ) {
50 0         0 my $upload = $self->request->uploads->{$upload_name};
51 0         0 my $basename = $upload->filename;
52 0         0 my $localfile = file( $upload->path )->dir->file($basename);
53 0         0 File::Copy::move( $upload->path, $localfile ); #TODO: autodie
54 0         0 $action_args->{$upload_name} = $localfile;
55             }
56              
57 56         1204 my $response;
58 56         596 my $pipe = IO::Pipe->new;
59              
60             run_fork {
61 10         56304 child { $self->child_proc( $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) }
62 46         212774 parent { my $child_pid = shift; $response = $self->parent_proc( $pipe, $child_pid ) }
  46         1394  
63 56     56   2006 error { croak "Failed to fork: $!" };
  0         0  
64 56         7269 };
65              
66 46         5397 return $response;
67             }
68              
69             #-------------------------------------------------------------------------------
70              
71             sub check_protocol_version {
72 59     59 0 135 my ($self) = @_;
73              
74             # NB: Format derived from GitHub: https://developer.github.com/v3/media
75 59         397 my $media_type_rx = qr{^ application / vnd [.] pinto [.] v(\d+) (?:[+] .+)? $}ix;
76              
77 59   100     1812 my $accept = $self->request->header('Accept') || '';
78 59 100       17421 my $version = $accept =~ $media_type_rx ? $1 : 0;
79              
80 59 100       472 return unless my $cmp = $version <=> $PINTO_PROTOCOL_VERSION;
81              
82 3         32 my $fmt = 'Your client is too %s for this server. You must upgrade %s.';
83 3 100       19 my ($age, $component) = $cmp > 0 ? qw(new pintod) : qw(old pinto);
84 3         22 my $msg = sprintf $fmt, $age, $component;
85              
86 3         24 return [ HTTP_UNSUPPORTED_MEDIA_TYPE, [], [$msg] ];
87             }
88              
89             #-------------------------------------------------------------------------------
90              
91             sub child_proc {
92 10     10 0 487 my ( $self, $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) = @_;
93              
94 10         521 my $writer = $pipe->writer;
95 10         8458 $writer->autoflush;
96              
97             # I'm not sure why, but cleanup isn't happening when we get
98             # a TERM signal from the parent process. I suspect it
99             # has something to do with File::NFSLock messing with %SIG
100 10     0   8523 local $SIG{TERM} = sub { File::Temp::cleanup; die $@ };
  0         0  
  0         0  
101              
102             ## no critic qw(PackageVar)
103 10         328 local $Pinto::Globals::current_username = delete $pinto_args->{username};
104 10         269 local $Pinto::Globals::current_time_offset = delete $pinto_args->{time_offset};
105             ## use critic;
106              
107 10         299 $chrome_args->{stdout} = $writer;
108 10         219 $chrome_args->{stderr} = $writer;
109              
110 10         1961 my $chrome = Pinto::Chrome::Net->new($chrome_args);
111 10         1058 my $pinto = Pinto->new( chrome => $chrome, root => $self->root );
112              
113             my $result =
114 10     10   2135 try { $pinto->run( ucfirst $action_name => %{$action_args} ) }
  10         474  
115 10     0   570 catch { print {$writer} $_; Pinto::Result->new->failed };
  0         0  
  0         0  
  0         0  
116              
117 10 100       676 print {$writer} $PINTO_PROTOCOL_STATUS_OK . "\n" if $result->was_successful;
  5         64  
118              
119 10 0       555 exit $result->was_successful ? 0 : 1;
120             }
121              
122             #-------------------------------------------------------------------------------
123              
124             sub parent_proc {
125 46     46 0 759 my ( $self, $pipe, $child_pid ) = @_;
126              
127 46         1197 my $reader = $pipe->reader;
128 46         33113 my $select = IO::Select->new($reader);
129 46         6628 $reader->blocking(0);
130              
131             my $response = sub {
132 46     46   4044 my $responder = shift;
133              
134 46         640 my $headers = ['Content-Type' => 'text/plain'];
135 46         500 my $writer = $responder->( [ HTTP_OK, $headers ] );
136 46         42554 my $socket = $self->request->env->{'psgix.io'};
137 46         1624 my $nullmsg = $PINTO_PROTOCOL_NULL_MESSAGE . "\n";
138              
139              
140 46         954 while (1) {
141              
142 324         748 my $input;
143 324 100       2400 if ( $select->can_read(1) ) {
144 92         40313373 $input = <$reader>; # Will block until \n
145 92 100       521 last if not defined $input; # We reached eof
146             }
147              
148 278         232288656 my $ok = eval {
149 278         10583 local $SIG{ALRM} = sub { die "Write timed out" };
  0         0  
150 278         1994 alarm(3);
151              
152 278   66     7189 $writer->write( $input || $nullmsg );
153 278         14326 1; # Write succeeded
154             };
155              
156 278         1071 alarm(0);
157 278 50 33     2246 unless ( $ok && ( !$socket || getpeername($socket) ) ) {
      33        
158 0         0 proc_terminate( $child_pid, max_wait => 10 );
159 0         0 last;
160             }
161             }
162              
163 46 50       1132 $writer->close if not $socket; # Hangs otherwise!
164 46         3045090 waitpid $child_pid, 0;
165 46         2041 };
166              
167 46         607 return $response;
168             }
169              
170             #-------------------------------------------------------------------------------
171              
172             __PACKAGE__->meta->make_immutable;
173              
174             #-------------------------------------------------------------------------------
175              
176             1;
177              
178             __END__
179              
180             =pod
181              
182             =encoding UTF-8
183              
184             =for :stopwords Jeffrey Ryan Thalhammer
185              
186             =head1 NAME
187              
188             Pinto::Server::Responder::Action - Responder for action requests
189              
190             =head1 VERSION
191              
192             version 0.13
193              
194             =head1 AUTHOR
195              
196             Jeffrey Ryan Thalhammer <jeff@stratopan.com>
197              
198             =head1 COPYRIGHT AND LICENSE
199              
200             This software is copyright (c) 2015 by Jeffrey Ryan Thalhammer.
201              
202             This is free software; you can redistribute it and/or modify it under
203             the same terms as the Perl 5 programming language system itself.
204              
205             =cut