The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# ABSTRACT: Responder for action requests

package Pinto::Server::Responder::Action;

use Moose;

use Carp;
use JSON;
use IO::Pipe;
use IO::Select;
use Try::Tiny;
use File::Temp;
use File::Copy;
use Proc::Fork;
use Path::Class;
use Proc::Terminator;
use Plack::Response;
use HTTP::Status qw(:constants);

use Pinto;
use Pinto::Result;
use Pinto::Chrome::Net;
use Pinto::Constants qw(:protocol);

#-------------------------------------------------------------------------------

our $VERSION = '0.12'; # VERSION

#-------------------------------------------------------------------------------

extends qw(Pinto::Server::Responder);

#-------------------------------------------------------------------------------

sub respond {
    my ($self) = @_;

    my $error_response = $self->check_protocol_version;
    return $error_response if $error_response;

    # path_info always has a leading slash, e.g. /action/list
    my ( undef, undef, $action_name ) = split '/', $self->request->path_info;

    my %params      = %{ $self->request->parameters };                         # Copying
    my $chrome_args = $params{chrome} ? decode_json( $params{chrome} ) : {};
    my $pinto_args  = $params{pinto}  ? decode_json( $params{pinto} )  : {};
    my $action_args = $params{action} ? decode_json( $params{action} ) : {};

    for my $upload_name ( $self->request->uploads->keys ) {
        my $upload    = $self->request->uploads->{$upload_name};
        my $basename  = $upload->filename;
        my $localfile = file( $upload->path )->dir->file($basename);
        File::Copy::move( $upload->path, $localfile );                         #TODO: autodie
        $action_args->{$upload_name} = $localfile;
    }

    my $response;
    my $pipe = IO::Pipe->new;

    run_fork {
        child  { $self->child_proc( $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) }
        parent { my $child_pid = shift; $response = $self->parent_proc( $pipe, $child_pid ) }
        error  { croak "Failed to fork: $!" };
    };

    return $response;
}

#-------------------------------------------------------------------------------

sub check_protocol_version {
    my ($self) = @_;

    # NB: Format derived from GitHub: https://developer.github.com/v3/media
    my $media_type_rx = qr{^ application / vnd [.] pinto [.] v(\d+) (?:[+] .+)? $}ix;

    my $accept = $self->request->header('Accept') || '';
    my $version = $accept =~ $media_type_rx ? $1 : 0;

    return unless my $cmp = $version <=> $PINTO_PROTOCOL_VERSION;

    my $fmt = 'Your client is too %s for this server. You must upgrade %s.';
    my ($age, $component) = $cmp > 0 ? qw(new pintod) : qw(old pinto);
    my $msg = sprintf $fmt, $age, $component;

    return [ HTTP_UNSUPPORTED_MEDIA_TYPE, [], [$msg] ];
}

#-------------------------------------------------------------------------------

sub child_proc {
    my ( $self, $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) = @_;

    my $writer = $pipe->writer;
    $writer->autoflush;

    # I'm not sure why, but cleanup isn't happening when we get
    # a TERM signal from the parent process.  I suspect it
    # has something to do with File::NFSLock messing with %SIG
    local $SIG{TERM} = sub { File::Temp::cleanup; die $@ };

    ## no critic qw(PackageVar)
    local $Pinto::Globals::current_username    = delete $pinto_args->{username};
    local $Pinto::Globals::current_time_offset = delete $pinto_args->{time_offset};
    ## use critic;

    $chrome_args->{stdout} = $writer;
    $chrome_args->{stderr} = $writer;

    my $chrome = Pinto::Chrome::Net->new($chrome_args);
    my $pinto = Pinto->new( chrome => $chrome, root => $self->root );

    my $result =
        try   { $pinto->run( ucfirst $action_name => %{$action_args} ) }
        catch { print {$writer} $_; Pinto::Result->new->failed };

    print {$writer} $PINTO_PROTOCOL_STATUS_OK . "\n" if $result->was_successful;

    exit $result->was_successful ? 0 : 1;
}

#-------------------------------------------------------------------------------

sub parent_proc {
    my ( $self, $pipe, $child_pid ) = @_;

    my $reader = $pipe->reader;
    my $select = IO::Select->new($reader);
    $reader->blocking(0);

    my $response = sub {
        my $responder = shift;

        my $headers   = ['Content-Type' => 'text/plain'];
        my $writer    = $responder->( [ HTTP_OK, $headers ] );
        my $socket    = $self->request->env->{'psgix.io'};
        my $nullmsg   = $PINTO_PROTOCOL_NULL_MESSAGE . "\n";


        while (1) {

            my $input;
            if ( $select->can_read(1) ) {
                $input = <$reader>;    # Will block until \n
                last if not defined $input;    # We reached eof
            }

            my $ok = eval {
                local $SIG{ALRM} = sub { die "Write timed out" };
                alarm(3);

                $writer->write( $input || $nullmsg );
                1;                             # Write succeeded
            };

            alarm(0);
            unless ( $ok && ( !$socket || getpeername($socket) ) ) {
                proc_terminate( $child_pid, max_wait => 10 );
                last;
            }
        }

        $writer->close if not $socket;         # Hangs otherwise!
        waitpid $child_pid, 0;
    };

    return $response;
}

#-------------------------------------------------------------------------------

__PACKAGE__->meta->make_immutable;

#-------------------------------------------------------------------------------

1;

__END__

=pod

=encoding UTF-8

=for :stopwords Jeffrey Ryan Thalhammer

=head1 NAME

Pinto::Server::Responder::Action - Responder for action requests

=head1 VERSION

version 0.12

=head1 AUTHOR

Jeffrey Ryan Thalhammer <jeff@stratopan.com>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by Jeffrey Ryan Thalhammer.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut