tokuhirom's Blog

Plack::App::WrapCGI にちゃんとした CGI 実行機能をつける話

P::App::WrapCGI は、perl でかいた CGI しか実行できないので、ちゃんと exec してうごく一般的な CGI がうごくようにしてみる施策。

環境変数をセットアップして、双方向パイプで IPC しているだけです。

diff --git a/lib/Plack/App/WrapCGI.pm b/lib/Plack/App/WrapCGI.pm
index 7f0a7a7..0ce15d6 100644
--- a/lib/Plack/App/WrapCGI.pm
+++ b/lib/Plack/App/WrapCGI.pm
@@ -2,7 +2,7 @@ package Plack::App::WrapCGI;
 use strict;
 use warnings;
 use parent qw(Plack::Component);
-use Plack::Util::Accessor qw(script _app);
+use Plack::Util::Accessor qw(script execute _app);
 use CGI::Emulate::PSGI;
 use CGI::Compile;
 use Carp;
@@ -12,10 +12,62 @@ sub prepare_app {
     my $script = $self->script
         or croak "'script' is not set";
 
-    my $sub = CGI::Compile->compile($script);
-    my $app = CGI::Emulate::PSGI->handler($sub);
+    if ($self->execute) {
+        my $app = sub {
+            my $env = shift;
 
-    $self->_app($app);
+            pipe( my $stdoutr, my $stdoutw );
+            pipe( my $stdinr,  my $stdinw );
+
+
+            my $pid = fork();
+            Carp::croak("fork failed: $!") unless defined $pid;
+
+
+            if ($pid == 0) { # child
+                local $SIG{__DIE__} = sub {
+                    print STDERR @_;
+                    exit(1);
+                };
+
+                close $stdoutr;
+                close $stdinw;
+
+                local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
+
+                open( STDOUT, ">&=" . fileno($stdoutw) )
+                  or Carp::croak "Cannot dup STDOUT: $!";
+                open( STDIN, "<&=" . fileno($stdinr) )
+                  or Carp::croak "Cannot dup STDIN: $!";
+
+                exec($script) or Carp::croak("cannot exec: $!");
+
+                exit(2);
+            }
+
+            close $stdoutw;
+            close $stdinr;
+
+            syswrite($stdinw, do {
+                local $/;
+                my $fh = $env->{'psgi.input'};
+                <$fh>;
+            });
+
+            1 while waitpid( $pid, 0 ) <= 0;
+            if (POSIX::WIFEXITED($?)) {
+                return CGI::Parse::PSGI::parse_cgi_output($stdoutr);
+            } else {
+                Carp::croak("Error at execute CGI: $!");
+            }
+        };
+        $self->_app($app);
+    } else {
+        my $sub = CGI::Compile->compile($script);
+        my $app = CGI::Emulate::PSGI->handler($sub);
+
+        $self->_app($app);
+    }
 }
 
 sub call {
@@ -37,6 +89,9 @@ Plack::App::WrapCGI - Compiles a CGI script as PSGI application
 
   my $app = Plack::App::WrapCGI->new(script => "/path/to/script.pl")->to_app;
 
+  # if you want to execute as real CGI.
+  my $app = Plack::App::WrapCGI->new(script => "/path/to/script.rb", execute => 1)->to_app;
+
 =head1 DESCRIPTION
 
 Plack::App::WrapCGI compiles a CGI script into a PSGI application
diff --git a/t/Plack-Middleware/wrapcgi.t b/t/Plack-Middleware/wrapcgi.t
index 9e72592..edac680 100644
--- a/t/Plack-Middleware/wrapcgi.t
+++ b/t/Plack-Middleware/wrapcgi.t
@@ -4,6 +4,8 @@ use Test::Requires { 'CGI::Emulate::PSGI' => 0, 'CGI::Compile' => 0.03 };
 use Plack::Test;
 use HTTP::Request::Common;
 use Plack::App::WrapCGI;
+use IO::File;
+use File::Temp;
 
 my $app = Plack::App::WrapCGI->new(script => "t/Plack-Middleware/cgi-bin/hello.cgi")->to_app;
 
@@ -19,4 +21,33 @@ test_psgi app => $app, client => sub {
     is $res->content, "Hello bar counter=2";
 };
 
+{
+    my $tmp = File::Temp->new(CLEANUP => 1);
+    print $tmp <<"...";
+#!$^X
+use CGI;
+my \$q = CGI->new;
+print \$q->header, "Hello ", \$q->param('name'), " counter=", ++\$COUNTER;
+...
+    close $tmp;
+
+    chmod(oct("0700"), $tmp->filename) or die "Cannot chmod";
+
+    my $app_exec = Plack::App::WrapCGI->new(script => "$tmp", 'execute' => 1)->to_app;
+    test_psgi app => $app_exec, client => sub {
+        my $cb = shift;
+
+        my $res = $cb->(GET "http://localhost/?name=foo");
+        is $res->code, 200;
+        is $res->content, "Hello foo counter=1";
+
+        $res = $cb->(POST "http://localhost/", ['name' => 'bar']);
+        is $res->code, 200;
+        is $res->content, "Hello bar counter=1";
+    };
+
+    undef $tmp;
+};
+
+
 done_testing;