#
# This package is loaded by the Gimp, and is !private!, so don't
# use it standalone, it won't work.
#
package Gimp::Net;

use strict 'vars';
use vars qw(
   $VERSION
   $default_tcp_port $default_unix_dir $default_unix_sock
   $server_fh $trace_level $trace_res $auth $gimp_pid
);
use subs qw(gimp_call_procedure);
use base qw(DynaLoader);

use Socket; # IO::Socket is _really_ slow, so don't use it!

use Gimp ('croak','__');
use Fcntl qw(F_SETFD);

require DynaLoader;

$VERSION = 1.211;

bootstrap Gimp::Net $VERSION;

$default_tcp_port  = 10009;
$default_unix_dir  = "/tmp/gimp-perl-serv-uid-$>/";
$default_unix_sock = "gimp-perl-serv";

$trace_res = *STDERR;
$trace_level = 0;

my $initialized = 0;

sub initialized { $initialized }

sub import {
   my $pkg = shift;

   return if @_;

   # overwrite some destroy functions
   *Gimp::Tile::DESTROY=
   *Gimp::PixelRgn::DESTROY=
   *Gimp::GDrawable::DESTROY=sub {
      my $req="DTRY".args2net(0,@_);
      print $server_fh pack("N",length($req)).$req;

      # make this synchronous to avoid deadlock due to using non sys*-type functions
      my $len;
      read($server_fh,$len,4) == 4 or die "protocol error (11)";
   };
}

sub _gimp_procedure_available {
   my $req="TEST".$_[0];
   print $server_fh pack("N",length($req)).$req;
   read($server_fh,$req,1);
   return $req;
}

# this is hardcoded into gimp_call_procedure!
sub response {
   my($len,$req);
   read($server_fh,$len,4) == 4 or die "protocol error (1)";
   $len=unpack("N",$len);
   read($server_fh,$req,$len) == $len or die "protocol error (2)";
   net2args(0,$req);
}

# this is hardcoded into gimp_call_procedure!
sub command {
   my $req=shift;
   $req.=args2net(0,@_);
   print $server_fh pack("N",length($req)).$req;
}

my($len,@args,$trace,$req); # small speedup, these are really local to gimp_call_procedure

sub gimp_call_procedure {
   if ($trace_level) {
      $req="TRCE".args2net(0,$trace_level,@_);
      print $server_fh pack("N",length($req)).$req;
      do {
         read($server_fh,$len,4) == 4 or die "protocol error (3)";
         $len=unpack("N",$len);
         read($server_fh,$req,abs($len)) == $len or die "protocol error (4)";
         if ($len<0) {
            ($req,@args)=net2args(0,$req);
            print "ignoring callback $req\n";
            redo;
         }
         ($trace,$req,@args)=net2args(0,$req);
         if (ref $trace_res eq "SCALAR") {
            $$trace_res = $trace;
         } else {
            print $trace_res $trace;
         }
      } while 0;
   } else {
      $req="EXEC".args2net(0,@_);
      print $server_fh pack("N",length($req)).$req;
      do {
         read($server_fh,$len,4) == 4 or die "protocol error (5)";
         $len=unpack("N",$len);
         read($server_fh,$req,abs($len)) == $len or die "protocol error (6)";
         if ($len<0) {
            ($req,@args)=net2args(0,$req);
            print "ignoring callback $req\n";
            redo;
         }
         ($req,@args)=net2args(0,$req);
      } while 0;
   }
   croak $req if $req;
   wantarray ? @args : $args[0];
}

sub server_quit {
   print $server_fh pack("N",4)."QUIT";
   undef $server_fh;
}

sub lock {
   print $server_fh pack("N",12)."LOCK".pack("N*",1,0);
}

sub unlock {
   print $server_fh pack("N",12)."LOCK".pack("N*",0,0);
}

sub set_trace {
   my($trace)=@_;
   my $old_level = $trace_level;
   if(ref $trace) {
      $trace_res=$trace;
   } elsif (defined $trace) {
      $trace_level=$trace;
   }
   $old_level;
}

sub start_server {
   my $opt = shift;
   $opt = $Gimp::spawn_opts unless $opt;
   print __"trying to start gimp with options \"$opt\"\n" if $Gimp::verbose;
   $server_fh=local *SERVER_FH;
   my $gimp_fh=local *CLIENT_FH;
   socketpair $server_fh,$gimp_fh,AF_UNIX,SOCK_STREAM,PF_UNSPEC
      or socketpair $server_fh,$gimp_fh,AF_LOCAL,SOCK_STREAM,PF_UNSPEC
      or croak __"unable to create socketpair for gimp communications: $!";

   # do it here so it i done only once
   require Gimp::Config;
   $gimp_pid = fork;
   if ($gimp_pid > 0) {
      Gimp::ignore_functions(@Gimp::gimp_gui_functions) unless $opt=~s/(^|:)gui//;
      return $server_fh;
   } elsif ($gimp_pid == 0) {
      close $server_fh;
      fcntl $gimp_fh, F_SETFD, 0;
      delete $ENV{GIMP_HOST};
      unless ($Gimp::verbose) {
         open STDIN,"</dev/null";
         open STDOUT,">/dev/null";
         open STDERR,">&1";
      }
      my @args;
      my $args = &Gimp::RUN_NONINTERACTIVE." ".
                 (&Gimp::_PS_FLAG_BATCH | &Gimp::_PS_FLAG_QUIET)." ".
                 fileno($gimp_fh);
      push(@args,"--no-data") if $opt=~s/(^|:)no-?data//;
      push(@args,"-i") unless $opt=~s/(^|:)gui//;
      push(@args,"--verbose") if $Gimp::verbose;
      { # block to suppress warning with broken perls (e.g. 5.004)
         exec $Gimp::Config{GIMP},
              "--no-splash",
              "--no-splash-image",
              "--enable-stack-trace", "never",
              "--console-messages",
              @args,
              "-b",
              "(extension-perl-server $args)",
              "(gimp-quit 0)";
      }
      exit(255);
   } else {
      croak __"unable to fork: $!";
   }
}

sub try_connect {
   local $_=$_[0];
   my $fh;
   $auth = s/^(.*)\@// ? $1 : "";	# get authorization
   if ($_ ne "") {
      if (s{^spawn/}{}) {
         return start_server($_);
      } elsif (s{^unix/}{/}) {
         my $server_fh=local *FH;
         return ((socket($server_fh,AF_UNIX,SOCK_STREAM,PF_UNSPEC)
                 || socket $server_fh,AF_LOCAL,SOCK_STREAM,PF_UNSPEC)
                && connect($server_fh,sockaddr_un $_)
                ? $server_fh : ());
      } else {
         s{^tcp/}{};
         my($host,$port)=split /:/,$_;
         $port=$default_tcp_port unless $port;
         my $server_fh=local *FH;
         return socket($server_fh,PF_INET,SOCK_STREAM,scalar getprotobyname('tcp') || 6)
                && connect($server_fh,sockaddr_in $port,inet_aton $host)
                ? $server_fh : ();
      }
   } else {
      return $fh if $fh = try_connect ("$auth\@unix$default_unix_dir$default_unix_sock");
      return $fh if $fh = try_connect ("$auth\@tcp/127.1:$default_tcp_port");
      return $fh if $fh = try_connect ("$auth\@spawn/");
   }
   undef $auth;
}

sub gimp_init {
   $Gimp::in_top=1;
   if (@_) {
      $server_fh = try_connect ($_[0]);
   } elsif (defined($Gimp::host)) {
      $server_fh = try_connect ($Gimp::host);
   } elsif (defined($ENV{GIMP_HOST})) {
      $server_fh = try_connect ($ENV{GIMP_HOST});
   } else {
      $server_fh = try_connect ("");
   }
   defined $server_fh or croak __"could not connect to the gimp server (make sure Perl-Server is running)";
   { my $fh = select $server_fh; $|=1; select $fh }
   
   my @r = response;
   
   die __"expected perl-server at other end of socket, got @r\n"
      unless $r[0] eq "PERL-SERVER";
   shift @r;
   die __"expected protocol version $Gimp::_PROT_VERSION, but server uses $r[0]\n"
      unless $r[0] eq $Gimp::_PROT_VERSION;
   shift @r;
   
   for(@r) {
      if($_ eq "AUTH") {
         die __"server requests authorization, but no authorization available\n"
            unless $auth;
         my $req = "AUTH".$auth;
         print $server_fh pack("N",length($req)).$req;
         my @r = response;
         die __"authorization failed: $r[1]\n" unless $r[0];
         print __"authorization ok, but: $r[1]\n" if $Gimp::verbose and $r[1];
      }
   }

   $initialized = 1;
   Gimp::_initialized_callback;
}

sub gimp_end {
   $initialized = 0;

   #close $server_fh if $server_fh;
   undef $server_fh;
   kill 'KILL',$gimp_pid if $gimp_pid;
   undef $gimp_pid;
}

sub gimp_main {
   gimp_init;
   no strict 'refs';
   $Gimp::in_top=0;
   eval { Gimp::callback("-net") };
   if($@ && $@ ne "IGNORE THIS MESSAGE\n") {
      Gimp::logger(message => substr($@,0,-1), fatal => 1, function => 'DIE');
      gimp_end;
      -1;
   } else {
      gimp_end;
      0;
   }
}

sub get_connection() {
   [$server_fh,$gimp_pid];
}

sub set_connection($) {
   ($server_fh,$gimp_pid)=@{+shift};
}

END {
   gimp_end;
}

1;
__END__

=head1 NAME

Gimp::Net - Communication module for the gimp-perl server.

=head1 SYNOPSIS

  use Gimp;

=head1 DESCRIPTION

For Gimp::Net (and thus commandline and remote scripts) to work, you first have to
install the "Perl-Server" extension somewhere where Gimp can find it (e.g in
your .gimp/plug-ins/ directory). Usually this is done automatically while installing
the Gimp extension. If you have a menu entry C<<Xtns>/Perl-Server>
then it is probably installed.

The Perl-Server can either be started from the C<<Xtns>> menu in Gimp, or automatically
when a perl script can't find a running Perl-Server.

When started from within The Gimp, the Perl-Server will create a unix
domain socket to which local clients can connect. If an authorization
password is given to the Perl-Server (by defining the environment variable
C<GIMP_HOST> before starting The Gimp), it will also listen on a tcp port
(default 10009). Since the password is transmitted in cleartext, using the
Perl-Server over tcp effectively B<lowers the security of your network to
the level of telnet>. Even worse: the current Gimp::Net-protocol can be
used for denial of service attacks, i.e. crashing the Perl-Server. There
also *might* be buffer-overflows (although I do care a lot for these).

=head1 ENVIRONMENT

The environment variable C<GIMP_HOST> specifies the default server to
contact and/or the password to use. The syntax is
[auth@][tcp/]hostname[:port] for tcp, [auth@]unix/local/socket/path for unix
and spawn/ for a private gimp instance. Examples are:

 www.yahoo.com               # just kidding ;)
 yahoo.com:11100             # non-standard port
 tcp/yahoo.com               # make sure it uses tcp
 authorize@tcp/yahoo.com:123 # full-fledged specification
 
 unix/tmp/unx                # use unix domain socket
 password@unix/tmp/test      # additionally use a password
 
 authorize@                  # specify authorization only
 
 spawn/                      # use a private gimp instance
 spawn/nodata                # pass --no-data switch
 spawn/gui                   # don't pass -n switch

=head1 CALLBACKS

=over 4

=item net()

is called after we have succesfully connected to the server. Do your dirty
work in this function, or see L<Gimp::Fu> for a better solution.

=back

=head1 FUNCTIONS

=over 4

=item server_quit()

sends the perl server a quit command.

=item get_connection()

return a connection id which uniquely identifies the current connection.

=item set_connection(conn_id)

set the connection to use on subsequent commands. C<conn_id> is the
connection id as returned by get_connection().

=back

=head1 BUGS

(Ver 0.04) This module is much faster than it ought to be... Silly that I wondered
wether I should implement it in perl or C, since perl is soo fast.

=head1 AUTHOR

Marc Lehmann <pcg@goof.com>

=head1 SEE ALSO

perl(1), L<Gimp>.

=cut
