PageRenderTime 44ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Pinto/Server/Responder/Action.pm

https://github.com/thaljef/Pinto
Perl | 176 lines | 108 code | 51 blank | 17 comment | 11 complexity | fc4cc2b6a44be101c2ef7d64be559d4b MD5 | raw file
  1. # ABSTRACT: Responder for action requests
  2. package Pinto::Server::Responder::Action;
  3. use Moose;
  4. use Carp;
  5. use JSON;
  6. use IO::Pipe;
  7. use IO::Select;
  8. use Try::Tiny;
  9. use File::Temp;
  10. use File::Copy;
  11. use Proc::Fork;
  12. use Path::Class;
  13. use Proc::Terminator;
  14. use Plack::Response;
  15. use HTTP::Status qw(:constants);
  16. use Pinto;
  17. use Pinto::Result;
  18. use Pinto::Chrome::Net;
  19. use Pinto::Constants qw(:protocol);
  20. #-------------------------------------------------------------------------------
  21. # VERSION
  22. #-------------------------------------------------------------------------------
  23. extends qw(Pinto::Server::Responder);
  24. #-------------------------------------------------------------------------------
  25. sub respond {
  26. my ($self) = @_;
  27. my $error_response = $self->check_protocol_version;
  28. return $error_response if $error_response;
  29. # path_info always has a leading slash, e.g. /action/list
  30. my ( undef, undef, $action_name ) = split '/', $self->request->path_info;
  31. my %params = %{ $self->request->parameters }; # Copying
  32. my $chrome_args = $params{chrome} ? decode_json( $params{chrome} ) : {};
  33. my $pinto_args = $params{pinto} ? decode_json( $params{pinto} ) : {};
  34. my $action_args = $params{action} ? decode_json( $params{action} ) : {};
  35. for my $upload_name ( $self->request->uploads->keys ) {
  36. my $upload = $self->request->uploads->{$upload_name};
  37. my $basename = $upload->filename;
  38. my $localfile = file( $upload->path )->dir->file($basename);
  39. File::Copy::move( $upload->path, $localfile ); #TODO: autodie
  40. $action_args->{$upload_name} = $localfile;
  41. }
  42. my $response;
  43. my $pipe = IO::Pipe->new;
  44. run_fork {
  45. child { $self->child_proc( $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) }
  46. parent { my $child_pid = shift; $response = $self->parent_proc( $pipe, $child_pid ) }
  47. error { croak "Failed to fork: $!" };
  48. };
  49. return $response;
  50. }
  51. #-------------------------------------------------------------------------------
  52. sub check_protocol_version {
  53. my ($self) = @_;
  54. # NB: Format derived from GitHub: https://developer.github.com/v3/media
  55. my $media_type_rx = qr{^ application / vnd [.] pinto [.] v(\d+) (?:[+] .+)? $}ix;
  56. my $accept = $self->request->header('Accept') || '';
  57. my $version = $accept =~ $media_type_rx ? $1 : 0;
  58. return unless my $cmp = $version <=> $PINTO_PROTOCOL_VERSION;
  59. my $fmt = 'Your client is too %s for this server. You must upgrade %s.';
  60. my ($age, $component) = $cmp > 0 ? qw(new pintod) : qw(old pinto);
  61. my $msg = sprintf $fmt, $age, $component;
  62. return [ HTTP_UNSUPPORTED_MEDIA_TYPE, [], [$msg] ];
  63. }
  64. #-------------------------------------------------------------------------------
  65. sub child_proc {
  66. my ( $self, $pipe, $chrome_args, $pinto_args, $action_name, $action_args ) = @_;
  67. my $writer = $pipe->writer;
  68. $writer->autoflush;
  69. # I'm not sure why, but cleanup isn't happening when we get
  70. # a TERM signal from the parent process. I suspect it
  71. # has something to do with File::NFSLock messing with %SIG
  72. local $SIG{TERM} = sub { File::Temp::cleanup; die $@ };
  73. ## no critic qw(PackageVar)
  74. local $Pinto::Globals::current_username = delete $pinto_args->{username};
  75. local $Pinto::Globals::current_time_offset = delete $pinto_args->{time_offset};
  76. ## use critic;
  77. $chrome_args->{stdout} = $writer;
  78. $chrome_args->{stderr} = $writer;
  79. my $chrome = Pinto::Chrome::Net->new($chrome_args);
  80. my $pinto = Pinto->new( chrome => $chrome, root => $self->root );
  81. my $result =
  82. try { $pinto->run( ucfirst $action_name => %{$action_args} ) }
  83. catch { print {$writer} $_; Pinto::Result->new->failed };
  84. print {$writer} $PINTO_PROTOCOL_STATUS_OK . "\n" if $result->was_successful;
  85. exit $result->was_successful ? 0 : 1;
  86. }
  87. #-------------------------------------------------------------------------------
  88. sub parent_proc {
  89. my ( $self, $pipe, $child_pid ) = @_;
  90. my $reader = $pipe->reader;
  91. my $select = IO::Select->new($reader);
  92. $reader->blocking(0);
  93. my $response = sub {
  94. my $responder = shift;
  95. my $headers = ['Content-Type' => 'text/plain'];
  96. my $writer = $responder->( [ HTTP_OK, $headers ] );
  97. my $socket = $self->request->env->{'psgix.io'};
  98. my $nullmsg = $PINTO_PROTOCOL_NULL_MESSAGE . "\n";
  99. while (1) {
  100. my $input;
  101. if ( $select->can_read(1) ) {
  102. $input = <$reader>; # Will block until \n
  103. last if not defined $input; # We reached eof
  104. }
  105. my $ok = eval {
  106. local $SIG{ALRM} = sub { die "Write timed out" };
  107. alarm(3);
  108. $writer->write( $input || $nullmsg );
  109. 1; # Write succeeded
  110. };
  111. alarm(0);
  112. unless ( $ok && ( !$socket || getpeername($socket) ) ) {
  113. proc_terminate( $child_pid, max_wait => 10 );
  114. last;
  115. }
  116. }
  117. $writer->close if not $socket; # Hangs otherwise!
  118. waitpid $child_pid, 0;
  119. };
  120. return $response;
  121. }
  122. #-------------------------------------------------------------------------------
  123. __PACKAGE__->meta->make_immutable;
  124. #-------------------------------------------------------------------------------
  125. 1;