[1/2] testbot/TestAgent: Change the protocol and API for increased flexibility.

Francois Gouget fgouget at codeweavers.com
Tue Dec 4 22:37:34 CST 2012


The old protocol relied on being able to close the sending side of the connection to signal the end of the request, and then iusing the other side to read the reply. However this is not always feasible, especially when adding support for tunneling. Also it makes it impossible to use a single connection for multiple requests.
In the new protocol each request is composed of an RPC id, an argument count, and for each argument its type, size and then data. So the other side always knows exactly how much to read and thus a given connection can be used for multiple RPCs.
The Perl TestAgent API is now object-oriented which makes connection reuse simple.
The set of operations provided by the Perl TestAgent module has also been changed to be more flexible. In particular, where RunScript() was duplicating the functionality of SendFile(), we now have Run(), Wait() and Rm() which each perform a single function.
This change also unburdens the TestAgent server from the selection of temporary filenames, and eliminates a hard-coded path.
The internal client and server APIs have also been tweaked to make sending and receiving the RPCs and their replies easier.
---
 testbot/lib/WineTestBot/TestAgent.pm      | 1020 +++++++++++++++++++++++-----
 testbot/lib/WineTestBot/VMs.pm            |   31 +-
 testbot/scripts/TestAgent                 |  194 ++++--
 testbot/src/testagentd/Makefile           |    5 +-
 testbot/src/testagentd/list.h             |  232 +++++++
 testbot/src/testagentd/platform.h         |   54 +-
 testbot/src/testagentd/platform_unix.c    |  157 +++--
 testbot/src/testagentd/platform_windows.c |  214 ++++--
 testbot/src/testagentd/testagentd.c       | 1028 ++++++++++++++++++++++-------
 9 files changed, 2345 insertions(+), 590 deletions(-)
 create mode 100644 testbot/src/testagentd/list.h

diff --git a/testbot/lib/WineTestBot/TestAgent.pm b/testbot/lib/WineTestBot/TestAgent.pm
index 40f1bed..a5f8083 100644
--- a/testbot/lib/WineTestBot/TestAgent.pm
+++ b/testbot/lib/WineTestBot/TestAgent.pm
@@ -21,247 +21,945 @@
 package TestAgent;
 use strict;
 
-use WineTestBot::Config;
-use WineTestBot::Log;
+use vars qw (@ISA @EXPORT_OK $SENDFILE_EXE $RUN_DNT);
 
-my $DONE_READING = 0;
-my $DONE_WRITING = 1;
+require Exporter;
+ at ISA = qw(Exporter);
+ at EXPORT_OK = qw(new);
 
+my $BLOCK_SIZE = 4096;
 
-sub create_ip_socket(@)
+my $RPC_PING = 0;
+my $RPC_GETFILE = 1;
+my $RPC_SENDFILE = 2;
+my $RPC_RUN = 3;
+my $RPC_WAIT = 4;
+my $RPC_RM = 5;
+
+my %RpcNames=(
+    $RPC_PING => 'ping',
+    $RPC_GETFILE => 'getfile',
+    $RPC_SENDFILE => 'sendfile',
+    $RPC_RUN => 'run',
+    $RPC_WAIT => 'wait',
+    $RPC_RM => 'rm',
+);
+
+my $Debug = 1;
+sub debug(@)
 {
-  my $socket;
-  eval { $socket = IO::Socket::IP->new(@_); };
-  return $socket;
+    print STDERR @_ if ($Debug);
 }
 
-sub create_inet_socket(@)
+sub new($$$)
 {
-  return IO::Socket::INET->new(@_);
+  my ($class, $Hostname, $Port) = @_;
+
+  my $self = {
+    agenthost  => $Hostname,
+    agentport  => $Port,
+    connection => "$Hostname:$Port",
+    ctimeout   => 30,
+    timeout    => 0,
+    fd         => undef,
+    deadline   => undef,
+    err        => undef};
+
+  $self = bless $self, $class;
+  return $self;
 }
 
-my $create_socket = \&create_ip_socket;
-eval "use IO::Socket::IP";
-if ($@)
+sub Disconnect($)
 {
-  use IO::Socket::INET;
-  $create_socket = \&create_inet_socket;
+  my ($self) = @_;
+
+  if ($self->{fd})
+  {
+      close($self->{fd});
+      $self->{fd} = undef;
+  }
+  $self->{agentversion} = undef;
 }
 
-sub _Connect($;$)
+sub SetConnectTimeout($$)
 {
-  my ($Hostname, $Timeout) = @_;
+  my ($self, $Timeout) = @_;
+  my $OldTimeout = $self->{ctimeout};
+  $self->{ctimeout} = $Timeout;
+  return $OldTimeout;
+}
 
-  $Timeout ||= 10;
-  my $Deadline = time() + $Timeout;
-  while (1)
+sub SetTimeout($$)
+{
+  my ($self, $Timeout) = @_;
+  my $OldTimeout = $self->{timeout};
+  $self->{timeout} = $Timeout;
+  return $OldTimeout;
+}
+
+sub _SetAlarm($)
+{
+  my ($self) = @_;
+  if ($self->{deadline})
   {
-    my $ConnectTimeout = $Timeout < 30 ? $Timeout : 30;
-    my $socket = &$create_socket(PeerHost => $Hostname, PeerPort => $AgentPort,
-                                 Type => SOCK_STREAM, Timeout => $ConnectTimeout);
-    return $socket if ($socket);
+    my $Timeout = $self->{deadline} - time();
+    die "timeout" if ($Timeout <= 0);
+    alarm($Timeout);
+  }
+}
+
+
+#
+# Error handling
+#
 
-    $Timeout = $Deadline - time();
-    last if ($Timeout <= 0);
-    # We ignore the upcoming delay in our timeout calculation
+my $ERROR = 0;
+my $FATAL = 1;
 
-    sleep(1);
-    # We will retry just in case this is a temporary network failure
+sub _SetError($$$)
+{
+  my ($self, $Level, $Msg) = @_;
+
+  # Only overwrite non-fatal errors
+  if ($self->{fd})
+  {
+    # Cleanup errors coming from the server
+    $self->{err} = $Msg;
+
+    # And disconnect on fatal errors since the connection is unusable anyway
+    $self->Disconnect() if ($Level == $FATAL);
   }
-  $@ = "Unable to connect to $Hostname:$AgentPort: $@";
-  return undef;
+  elsif (!$self->{err})
+  {
+    # We did not even manage to connect but record the error anyway
+    $self->{err} = $Msg;
+  }
+  debug($RpcNames{$self->{rpcid}} || $self->{rpcid}, ": $self->{err}\n");
 }
 
-sub _ReadStatus($$)
+sub GetLastError($)
 {
-  my ($fh, $Timeout) = @_;
-  my ($Status, $Err) = ("", undef);
+  my ($self) = @_;
+  return $self->{err};
+}
+
+
+#
+# Low-level functions to receive raw data
+#
+
+sub _RecvRawData($$)
+{
+  my ($self, $Size) = @_;
+  return undef if (!defined $self->{fd});
+
+  my $Result;
   eval
   {
-    local $SIG{ALRM} = sub { die "read status timed out\n" }; # NB: \n required
-    alarm($Timeout || 10);
-    while ($Status !~ /\n/)
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->_SetAlarm();
+
+    my $Data = "";
+    while ($Size)
     {
-      # Note that the status is the last thing we read from the file descriptor
-      # so we don't worry about reading too much
       my $Buffer;
-      my $n = sysread($fh, $Buffer, 1024);
+      my $r = $self->{fd}->read($Buffer, $Size);
+      if (!defined $r)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "network read error: $!");
+        return;
+      }
+      if ($r == 0)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "got a premature network EOF");
+        return;
+      }
+      $Data .= $Buffer;
+      $Size -= $r;
+    }
+    alarm(0);
+    $Result = $Data;
+  };
+  if ($@)
+  {
+    $@ = "network read timed out" if ($@ =~ /^timeout /);
+    $self->_SetError($FATAL, $@);
+  }
+  return $Result;
+}
+
+sub _SkipRawData($$)
+{
+  my ($self, $Size) = @_;
+  return undef if (!defined $self->{fd});
+
+  my $Success;
+  eval
+  {
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->_SetAlarm();
+
+    while ($Size)
+    {
+      my $Buffer;
+      my $s = $Size < $BLOCK_SIZE ? $Size : $BLOCK_SIZE;
+      my $n = $self->{fd}->read($Buffer, $s);
       if (!defined $n)
       {
-        $Err = $!;
-        last;
+        alarm(0);
+        $self->_SetError($FATAL, "network skip failed: $!");
+        return;
+      }
+      if ($n == 0)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "got a premature network EOF");
+        return;
       }
-      last if ($n == 0);
-      $Status .= $Buffer;
+      $Size -= $n;
     }
     alarm(0);
+    $Success = 1;
   };
-  return (undef, $Err) if ($Err);
-  return (undef, $@) if ($@);
-  return ($Status, undef);
+  if ($@)
+  {
+    $@ = "network skip timed out" if ($@ =~ /^timeout /);
+    $self->_SetError($FATAL, $@);
+  }
+  return $Success;
 }
 
-sub GetStatus($;$)
+sub _RecvRawUInt32($)
 {
-  my ($Hostname, $Timeout) = @_;
+  my ($self) = @_;
 
-  my $nc = _Connect($Hostname, $Timeout);
-  return (undef, $@) if (!$nc);
-  $nc->send("status\n", 0);
-  $nc->shutdown($DONE_WRITING);
-  my ($Status, $Err) = _ReadStatus($nc, $Timeout);
-  close($nc);
-  return ($Status, $Err);
+  my $Data = $self->_RecvRawData(4);
+  return undef if (!defined $Data);
+  return unpack('N', $Data);
 }
 
-# This is a workaround for bug #8611 which affects File::Copy::copy(),
-# causing the script to die instantly if we cannot write to the destination
-# file descriptor.
-# http://www.nntp.perl.org/group/perl.perl5.porters/2002/02/msg52726.html
-sub _Copy($$)
+sub _RecvRawUInt64($)
 {
-    my ($src, $dst) = @_;
+  my ($self) = @_;
 
-    while (1)
-    {
-        my $buf;
-        my $r = sysread($src, $buf, 4096);
-        return 0 if (!defined $r);
-        last if ($r == 0);
-        my $w = syswrite($dst, $buf, $r);
-        return 0 if (!defined $w);
-        return 0 if ($w != $r);
-    }
-    return 1;
+  my $Data = $self->_RecvRawData(8);
+  return undef if (!defined $Data);
+  my ($High, $Low) = unpack('NN', $Data);
+  return $High << 32 | $Low;
+}
+
+
+#
+# Low-level functions to result lists
+#
+
+sub _RecvEntryHeader($)
+{
+  my ($self) = @_;
+
+  my $Data = $self->_RecvRawData(9);
+  return (undef, undef) if (!defined $Data);
+  my ($Type, $High, $Low) = unpack('cNN', $Data);
+  $Type = chr($Type);
+  return ($Type, $High << 32 | $Low);
 }
 
-sub SendFile($$$)
+sub _ExpectEntryHeader($$$)
 {
-  my ($Hostname, $LocalPathName, $ServerPathName) = @_;
-  LogMsg "SendFile $LocalPathName -> $Hostname $ServerPathName\n";
+  my ($self, $Type, $Size) = @_;
 
-  my $fh;
-  if (!open($fh, "<", $LocalPathName))
+  my ($HType, $HSize) = $self->_RecvEntryHeader();
+  return undef if (!defined $HType);
+  if ($HType ne $Type)
   {
-    return "unable to open '$LocalPathName' for reading: $!";
+    $self->_SetError($ERROR, "Expected a $Type entry but got $HType instead");
   }
-
-  my $Err;
-  my $nc = _Connect($Hostname);
-  if ($nc)
-  {
-    $nc->send("write\n$ServerPathName\n", 0);
-    $Err = $! if (!_Copy($fh, $nc));
-    $nc->shutdown($DONE_WRITING);
-
-    # Now get the status
-    my $Status;
-    ($Status, $Err) = _ReadStatus($nc, 10);
-    $Err = !$Status ? $! : ($Status =~ /^ok:/ ? undef : $Status);
-    close($nc);
+  elsif (defined $Size and $HSize != $Size)
+  {
+    $self->_SetError($ERROR, "Expected an entry of size $Size but got $HSize instead");
   }
   else
   {
-    $Err = $@;
+    return $HSize;
   }
-  close($fh);
-  return $Err;
+  if ($HType eq 'e')
+  {
+    # The expected data was replaced with an error message
+    my $Message = $self->_RecvRawData($HSize);
+    return undef if (!defined $Message);
+    $self->_SetError($ERROR, $Message);
+  }
+  else
+  {
+    $self->_SkipRawData($HSize);
+  }
+  return undef;
 }
 
-sub GetFile($$$)
+sub _ExpectEntry($$$)
+{
+  my ($self, $Type, $Size) = @_;
+
+  $Size = $self->_ExpectEntryHeader($Type, $Size);
+  return undef if (!defined $Size);
+  return $self->_RecvRawData($Size);
+}
+
+sub _RecvUInt32($)
+{
+  my ($self) = @_;
+
+  return undef if (!defined $self->_ExpectEntryHeader('I', 4));
+  my $Value = $self->_RecvRawUInt32();
+  debug("  RecvUInt32() -> $Value\n") if (defined $Value);
+  return $Value;
+}
+
+sub _RecvUInt64($)
+{
+  my ($self) = @_;
+
+  return undef if (!defined $self->_ExpectEntryHeader('Q', 8));
+  my $Value = $self->_RecvRawUInt64();
+  debug("  RecvUInt64() -> $Value\n") if (defined $Value);
+  return $Value;
+}
+
+sub _RecvString($;$)
 {
-  my ($Hostname, $ServerPathName, $LocalPathName) = @_;
-  LogMsg "GetFile $Hostname $ServerPathName -> $LocalPathName\n";
+  my ($self, $EType) = @_;
 
-  my $fh;
-  if (!open($fh, ">", $LocalPathName))
+  my $Str = $self->_ExpectEntry($EType || 's');
+  if (defined $Str)
   {
-    return "unable to open '$LocalPathName' for writing: $!";
+    # Remove the trailing '\0'
+    chop $Str;
+    debug("  RecvString() -> '$Str'\n");
   }
+  return $Str;
+}
 
-  my ($Err, $ServerSize);
-  my $nc = _Connect($Hostname);
-  if ($nc)
+sub _RecvFile($$$)
+{
+  my ($self, $Dst, $Filename) = @_;
+  return undef if (!defined $self->{fd});
+  debug("  RecvFile($Filename)\n");
+
+  my $Size = $self->_RecvEntryHeader('d');
+  return undef if (!defined $Size);
+
+  my $Success;
+  eval
   {
-    $nc->send("read\n$ServerPathName\n", 0);
-    # The status of the open operation is returned first so it does not
-    # get mixed up with the file data. However we must not mix buffered
-    # (<> or read()) and unbuffered (File:Copy::copy()) read operations on
-    # the socket.
-    if (sysread($nc, $Err, 1024) <= 0)
-    {
-      $Err = $!;
-    }
-    elsif ($Err =~ s/^ok: size=(-?[0-9]+)\n//)
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->_SetAlarm();
+
+    while ($Size)
     {
-      $ServerSize = $1;
-      if ($Err ne "" and syswrite($fh, $Err, length($Err)) < 0)
+      my $Buffer;
+      my $s = $Size < $BLOCK_SIZE ? $Size : $BLOCK_SIZE;
+      my $r = $self->{fd}->read($Buffer, $s);
+      if (!defined $r)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "got a network error while receiving '$Filename': $!");
+        return;
+      }
+      if ($r == 0)
       {
-        $Err = $!;
+        alarm(0);
+        $self->_SetError($FATAL, "got a premature EOF while receiving '$Filename'");
+        return;
       }
-      else
+      $Size -= $r;
+      my $w = syswrite($Dst, $Buffer, $r, 0);
+      if (!defined $w or $w != $r)
       {
-        $Err = _Copy($nc, $fh) ? undef : $!;
+        alarm(0);
+        $self->_SetError($ERROR, "an error occurred while writing to '$Filename': $!");
+        $self->_SkipRawData($Size);
+        return;
       }
     }
-    close($nc);
+    alarm(0);
+    $Success = 1;
+  };
+  if ($@)
+  {
+    $@ = "timed out while receiving '$Filename'" if ($@ =~ /^timeout /);
+    $self->_SetError($FATAL, $@);
   }
-  else
+  return $Success;
+}
+
+sub _SkipEntries($$)
+{
+  my ($self, $Count) = @_;
+  debug("  SkipEntries($Count)\n");
+
+  while ($Count)
   {
-    $Err = $@;
+    my ($Type, $Size) = $self->_RecvEntryHeader();
+    return undef if (!defined $Type);
+    if ($Type eq 'e')
+    {
+      # The expected data was replaced with an error message
+      my $Message = $self->_RecvRawData($Size);
+      return undef if (!defined $Message);
+      $self->_SetError($ERROR, $Message);
+    }
+    elsif (!$self->_SkipRawData($Size))
+    {
+      return undef;
+    }
+    $Count--;
   }
-  close($fh);
-  my $LocalSize = -s $LocalPathName;
-  if (!defined $Err and $LocalSize != $ServerSize)
-  {
-    # Something still went wrong during the transfer. Get the last operation
-    # status
-    my $StatusErr;
-    ($Err, $StatusErr) = GetStatus($Hostname);
-    $Err = $StatusErr if (!defined $StatusErr);
+  return 1;
+}
+
+sub _RecvListSize($)
+{
+  my ($self) = @_;
+
+  my $Value = $self->_RecvRawUInt32();
+  debug("  RecvListSize() -> $Value\n") if (defined $Value);
+  return $Value;
+}
+
+sub _RecvList($$)
+{
+  my ($self, $ETypes) = @_;
+
+  debug("  RecvList($ETypes)\n");
+  my $HCount = $self->_RecvListSize();
+  return undef if (!defined $HCount);
+
+  my $Count = length($ETypes);
+  if ($HCount != $Count)
+  {
+    $self->_SetError($ERROR, "Expected $Count results but got $HCount instead");
+    $self->_SkipEntries($HCount);
+    return undef;
   }
-  unlink $LocalPathName if ($Err);
-  return $Err;
+
+  my @List;
+  foreach my $EType (split //, $ETypes)
+  {
+    # '.' is a placeholder for data handled by the caller so let it handle
+    # the rest
+    last if ($EType eq '.');
+
+    my $Data;
+    if ($EType eq 'I')
+    {
+      $Data = $self->_RecvUInt32();
+      $Count--;
+    }
+    elsif ($EType eq 'Q')
+    {
+      $Data = $self->_RecvUInt64();
+      $Count--;
+    }
+    elsif ($EType eq 's')
+    {
+      $Data = $self->_RecvString();
+      $Count--;
+    }
+    else
+    {
+      $self->_SetError($ERROR, "_RecvList() cannot receive a result of type $EType");
+    }
+    if (!defined $Data)
+    {
+      $self->_SkipEntries($Count);
+      return undef;
+    }
+    push @List, $Data;
+  }
+  return 1 if (!@List);
+  return $List[0] if (@List == 1);
+  return @List;
 }
 
-sub RunScript($$$)
+sub _RecvErrorList($)
 {
-  my ($Hostname, $ScriptText, $Timeout) = @_;
-  LogMsg "RunScript $Hostname ", ($Timeout || 0), " [$ScriptText]\n";
+  my ($self) = @_;
 
-  my $Err;
-  my $nc = _Connect($Hostname);
-  if ($nc)
-  {
-    $nc->send("runscript\n$ScriptText", 0);
-    $nc->shutdown($DONE_WRITING);
-    my $Status;
-    ($Status, $Err) = _ReadStatus($nc, $Timeout);
-    $Err = $Status if (defined $Status and $Status !~ /^ok:/);
-    close($nc);
-    if (!$Err)
+  my $Count = $self->_RecvListSize();
+  return $self->GetLastError() if (!defined $Count);
+  return undef if (!$Count);
+
+  my $Errors = [];
+  while ($Count--)
+  {
+    my ($Type, $Size) = $self->_RecvEntryHeader();
+    if ($Type eq 'u')
+    {
+      debug("  RecvUndef()\n");
+      push @$Errors, undef;
+    }
+    elsif ($Type eq 's')
+    {
+      my $Status = $self->_RecvRawData($Size);
+      return $self->GetLastError() if (!defined $Status);
+      debug("  RecvStatus() -> '$Status'\n");
+      push @$Errors, $Status;
+    }
+    elsif ($Type eq 'e')
+    {
+      # The expected data was replaced with an error message
+      my $Message = $self->_RecvRawData($Size);
+      if (defined $Message)
+      {
+        debug("  RecvError() -> '$Message'\n");
+        $self->_SetError($ERROR, $Message);
+      }
+      $self->_SkipEntries($Count);
+      return $self->GetLastError();
+    }
+    else
+    {
+      $self->_SetError($ERROR, "Expected an s, u or e entry but got $Type instead");
+      $self->_SkipRawData($Size);
+      $self->_SkipEntries($Count);
+      return $self->GetLastError();
+    }
+  }
+  return $Errors;
+}
+
+
+#
+# Low-level functions to send raw data
+#
+
+sub _SendRawData($$)
+{
+  my ($self, $Data) = @_;
+  return undef if (!defined $self->{fd});
+
+  my $Success;
+  eval
+  {
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->_SetAlarm();
+
+    my $Size = length($Data);
+    my $Pos = 0;
+    while ($Size)
     {
-      $nc = _Connect($Hostname);
-      if ($nc)
+      my $n = syswrite($self->{fd}, $Data, $Size, $Pos);
+      if (!defined $n)
       {
-        $nc->send("waitchild\n", 0);
-        my $Status;
-        ($Status, $Err) = _ReadStatus($nc, $Timeout);
-        $nc->shutdown($DONE_WRITING);
-        $Err = $Status if (defined $Status and $Status !~ /^ok:/);
-        close($nc);
+        alarm(0);
+        $self->_SetError($FATAL, "network write error: $!");
+        return;
       }
-      else
+      if ($n == 0)
       {
-        $Err = $@;
+        alarm(0);
+        $self->_SetError($FATAL, "unable to send more data");
+        return;
       }
+      $Pos += $n;
+      $Size -= $n;
     }
+    alarm(0);
+    $Success = 1;
+  };
+  if ($@)
+  {
+    $@ = "network write timed out" if ($@ =~ /^timeout /);
+    $self->_SetError($FATAL, $@);
   }
-  else
+  return $Success;
+}
+
+sub _SendRawUInt32($$)
+{
+  my ($self, $Value) = @_;
+
+  return $self->_SendRawData(pack('N', $Value));
+}
+
+sub _SendRawUInt64($$)
+{
+  my ($self, $Value) = @_;
+
+  my ($High, $Low) = ($Value >> 32, $Value & 0xffffffff);
+  return $self->_SendRawData(pack('NN', $High, $Low));
+}
+
+
+#
+# Functions to send parameter lists
+#
+
+sub _SendListSize($$)
+{
+  my ($self, $Size) = @_;
+
+  debug("  SendListSize($Size)\n");
+  return $self->_SendRawUInt32($Size);
+}
+
+sub _SendEntryHeader($$$)
+{
+  my ($self, $Type, $Size) = @_;
+
+  my ($High, $Low) = ($Size >> 32, $Size & 0xffffffff);
+  return $self->_SendRawData(pack('cNN', ord($Type), $High, $Low));
+}
+
+sub _SendUInt32($$)
+{
+  my ($self, $Value) = @_;
+
+  debug("  SendUInt32($Value)\n");
+  return $self->_SendEntryHeader('I', 4) &&
+         $self->_SendRawUInt32($Value);
+}
+
+sub _SendUInt64($$)
+{
+  my ($self, $Value) = @_;
+
+  debug("  SendUInt64($Value)\n");
+  return $self->_SendEntryHeader('Q', 8) &&
+         $self->_SendRawUInt64($Value);
+}
+
+sub _SendString($$;$)
+{
+  my ($self, $Str, $Type) = @_;
+
+  debug("  SendString('$Str')\n");
+  $Str .= "\0";
+  return $self->_SendEntryHeader($Type || 's', length($Str)) &&
+         $self->_SendRawData($Str);
+}
+
+sub _SendFile($$$)
+{
+  my ($self, $Src, $Filename) = @_;
+  return undef if (!defined $self->{fd});
+  debug("  SendFile($Filename)\n");
+
+  my $Success;
+  eval
   {
-    $Err = $@;
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->_SetAlarm();
+
+    my $Size = -s $Filename;
+    return if (!$self->_SendEntryHeader('d', $Size));
+    while ($Size)
+    {
+      my $Buffer;
+      my $s = $Size < $BLOCK_SIZE ? $Size : $BLOCK_SIZE;
+      my $r = sysread($Src, $Buffer, $s);
+      if (!defined $r)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "an error occurred while reading from '$Filename': $!");
+        return;
+      }
+      if ($r == 0)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "got a premature EOF while reading from '$Filename'");
+        return;
+      }
+      $Size -= $r;
+      my $w = syswrite($self->{fd}, $Buffer, $r, 0);
+      if (!defined $w or $w != $r)
+      {
+        alarm(0);
+        $self->_SetError($FATAL, "got a network error while sending '$Filename': $!");
+        return;
+      }
+    }
+    alarm(0);
+    $Success = 1;
+  };
+  if ($@)
+  {
+    $@ = "timed out while sending '$Filename'" if ($@ =~ /^timeout /);
+    $self->_SetError($FATAL, $@);
   }
-  return $Err;
+  return $Success;
+}
+
+
+#
+# Connection management functions
+#
+
+sub create_ip_socket(@)
+{
+  my $socket;
+  eval { $socket = IO::Socket::IP->new(@_); };
+  return $socket;
+}
+
+sub create_inet_socket(@)
+{
+  return IO::Socket::INET->new(@_);
+}
+
+my $create_socket = \&create_ip_socket;
+eval "use IO::Socket::IP";
+if ($@)
+{
+  use IO::Socket::INET;
+  $create_socket = \&create_inet_socket;
+}
+
+sub _Connect($)
+{
+  my ($self) = @_;
+
+  my $Err;
+  eval
+  {
+    local $SIG{ALRM} = sub { die "timeout" };
+    $self->{deadline} = $self->{ctimeout} ? time() + $self->{ctimeout} : undef;
+    $self->_SetAlarm();
+
+    while (1)
+    {
+      $self->{fd} = &$create_socket(PeerHost => $self->{agenthost},
+                                    PeerPort => $self->{agentport},
+                                    Type => SOCK_STREAM);
+      last if ($self->{fd});
+      $Err = $!;
+      # Ideally we should probably not retry on errors that are likely
+      # permanent, like a hostname that does not resolve. Instead we just
+      # rate-limit our connection attempts.
+      sleep(1);
+    }
+    alarm(0);
+  };
+  if (!$self->{fd})
+  {
+    $Err ||= $@;
+    $Err = "connection timed out" if ($Err =~ /^timeout /);
+    $self->_SetError($FATAL, "Unable to connect to $self->{connection}: $Err");
+    return undef;
+  }
+
+  # Get the protocol version supported by the server.
+  # This also lets us verify that the connection really works.
+  $self->{agentversion} = $self->_RecvString();
+  if (!defined $self->{agentversion})
+  {
+    # We have already been disconnected at this point
+    $self->{err} = "Unable to get the protocol version spoken by the server: $self->{err}";
+    return undef;
+  }
+
+  return 1;
+}
+
+sub _Ping($)
+{
+  my ($self) = @_;
+
+  # Send the RPC and get the reply
+  return $self->_SendRawUInt32($RPC_PING) &&
+         $self->_SendListSize(0) &&
+         $self->_RecvList('');
+}
+
+sub _StartRPC($$)
+{
+  my ($self, $RpcId) = @_;
+
+  # Set up the new RPC
+  $self->{rpcid} = $RpcId;
+  $self->{err} = undef;
+
+  # First assume all is well and that we already have a working connection
+  $self->{deadline} = $self->{timeout} ? time() + $self->{timeout} : undef;
+  if (!$self->_SendRawUInt32($RpcId))
+  {
+    # No dice, clean up whatever was left of the old connection
+    $self->Disconnect();
+
+    # And reconnect
+    return undef if (!$self->_Connect());
+    debug("Using protocol '$self->{agentversion}'\n");
+
+    # Reconnecting reset the operation deadline
+    $self->{deadline} = $self->{timeout} ? time() + $self->{timeout} : undef;
+    return $self->_SendRawUInt32($RpcId);
+  }
+  return 1;
+}
+
+
+#
+# Implement the high-level RPCs
+#
+
+sub Ping($)
+{
+  my ($self) = @_;
+  return $self->_StartRPC($RPC_PING);
+}
+
+sub GetVersion($)
+{
+  my ($self) = @_;
+
+  if (!$self->{agentversion})
+  {
+    # Force a connection
+    $self->Ping();
+  }
+  # And return the version we got.
+  # If the connection failed it will be undef as expected.
+  return $self->{agentversion};
+}
+
+$SENDFILE_EXE = 1;
+
+sub _SendStringOrFile($$$$$$)
+{
+  my ($self, $Data, $fh, $LocalPathName, $ServerPathName, $Flags) = @_;
+
+  # Send the RPC and get the reply
+  return $self->_StartRPC($RPC_SENDFILE) &&
+         $self->_SendListSize(3) &&
+         $self->_SendString($ServerPathName) &&
+         $self->_SendUInt32($Flags || 0) &&
+         ($fh ? $self->_SendFile($fh, $LocalPathName) :
+                $self->_SendString($Data, 'd')) &&
+         $self->_RecvList('');
+}
+
+sub SendFile($$$;$)
+{
+  my ($self, $LocalPathName, $ServerPathName, $Flags) = @_;
+  debug("SendFile $LocalPathName -> $self->{agenthost} $ServerPathName\n");
+
+  if (open(my $fh, "<", $LocalPathName))
+  {
+    my $Success = $self->_SendStringOrFile(undef, $fh, $LocalPathName,
+                                           $ServerPathName, $Flags);
+    close($fh);
+    return $Success;
+  }
+  $self->_SetError($ERROR, "Unable to open '$LocalPathName' for reading: $!");
+  return undef;
+}
+
+sub SendFileFromString($$$;$)
+{
+  my ($self, $Data, $ServerPathName, $Flags) = @_;
+  debug("SendFile String -> $self->{agenthost} $ServerPathName\n");
+  return $self->_SendStringOrFile($Data, undef, undef, $ServerPathName, $Flags);
+}
+
+sub _GetFileOrString($$$)
+{
+  my ($self, $ServerPathName, $LocalPathName, $fh) = @_;
+
+  # Send the RPC and get the reply
+  my $Success = $self->_StartRPC($RPC_GETFILE) &&
+                $self->_SendListSize(1) &&
+                $self->_SendString($ServerPathName) &&
+                $self->_RecvList('.');
+  return undef if (!$Success);
+  return $self->_RecvFile($fh, $LocalPathName) if ($fh);
+  return $self->_RecvString('d');
+}
+
+sub GetFile($$$)
+{
+  my ($self, $ServerPathName, $LocalPathName) = @_;
+  debug("GetFile $self->{agenthost} $ServerPathName -> $LocalPathName\n");
+
+  if (open(my $fh, ">", $LocalPathName))
+  {
+    my $Success = $self->_GetFileOrString($ServerPathName, $LocalPathName, $fh);
+    close($fh);
+    return $Success;
+  }
+  $self->_SetError($ERROR, "Unable to open '$LocalPathName' for writing: $!");
+  return undef;
+}
+
+sub GetFileToString($$)
+{
+  my ($self, $ServerPathName) = @_;
+  debug("GetFile $self->{agenthost} $ServerPathName -> String\n");
+
+  return $self->_GetFileOrString($ServerPathName, undef, undef);
+}
+
+$RUN_DNT = 1;
+
+sub Run($$$;$$$)
+{
+  my ($self, $Argv, $Flags, $ServerInPath, $ServerOutPath, $ServerErrPath) = @_;
+  debug("Run $self->{agenthost} '", join("' '", @$Argv), "'\n");
+
+  if (!$self->_StartRPC($RPC_RUN) or
+      !$self->_SendListSize(4 + @$Argv) or
+      !$self->_SendUInt32($Flags) or
+      !$self->_SendString($ServerInPath || "") or
+      !$self->_SendString($ServerOutPath || "") or
+      !$self->_SendString($ServerErrPath || ""))
+  {
+    return undef;
+  }
+  foreach my $Arg (@$Argv)
+  {
+      return undef if (!$self->_SendString($Arg));
+  }
+
+  # Get the reply
+  return $self->_RecvList('Q');
+}
+
+sub Wait($$)
+{
+  my ($self, $Pid) = @_;
+  debug("Wait $Pid\n");
+
+  # Send the command
+  if (!$self->_StartRPC($RPC_WAIT) or
+      !$self->_SendListSize(1) or
+      !$self->_SendUInt64($Pid))
+  {
+    return undef;
+  }
+
+  # Get the reply
+  return $self->_RecvList('I');
+}
+
+sub Rm($@)
+{
+  my $self = shift @_;
+  debug("Rm\n");
+
+  # Send the command
+  if (!$self->_StartRPC($RPC_RM) or
+      !$self->_SendListSize(scalar(@_)))
+  {
+    return $self->GetLastError();
+  }
+  foreach my $Filename (@_)
+  {
+    return $self->GetLastError() if (!$self->_SendString($Filename));
+  }
+
+  # Get the reply
+  return $self->_RecvErrorList();
 }
 
 1;
diff --git a/testbot/lib/WineTestBot/VMs.pm b/testbot/lib/WineTestBot/VMs.pm
index da10233..0a2ce64 100644
--- a/testbot/lib/WineTestBot/VMs.pm
+++ b/testbot/lib/WineTestBot/VMs.pm
@@ -348,27 +348,46 @@ sub WaitForToolsInGuest($$)
 {
   my ($self, $Timeout) = @_;
 
-  my ($Status, $Err) = TestAgent::GetStatus($self->Hostname, $Timeout);
-  # In fact we don't care about the status
-  return $Err;
+  my $TA = TestAgent->new($self->Hostname, $AgentPort);
+  $TA->SetConnectTimeout($Timeout);
+  my $Success = $TA->Ping();
+  $TA->Disconnect();
+  return $Success ? undef : $TA->GetLastError();
 }
 
 sub CopyFileFromHostToGuest($$$)
 {
   my ($self, $HostPathName, $GuestPathName) = @_;
-  return TestAgent::SendFile($self->Hostname,  $HostPathName, $GuestPathName);
+  my $TA = TestAgent->new($self->Hostname, $AgentPort);
+  my $Success = $TA->SendFile($HostPathName, $GuestPathName);
+  $TA->Disconnect();
+  return $Success ? undef : $TA->GetLastError();
 }
 
 sub CopyFileFromGuestToHost($$$)
 {
   my ($self, $GuestPathName, $HostPathName) = @_;
-  return TestAgent::GetFile($self->Hostname,  $GuestPathName, $HostPathName);
+  my $TA = TestAgent->new($self->Hostname, $AgentPort);
+  my $Err = $TA->GetFile($GuestPathName, $HostPathName);
+  $TA->Disconnect();
+  return $Success ? undef : $TA->GetLastError();
 }
 
 sub RunScriptInGuestTimeout($$$)
 {
   my ($self, $ScriptText, $Timeout) = @_;
-  return TestAgent::RunScript($self->Hostname, $ScriptText, $Timeout);
+  my $TA = TestAgent->new($self->Hostname, $AgentPort);
+  $TA->SetTimeout($Timeout);
+
+  my $Success;
+  if ($TA->SendFileFromString($ScriptText, "./script.bat", $TestAgent::SENDFILE_EXE))
+  {
+    my $Pid = $TA->Run(["./script.bat"], 0);
+    $Success = 1 if ($Pid and defined $TA->Wait($Pid));
+    $TA->Rm("./script.bat");
+  }
+  $TA->Disconnect();
+  return $Success ? undef : $TA->GetLastError();
 }
 
 my %StreamData;
diff --git a/testbot/scripts/TestAgent b/testbot/scripts/TestAgent
index c917b0a..10f6f2e 100755
--- a/testbot/scripts/TestAgent
+++ b/testbot/scripts/TestAgent
@@ -42,9 +42,11 @@ sub error(@)
     print STDERR "$name0:error: ", @_;
 }
 
-my ($Cmd, $Hostname, $LocalPathName, $ServerPathName, $Script, $ScriptTimeout);
-my $Port;
-my $Timeout;
+my ($Cmd, $Hostname, $LocalFilename, $ServerFilename, @Rm);
+my (@Run, $RunIn, $RunOut, $RunErr);
+my $SendFlags = 0;
+my $RunFlags = 0;
+my ($Port, $ConnectTimeout, $Timeout);
 my $Usage;
 
 sub check_opt_val($$)
@@ -76,32 +78,67 @@ while (@ARGV)
     {
         $Port = check_opt_val($arg, $Port);
     }
+    elsif ($arg eq "--connect-timeout")
+    {
+        $ConnectTimeout = check_opt_val($arg, $ConnectTimeout);
+    }
     elsif ($arg eq "--timeout")
     {
-        $ScriptTimeout = check_opt_val($arg, $ScriptTimeout);
+        $Timeout = check_opt_val($arg, $Timeout);
+    }
+    elsif ($arg eq "--sendfile-exe")
+    {
+        $SendFlags |= $TestAgent::SENDFILE_EXE;
+    }
+    elsif ($arg eq "--run-no-wait")
+    {
+        $RunFlags |= $TestAgent::RUN_DNT;
+    }
+    elsif ($arg eq "--run-in")
+    {
+        $RunIn = check_opt_val($arg, $RunIn);
+    }
+    elsif ($arg eq "--run-out")
+    {
+        $RunOut = check_opt_val($arg, $RunOut);
+    }
+    elsif ($arg eq "--run-err")
+    {
+        $RunErr = check_opt_val($arg, $RunErr);
     }
     elsif (!defined $Hostname)
     {
         $Hostname = $arg;
     }
-    elsif ($arg eq "send")
+    elsif ($arg eq "sendfile")
     {
-        $LocalPathName = check_opt_val($arg, $LocalPathName);
-        $ServerPathName = check_opt_val($arg, $ServerPathName) if (!$Usage);
         $Cmd = $arg;
+        $LocalFilename = check_opt_val($arg, $LocalFilename);
+        $ServerFilename = check_opt_val($arg, $ServerFilename) if (!$Usage);
     }
-    elsif ($arg eq "get")
+    elsif ($arg eq "getfile")
     {
-        $ServerPathName = check_opt_val($arg, $ServerPathName);
-        $LocalPathName = check_opt_val($arg, $LocalPathName) if (!$Usage);
         $Cmd = $arg;
+        $ServerFilename = check_opt_val($arg, $ServerFilename);
+        $LocalFilename = check_opt_val($arg, $LocalFilename) if (!$Usage);
     }
-    elsif ($arg eq "runscript")
+    elsif ($arg eq "run")
     {
-        $Script = check_opt_val($arg, $Script);
+        @Run = @ARGV;
         $Cmd = $arg;
+        last;
     }
-    elsif ($arg eq "status")
+    elsif ($arg eq "rm")
+    {
+        $Cmd = $arg;
+        @Rm = @ARGV;
+        last;
+    }
+    elsif ($arg eq "getversion")
+    {
+        $Cmd = $arg;
+    }
+    elsif ($arg eq "ping")
     {
         $Cmd = $arg;
     }
@@ -116,7 +153,28 @@ if (!defined $Usage)
 {
     if (!defined $Cmd)
     {
-        error("you must specify a command to run\n");
+        error("you must specify a command\n");
+        $Usage = 2;
+    }
+    elsif ($Cmd eq "run" and !@Run)
+    {
+        error("you must specify the command to run\n");
+        $Usage = 2;
+    }
+    elsif ($Cmd eq "rm" and !@Rm)
+    {
+        error("you must specify the server files to delete\n");
+        $Usage = 2;
+    }
+    if ($Cmd ne "run" and ($RunFlags or defined $RunIn or defined $RunOut or
+                           defined $RunErr))
+    {
+        error("the --run-xxx options can only be used with the run command\n");
+        $Usage = 2;
+    }
+    elsif ($Cmd ne "sendfile" and $SendFlags)
+    {
+        error("the --sendfile-xxx options can only be used with the sendfile command\n");
         $Usage = 2;
     }
     $AgentPort = $Port if (defined $Port);
@@ -128,53 +186,105 @@ if (defined $Usage)
         error("try '$name0 --help' for more information\n");
         exit $Usage;
     }
-    print "Usage: $name0 [options] <hostname> send <localpath> <serverpath>\n";
-    print "or     $name0 [options] <hostname> get <serverpath> <localpath>\n";
-    print "or     $name0 [options] <hostname> runscript <command>\n";
-    print "or     $name0 [options] <hostname> status\n";
+    print "Usage: $name0 [options] <hostname> sendfile <localpath> <serverpath>\n";
+    print "or     $name0 [options] <hostname> getfile <serverpath> <localpath>\n";
+    print "or     $name0 [options] <hostname> run <command> <arguments>\n";
+    print "or     $name0 [options] <hostname> rm <serverfiles>\n";
+    print "or     $name0 [options] <hostname> [ping|version]\n";
     print "\n";
-    print "This is a testagentd client. It can be used to send/receive files and to run scripts on the specified server.\n";
+    print "This is a testagentd client. It can be used to send/receive files and to run commands on the server.\n";
     print "\n";
     print "Where:\n";
-    print "  send          Sends the <localpath> file and saves it as <serverpath> on the\n";
-    print "                server.\n";
-    print "  get           Retrieves the <serverpath> file from the server and saves it as\n";
-    print "                <localpath>.\n";
-    print "  runscript     Runs the specified <command> on the server.\n";
-    print "  status        Retrieves the status of the last command that was run on the\n";
+    print "  sendfile      Sends the <localpath> file and saves it as <serverpath> on the\n";
     print "                server.\n";
-    print "  <hostname>    Is the hostname of the TestAgent server.\n";
+    print "    --sendfile-exe Make the sent file executable.\n";
+    print "  getfile       Retrieves the <serverpath> file from the server and saves it\n";
+    print "                as <localpath>.\n";
+    print "  run           Runs the specified <command> on the server.\n";
+    print "    --run-no-wait Don't wait for the command.\n";
+    print "    --run-in <serverpath> Redirect the stdin or the command being run to the\n";
+    print "                  specified server file.\n";
+    print "    --run-out <serverpath> Redirect the stdout or the command being run to the\n";
+    print "                  specified server file.\n";
+    print "    --run-err <serverpath> Redirect the stderr or the command being run to the\n";
+    print "                  specified server file.\n";
+    print "  rm            Deletes the specified files on the server.\n";
+    print "  getversion    Returns the version of the server.\n";
+    print "  ping          Makes sure the server is still alive.\n";
+    print "  <hostname>    Is the hostname of the server.\n";
     print "  --port <port> Use the specified port number instead of the default one.\n";
+    print "  --connect-timeout <timeout> Use the specified timeout (in seconds) when\n";
+    print "                connecting instead of the default one.\n";
     print "  --timeout <timeout> Use the specified timeout (in seconds) instead of the\n";
-    print "                default one.\n";
+    print "                default one for the operation.\n";
     print "  --help        Shows this usage message.\n";
     exit 0;
 }
 
-my $Err;
-if ($Cmd eq "send")
+my $TA = TestAgent->new($Hostname, $AgentPort);
+$TA->SetConnectTimeout($ConnectTimeout) if (defined $ConnectTimeout);
+$TA->SetTimeout($Timeout) if (defined $Timeout);
+
+my $RC = 0;
+my $Result;
+if ($Cmd eq "sendfile")
+{
+    $Result = $TA->SendFile($LocalFilename, $ServerFilename, $SendFlags);
+}
+elsif ($Cmd eq "getfile")
 {
-    $Err = TestAgent::SendFile($Hostname, $LocalPathName, $ServerPathName);
+    $Result = $TA->GetFile($ServerFilename, $LocalFilename);
 }
-elsif ($Cmd eq "get")
+elsif ($Cmd eq "run")
 {
-    $Err = TestAgent::GetFile($Hostname, $ServerPathName, $LocalPathName);
+    my $Pid = $TA->Run(\@Run, $RunFlags, $RunIn, $RunOut, $RunErr);
+    if ($Pid)
+    {
+        print "Started process $Pid\n";
+        if (!($RunFlags & $TestAgent::RUN_DNT))
+        {
+            $Result = $TA->Wait($Pid);
+            print "Child exit status: $Result\n" if (defined $Result);
+        }
+    }
+}
+elsif ($Cmd eq "rm")
+{
+    $Result = $TA->Rm(@Rm);
+    if (ref($Result) eq "ARRAY")
+    {
+        foreach my $Error (@$Result)
+        {
+            error("$Error\n") if (defined $Error);
+        }
+        $RC = 1;
+    }
+    elsif (defined $Result)
+    {
+        error("$Result\n");
+        $RC = 1;
+    }
+    else
+    {
+        $Result = 1;
+    }
 }
-elsif ($Cmd eq "runscript")
+elsif ($Cmd eq "getversion")
 {
-    $Err = TestAgent::RunScript($Hostname, $Script, $ScriptTimeout);
+    $Result = $TA->GetVersion();
+    print "Version=$Result\n" if (defined $Result);
 }
-elsif ($Cmd eq "status")
+elsif ($Cmd eq "ping")
 {
-    my $Status;
-    ($Status, $Err) = TestAgent::GetStatus($Hostname);
-    print "Status=$Status";
+    $Result = $TA->Ping();
+    print "The server is alive\n" if ($Result);
 }
+$TA->Disconnect();
 
-if ($Err)
+if (!defined $Result)
 {
-    error("$Err\n");
-    exit 1;
+    error($TA->GetLastError() . "\n");
+    $RC = 1;
 }
 
-exit 0;
+exit $RC;
diff --git a/testbot/src/testagentd/Makefile b/testbot/src/testagentd/Makefile
index b1761c6..97a12a2 100644
--- a/testbot/src/testagentd/Makefile
+++ b/testbot/src/testagentd/Makefile
@@ -24,8 +24,9 @@ TestAgentd.exe: testagentd.obj platform_windows.obj
 .c.obj:
 	$(CROSSCC32) -Wall -c -o $@ $<
 
-C_SRCS = testagentd.c platform_unix.c platform_windows.c
-$(C_SRCS:.c=.o) $(C_SRCS:.c=.obj): platform.h
+testagentd.o testagentd.obj: platform.h
+platform_unix.o: platform.h list.h
+platform_windows.obj: platform.h list.h
 
 iso: winetestbot.iso
 
diff --git a/testbot/src/testagentd/list.h b/testbot/src/testagentd/list.h
new file mode 100644
index 0000000..9712603
--- /dev/null
+++ b/testbot/src/testagentd/list.h
@@ -0,0 +1,232 @@
+/*
+ * Linked lists support
+ *
+ * Copyright (C) 2002 Alexandre Julliard
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+ */
+
+#ifndef __WINE_SERVER_LIST_H
+#define __WINE_SERVER_LIST_H
+
+struct list
+{
+    struct list *next;
+    struct list *prev;
+};
+
+/* Define a list like so:
+ *
+ *   struct gadget
+ *   {
+ *       struct list  entry;   <-- doesn't have to be the first item in the struct
+ *       int          a, b;
+ *   };
+ *
+ *   static struct list global_gadgets = LIST_INIT( global_gadgets );
+ *
+ * or
+ *
+ *   struct some_global_thing
+ *   {
+ *       struct list gadgets;
+ *   };
+ *
+ *   list_init( &some_global_thing->gadgets );
+ *
+ * Manipulate it like this:
+ *
+ *   list_add_head( &global_gadgets, &new_gadget->entry );
+ *   list_remove( &new_gadget->entry );
+ *   list_add_after( &some_random_gadget->entry, &new_gadget->entry );
+ *
+ * And to iterate over it:
+ *
+ *   struct gadget *gadget;
+ *   LIST_FOR_EACH_ENTRY( gadget, &global_gadgets, struct gadget, entry )
+ *   {
+ *       ...
+ *   }
+ *
+ */
+
+/* add an element after the specified one */
+static inline void list_add_after( struct list *elem, struct list *to_add )
+{
+    to_add->next = elem->next;
+    to_add->prev = elem;
+    elem->next->prev = to_add;
+    elem->next = to_add;
+}
+
+/* add an element before the specified one */
+static inline void list_add_before( struct list *elem, struct list *to_add )
+{
+    to_add->next = elem;
+    to_add->prev = elem->prev;
+    elem->prev->next = to_add;
+    elem->prev = to_add;
+}
+
+/* add element at the head of the list */
+static inline void list_add_head( struct list *list, struct list *elem )
+{
+    list_add_after( list, elem );
+}
+
+/* add element at the tail of the list */
+static inline void list_add_tail( struct list *list, struct list *elem )
+{
+    list_add_before( list, elem );
+}
+
+/* remove an element from its list */
+static inline void list_remove( struct list *elem )
+{
+    elem->next->prev = elem->prev;
+    elem->prev->next = elem->next;
+}
+
+/* get the next element */
+static inline struct list *list_next( const struct list *list, const struct list *elem )
+{
+    struct list *ret = elem->next;
+    if (elem->next == list) ret = NULL;
+    return ret;
+}
+
+/* get the previous element */
+static inline struct list *list_prev( const struct list *list, const struct list *elem )
+{
+    struct list *ret = elem->prev;
+    if (elem->prev == list) ret = NULL;
+    return ret;
+}
+
+/* get the first element */
+static inline struct list *list_head( const struct list *list )
+{
+    return list_next( list, list );
+}
+
+/* get the last element */
+static inline struct list *list_tail( const struct list *list )
+{
+    return list_prev( list, list );
+}
+
+/* check if a list is empty */
+static inline int list_empty( const struct list *list )
+{
+    return list->next == list;
+}
+
+/* initialize a list */
+static inline void list_init( struct list *list )
+{
+    list->next = list->prev = list;
+}
+
+/* count the elements of a list */
+static inline unsigned int list_count( const struct list *list )
+{
+    unsigned count = 0;
+    const struct list *ptr;
+    for (ptr = list->next; ptr != list; ptr = ptr->next) count++;
+    return count;
+}
+
+/* move all elements from src to the tail of dst */
+static inline void list_move_tail( struct list *dst, struct list *src )
+{
+    if (list_empty(src)) return;
+
+    dst->prev->next = src->next;
+    src->next->prev = dst->prev;
+    dst->prev = src->prev;
+    src->prev->next = dst;
+    list_init(src);
+}
+
+/* move all elements from src to the head of dst */
+static inline void list_move_head( struct list *dst, struct list *src )
+{
+    if (list_empty(src)) return;
+
+    dst->next->prev = src->prev;
+    src->prev->next = dst->next;
+    dst->next = src->next;
+    src->next->prev = dst;
+    list_init(src);
+}
+
+/* iterate through the list */
+#define LIST_FOR_EACH(cursor,list) \
+    for ((cursor) = (list)->next; (cursor) != (list); (cursor) = (cursor)->next)
+
+/* iterate through the list, with safety against removal */
+#define LIST_FOR_EACH_SAFE(cursor, cursor2, list) \
+    for ((cursor) = (list)->next, (cursor2) = (cursor)->next; \
+         (cursor) != (list); \
+         (cursor) = (cursor2), (cursor2) = (cursor)->next)
+
+/* iterate through the list using a list entry */
+#define LIST_FOR_EACH_ENTRY(elem, list, type, field) \
+    for ((elem) = LIST_ENTRY((list)->next, type, field); \
+         &(elem)->field != (list); \
+         (elem) = LIST_ENTRY((elem)->field.next, type, field))
+
+/* iterate through the list using a list entry, with safety against removal */
+#define LIST_FOR_EACH_ENTRY_SAFE(cursor, cursor2, list, type, field) \
+    for ((cursor) = LIST_ENTRY((list)->next, type, field), \
+         (cursor2) = LIST_ENTRY((cursor)->field.next, type, field); \
+         &(cursor)->field != (list); \
+         (cursor) = (cursor2), \
+         (cursor2) = LIST_ENTRY((cursor)->field.next, type, field))
+
+/* iterate through the list in reverse order */
+#define LIST_FOR_EACH_REV(cursor,list) \
+    for ((cursor) = (list)->prev; (cursor) != (list); (cursor) = (cursor)->prev)
+
+/* iterate through the list in reverse order, with safety against removal */
+#define LIST_FOR_EACH_SAFE_REV(cursor, cursor2, list) \
+    for ((cursor) = (list)->prev, (cursor2) = (cursor)->prev; \
+         (cursor) != (list); \
+         (cursor) = (cursor2), (cursor2) = (cursor)->prev)
+
+/* iterate through the list in reverse order using a list entry */
+#define LIST_FOR_EACH_ENTRY_REV(elem, list, type, field) \
+    for ((elem) = LIST_ENTRY((list)->prev, type, field); \
+         &(elem)->field != (list); \
+         (elem) = LIST_ENTRY((elem)->field.prev, type, field))
+
+/* iterate through the list in reverse order using a list entry, with safety against removal */
+#define LIST_FOR_EACH_ENTRY_SAFE_REV(cursor, cursor2, list, type, field) \
+    for ((cursor) = LIST_ENTRY((list)->prev, type, field), \
+         (cursor2) = LIST_ENTRY((cursor)->field.prev, type, field); \
+         &(cursor)->field != (list); \
+         (cursor) = (cursor2), \
+         (cursor2) = LIST_ENTRY((cursor)->field.prev, type, field))
+
+/* macros for statically initialized lists */
+#undef LIST_INIT
+#define LIST_INIT(list)  { &(list), &(list) }
+
+/* get pointer to object containing list element */
+#undef LIST_ENTRY
+#define LIST_ENTRY(elem, type, field) \
+    ((type *)((char *)(elem) - (size_t)(&((type *)0)->field)))
+
+#endif  /* __WINE_SERVER_LIST_H */
diff --git a/testbot/src/testagentd/platform.h b/testbot/src/testagentd/platform.h
index cc30d7f..3c25271 100644
--- a/testbot/src/testagentd/platform.h
+++ b/testbot/src/testagentd/platform.h
@@ -15,6 +15,12 @@
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
  */
+
+
+/*
+ * Compatiblity definitions.
+ */
+
 #ifdef WIN32
 # include <ws2tcpip.h>
 # include <windows.h>
@@ -22,19 +28,23 @@
 # ifndef SHUT_RD
 #  define SHUT_RD SD_RECEIVE
 # endif
+
+typedef unsigned int uint32_t;
+typedef ULONGLONG uint64_t;
+#define U64FMT "%I64u"
+
 #else
+
 # include <arpa/inet.h>
 # include <sys/types.h>
 # include <sys/socket.h>
 # include <sys/select.h>
 # include <netdb.h>
 
-/*
- * Platform-specific functions.
- */
-
 typedef int SOCKET;
 # define closesocket(sock) close((sock))
+
+#define U64FMT "%lu"
 #endif
 
 #ifndef O_BINARY
@@ -42,13 +52,20 @@ typedef int SOCKET;
 #endif
 
 
-int init_platform(void);
-char* get_script_path(void);
+/*
+ * Platform-specific unctions.
+ */
+
+int platform_init(void);
+
+enum run_flags_t {
+    RUN_DNT = 1,
+};
 
 /* Starts the specified command in the background and reports the status to
  * the client.
  */
-void start_child(SOCKET client, char* path);
+uint64_t platform_run(char** argv, uint32_t flags, char** redirects);
 
 /* If a command was started in the background, waits until either that command
  * terminates or the client disconnects (typically because it got tired of
@@ -56,10 +73,7 @@ void start_child(SOCKET client, char* path);
  * If no command was started in the background, then reports an error
  * immediately.
  */
-void wait_for_child(SOCKET client);
-
-/* Releases the resources used for tracking the last command. */
-void cleanup_child(void);
+int platform_wait(SOCKET client, uint64_t pid, uint32_t *childstatus);
 
 /* Returns a string describing the last socket-related error */
 int sockeintr(void);
@@ -78,8 +92,18 @@ void ta_freeaddrinfo(struct addrinfo *addresses);
  * testagentd functions
  */
 
-void error(const char* format, ...);
-void debug(const char* format, ...);
-void report_status(SOCKET client, const char* format, ...);
+#ifdef __GNUC__
+# define FORMAT(fmt, arg1)    __attribute__((format (printf, fmt, arg1) ))
+#else
+# define FORMAT(fmt, arg1)
+#endif
+
+void error(const char* format, ...) FORMAT(1,2);
+void debug(const char* format, ...) FORMAT(1,2);
+
+#define ST_OK       0
+#define ST_ERROR    1
+#define ST_FATAL    2
+void set_status(int status, const char* format, ...) FORMAT(2,3);
 
-void* sockaddr_getaddr(struct sockaddr* sa, socklen_t* len);
+void* sockaddr_getaddr(const struct sockaddr* sa, socklen_t* len);
diff --git a/testbot/src/testagentd/platform_unix.c b/testbot/src/testagentd/platform_unix.c
index 56699b9..d7d8f54 100644
--- a/testbot/src/testagentd/platform_unix.c
+++ b/testbot/src/testagentd/platform_unix.c
@@ -23,111 +23,148 @@
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
 #include <sys/stat.h>
 #include <sys/wait.h>
 #include <signal.h>
-#include "platform.h"
 
-# define WINEBOTDIR  "/home/winehq/tools/testbot/var/staging"
+#include "platform.h"
+#include "list.h"
 
 
-char* get_script_path(void)
+struct child_t
 {
-    struct stat st;
-    char *dir, *script;
-
-    if (stat(WINEBOTDIR, &st) == 0 && S_ISDIR(st.st_mode) &&
-        access(WINEBOTDIR, W_OK) == 0)
-        dir = WINEBOTDIR;
-    else if (getenv("TMPDIR"))
-        dir = getenv("TMPDIR");
-    else
-        dir = "/tmp";
-    script = malloc(strlen(dir)+7+1);
-    sprintf(script, "%s/script", dir);
-    return script;
-}
+    struct list entry;
+    uint64_t pid;
+    int reaped;
+    uint32_t status;
+};
+
+static struct list children = LIST_INIT(children);
 
-static pid_t child = 0;
-static char* child_path;
-static pid_t reaped = 0;
-static int reaped_status;
-void cleanup_child(void)
-{
-    if (child_path)
-    {
-        unlink(child_path);
-        free(child_path);
-        child_path = NULL;
-    }
-    child = 0;
-}
 
 void reaper(int signum)
 {
+    struct child_t* child;
     pid_t pid;
     int status;
 
     pid = wait(&status);
-    debug("process %u returned %d\n", (unsigned)pid, status);
-    if (pid == child)
+    debug("process %u returned %u\n", pid, status);
+
+    LIST_FOR_EACH_ENTRY(child, &children, struct child_t, entry)
     {
-        cleanup_child();
-        reaped_status = status;
-        reaped = pid;
+        if (child->pid == pid)
+        {
+            child->status = status;
+            child->reaped = 1;
+            break;
+        }
     }
 }
 
-void start_child(SOCKET client, char* path)
+uint64_t platform_run(char** argv, uint32_t flags, char** redirects)
 {
     pid_t pid;
+    int fds[3] = {-1, -1, -1};
+    int i;
+
+    for (i = 0; i < 3; i++)
+    {
+        if (redirects[i][0] == '\0')
+            continue;
+        fds[i] = open(redirects[i], (i ? O_WRONLY : O_RDONLY) | O_CREAT | O_TRUNC);
+        if (fds[i] < 0)
+        {
+            set_status(ST_ERROR, "unable to open '%s' for %s: %s", redirects[i], i ? "writing" : "reading", strerror(errno));
+            while (i > 0)
+            {
+                if (fds[i] != -1)
+                    close(fds[i]);
+                i--;
+            }
+            return 0;
+        }
+    }
 
-    chmod(path, 0700);
-    child_path = path;
-    child = pid = fork();
+    pid = fork();
     if (pid == 0)
     {
-        char* argv[2];
-        argv[0] = path;
-        argv[1] = NULL;
-        execve(path, argv, NULL);
-        error("could not run '%s': %s\n", strerror(errno));
+        for (i = 0; i < 3; i++)
+        {
+            if (fds[i] != -1)
+            {
+                dup2(fds[i], i);
+                close(fds[i]);
+            }
+        }
+        execvp(argv[0], argv);
+        error("could not run '%s': %s\n", argv[0], strerror(errno));
         exit(1);
     }
     if (pid < 0)
     {
-        cleanup_child();
-        report_status(client, "error: could not fork: %s\n", strerror(errno));
-        return;
+        set_status(ST_ERROR, "could not fork: %s", strerror(errno));
+        pid = 0;
+    }
+    else
+    {
+        if (!(flags & RUN_DNT))
+        {
+            struct child_t* child;
+            child = malloc(sizeof(*child));
+            child->pid = pid;
+            child->reaped = 0;
+            list_add_head(&children, &child->entry);
+        }
     }
-    report_status(client, "ok: started process %d\n", pid);
+    for (i = 0; i < 3; i++)
+        if (fds[i] != -1)
+            close(fds[i]);
+    return pid;
 }
 
-void wait_for_child(SOCKET client)
+int platform_wait(SOCKET client, uint64_t pid, uint32_t *childstatus)
 {
-    while (child)
+    struct child_t* child;
+
+    LIST_FOR_EACH_ENTRY(child, &children, struct child_t, entry)
+    {
+        if (child->pid == pid)
+            break;
+    }
+    if (!child || child->pid != pid)
+    {
+        set_status(ST_ERROR, "the " U64FMT " process does not exist or is not a child process", pid);
+        return 0;
+    }
+
+    while (!child->reaped)
     {
         fd_set rfds;
-        char buf;
+        char buffer;
 
         /* select() blocks until either the client disconnects or until, or
          * the SIGCHLD signal indicates the child has exited. The recv() call
          * tells us if it is the former.
          */
+        debug("Waiting for " U64FMT "\n", pid);
         FD_ZERO(&rfds);
         FD_SET(client, &rfds);
         if (select(client+1, &rfds, NULL, NULL, NULL) == 1 &&
             FD_ISSET(client, &rfds) &&
-            recv(client, &buf, 1, MSG_PEEK | MSG_DONTWAIT) <= 0)
+            recv(client, &buffer, 1, MSG_PEEK | MSG_DONTWAIT) <= 0)
         {
-            report_status(client, "error: connection closed\n");
-            return;
+            set_status(ST_FATAL, "connection closed");
+            return 0;
         }
     }
-    if (reaped)
-        report_status(client, "ok: process %d returned status %d\n", reaped, reaped_status);
-    else
-        report_status(client, "error: no process to wait for\n");
+    debug("process " U64FMT " returned status %u\n", pid, child->status);
+    *childstatus = child->status;
+    list_remove(&child->entry);
+    free(child);
+    return 1;
 }
 
 int sockeintr(void)
@@ -188,7 +225,7 @@ void ta_freeaddrinfo(struct addrinfo *addresses)
     return freeaddrinfo(addresses);
 }
 
-int init_platform(void)
+int platform_init(void)
 {
     struct sigaction sa, osa;
     sa.sa_handler = reaper;
diff --git a/testbot/src/testagentd/platform_windows.c b/testbot/src/testagentd/platform_windows.c
index b17b8c0..ab4bf7b 100644
--- a/testbot/src/testagentd/platform_windows.c
+++ b/testbot/src/testagentd/platform_windows.c
@@ -19,97 +19,189 @@
  */
 
 #include <stdio.h>
+
 #include "platform.h"
+#include "list.h"
 
-char* get_script_path(void)
+struct child_t
 {
-    static char path[MAX_PATH+11];
-    if (!GetTempPathA(sizeof(path), path))
-    {
-        error("unable to retrieve the temporary directory path\n");
-        exit(1);
-    }
-    strcat(path, "\\script.bat");
-    return path;
-}
+    struct list entry;
+    DWORD pid;
+    HANDLE handle;
+};
 
-static HANDLE child = NULL;
-static DWORD child_pid;
-static char* child_path;
-void cleanup_child(void)
-{
-    if (child)
-    {
-        DeleteFile(child_path);
-        free(child_path);
-        child_path = NULL;
+static struct list children = LIST_INIT(children);
 
-        CloseHandle(child);
-        child = NULL;
-        child_pid = 0;
-    }
-}
 
-void start_child(SOCKET client, char* path)
+uint64_t platform_run(char** argv, uint32_t flags, char** redirects)
 {
+    DWORD stdhandles[3] = {STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE};
+    HANDLE fhs[3] = {INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE, INVALID_HANDLE_VALUE};
+    SECURITY_ATTRIBUTES sa;
     STARTUPINFO si;
     PROCESS_INFORMATION pi;
+    int has_redirects, i, cmdsize;
+    char *cmdline, *d, **arg;
+
+    sa.nLength = sizeof(sa);
+    sa.lpSecurityDescriptor = NULL;
+    sa.bInheritHandle = TRUE;
+
+    /* Build the windows command line */
+    cmdsize = 0;
+    for (arg = argv; *arg; arg++)
+    {
+        char* s = *arg;
+        while (*s)
+            cmdsize += (*s++ == '"' ? 2 : 1);
+        cmdsize += 3; /* 2 quotes and either a space or trailing '\0' */
+    }
+    cmdline = malloc(cmdsize);
+    if (!cmdline)
+    {
+        set_status(ST_ERROR, "malloc() failed: %s", strerror(errno));
+        return 0;
+    }
+    d = cmdline;
+    for (arg = argv; *arg; arg++)
+    {
+        char* s = *arg;
+        *d++ = '"';
+        while (*s)
+        {
+            if (*s == '"')
+                *d++ = '\\';
+            *d++ = *s++;
+        }
+        *d++ = '"';
+        *d++ = ' ';
+    }
+    *(d-1) = '\0';
+
+    /* Prepare the redirections */
+    has_redirects = 0;
+    for (i = 0; i < 3; i++)
+    {
+        if (redirects[i][0] == '\0')
+        {
+            fhs[i] = GetStdHandle(stdhandles[i]);
+            continue;
+        }
+        has_redirects = 1;
+        fhs[i] = CreateFile(redirects[i], (i ? GENERIC_WRITE : GENERIC_READ), 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+        if (fhs[i] == INVALID_HANDLE_VALUE)
+        {
+            set_status(ST_ERROR, "unable to open '%s' for %s: %lu", redirects[i], i ? "writing" : "reading", GetLastError());
+            free(cmdline);
+            while (i > 0)
+            {
+                if (fhs[i] != INVALID_HANDLE_VALUE)
+                    CloseHandle(fhs[i]);
+                i--;
+            }
+            return 0;
+        }
+    }
 
-    child_path = path;
     memset(&si, 0, sizeof(si));
     si.cb = sizeof(si);
-    if (CreateProcess(path, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS,
-                       NULL, NULL, &si, &pi))
+    si.dwFlags = has_redirects ? STARTF_USESTDHANDLES : 0;
+    si.hStdInput = fhs[0];
+    si.hStdOutput = fhs[1];
+    si.hStdError = fhs[2];
+    if (!CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS,
+                        NULL, NULL, &si, &pi))
     {
-        report_status(client, "ok: started process %u\n", pi.dwProcessId);
-        child = pi.hProcess;
-        child_pid = pi.dwProcessId;
-        CloseHandle(pi.hThread);
+        set_status(ST_ERROR, "could not run '%s': %lu", cmdline, GetLastError());
+        return 0;
     }
+    CloseHandle(pi.hThread);
+
+    if (flags & RUN_DNT)
+        CloseHandle(pi.hProcess);
     else
     {
-        report_status(client, "error: could not run '%s': %u\n", path, GetLastError());
+        struct child_t* child;
+        child = malloc(sizeof(*child));
+        child->pid = pi.dwProcessId;
+        child->handle = pi.hProcess;
+        list_add_head(&children, &child->entry);
     }
+
+    free(cmdline);
+    for (i = 0; i < 3; i++)
+        if (redirects[i][0])
+            CloseHandle(fhs[i]);
+
+    return pi.dwProcessId;
 }
 
-void wait_for_child(SOCKET client)
+int platform_wait(SOCKET client, uint64_t pid, uint32_t *childstatus)
 {
+    struct child_t *child;
     HANDLE handles[2];
-    DWORD r;
+    u_long nbio;
+    DWORD r, success;
+
+    LIST_FOR_EACH_ENTRY(child, &children, struct child_t, entry)
+    {
+        if (child->pid == pid)
+            break;
+    }
+    if (!child || child->pid != pid)
+    {
+        set_status(ST_ERROR, "the " U64FMT " process does not exist or is not a child process", pid);
+        return 0;
+    }
 
+    /* Wait for either the socket to be closed, indicating a client-side
+     * timeout, or for the child process to exit.
+     */
     handles[0] = WSACreateEvent();
     WSAEventSelect(client, handles[0], FD_CLOSE);
-    handles[1] = child;
-    while (child)
+    handles[1] = child->handle;
+    r = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
+
+    success = 0;
+    switch (r)
     {
-        r = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
-        switch (r)
+    case WAIT_OBJECT_0:
+        set_status(ST_ERROR, "connection closed");
+        break;
+
+    case WAIT_OBJECT_0 + 1:
+        if (GetExitCodeProcess(child->handle, &r))
         {
-        case WAIT_OBJECT_0:
-            report_status(client, "error: connection closed\n");
-            CloseHandle(handles[0]);
-            return;
-        case WAIT_OBJECT_0 + 1:
-            if (GetExitCodeProcess(child, &r))
-            {
-                report_status(client, "ok: process %u returned status %u\n", child_pid, r << 8);
-                CloseHandle(handles[0]);
-                cleanup_child();
-                return;
-            }
-            break;
-        default:
-            debug("WaitForMultipleObjects() returned %u! Retrying...\n", r);
-            break;
+            debug("  process %lu returned status %lu\n", child->pid, r);
+            *childstatus = r;
+            success = 1;
         }
+        else
+            debug("GetExitCodeProcess() failed (%lu). Giving up!\n", GetLastError());
+        break;
+    default:
+        debug("WaitForMultipleObjects() returned %lu (le=%lu). Giving up!\n", r, GetLastError());
+        break;
     }
+    CloseHandle(child->handle);
+    list_remove(&child->entry);
+    free(child);
+
+    /* We must reset WSAEventSelect before we can make
+     * the socket blocking again.
+     */
+    WSAEventSelect(client, handles[0], 0);
     CloseHandle(handles[0]);
-    report_status(client, "error: no process to wait for\n");
+    nbio = 0;
+    if (WSAIoctl(client, FIONBIO, &nbio, sizeof(nbio), &nbio, sizeof(nbio), &r, NULL, NULL) == SOCKET_ERROR)
+        debug("WSAIoctl(FIONBIO) failed: %s\n", sockerror());
+
+    return success;
 }
 
-int sockeintr(void)
+int sockretry(void)
 {
-    return WSAGetLastError() == WSAEINTR;
+    return (WSAGetLastError() == WSAEINTR);
 }
 
 const char* sockerror(void)
@@ -130,7 +222,7 @@ char* sockaddr_to_string(struct sockaddr* sa, socklen_t len)
     static char name[256+6];
     DWORD size = sizeof(name);
     /* This also appends the port number */
-    if (WSAAddressToString(sa,len, NULL, name, &size))
+    if (WSAAddressToString(sa, len, NULL, name, &size))
         sprintf(name, "unknown host (family %d)", sa->sa_family);
     return name;
 }
@@ -245,7 +337,7 @@ void ta_freeaddrinfo(struct addrinfo *addresses)
     }
 }
 
-int init_platform(void)
+int platform_init(void)
 {
     HMODULE hdll;
     WORD wVersionRequested;
diff --git a/testbot/src/testagentd/testagentd.c b/testbot/src/testagentd/testagentd.c
index 4973fd5..edb1a24 100644
--- a/testbot/src/testagentd/testagentd.c
+++ b/testbot/src/testagentd/testagentd.c
@@ -29,12 +29,17 @@
 
 #include "platform.h"
 
-#define CHUNK_SIZE 4096
+#define PROTOCOL_VERSION "testagentd 1.0"
+#define BLOCK_SIZE       4096
 
 const char *name0;
 int opt_debug = 0;
 
 
+/*
+ * Local error reporting
+ */
+
 void error(const char* format, ...)
 {
     va_list valist;
@@ -55,333 +60,863 @@ void debug(const char* format, ...)
     }
 }
 
-static char buffer[CHUNK_SIZE];
-static int buf_pos, buf_size;
 
-static void reset_buffer(void)
+/*
+ * Functions related to the list of known RPCs.
+ */
+
+enum rpc_ids_t
 {
-    buf_pos = buf_size = 0;
-}
+    RPCID_PING = 0,
+    RPCID_GETFILE,
+    RPCID_SENDFILE,
+    RPCID_RUN,
+    RPCID_WAIT,
+    RPCID_RM,
+};
+
+/* This is the RPC currently being processed */
+#define NO_RPCID         (~((uint32_t)0))
+static uint32_t rpcid = NO_RPCID;
 
-static int fill_buffer(SOCKET sock)
+static const char* rpc_name(uint32_t id)
 {
-    int n;
+    static char unknown[11];
+    static const char* names[] = {
+        "ping",
+        "getfile",
+        "sendfile",
+        "run",
+        "wait",
+        "rm",
+    };
+
+    if (id < sizeof(names) / sizeof(*names))
+        return names[id];
+    if (id == NO_RPCID)
+        return "norpc";
+    sprintf(unknown, "%u", id);
+    return unknown;
+}
 
-    if (buf_pos == buf_size)
+
+/*
+ * Functions to set the status of the last operation.
+ * This is sort of like an errno variable which is meant to be sent to the
+ * client to indicate the result of the last operation.
+ */
+
+/* status can take three values:
+ * - ST_OK    indicates that the operation was successful
+ * - ST_ERROR the operation failed but we can still perform other operations
+ * - ST_FATAL the connection is in an undefined state and should be closed
+ */
+static int status = ST_OK;
+const char* status_names[] = {"ok:", "error:", "fatal:"};
+
+/* If true, then the current connection is in a broken state */
+static int broken = 0;
+
+/* This is a message which indicates the reason for the status */
+static char* status_msg = NULL;
+static unsigned status_size = 0;
+static void vset_status_msg(const char* format, va_list valist)
+{
+    int len;
+    va_list args;
+    len = 1;
+    do
     {
-        /* Everything has been read, empty the buffer */
-        buf_pos = buf_size = 0;
+        if (len >= status_size)
+        {
+            /* len does not count the trailing '\0'. So add 1 and round up
+             * to the next 16 bytes multiple.
+             */
+            status_size = (len + 1 + 0xf) & ~0xf;
+            status_msg = realloc(status_msg, status_size);
+        }
+        va_copy(args, valist);
+        len = vsnprintf(status_msg, status_size, format, args);
+        va_end(args);
+        if (len < 0)
+            len = status_size * 1.1;
     }
-    if (buf_size == sizeof(buffer))
+    while (len >= status_size);
+    if (opt_debug || status != ST_OK)
+        fprintf(stderr, "%s%s: %s\n", status_names[status], rpc_name(rpcid), status_msg);
+}
+
+void set_status(int newstatus, const char* format, ...)
+{
+    va_list valist;
+    /* Don't let an error erase a fatal error */
+    if (newstatus != ST_ERROR || status != ST_FATAL)
     {
-        /* The buffer is full */
-        return 0;
+        status = newstatus;
+        if (newstatus == ST_FATAL)
+            broken = 1;
+        va_start(valist, format);
+        vset_status_msg(format, valist);
+        va_end(valist);
     }
+}
+
+
+/*
+ * Low-level functions to receive raw data
+ */
 
-    n = recv(sock, buffer + buf_size, sizeof(buffer) - buf_size, 0);
-    if (n > 0)
-        buf_size += n;
-    return n;
+static int skip_raw_data(SOCKET client, uint64_t size)
+{
+    char buf[BLOCK_SIZE];
+
+    if (broken)
+        return 0;
+
+    while (size)
+    {
+        int s = size < sizeof(buf) ? size : sizeof(buf);
+        int r = recv(client, buf, s, 0);
+        if (r < 0)
+        {
+            set_status(ST_FATAL, "skip_raw_data() failed: %s", sockerror());
+            return 0;
+        }
+        size -= r;
+    }
+    return 1;
 }
 
-static char* _get_string(void)
+static int recv_raw_data(SOCKET client, void* data, uint64_t size)
 {
-    char *str, *e;
-    char *eod = buffer + buf_size;
-    str = e = buffer + buf_pos;
-    while (e < eod)
+    char* d = data;
+
+    if (broken)
+        return 0;
+
+    while (size)
     {
-        if (*e == '\n')
+        int r = recv(client, d, size, 0);
+        if (r == 0)
         {
-            *e = '\0';
-            buf_pos = e + 1 - buffer;
-            return str;
+            set_status(ST_FATAL, "recv_raw_data() got a premature EOF");
+            return 0;
         }
-        e++;
+        if (r < 0)
+        {
+            set_status(ST_FATAL, "recv_raw_data() failed: %s", sockerror());
+            return 0;
+        }
+        d += r;
+        size -= r;
     }
-    return NULL;
+    return 1;
+}
+
+static int recv_raw_uint32(SOCKET client, uint32_t *u32)
+{
+    if (broken)
+        return 0;
+
+    if (!recv_raw_data(client, u32, sizeof(*u32)))
+        return 0;
+    *u32 = ntohl(*u32);
+    return 1;
+}
+
+static int recv_raw_uint64(SOCKET client, uint64_t *u64)
+{
+    uint32_t high, low;
+
+    if (broken)
+        return 0;
+
+    if (!recv_raw_uint32(client, &high) || !recv_raw_uint32(client, &low))
+        return 0;
+    *u64 = ((uint64_t)high) << 32 | low;
+    return 1;
+}
+
+
+/*
+ * Functions to receive argument lists
+ */
+
+static int recv_entry_header(SOCKET client, char *type, uint64_t *size)
+{
+    if (broken)
+        return 0;
+
+    return recv_raw_data(client, type, sizeof(*type)) &&
+           recv_raw_uint64(client, size);
 }
 
-/* Only strings smaller than the buffer size are supported */
-static char* get_string(SOCKET sock)
+#define ANY_SIZE   (~((uint64_t)0))
+
+static int expect_entry_header(SOCKET client, char type, uint64_t *size)
 {
-    char* str;
+    char htype;
+    uint64_t hsize;
+    int success;
 
-    if (buf_pos == buf_size)
-        fill_buffer(sock);
-    str = _get_string();
-    if (str || buf_pos == 0)
-        return str;
+    if (broken)
+        return 0;
 
-    /* Try to grab some more data */
-    memcpy(buffer, buffer + buf_pos, buf_size - buf_pos);
-    buf_size -= buf_pos;
-    buf_pos = 0;
-    fill_buffer(sock);
+    if (!recv_entry_header(client, &htype, &hsize))
+        return 0;
 
-    return _get_string();
+    if (type != htype)
+    {
+        set_status(ST_ERROR, "Expected a parameter of type %c but got %c instead", type, htype);
+        success = 0;
+    }
+    else if (*size != ANY_SIZE && *size != hsize)
+    {
+        set_status(ST_ERROR, "Expected a parameter of size " U64FMT " but got " U64FMT " instead", *size, hsize);
+        success = 0;
+    }
+    else
+    {
+        *size = hsize;
+        success = 1;
+    }
+    if (!success)
+        skip_raw_data(client, hsize);
+    return success;
 }
 
-static char* get_data(SOCKET sock, int *len)
+static int recv_uint32(SOCKET client, uint32_t *u32)
 {
-    char* data;
-    if (buf_pos == buf_size)
+    uint64_t size = sizeof(*u32);
+    int success = expect_entry_header(client, 'I', &size) &&
+                  recv_raw_uint32(client, u32);
+    if (success)
+        debug("  recv_uint32() -> %u\n", *u32);
+    return success;
+}
+
+static int recv_uint64(SOCKET client, uint64_t *u64)
+{
+    uint64_t size = sizeof(*u64);
+    int success = expect_entry_header(client, 'Q', &size) &&
+                  recv_raw_uint64(client, u64);
+    if (success)
+        debug("  recv_uint64() -> " U64FMT "\n", *u64);
+    return success;
+}
+
+static int recv_string(SOCKET client, char* *str)
+{
+    uint64_t size = ANY_SIZE;
+    int success;
+
+    *str = NULL;
+    if (!expect_entry_header(client, 's', &size))
+        return 0;
+
+    *str = malloc(size);
+    if (!*str)
     {
-        *len = fill_buffer(sock);
-        if (*len <= 0)
-            return NULL;
+        set_status(ST_ERROR, "malloc() failed: %s", strerror(errno));
+        skip_raw_data(client, size);
+        return 0;
     }
+    success = recv_raw_data(client, *str, size);
+    if (success)
+        debug("  recv_string() -> '%s'\n", *str);
     else
-        *len = buf_size - buf_pos;
-    data = buffer + buf_pos;
-    buf_pos = buf_size;
-    return data;
+    {
+        free(*str);
+        *str = NULL;
+    }
+    return success;
 }
 
-static const char* stream_from_net(SOCKET src, int dst)
+static int recv_file(SOCKET client, int fd, const char* filename)
 {
-    char buffer[CHUNK_SIZE];
-    while (1)
+    uint64_t size = ANY_SIZE;
+
+    debug("  recv_file(%s)\n", filename);
+    if (!expect_entry_header(client, 'd', &size))
+        return 0;
+
+    while (size)
     {
-        int r, w;
-        r = recv(src, buffer, sizeof(buffer), 0);
-        if (r == 0) /* EOF */
-            return NULL;
+        char buffer[BLOCK_SIZE];
+        int c, r, w;
+        c = size < sizeof(buffer) ? size : sizeof(buffer);
+        r = recv(client, buffer, c, 0);
+        if (r == 0)
+        {
+            debug("  got disconnected with " U64FMT " bytes still to be read!\n", size);
+            set_status(ST_FATAL, "got disconnected prematurely");
+            return 0;
+        }
         if (r < 0)
-            return sockerror();
-        w = write(dst, buffer, r);
+        {
+            set_status(ST_FATAL, "an error occurred while reading: %s", sockerror());
+            return 0;
+        }
+        size -= r;
+        w = write(fd, buffer, r);
         if (w != r)
         {
-            error("could only write %d bytes out of %d: %s\n", w, r, strerror(errno));
-            return strerror(errno);
+            set_status(ST_ERROR, "an error occurred while writing to '%s': %s", filename, strerror(errno));
+            debug("  could only write %d bytes out of %d: %s\n", w, r, strerror(errno));
+            skip_raw_data(client, size);
+            return 0;
         }
     }
-    return NULL;
+    debug("  File reception complete\n");
+    return 1;
 }
 
-/* This cannot be merged with stream_from_net() because on Windows
- * read()/write() cannot operate on sockets and recv()/send() cannot operate
- * on file descriptors :-(
- */
-static const char* stream_to_net(int src, SOCKET dst)
+static int skip_entries(SOCKET client, uint32_t count)
 {
-    char buffer[CHUNK_SIZE];
-    while (1)
+    while (count)
     {
-        int r, w;
-        r = read(src, buffer, sizeof(buffer));
-        if (r == 0) /* EOF */
-            return NULL;
-        if (r < 0)
-            return strerror(errno);
-        w = send(dst, buffer, r, 0);
-        if (w != r)
-        {
-            error("could only send %d bytes out of %d: %s\n", w, r, sockerror());
-            return sockerror();
-        }
+        char type;
+        uint64_t size;
+        if (!recv_entry_header(client, &type, &size) ||
+            !skip_raw_data(client, size))
+            return 0;
+        count--;
     }
-    return NULL;
+    return 1;
 }
 
-static char* status = NULL;
-static unsigned status_size = 0;
-static void vset_status(const char* format, va_list valist)
+static int recv_list_size(SOCKET client, uint32_t *u32)
 {
-    int len;
-    va_list args;
-    len = 0;
-    do
+    int success = recv_raw_uint32(client, u32);
+    if (success)
+        debug("  recv_list_size() -> %u\n", *u32);
+    return success;
+}
+
+static int expect_list_size(SOCKET client, uint32_t expected)
+{
+    uint32_t size;
+
+    if (!recv_list_size(client, &size))
+        return 0;
+
+    if (size == expected)
+        return 1;
+
+    set_status(ST_ERROR, "Invalid number of parameters (%u instead of %u)", size, expected);
+    skip_entries(client, size);
+    return 0;
+}
+
+
+/*
+ * Low-level functions to send raw data
+ */
+
+
+static int send_raw_data(SOCKET client, const void* data, uint64_t size)
+{
+    const char* d = data;
+
+    if (broken)
+        return 0;
+
+    while (size)
     {
-        if (len >= status_size)
+        int w = send(client, d, size, 0);
+        if (w < 0)
         {
-            /* len does not count the trailing '\0'. So add 1 and round up
-             * to the next 16 bytes multiple.
-             */
-            status_size = (len + 1 + 0xf) & ~0xf;
-            status = realloc(status, status_size);
+            set_status(ST_FATAL, "send_raw_data() failed: %s", sockerror());
+            return 0;
         }
-        va_copy(args, valist);
-        len = vsnprintf(status, status_size, format, args);
-        va_end(args);
-        if (len < 0)
-            len = status_size * 1.1;
+        d += w;
+        size -= w;
     }
-    while (len >= status_size);
-    if (opt_debug || strncmp(status, "ok:", 3) != 0)
-        fprintf(stderr, "%s", status);
+    return 1;
 }
 
-static void set_status(const char* format, ...)
+static int send_raw_uint32(SOCKET client, uint32_t u32)
 {
-    va_list valist;
-    va_start(valist, format);
-    vset_status(format, valist);
-    va_end(valist);
+    u32 = htonl(u32);
+    return send_raw_data(client, &u32, sizeof(u32));
 }
 
-void report_status(SOCKET client, const char* format, ...)
+static int send_raw_uint64(SOCKET client, uint64_t u64)
 {
-    va_list valist;
-    shutdown(client, SHUT_RD);
-    va_start(valist, format);
-    vset_status(format, valist);
-    va_end(valist);
-    send(client, status, strlen(status), 0);
+    return send_raw_uint32(client, u64 >> 32) &&
+           send_raw_uint32(client, u64 & 0xffffffff);
 }
 
-static void process_command(SOCKET client)
+
+/*
+ * Functions to send argument lists
+ */
+
+static int send_list_size(SOCKET client, uint32_t u32)
+{
+    debug("  send_list_size(%u)\n", u32);
+    return send_raw_uint32(client, u32);
+}
+
+static int send_entry_header(SOCKET client, char type, uint64_t size)
 {
-    char *command;
-    const char* err;
+    return send_raw_data(client, &type, sizeof(type)) &&
+           send_raw_uint64(client, size);
+}
 
-    reset_buffer();
-    command = get_string(client);
-    debug("Processing command %s\n", command ? command : "(null)");
-    if (!command)
+static int _send_status(SOCKET client, char type)
+{
+    int stlen, msglen;
+
+    msglen = strlen(status_msg);
+    if (status == ST_ERROR)
     {
-        report_status(client, "error: could not read the command\n");
+        /* Omit the 'error' prefix */
+        debug("  send_status('%c', '%s')\n", type, status_msg);
+        return send_entry_header(client, type, msglen + 1) &&
+               send_raw_data(client, status_msg, msglen + 1);
     }
-    else if (strcmp(command, "read") == 0)
+    else
+    {
+        /* Include the 'fatal' prefix for fatal errors */
+        stlen = strlen(status_names[status]);
+        debug("  send_status('%c', '%s %s')\n", type, status_names[status], status_msg);
+        return send_entry_header(client, type, stlen + 1 + msglen + 1) &&
+            send_raw_data(client, status_names[status], stlen) &&
+            send_raw_data(client, " ", 1) &&
+            send_raw_data(client, status_msg, msglen + 1);
+    }
+}
+
+static int send_status(SOCKET client)
+{
+    return _send_status(client, 's');
+}
+
+static int send_error(SOCKET client)
+{
+    /* We send only one result string */
+    return send_list_size(client, 1) &&
+           _send_status(client, 'e');
+}
+
+static int send_undef(SOCKET client)
+{
+    debug("  send_undef()\n");
+    return send_entry_header(client, 'u', 0);
+}
+
+static int send_uint32(SOCKET client, uint32_t u32)
+{
+    debug("  send_uint32(%u)\n", u32);
+    return send_entry_header(client, 'I', sizeof(u32)) &&
+           send_raw_uint32(client, u32);
+}
+
+static int send_uint64(SOCKET client, uint64_t u64)
+{
+    debug("  send_uint64(" U64FMT ")\n", u64);
+    return send_entry_header(client, 'Q', sizeof(u64)) &&
+           send_raw_uint64(client, u64);
+}
+
+static int send_string(SOCKET client, const char* str)
+{
+    uint64_t size;
+
+    debug("  send_string(%s)\n", str);
+    size = strlen(str) + 1;
+    return send_entry_header(client, 's', size) &&
+           send_raw_data(client, str, size);
+}
+
+static int send_file(SOCKET client, int fd, const char* filename)
+{
+    char buffer[BLOCK_SIZE];
+    struct stat st;
+    uint64_t size;
+
+    if (broken)
+        return 0;
+
+    debug("  send_file(%s)\n", filename);
+    if (fstat(fd, &st))
     {
-        /* Read the specified file */
-        char* filename;
-        int fd;
-        struct stat st;
-        char str[80];
+        set_status(ST_ERROR, "unable to compute the size of '%s': %s", filename, strerror(errno));
+        send_error(client);
+        return 0;
+    }
+    size = st.st_size;
+    send_entry_header(client, 'd', size);
 
-        filename = get_string(client);
-        if (!filename)
+    while (size)
+    {
+        int r, w;
+        int c;
+        c = size < sizeof(buffer) ? size : sizeof(buffer);
+        r = read(fd, buffer, c);
+        if (r == 0)
         {
-            report_status(client, "error: missing filename parameter for read\n");
-            return;
+            debug("  reached EOF with " U64FMT " bytes still to be read!\n", size);
+            set_status(ST_FATAL, "reached the '%s' EOF prematurely", filename);
+            return 0;
         }
-        debug("read '%s'\n", filename);
-        fd = open(filename, O_RDONLY | O_BINARY);
-        if (fd < 0)
+        if (r < 0)
         {
-            report_status(client, "error: unable to open '%s' for reading: %s\n", filename, strerror(errno));
-            return;
+            set_status(ST_FATAL, "an error occurred while reading '%s': %s", filename, strerror(errno));
+            return 0;
         }
-        if (fstat(fd, &st))
-            st.st_size = -1;
-        sprintf(str, "ok: size=%ld\n", st.st_size);
-        send(client, str, strlen(str), 0);
-        err = stream_to_net(fd, client);
-        close(fd);
-        if (err)
+        size -= r;
+        w = send(client, buffer, r, 0);
+        if (w != r)
         {
-            /* We cannot report the error now because it would get mixed
-             * with the file data
-             */
-            set_status("error: an error occurred while reading '%s': %s\n", filename, err);
-            return;
+            set_status(ST_FATAL, "an error occurred while sending: %s", sockerror());
+            debug("  could only send %d bytes out of %d: %s\n", w, r, sockerror());
+            return 0;
         }
-        set_status("ok: read done\n");
     }
-    else if (strcmp(command, "write") == 0)
+    debug("  File successfully sent\n");
+    return 1;
+}
+
+
+/*
+ * High-level operations.
+ */
+
+static void do_ping(SOCKET client)
+{
+    if (expect_list_size(client, 0))
+        send_list_size(client, 0);
+    else
+        send_error(client);
+}
+
+static void do_getfile(SOCKET client)
+{
+    char* filename;
+    int fd;
+
+    if (!expect_list_size(client, 1) ||
+        !recv_string(client, &filename))
     {
-        /* Write to the specified file */
-        char* filename;
-        int fd;
-        char* data;
-        int len;
+        send_error(client);
+        return;
+    }
 
-        filename = get_string(client);
-        if (!filename)
-        {
-            report_status(client, "error: missing filename parameter for write\n");
-            return;
-        }
-        debug("write '%s'\n", filename);
-        fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0600);
-        if (fd < 0)
-        {
-            report_status(client, "error: unable to open '%s' for writing: %s\n", filename, strerror(errno));
-            unlink(filename);
-            return;
-        }
-        data = get_data(client, &len);
-        if (data && write(fd, data, len) != len)
-        {
-            report_status(client, "error: an error occurred while writing to '%s': %s\n", filename, strerror(errno));
-            close(fd);
-            unlink(filename);
-            return;
-        }
-        err = stream_from_net(client, fd);
+    fd = open(filename, O_RDONLY | O_BINARY);
+    if (fd < 0)
+    {
+        set_status(ST_ERROR, "unable to open '%s' for reading: %s", filename, strerror(errno));
+        send_error(client);
+    }
+    else
+    {
+        send_list_size(client, 1);
+        send_file(client, fd, filename);
         close(fd);
-        if (err)
-        {
-            report_status(client, "error: an error occurred while writing to '%s': %s\n", filename, err);
-            unlink(filename);
-            return;
-        }
-        else
-            report_status(client, "ok: write done\n");
+        /* Trying to report the status now would be pointless: either
+         * the client got all the data fine or the connection is busted.
+         */
     }
-    else if (strcmp(command, "runscript") == 0)
+    free(filename);
+}
+
+enum sendfile_flags_t {
+    SF_EXECUTABLE = 1,
+};
+
+static void do_sendfile(SOCKET client)
+{
+    char *filename;
+    uint32_t flags;
+    mode_t mode;
+    int fd, success;
+
+    if (!expect_list_size(client, 3) ||
+        !recv_string(client, &filename) ||
+        !recv_uint32(client, &flags)
+        /* Next entry is the file data */
+        )
     {
-        /* Run the specified script */
-        int fd, len;
-        char *data, *script;
+        free(filename); /* filename is either NULL or malloc()-ed here */
+        send_error(client);
+        return;
+    }
 
-        cleanup_child();
-        script = get_script_path();
-        debug("runscript '%s'\n", script);
-        fd = open(script, O_WRONLY | O_CREAT | O_TRUNC, 0600);
-        if (fd < 0)
-        {
-            report_status(client, "error: unable to open '%s' for writing: %s\n", script, strerror(errno));
-            free(script);
-            return;
-        }
-        data = get_data(client, &len);
-#ifndef WIN32
-        /* Use the standard shell if none is specified */
-        if (data && strncmp(data, "#!/", 3) && strncmp(data, "# !/", 4))
-        {
-            const char shell[]="#!/bin/sh\n";
-            write(fd, shell, sizeof(shell));
-        }
-#endif
-        if (data && write(fd, data, len) != len)
+    unlink(filename); /* To force re-setting the mode */
+    mode = (flags & SF_EXECUTABLE) ? 0700 : 0600;
+    fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, mode);
+    if (fd < 0)
+    {
+        skip_entries(client, 1);
+        set_status(ST_ERROR, "unable to open '%s' for writing: %s", filename, strerror(errno));
+        success = 0;
+    }
+    else
+    {
+        success = recv_file(client, fd, filename);
+        close(fd);
+    }
+
+    if (!success)
+        unlink(filename);
+    free(filename);
+
+    if (success)
+        send_list_size(client, 0);
+    else
+        send_error(client);
+}
+
+static void do_run(SOCKET client)
+{
+    uint32_t argc, i;
+    char** argv;
+    uint32_t flags;
+    int failed;
+    char *redirects[3];
+    uint64_t pid;
+
+    /* Get and check argc */
+    if (!recv_list_size(client, &argc))
+    {
+        send_error(client);
+        return;
+    }
+    argv = NULL;
+    if (argc < 5)
+        set_status(ST_ERROR, "expected 5 or more parameters");
+    else
+    {
+        /* Allocate an extra entry for the trailing NULL pointer */
+        argv = malloc((argc - 4 + 1) * sizeof(*argv));
+        if (!argv)
+            set_status(ST_ERROR, "malloc() failed: %s", strerror(errno));
+    }
+    if (!argv)
+    {
+        skip_entries(client, argc);
+        send_error(client);
+        return;
+    }
+    argc -= 4;
+
+    /* Retrieve the parameters */
+    failed = 0;
+    memset(redirects, 0, sizeof(redirects));
+    memset(argv, 0, (argc + 1) * sizeof(*argv));
+    if (recv_uint32(client, &flags) &&
+        recv_string(client, &redirects[0]) &&
+        recv_string(client, &redirects[1]) &&
+        recv_string(client, &redirects[2]))
+    {
+        for (i = 0; i < argc; i++)
+            if (!recv_string(client, &argv[i]))
+            {
+                failed = 1;
+                break;
+            }
+    }
+    else
+        failed = 1;
+
+    if (!failed)
+    {
+        debug("  run '%s", argv[0]);
+        for (i = 1; i < argc; i++)
+            debug("' '%s", argv[i]);
+        debug("' %s%s%s%s%s%s\n",
+              redirects[0][0] ? " <" : "", redirects[0],
+              redirects[1][0] ? " >" : "", redirects[1],
+              redirects[2][0] ? " 2>" : "", redirects[2]);
+
+        pid = platform_run(argv, flags, redirects);
+        if (!pid)
+            failed = 1;
+    }
+
+    /* Free all the memory */
+    free(redirects[0]);
+    free(redirects[1]);
+    free(redirects[2]);
+    for (i = 0; i < argc; i++)
+        free(argv[i]);
+    free(argv);
+
+    if (failed)
+        send_error(client);
+    else
+    {
+        send_list_size(client, 1);
+        send_uint64(client, pid);
+    }
+}
+
+static void do_wait(SOCKET client)
+{
+    uint64_t pid;
+    uint32_t childstatus;
+
+    if (!expect_list_size(client, 1) ||
+        !recv_uint64(client, &pid))
+    {
+        send_error(client);
+        return;
+    }
+
+    if (platform_wait(client, pid, &childstatus))
+    {
+        send_list_size(client, 1);
+        send_uint32(client, childstatus);
+    }
+    else
+        send_error(client);
+}
+
+static void do_rm(SOCKET client)
+{
+    int got_errors;
+    uint32_t argc, i;
+    char** filenames;
+
+    /* Get and check the parameter count */
+    if (!recv_list_size(client, &argc))
+    {
+        send_error(client);
+        return;
+    }
+
+    filenames = malloc(argc * sizeof(*filenames));
+    if (!filenames)
+    {
+        set_status(ST_ERROR, "malloc() failed: %s", strerror(errno));
+        skip_entries(client, argc);
+        send_error(client);
+        return;
+    }
+
+    /* Retrieve the parameters */
+    memset(filenames, 0, argc * sizeof(*filenames));
+    got_errors = 0;
+    for (i = 0; i < argc; i++)
+    {
+        if (!recv_string(client, &filenames[i]))
         {
-            report_status(client, "error: an error occurred while writing to '%s': %s\n", script, strerror(errno));
-            close(fd);
-            unlink(script);
-            free(script);
-            return;
+            got_errors = 1;
+            send_error(client);
+            break;
         }
-        err = stream_from_net(client, fd);
-        close(fd);
-        if (err)
+    }
+
+    if (!got_errors)
+    {
+        for (i = 0; i < argc; i++)
         {
-            report_status(client, "error: an error occurred while saving the script to '%s': %s\n", script, err);
-            unlink(script);
-            free(script);
-            return;
+            debug("rm '%s'\n", filenames[i]);
+            if (unlink(filenames[i]) < 0 && errno != ENOENT && errno != ENOTDIR)
+            {
+                int err = errno;
+                if (!got_errors)
+                {
+                    int f;
+                    got_errors = 1;
+                    /* In case of error report on the success / failure
+                     * for each file.
+                     */
+                    send_list_size(client, argc);
+                    for (f = 0; f < i; f++)
+                        if (!send_undef(client))
+                            break;
+                }
+                set_status(ST_ERROR, "Could not delete '%s': %s", filenames[i], strerror(err));
+                if (!send_status(client))
+                    break;
+            }
+            else if (got_errors)
+            {
+                if (!send_undef(client))
+                    break;
+            }
         }
-        start_child(client, script);
     }
-    else if (strcmp(command, "waitchild") == 0)
+
+    /* Free all the memory */
+    for (i = 0; i < argc; i++)
+        free(filenames[i]);
+    free(filenames);
+
+    if (!got_errors)
+    {
+        /* If all the deletions succeeded, then return an empty list to mean
+         * nothing to report
+         */
+        send_list_size(client, 0);
+    }
+}
+
+static void do_unknown(SOCKET client, uint32_t id)
+{
+    uint32_t argc;
+
+    if (recv_list_size(client, &argc))
+        skip_entries(client, argc);
+
+    send_error(client);
+}
+
+static void process_rpc(SOCKET client)
+{
+    int r;
+
+    debug("Waiting for an RPC\n");
+    r = recv(client, (void*)&rpcid, 1, MSG_PEEK);
+    if (r == 0)
     {
-        /* Wait for the last process we started */
-        wait_for_child(client);
+        /* The client disconnected normally */
+        broken = 1;
+        return;
     }
-    else if (strcmp(command, "status") == 0)
+    else if (r < 0)
     {
-        /* Return the status of the previous command */
-        shutdown(client, SHUT_RD);
-        send(client, status, strlen(status), 0);
+        /* Some error occurred */
+        debug("No RPC: %s\n", sockerror());
+        broken = 1;
+        return;
     }
-    else
+    if (!recv_raw_uint32(client, &rpcid))
+    {
+        set_status(ST_FATAL, "no RPC id");
+        return;
+    }
+
+    debug("-> %s\n", rpc_name(rpcid));
+    switch (rpcid)
     {
-        report_status(client, "error: unknown command: %s\n", command);
+    case RPCID_PING:
+        do_ping(client);
+        break;
+    case RPCID_GETFILE:
+        do_getfile(client);
+        break;
+    case RPCID_SENDFILE:
+        do_sendfile(client);
+        break;
+    case RPCID_RUN:
+        do_run(client);
+        break;
+    case RPCID_WAIT:
+        do_wait(client);
+        break;
+    case RPCID_RM:
+        do_rm(client);
+        break;
+    default:
+        do_unknown(client, rpcid);
     }
 }
 
-void* sockaddr_getaddr(struct sockaddr* sa, socklen_t* len)
+void* sockaddr_getaddr(const struct sockaddr* sa, socklen_t* len)
 {
     switch (sa->sa_family)
     {
@@ -421,6 +956,7 @@ static int is_host_allowed(SOCKET client, const char* srchost, int addrlen)
     socklen_t peerlen;
     int rc;
 
+    debug("checking source address\n");
     if (!srchost)
         return 1;
 
@@ -521,7 +1057,7 @@ int main(int argc, char** argv)
             opt_usage = 2;
         }
 
-        if (!init_platform())
+        if (!platform_init())
         {
             if (!opt_usage)
                 exit(1);
@@ -579,6 +1115,7 @@ int main(int argc, char** argv)
     }
     for (addrp = addresses; addrp; addrp = addrp->ai_next)
     {
+        debug("trying family=%d\n", addrp->ai_family);
         if (addrp->ai_family != PF_INET)
             continue;
         master = socket(addrp->ai_family, addrp->ai_socktype, addrp->ai_protocol);
@@ -604,8 +1141,6 @@ int main(int argc, char** argv)
         error("listen() failed: %s\n", sockerror());
         exit(1);
     }
-    set_status("ok: ready\n");
-
     while (1)
     {
         SOCKET client;
@@ -614,7 +1149,15 @@ int main(int argc, char** argv)
         if (client >= 0)
         {
             if (is_host_allowed(client, opt_srchost, addrlen))
-                process_command(client);
+            {
+                broken = 0;
+                /* Send the version right away */
+                send_string(client, PROTOCOL_VERSION);
+
+                while (!broken)
+                    process_rpc(client);
+            }
+            debug("closing client socket\n");
             closesocket(client);
         }
         else if (!sockeintr())
@@ -624,6 +1167,5 @@ int main(int argc, char** argv)
         }
     }
 
-    cleanup_child();
     return 0;
 }
-- 
1.7.10.4




More information about the wine-patches mailing list