[1/2] testbot/VMs: Use a TestAgent tool instead of VMware to run scripts on or copy files from/to the VMs.

Francois Gouget fgouget at codeweavers.com
Thu Oct 18 08:34:36 CDT 2012


---

Before this patch WineTestBot used VMware's facilities to exchange files 
and run scripts on the VMs. But Libvirt, QEmu and other virtualisation 
systems don't provide an equivalent. So in removing this dependency, 
this patch is the first big step towards migrating from VMware to 
Libvirt.

Compatibility Note: This patch could in theory be applied to an existing 
VMware-based WineTestBot deployment and everything should still work 
afterwards providing the following steps are also performed:
 - update the database schema
 - add hostname information to all the VMs
 - reconfigure the VMs (including the build VM) to run the testagentd server


 testbot/bin/WineRunBuild.pl                    |    2 +-
 testbot/bin/WineRunReconfig.pl                 |    2 +-
 testbot/bin/WineRunTask.pl                     |    2 +-
 testbot/bin/build/Reconfig.pl                  |   33 ++
 testbot/ddl/winetestbot.sql                    |    1 +
 testbot/doc/INSTALL.txt                        |   14 +-
 testbot/doc/winetestbot-schema.dia             |   24 +-
 testbot/lib/WineTestBot/Config.pm              |   10 +-
 testbot/lib/WineTestBot/ConfigLocalTemplate.pl |    3 +
 testbot/lib/WineTestBot/TestAgent.pm           |  250 ++++++++++
 testbot/lib/WineTestBot/VMs.pm                 |  109 +----
 testbot/src/testagentd/.gitignore              |    4 +
 testbot/src/testagentd/Makefile                |   40 ++
 testbot/src/testagentd/platform.h              |   85 ++++
 testbot/src/testagentd/platform_unix.c         |  203 ++++++++
 testbot/src/testagentd/platform_windows.c      |  267 ++++++++++
 testbot/src/testagentd/testagentd.c            |  626 ++++++++++++++++++++++++
 17 files changed, 1574 insertions(+), 101 deletions(-)
 create mode 100644 testbot/lib/WineTestBot/TestAgent.pm
 create mode 100644 testbot/src/testagentd/.gitignore
 create mode 100644 testbot/src/testagentd/Makefile
 create mode 100644 testbot/src/testagentd/platform.h
 create mode 100644 testbot/src/testagentd/platform_unix.c
 create mode 100644 testbot/src/testagentd/platform_windows.c
 create mode 100644 testbot/src/testagentd/testagentd.c

diff --git a/testbot/bin/WineRunBuild.pl b/testbot/bin/WineRunBuild.pl
index 868b378..5c8a7ef 100755
--- a/testbot/bin/WineRunBuild.pl
+++ b/testbot/bin/WineRunBuild.pl
@@ -248,7 +248,7 @@ if ($Run64)
   $Script .= ",64";
 }
 $Script .= "\n";
-$ErrMessage = $VM->RunScriptInGuestTimeout("", $Script, $Task->Timeout);
+$ErrMessage = $VM->RunScriptInGuestTimeout($Script, $Task->Timeout);
 if (defined($ErrMessage))
 {
   $VM->CopyFileFromGuestToHost("$LogDir/Build.log",
diff --git a/testbot/bin/WineRunReconfig.pl b/testbot/bin/WineRunReconfig.pl
index ae102e9..2d8618e 100755
--- a/testbot/bin/WineRunReconfig.pl
+++ b/testbot/bin/WineRunReconfig.pl
@@ -195,7 +195,7 @@ if (defined($ErrMessage))
              $FullErrFileName, $Job, $Step, $Task;
 }
 my $Script = "#!/bin/sh\n$BinDir/build/Reconfig.pl\n";
-$ErrMessage = $VM->RunScriptInGuestTimeout("", $Script, $Task->Timeout);
+$ErrMessage = $VM->RunScriptInGuestTimeout($Script, $Task->Timeout);
 if (defined($ErrMessage))
 {
   $VM->CopyFileFromGuestToHost("$LogDir/Reconfig.log",
diff --git a/testbot/bin/WineRunTask.pl b/testbot/bin/WineRunTask.pl
index 836dab2..00e0c35 100755
--- a/testbot/bin/WineRunTask.pl
+++ b/testbot/bin/WineRunTask.pl
@@ -317,7 +317,7 @@ elsif ($Step->Type eq "suite")
 # Needed to exit the command prompt on Win9x/WinMe
 $Script .= "cls\r\n";
 
-$ErrMessage = $VM->RunScriptInGuestTimeout("", $Script, $Task->Timeout + 15);
+$ErrMessage = $VM->RunScriptInGuestTimeout($Script, $Task->Timeout + 15);
 if (defined($ErrMessage))
 {
   RetrieveLogFile $Job, $Step, $Task, "C:\\winetest\\$RptFileName",
diff --git a/testbot/bin/build/Reconfig.pl b/testbot/bin/build/Reconfig.pl
index 4661727..daa03b1 100755
--- a/testbot/bin/build/Reconfig.pl
+++ b/testbot/bin/build/Reconfig.pl
@@ -75,6 +75,34 @@ sub CountCPUs()
     $ncpus ||= 1;
 }
 
+sub BuildTestAgentd
+{
+  # If testagentd already exists it's likely already running
+  # so don't rebuild it.
+  if (! -x "$BinDir/build/testagentd")
+  {
+    system("( cd $BinDir/../src/testagentd && set -x && " .
+           "  time make -j$ncpus build " .
+           ") >>$LogDir/Reconfig.log 2>&1");
+    if ($? != 0)
+    {
+      LogMsg "Build testagentd failed\n";
+      return !1;
+    }
+  }
+
+  system("( cd $BinDir/../src/testagentd && set -x && " .
+         "  time make -j$ncpus iso " .
+         ") >>$LogDir/Reconfig.log 2>&1");
+  if ($? != 0)
+  {
+    LogMsg "Build winetestbot.iso failed\n";
+    return !1;
+  }
+
+  return 1;
+}
+
 sub BuildNative
 {
   mkdir "$DataDir/build-native" if (! -d "$DataDir/build-native");
@@ -133,6 +161,11 @@ if (! GitPull())
 
 CountCPUs();
 
+if (! BuildTestAgentd())
+{
+  exit(1);
+}
+
 if (! BuildNative())
 {
   exit(1);
diff --git a/testbot/ddl/winetestbot.sql b/testbot/ddl/winetestbot.sql
index 8734f9b..d42298e 100644
--- a/testbot/ddl/winetestbot.sql
+++ b/testbot/ddl/winetestbot.sql
@@ -52,6 +52,7 @@ CREATE TABLE VMs
   VmxHost      VARCHAR(64)      NULL,
   VmxFilePath  VARCHAR(64)      NOT NULL,
   IdleSnapshot VARCHAR(32)      NOT NULL,
+  Hostname     VARCHAR(64)      NOT NULL,
   Interactive  ENUM('Y', 'N')   NOT NULL,
   Description  VARCHAR(40)      NULL,
   PRIMARY KEY (Name)
diff --git a/testbot/doc/INSTALL.txt b/testbot/doc/INSTALL.txt
index 54e5e31..6098a0c 100644
--- a/testbot/doc/INSTALL.txt
+++ b/testbot/doc/INSTALL.txt
@@ -7,6 +7,7 @@ Dependencies:
 - Perl DBD and DBI::mysql modules
 - Sendmail
 - VMware Vix API (http://www.vmware.com/support/developer/vix-api)
+- IO::Socket::IP (libio-socket-ip-perl)
 
 MySQL setup:
 - Create a new 'winetestbot' database and its tables using the
@@ -131,7 +132,6 @@ Dependencies:
   with MinGW. For instance on Debian you should install autoconf,
   bison, flex, gcc, gcc-mingw-w64, git and make. If you are going to
   have 64bit VMs then make sure MinGW can generate 64bit PE executables.
-- Install netcat.
 - Create a new system group 'winehq' and a system user 'winehq',
   make sure to make user winehq a member of group winehq.
 - Clone Wine's tools repository to $HOME/tools (so this document
@@ -144,6 +144,10 @@ Dependencies:
 - In the winehq account, run Reconfig.pl.
   Check $HOME/tools/testbot/log/Reconfig.log to make sure it
   succeeded.
+- Start the TestAgent server: ./bin/build/testagentd PORT SRCHOST
+  Where PORT is the $AgentPort that was configured in ConfigLocal.pl
+  on the WineTestBot server, and SRCHOST is either omitted or the hostname
+  of the WineTestBot server.
 - Take a snapshot of the running VM. Make sure restoring this snapshot
   will result in a running build VM.
 - Register this VM as a build VM on the web site.
@@ -152,7 +156,13 @@ Dependencies:
 4. Windows test VM setup
 ------------------------
 
-- FIXME: To document
+- Grab winetestbot.iso from the build VM.
+- Add it to the Windows VM and copy the winetest directory to c:\winetest.
+- Then remove the iso from the Windows VM again.
+- Start the TestAgent server: TestAgentd PORT SRCHOST
+  Where PORT is the $AgentPort that was configured in ConfigLocal.pl
+  on the WineTestBot server, and SRCHOST is either omitted or the hostname
+  of the WineTestBot server.
 - Take a snapshot of the running VM. Make sure restoring this snapshot
   will result in a running Windows VM.
 - Register this VM on the web site. On a production WineTestBot server
diff --git a/testbot/doc/winetestbot-schema.dia b/testbot/doc/winetestbot-schema.dia
index 145a4e9..41454d0 100644
--- a/testbot/doc/winetestbot-schema.dia
+++ b/testbot/doc/winetestbot-schema.dia
@@ -2088,7 +2088,7 @@
         <dia:point val="25.6708,-2.02919"/>
       </dia:attribute>
       <dia:attribute name="obj_bb">
-        <dia:rectangle val="25.6708,-2.02919;35.8258,7.37081"/>
+        <dia:rectangle val="25.6708,-2.02919;35.8258,8.17081"/>
       </dia:attribute>
       <dia:attribute name="meta">
         <dia:composite type="dict"/>
@@ -2100,7 +2100,7 @@
         <dia:real val="10.155000000000001"/>
       </dia:attribute>
       <dia:attribute name="elem_height">
-        <dia:real val="9.3999999999999986"/>
+        <dia:real val="10.199999999999999"/>
       </dia:attribute>
       <dia:attribute name="text_colour">
         <dia:color val="#000000"/>
@@ -2313,6 +2313,26 @@
         </dia:composite>
         <dia:composite type="table_attribute">
           <dia:attribute name="name">
+            <dia:string>#Hostname#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="type">
+            <dia:string>#VARCHAR(64)#</dia:string>
+          </dia:attribute>
+          <dia:attribute name="comment">
+            <dia:string>##</dia:string>
+          </dia:attribute>
+          <dia:attribute name="primary_key">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="nullable">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+          <dia:attribute name="unique">
+            <dia:boolean val="false"/>
+          </dia:attribute>
+        </dia:composite>
+        <dia:composite type="table_attribute">
+          <dia:attribute name="name">
             <dia:string>#Interactive#</dia:string>
           </dia:attribute>
           <dia:attribute name="type">
diff --git a/testbot/lib/WineTestBot/Config.pm b/testbot/lib/WineTestBot/Config.pm
index 353b429..bcff569 100644
--- a/testbot/lib/WineTestBot/Config.pm
+++ b/testbot/lib/WineTestBot/Config.pm
@@ -33,8 +33,8 @@ use vars qw (@ISA @EXPORT @EXPORT_OK $UseSSL $LogDir $DataDir $BinDir
              $BuildTimeout $ReconfigTimeout $OverheadTimeout $TagPrefix
              $ProjectName $PatchesMailingList $PatchResultsEMail $LDAPServer
              $LDAPBindDN $LDAPSearchBase $LDAPSearchFilter
-             $LDAPRealNameAttribute $LDAPEMailAttribute $JobPurgeDays
-             $JobArchiveDays $WebHostName);
+             $LDAPRealNameAttribute $LDAPEMailAttribute $AgentPort
+             $JobPurgeDays $JobArchiveDays $WebHostName);
 
 require Exporter;
 @ISA = qw(Exporter);
@@ -47,8 +47,8 @@ require Exporter;
              $SingleTimeout $BuildTimeout $ReconfigTimeout $OverheadTimeout
              $TagPrefix $ProjectName $PatchesMailingList $PatchResultsEMail
              $LDAPServer $LDAPBindDN $LDAPSearchBase $LDAPSearchFilter
-             $LDAPRealNameAttribute $LDAPEMailAttribute $JobPurgeDays
-             $JobArchiveDays $WebHostName);
+             $LDAPRealNameAttribute $LDAPEMailAttribute $AgentPort
+             $JobPurgeDays $JobArchiveDays $WebHostName);
 @EXPORT_OK = qw($DbDataSource $DbUsername $DbPassword);
 
 $LogDir = "/home/winehq/tools/testbot/var";
@@ -59,7 +59,7 @@ $MaxRevertingVMs = 1;
 $MaxRunningVMs = 2;
 $MaxExtraPoweredOnVms = 2;
 $SleepAfterRevert = 30;
-$WaitForToolsInVM = 60;
+$WaitForToolsInVM = 30;
 
 $SuiteTimeout = 30 * 60;
 $SingleTimeout = 2 * 60;
diff --git a/testbot/lib/WineTestBot/ConfigLocalTemplate.pl b/testbot/lib/WineTestBot/ConfigLocalTemplate.pl
index 9469784..ebc38d3 100644
--- a/testbot/lib/WineTestBot/ConfigLocalTemplate.pl
+++ b/testbot/lib/WineTestBot/ConfigLocalTemplate.pl
@@ -105,4 +105,7 @@ $WineTestBot::Config::LDAPRealNameAttribute = undef;
 # LDAP attribute for a users email address
 $WineTestBot::Config::LDAPEMailAttribute = undef;
 
+# The port the VM agents are listening on
+$WineTestBot::Config::AgentPort = undef;
+
 1;
diff --git a/testbot/lib/WineTestBot/TestAgent.pm b/testbot/lib/WineTestBot/TestAgent.pm
new file mode 100644
index 0000000..513b50f
--- /dev/null
+++ b/testbot/lib/WineTestBot/TestAgent.pm
@@ -0,0 +1,250 @@
+# Interface with testagentd to send to and receive files from the VMs and
+# to run scripts.
+#
+# Copyright 2009 Ge van Geldorp
+# Copyright 2012 Francois Gouget
+#
+# 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
+
+package TestAgent;
+use strict;
+
+use IO::Socket::IP;
+
+use WineTestBot::Config;
+use WineTestBot::Log;
+
+my $DONE_READING = 0;
+my $DONE_WRITING = 1;
+
+sub _Connect($;$)
+{
+  my ($Hostname, $Timeout) = @_;
+
+  $Timeout ||= 10;
+  my $Deadline = time() + $Timeout;
+  while (1)
+  {
+    my $ConnectTimeout = $Timeout < 30 ? $Timeout : 30;
+    my $socket = IO::Socket::IP->new(PeerHost => $Hostname,
+                                     PeerPort => $AgentPort,
+                                     Type => SOCK_STREAM,
+                                     Timeout => $ConnectTimeout);
+    return $socket if ($socket);
+
+    $Timeout = $Deadline - time();
+    last if ($Timeout <= 0);
+    # We ignore the upcoming delay in our timeout calculation
+
+    sleep(1);
+    # We will retry just in case this is a temporary network failure
+  }
+  $@ = "Unable to connect to $Hostname:$AgentPort: $@";
+  return undef;
+}
+
+sub _ReadStatus($$)
+{
+  my ($fh, $Timeout) = @_;
+  my ($Status, $Err) = ("", undef);
+  eval
+  {
+    local $SIG{ALRM} = sub { die "read status timed out\n" }; # NB: \n required
+    alarm($Timeout || 10);
+    while ($Status !~ /\n/)
+    {
+      # 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);
+      if (!defined $n)
+      {
+        $Err = $!;
+        last;
+      }
+      last if ($n == 0);
+      $Status .= $Buffer;
+    }
+    alarm(0);
+  };
+  return (undef, $Err) if ($Err);
+  return (undef, $@) if ($@);
+  return ($Status, undef);
+}
+
+sub GetStatus($;$)
+{
+  my ($Hostname, $Timeout) = @_;
+
+  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);
+}
+
+# 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($$)
+{
+    my ($src, $dst) = @_;
+
+    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;
+}
+
+sub SendFile($$$)
+{
+  my ($Hostname, $HostPathName, $GuestPathName) = @_;
+  LogMsg("SendFile $HostPathName -> $Hostname $GuestPathName\n");
+
+  my $fh;
+  if (!open($fh, "<", $HostPathName))
+  {
+    return "unable to open '$HostPathName' for reading: $!";
+  }
+
+  my $Err;
+  my $nc = _Connect($Hostname);
+  if ($nc)
+  {
+    $nc->send("write\n$GuestPathName\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);
+  }
+  else
+  {
+    $Err = $@;
+  }
+  close($fh);
+  return $Err;
+}
+
+sub GetFile($$$)
+{
+  my ($Hostname, $GuestPathName, $HostPathName) = @_;
+  LogMsg("GetFile $Hostname $GuestPathName -> $HostPathName\n");
+
+  my $fh;
+  if (!open($fh, ">", $HostPathName))
+  {
+    return "unable to open '$HostPathName' for writing: $!";
+  }
+
+  my ($Err, $GuestSize);
+  my $nc = _Connect($Hostname);
+  if ($nc)
+  {
+    $nc->send("read\n$GuestPathName\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//)
+    {
+      $GuestSize=$1;
+      if ($Err ne "" and syswrite($fh, $Err, length($Err)) < 0)
+      {
+        $Err = $!;
+      }
+      else
+      {
+        $Err = _Copy($nc, $fh) ? undef : $!;
+      }
+    }
+    close($nc);
+  }
+  else
+  {
+    $Err = $@;
+  }
+  close($fh);
+  my $HostSize=-s $HostPathName;
+  if (!defined $Err and $HostSize != $GuestSize)
+  {
+    # Something still went wrong during the transfer. Get the last operation
+    # status
+    my $StatusErr;
+    ($Err, $StatusErr) = GetStatus($Hostname);
+    $Err = $StatusErr if (!defined $StatusErr);
+  }
+  unlink $HostPathName if ($Err);
+  return $Err;
+}
+
+sub RunScript($$$)
+{
+  my ($Hostname, $ScriptText, $Timeout) = @_;
+  LogMsg("RunScript $Hostname ", ($Timeout || 0), " [$ScriptText]\n");
+
+  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)
+    {
+      $nc = _Connect($Hostname);
+      if ($nc)
+      {
+        $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);
+      }
+      else
+      {
+        $Err = $@;
+      }
+    }
+  }
+  else
+  {
+    $Err = $@;
+  }
+  return $Err;
+}
+
+1;
diff --git a/testbot/lib/WineTestBot/VMs.pm b/testbot/lib/WineTestBot/VMs.pm
index 98f99f7..16f5ff0 100644
--- a/testbot/lib/WineTestBot/VMs.pm
+++ b/testbot/lib/WineTestBot/VMs.pm
@@ -85,10 +85,13 @@ package WineTestBot::VM;
 
 use VMware::Vix::Simple;
 use VMware::Vix::API::Constants;
+
 use ObjectModel::BackEnd;
 use WineTestBot::Config;
 use WineTestBot::Engine::Notify;
+use WineTestBot::TestAgent;
 use WineTestBot::WineTestBotObjects;
+use WineTestBot::Log;
 
 use vars qw (@ISA @EXPORT);
 
@@ -341,106 +344,33 @@ sub PowerOff
   return $self->UpdateStatus($VMHandle);
 }
 
-sub WaitForToolsInGuest
+sub WaitForToolsInGuest($;$)
 {
-  my $self = shift;
+  my ($self, $Timeout) = @_;
 
-  my ($ErrMessage, $VMHandle) = $self->GetVMHandle();
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  my $Err = VMWaitForToolsInGuest($VMHandle, WaitForToolsInVM);
-  return $self->CheckError($Err);
+  $Timeout ||= $WaitForToolsInVM;
+  LogMsg("Waiting for ", $self->Name, " (up to ${Timeout}s)\n");
+  my ($Status, $Err) = TestAgent::GetStatus($self->Hostname, $Timeout);
+  # In fact we don't care about the status
+  return $Err;
 }
 
-sub CopyFileFromHostToGuest
+sub CopyFileFromHostToGuest($$$)
 {
-  my $self = shift;
-  my ($HostPathName, $GuestPathName) = @_;
-
-  my ($ErrMessage, $VMHandle) = $self->GetVMHandle();
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  $ErrMessage = $self->LoginInGuest($VMHandle, !1);
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  my $Try = 0;
-  my $Err = -1;
-  while ($Err != VIX_OK && $Try < 5)
-  {
-    $Err = VMCopyFileFromHostToGuest($VMHandle, $HostPathName, $GuestPathName,
-                                     0, VIX_INVALID_HANDLE);
-    if ($Err != VIX_OK)
-    {
-      sleep(15);
-    }
-    $Try++;
-  }
-  return $self->CheckError($Err);
+  my ($self, $HostPathName, $GuestPathName) = @_;
+  return TestAgent::SendFile($self->Hostname,  $HostPathName, $GuestPathName);
 }
 
-sub CopyFileFromGuestToHost
+sub CopyFileFromGuestToHost($$$)
 {
-  my $self = shift;
-  my ($GuestPathName, $HostPathName) = @_;
-
-  my ($ErrMessage, $VMHandle) = $self->GetVMHandle();
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  $ErrMessage = $self->LoginInGuest($VMHandle, !1);
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  my $Err = VMCopyFileFromGuestToHost($VMHandle, $GuestPathName, $HostPathName,
-                                      0, VIX_INVALID_HANDLE);
-  return $self->CheckError($Err);
+  my ($self, $GuestPathName, $HostPathName) = @_;
+  return TestAgent::GetFile($self->Hostname,  $GuestPathName, $HostPathName);
 }
 
-sub RunScriptInGuestTimeout
+sub RunScriptInGuestTimeout($$$)
 {
-  my $self = shift;
-  my ($Interpreter, $ScriptText, $Timeout) = @_;
-
-  my ($ErrMessage, $VMHandle) = $self->GetVMHandle();
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  $ErrMessage = $self->LoginInGuest($VMHandle, $self->Interactive);
-  if (defined($ErrMessage))
-  {
-    return $ErrMessage;
-  }
-
-  my $Job = VMware::Vix::API::VM::RunScriptInGuest($VMHandle, $Interpreter,
-                                                   $ScriptText, 0,
-                                                   VIX_INVALID_HANDLE, undef,
-                                                   0);
-  my ($Complete, $Err, $Time) = VMware::Vix::API::Job::WaitTimeout($Job,
-                                                                   $Timeout);
-
-  VMware::Vix::API::API::ReleaseHandle($Job);
-
-  if (! $Complete)
-  {
-    return "Exceeded timeout limit of $Timeout sec";
-  }
-
-  return $self->CheckError($Err);
+  my ($self, $ScriptText, $Timeout) = @_;
+  return TestAgent::RunScript($self->Hostname, $ScriptText, $Timeout);
 }
 
 sub CaptureScreenImage
@@ -565,6 +495,7 @@ BEGIN
     CreateBasicPropertyDescriptor("VmxHost", "Host where VM is located", !1, !1, "A", 64),
     CreateBasicPropertyDescriptor("VmxFilePath", "Path to .vmx file", !1, 1, "A", 64),
     CreateBasicPropertyDescriptor("IdleSnapshot", "Name of idle snapshot", !1, 1, "A", 32),
+    CreateBasicPropertyDescriptor("Hostname", "The VM hostname", !1, 1, "A", 64),
     CreateBasicPropertyDescriptor("Interactive", "Needs interactive flag", !1, 1, "B", 1),
     CreateBasicPropertyDescriptor("Description", "Description", !1, !1, "A", 40),
   );
diff --git a/testbot/src/testagentd/.gitignore b/testbot/src/testagentd/.gitignore
new file mode 100644
index 0000000..1fbbafe
--- /dev/null
+++ b/testbot/src/testagentd/.gitignore
@@ -0,0 +1,4 @@
+*.o
+*.obj
+TestAgentd.exe
+winetestbot.iso
diff --git a/testbot/src/testagentd/Makefile b/testbot/src/testagentd/Makefile
new file mode 100644
index 0000000..ddd4fa0
--- /dev/null
+++ b/testbot/src/testagentd/Makefile
@@ -0,0 +1,40 @@
+builddir = ../../bin/build
+
+CROSSCC32    = i686-w64-mingw32-gcc
+CROSSSTRIP32 = i686-w64-mingw32-strip
+
+all: build iso
+build: $(builddir)/testagentd
+windows: TestAgentd.exe
+
+
+$(builddir)/testagentd: testagentd.o platform_unix.o
+	gcc -o $@ $^
+	strip $@
+
+.c.o:
+	gcc -Wall -c -o $@ $<
+
+
+TestAgentd.exe: testagentd.obj platform_windows.obj
+	$(CROSSCC32) -o $@ $^ -lws2_32
+	$(CROSSSTRIP32) $@
+
+.SUFFIXES: .obj
+.c.obj:
+	$(CROSSCC32) -Wall -c -o $@ $<
+
+
+iso: windows
+	mkdir winetest
+	cp TestAgentd.exe winetest
+	mkisofs="mkisofs"; \
+	type mkisofs >/dev/null 2>&1 || mkisofs="genisoimage"; \
+	"$$mkisofs" -quiet -J -r -V "WineTestBot" -input-charset "ascii" -o "winetestbot.iso" winetest
+	rm -rf winetest
+
+
+clean:
+	rm *.obj *.o
+	rm TestAgentd.exe
+	rm winetestbot.iso
diff --git a/testbot/src/testagentd/platform.h b/testbot/src/testagentd/platform.h
new file mode 100644
index 0000000..cc30d7f
--- /dev/null
+++ b/testbot/src/testagentd/platform.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012 Francois Gouget
+ *
+ * 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
+ */
+#ifdef WIN32
+# include <ws2tcpip.h>
+# include <windows.h>
+
+# ifndef SHUT_RD
+#  define SHUT_RD SD_RECEIVE
+# endif
+#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))
+#endif
+
+#ifndef O_BINARY
+# define O_BINARY 0
+#endif
+
+
+int init_platform(void);
+char* get_script_path(void);
+
+/* Starts the specified command in the background and reports the status to
+ * the client.
+ */
+void start_child(SOCKET client, char* path);
+
+/* If a command was started in the background, waits until either that command
+ * terminates or the client disconnects (typically because it got tired of
+ * waiting).
+ * 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);
+
+/* Returns a string describing the last socket-related error */
+int sockeintr(void);
+const char* sockerror(void);
+
+/* Converts a socket address into a string stored in a static buffer. */
+char* sockaddr_to_string(struct sockaddr* sa, socklen_t len);
+
+int ta_getaddrinfo(const char *node, const char *service,
+                   struct addrinfo **addresses);
+
+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, ...);
+
+void* sockaddr_getaddr(struct sockaddr* sa, socklen_t* len);
diff --git a/testbot/src/testagentd/platform_unix.c b/testbot/src/testagentd/platform_unix.c
new file mode 100644
index 0000000..56699b9
--- /dev/null
+++ b/testbot/src/testagentd/platform_unix.c
@@ -0,0 +1,203 @@
+/*
+ * Provides Unix-specific implementations of some TestAgentd functions.
+ *
+ * Copyright 2012 Francois Gouget
+ *
+ * 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
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <signal.h>
+#include "platform.h"
+
+# define WINEBOTDIR  "/home/winehq/tools/testbot/var/staging"
+
+
+char* get_script_path(void)
+{
+    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;
+}
+
+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)
+{
+    pid_t pid;
+    int status;
+
+    pid = wait(&status);
+    debug("process %u returned %d\n", (unsigned)pid, status);
+    if (pid == child)
+    {
+        cleanup_child();
+        reaped_status = status;
+        reaped = pid;
+    }
+}
+
+void start_child(SOCKET client, char* path)
+{
+    pid_t pid;
+
+    chmod(path, 0700);
+    child_path = path;
+    child = 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));
+        exit(1);
+    }
+    if (pid < 0)
+    {
+        cleanup_child();
+        report_status(client, "error: could not fork: %s\n", strerror(errno));
+        return;
+    }
+    report_status(client, "ok: started process %d\n", pid);
+}
+
+void wait_for_child(SOCKET client)
+{
+    while (child)
+    {
+        fd_set rfds;
+        char buf;
+
+        /* 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.
+         */
+        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)
+        {
+            report_status(client, "error: connection closed\n");
+            return;
+        }
+    }
+    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");
+}
+
+int sockeintr(void)
+{
+    return errno == EINTR;
+}
+
+const char* sockerror(void)
+{
+    return strerror(errno);
+}
+
+char* sockaddr_to_string(struct sockaddr* sa, socklen_t len)
+{
+    /* Store the name in a buffer large enough for DNS hostnames */
+    static char name[256+6];
+    void* addr;
+    u_short port;
+
+    addr = sockaddr_getaddr(sa, NULL);
+    if (!addr || !inet_ntop(sa->sa_family, addr, name, sizeof(name)))
+    {
+        sprintf(name, "unknown host (family %d)", sa->sa_family);
+        return NULL;
+    }
+    switch (sa->sa_family)
+    {
+    case AF_INET:
+        port = htons(((struct sockaddr_in*)sa)->sin_port);
+        break;
+    case AF_INET6:
+        port = htons(((struct sockaddr_in6*)sa)->sin6_port);
+        break;
+    default:
+        port = 0;
+    }
+    if (port)
+    {
+        snprintf(name+strlen(name), sizeof(name)-strlen(name), ":%hu", port);
+        name[sizeof(name)-1] = '\0';
+    }
+    return name;
+}
+
+int ta_getaddrinfo(const char *node, const char *service,
+                   struct addrinfo **addresses)
+{
+    struct addrinfo hints;
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_flags = AI_PASSIVE;
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+    return getaddrinfo(node, service, &hints, addresses);
+}
+
+void ta_freeaddrinfo(struct addrinfo *addresses)
+{
+    return freeaddrinfo(addresses);
+}
+
+int init_platform(void)
+{
+    struct sigaction sa, osa;
+    sa.sa_handler = reaper;
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = SA_RESTART;
+    if (sigaction(SIGCHLD, &sa, &osa) < 0)
+    {
+        error("could not set up the SIGCHLD handler: %s\n", strerror(errno));
+        return 0;
+    }
+    return 1;
+}
diff --git a/testbot/src/testagentd/platform_windows.c b/testbot/src/testagentd/platform_windows.c
new file mode 100644
index 0000000..b17b8c0
--- /dev/null
+++ b/testbot/src/testagentd/platform_windows.c
@@ -0,0 +1,267 @@
+/*
+ * Provides Windows-specific implementations of some TestAgentd functions.
+ *
+ * Copyright 2012 Francois Gouget
+ *
+ * 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
+ */
+
+#include <stdio.h>
+#include "platform.h"
+
+char* get_script_path(void)
+{
+    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;
+}
+
+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;
+
+        CloseHandle(child);
+        child = NULL;
+        child_pid = 0;
+    }
+}
+
+void start_child(SOCKET client, char* path)
+{
+    STARTUPINFO si;
+    PROCESS_INFORMATION pi;
+
+    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))
+    {
+        report_status(client, "ok: started process %u\n", pi.dwProcessId);
+        child = pi.hProcess;
+        child_pid = pi.dwProcessId;
+        CloseHandle(pi.hThread);
+    }
+    else
+    {
+        report_status(client, "error: could not run '%s': %u\n", path, GetLastError());
+    }
+}
+
+void wait_for_child(SOCKET client)
+{
+    HANDLE handles[2];
+    DWORD r;
+
+    handles[0] = WSACreateEvent();
+    WSAEventSelect(client, handles[0], FD_CLOSE);
+    handles[1] = child;
+    while (child)
+    {
+        r = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
+        switch (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;
+        }
+    }
+    CloseHandle(handles[0]);
+    report_status(client, "error: no process to wait for\n");
+}
+
+int sockeintr(void)
+{
+    return WSAGetLastError() == WSAEINTR;
+}
+
+const char* sockerror(void)
+{
+    static char msg[1024];
+
+    msg[0] = '\0';
+    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS  | FORMAT_MESSAGE_MAX_WIDTH_MASK,
+                   NULL, WSAGetLastError(), LANG_USER_DEFAULT,
+                   msg, sizeof(msg), NULL);
+    msg[sizeof(msg)-1] = '\0';
+    return msg;
+}
+
+char* sockaddr_to_string(struct sockaddr* sa, socklen_t len)
+{
+    /* Store the name in a buffer large enough for DNS hostnames */
+    static char name[256+6];
+    DWORD size = sizeof(name);
+    /* This also appends the port number */
+    if (WSAAddressToString(sa,len, NULL, name, &size))
+        sprintf(name, "unknown host (family %d)", sa->sa_family);
+    return name;
+}
+
+int (WINAPI *pgetaddrinfo)(const char *node, const char *service,
+                           const struct addrinfo *hints,
+                           struct addrinfo **addresses);
+void (WINAPI *pfreeaddrinfo)(struct addrinfo *addresses);
+
+int ta_getaddrinfo(const char *node, const char *service,
+                   struct addrinfo **addresses)
+{
+    struct servent* sent;
+    u_short port;
+    char dummy;
+    struct hostent* hent;
+    char** addr;
+    struct addrinfo *ai;
+    struct sockaddr_in *sin4;
+    struct sockaddr_in6 *sin6;
+
+    if (pgetaddrinfo)
+    {
+        struct addrinfo hints;
+        memset(&hints, 0, sizeof(hints));
+        hints.ai_flags = AI_PASSIVE;
+        hints.ai_family = AF_UNSPEC;
+        hints.ai_socktype = SOCK_STREAM;
+        return pgetaddrinfo(node, service, &hints, addresses);
+    }
+
+    sent = getservbyname(service, "tcp");
+    if (sent)
+        port = sent->s_port;
+    else if (!service)
+        port = 0;
+    else if (sscanf(service, "%hu%c", &port, &dummy) == 1)
+        port = htons(port);
+    else
+        return EAI_SERVICE;
+
+    *addresses = NULL;
+    hent = gethostbyname(node);
+    if (!hent)
+        return EAI_NONAME;
+    for (addr = hent->h_addr_list; *addr; addr++)
+    {
+        ai = malloc(sizeof(*ai));
+        switch (hent->h_addrtype)
+        {
+        case AF_INET:
+            ai->ai_addrlen = sizeof(*sin4);
+            ai->ai_addr = malloc(ai->ai_addrlen);
+            sin4 = (struct sockaddr_in*)ai->ai_addr;
+            sin4->sin_family = hent->h_addrtype;
+            sin4->sin_port = port;
+            memcpy(&sin4->sin_addr, *addr, hent->h_length);
+            break;
+        case AF_INET6:
+            ai->ai_addrlen = sizeof(*sin6);
+            ai->ai_addr = malloc(ai->ai_addrlen);
+            sin6 = (struct sockaddr_in6*)ai->ai_addr;
+            sin6->sin6_family = hent->h_addrtype;
+            sin4->sin_port = port;
+            sin6->sin6_flowinfo = 0;
+            memcpy(&sin6->sin6_addr, *addr, hent->h_length);
+            break;
+        default:
+            debug("ignoring unknown address type %u\n", hent->h_addrtype);
+            free(ai);
+            continue;
+        }
+        ai->ai_flags = 0;
+        ai->ai_family = hent->h_addrtype;
+        ai->ai_socktype = SOCK_STREAM;
+        ai->ai_protocol = IPPROTO_TCP;
+        ai->ai_canonname = NULL; /* We don't use it anyway */
+        ai->ai_next = *addresses;
+        *addresses = ai;
+    }
+    if (!node)
+    {
+        /* Add INADDR_ANY last so it is tried first */
+        ai = malloc(sizeof(*ai));
+        ai->ai_addrlen = sizeof(*sin4);
+        ai->ai_addr = malloc(ai->ai_addrlen);
+        sin4 = (struct sockaddr_in*)ai->ai_addr;
+        sin4->sin_family = ai->ai_family = AF_INET;
+        sin4->sin_port = port;
+        sin4->sin_addr.S_un.S_addr = INADDR_ANY;
+        ai->ai_flags = 0;
+        ai->ai_socktype = SOCK_STREAM;
+        ai->ai_protocol = IPPROTO_TCP;
+        ai->ai_canonname = NULL; /* We don't use it anyway */
+        ai->ai_next = *addresses;
+        *addresses = ai;
+    }
+    return 0;
+}
+
+void ta_freeaddrinfo(struct addrinfo *addresses)
+{
+    if (pfreeaddrinfo)
+        pfreeaddrinfo(addresses);
+    else
+    {
+        while (addresses)
+        {
+            free(addresses->ai_addr);
+            addresses = addresses->ai_next;
+        }
+    }
+}
+
+int init_platform(void)
+{
+    HMODULE hdll;
+    WORD wVersionRequested;
+    WSADATA wsaData;
+    int rc;
+
+    wVersionRequested = MAKEWORD(2, 2);
+    rc = WSAStartup(wVersionRequested, &wsaData);
+    if (rc)
+    {
+        error("unable to initialize winsock (%d)\n", rc);
+        return 0;
+    }
+
+    hdll = GetModuleHandle("ws2_32");
+    pgetaddrinfo = (void*)GetProcAddress(hdll, "getaddrinfo");
+    pfreeaddrinfo = (void*)GetProcAddress(hdll, "freeaddrinfo");
+    return 1;
+}
diff --git a/testbot/src/testagentd/testagentd.c b/testbot/src/testagentd/testagentd.c
new file mode 100644
index 0000000..4d5a13e
--- /dev/null
+++ b/testbot/src/testagentd/testagentd.c
@@ -0,0 +1,626 @@
+/*
+ * Provides a simple way to send/receive files and to run scripts.
+ *
+ * Copyright 2012 Francois Gouget
+ *
+ * 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
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "platform.h"
+
+#define CHUNK_SIZE 4096
+
+const char *name0;
+int opt_debug = 0;
+
+
+void error(const char* format, ...)
+{
+    va_list valist;
+    fprintf(stderr, "%s:error: ", name0);
+    va_start(valist, format);
+    vfprintf(stderr, format, valist);
+    va_end(valist);
+}
+
+void debug(const char* format, ...)
+{
+    if (opt_debug)
+    {
+        va_list valist;
+        va_start(valist, format);
+        vfprintf(stderr, format, valist);
+        va_end(valist);
+    }
+}
+
+static char buffer[CHUNK_SIZE];
+static int buf_pos, buf_size;
+
+static void reset_buffer(void)
+{
+    buf_pos = buf_size = 0;
+}
+
+static int fill_buffer(SOCKET sock)
+{
+    int n;
+
+    if (buf_pos == buf_size)
+    {
+        /* Everything has been read, empty the buffer */
+        buf_pos = buf_size = 0;
+    }
+    if (buf_size == sizeof(buffer))
+    {
+        /* The buffer is full */
+        return 0;
+    }
+
+    n = recv(sock, buffer + buf_size, sizeof(buffer) - buf_size, 0);
+    if (n > 0)
+        buf_size += n;
+    return n;
+}
+
+static char* _get_string(void)
+{
+    char *str, *e;
+    char *eod = buffer + buf_size;
+    str = e = buffer + buf_pos;
+    while (e < eod)
+    {
+        if (*e == '\n')
+        {
+            *e = '\0';
+            buf_pos = e + 1 - buffer;
+            return str;
+        }
+        e++;
+    }
+    return NULL;
+}
+
+/* Only strings smaller than the buffer size are supported */
+static char* get_string(SOCKET sock)
+{
+    char* str;
+
+    if (buf_pos == buf_size)
+        fill_buffer(sock);
+    str = _get_string();
+    if (str || buf_pos == 0)
+        return str;
+
+    /* 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);
+
+    return _get_string();
+}
+
+static char* get_data(SOCKET sock, int *len)
+{
+    char* data;
+    if (buf_pos == buf_size)
+    {
+        *len = fill_buffer(sock);
+        if (*len <= 0)
+            return NULL;
+    }
+    else
+        *len = buf_size - buf_pos;
+    data = buffer + buf_pos;
+    buf_pos = buf_size;
+    return data;
+}
+
+static const char* stream_from_net(SOCKET src, int dst)
+{
+    char buffer[CHUNK_SIZE];
+    while (1)
+    {
+        int r, w;
+        r = recv(src, buffer, sizeof(buffer), 0);
+        if (r == 0) /* EOF */
+            return NULL;
+        if (r < 0)
+            return sockerror();
+        w = write(dst, buffer, r);
+        if (w != r)
+        {
+            error("could only write %d bytes out of %d: %s\n", w, r, strerror(errno));
+            return strerror(errno);
+        }
+    }
+    return NULL;
+}
+
+/* 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)
+{
+    char buffer[CHUNK_SIZE];
+    while (1)
+    {
+        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();
+        }
+    }
+    return NULL;
+}
+
+static char* status = NULL;
+static unsigned status_size = 0;
+static void vset_status(const char* format, va_list valist)
+{
+    int len;
+    va_list args;
+    len = 1;
+    do
+    {
+        if (len >= status_size)
+        {
+            status_size = (len +0xf) & ~0xf;
+            status = realloc(status, status_size);
+        }
+        va_copy(args, valist);
+        len = vsnprintf(status, status_size, format, args);
+        va_end(args);
+        if (len < 0)
+            len = status_size * 1.1;
+    }
+    while (len >= status_size);
+    if (opt_debug || strncmp(status, "ok:", 3) != 0)
+        fprintf(stderr, "%s", status);
+}
+
+static void set_status(const char* format, ...)
+{
+    va_list valist;
+    va_start(valist, format);
+    vset_status(format, valist);
+    va_end(valist);
+}
+
+void report_status(SOCKET client, const char* format, ...)
+{
+    va_list valist;
+    shutdown(client, SHUT_RD);
+    va_start(valist, format);
+    vset_status(format, valist);
+    va_end(valist);
+    send(client, status, strlen(status), 0);
+}
+
+static void process_command(SOCKET client)
+{
+    char *command;
+    const char* err;
+
+    reset_buffer();
+    command = get_string(client);
+    debug("Processing command %s\n", command ? command : "(null)");
+    if (!command)
+    {
+        report_status(client, "error: could not read the command\n");
+    }
+    else if (strcmp(command, "read") == 0)
+    {
+        /* Read the specified file */
+        char* filename;
+        int fd;
+        struct stat st;
+        char str[80];
+
+        filename = get_string(client);
+        if (!filename)
+        {
+            report_status(client, "error: missing filename parameter for read\n");
+            return;
+        }
+        debug("read '%s'\n", filename);
+        fd = open(filename, O_RDONLY | O_BINARY);
+        if (fd < 0)
+        {
+            report_status(client, "error: unable to open '%s' for reading: %s\n", filename, strerror(errno));
+            return;
+        }
+        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)
+        {
+            /* 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("ok: read done\n");
+    }
+    else if (strcmp(command, "write") == 0)
+    {
+        /* Write to the specified file */
+        char* filename;
+        int fd;
+        char* data;
+        int len;
+
+        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);
+        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");
+    }
+    else if (strcmp(command, "runscript") == 0)
+    {
+        /* Run the specified script */
+        int fd, len;
+        char *data, *script;
+
+        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)
+        {
+            report_status(client, "error: an error occurred while writing to '%s': %s\n", script, strerror(errno));
+            close(fd);
+            unlink(script);
+            free(script);
+            return;
+        }
+        err = stream_from_net(client, fd);
+        close(fd);
+        if (err)
+        {
+            report_status(client, "error: an error occurred while saving the script to '%s': %s\n", script, err);
+            unlink(script);
+            free(script);
+            return;
+        }
+        start_child(client, script);
+    }
+    else if (strcmp(command, "waitchild") == 0)
+    {
+        /* Wait for the last process we started */
+        wait_for_child(client);
+    }
+    else if (strcmp(command, "status") == 0)
+    {
+        /* Return the status of the previous command */
+        shutdown(client, SHUT_RD);
+        send(client, status, strlen(status), 0);
+    }
+    else
+    {
+        report_status(client, "error: unknown command: %s\n", command);
+    }
+}
+
+void* sockaddr_getaddr(struct sockaddr* sa, socklen_t* len)
+{
+    switch (sa->sa_family)
+    {
+    case AF_INET:
+        if (len)
+            *len = sizeof(struct in_addr);
+        return &((struct sockaddr_in*)sa)->sin_addr;
+    case AF_INET6:
+        if (len)
+            *len = sizeof(struct in6_addr);
+        return &((struct sockaddr_in6*)sa)->sin6_addr;
+    }
+    if (len)
+        *len = 0;
+    return NULL;
+}
+
+static int sockaddr_equal(struct sockaddr* sa1, struct sockaddr* sa2)
+{
+    void *addr1, *addr2;
+    socklen_t len;
+
+    if (sa1->sa_family != sa2->sa_family)
+        return 0;
+
+    addr1 = sockaddr_getaddr(sa1, &len);
+    addr2 = sockaddr_getaddr(sa2, &len);
+    if (!addr1 || !addr2)
+        return 0;
+    return memcmp(addr1, addr2, len) == 0;
+}
+
+static int is_host_allowed(SOCKET client, const char* srchost, int addrlen)
+{
+    struct addrinfo *addresses, *addrp;
+    struct sockaddr *peeraddr;
+    socklen_t peerlen;
+    int rc;
+
+    if (!srchost)
+        return 1;
+
+    peerlen = addrlen;
+    peeraddr = malloc(peerlen);
+    if (getpeername(client, peeraddr, &peerlen))
+    {
+        error("unable to get the peer address: %s\n", sockerror());
+        free(peeraddr);
+        return 0;
+    }
+    debug("Received connection from %s\n", sockaddr_to_string(peeraddr, peerlen));
+    rc = ta_getaddrinfo(srchost, NULL, &addresses);
+    if (rc)
+    {
+        error("unable to resolve '%s': %s\n", srchost, gai_strerror(rc));
+        free(peeraddr);
+        return 0;
+    }
+
+    addrp = addresses;
+    do
+    {
+        debug("  checking against %s\n", sockaddr_to_string(addrp->ai_addr, addrp->ai_addrlen));
+        if (sockaddr_equal(peeraddr, addrp->ai_addr))
+        {
+            free(peeraddr);
+            ta_freeaddrinfo(addresses);
+            return 1;
+        }
+    }
+    while ((addrp = addrp->ai_next) != NULL);
+    ta_freeaddrinfo(addresses);
+
+    debug("  -> rejecting connection\n");
+    free(peeraddr);
+    return 0;
+}
+
+int main(int argc, char** argv)
+{
+    const char* p;
+    char** arg;
+    char* opt_port = NULL;
+    char* opt_srchost = NULL;
+    struct addrinfo *addresses, *addrp;
+    int rc, addrlen;
+    int opt_usage = 0;
+    SOCKET master;
+    int on = 1;
+
+    name0 = p = argv[0];
+    while (*p != '\0')
+    {
+        if (*p == '/' || *p == '\\')
+            name0 = p + 1;
+        p++;
+    }
+
+    arg = argv + 1;
+    while (*arg)
+    {
+        if (strcmp(*arg, "--debug") == 0)
+        {
+            opt_debug = 1;
+        }
+        else if (strcmp(*arg, "--help") == 0)
+        {
+            opt_usage = 1;
+        }
+        else if (**arg == '-')
+        {
+            error("unknown option '%s'\n", *arg);
+            opt_usage = 2;
+            break;
+        }
+        else if (opt_port == NULL)
+        {
+            opt_port = *arg;
+        }
+        else if (opt_srchost == NULL)
+        {
+            opt_srchost = *arg;
+        }
+        else
+        {
+            error("unexpected option '%s'\n", *arg);
+            opt_usage = 2;
+            break;
+        }
+        arg++;
+    }
+    if (!opt_usage)
+    {
+        if (!opt_port)
+        {
+            error("you must specify the port to listen on\n");
+            opt_usage = 2;
+        }
+
+        if (!init_platform())
+        {
+            if (!opt_usage)
+                exit(1);
+            /* else opt_usage will force us to exit early anyway */
+        }
+        else if (opt_srchost)
+        {
+            /* Verify that the specified source host is valid */
+            rc = ta_getaddrinfo(opt_srchost, NULL, &addresses);
+            if (rc)
+            {
+                error("unable to resolve '%s': %s\n", opt_srchost, gai_strerror(rc));
+                opt_usage = 2;
+            }
+            else
+            {
+                if (opt_debug)
+                {
+                    addrp = addresses;
+                    do
+                    {
+                        debug("Accepting connections from %s\n", sockaddr_to_string(addrp->ai_addr, addrp->ai_addrlen));
+                    }
+                    while ((addrp = addrp->ai_next) != NULL);
+                }
+                ta_freeaddrinfo(addresses);
+            }
+        }
+    }
+    if (opt_usage == 2)
+    {
+        error("try '%s --help' for more information\n", name0);
+        exit(2);
+    }
+    if (opt_usage)
+    {
+        printf("Usage: %s [--debug] [--help] PORT [SRCHOST]\n", name0);
+        printf("\n");
+        printf("Provides a simple way to send/receive files and to run scripts on this host.\n");
+        printf("\n");
+        printf("Where:\n");
+        printf("  PORT     The port to listen on for connections.\n");
+        printf("  SRCHOST  If specified, only connections from this host will be accepted.\n");
+        printf("  --debug  Prints detailed information about what happens.\n");
+        printf("  --help   Shows this usage message.\n");
+        exit(0);
+    }
+
+    /* Bind to the host in a protocol neutral way */
+    rc = ta_getaddrinfo(NULL, opt_port, &addresses);
+    if (rc)
+    {
+        error("unable to get the host address for port %s: %s\n", opt_port, gai_strerror(rc));
+        exit(1);
+    }
+    for (addrp = addresses; addrp; addrp = addrp->ai_next)
+    {
+        if (addrp->ai_family != PF_INET)
+            continue;
+        master = socket(addrp->ai_family, addrp->ai_socktype, addrp->ai_protocol);
+        if (master < 0)
+            continue;
+        setsockopt(master, SOL_SOCKET, SO_REUSEADDR, (void*)&on, sizeof(on));
+
+        debug("Trying to bind to %s\n", sockaddr_to_string(addrp->ai_addr, addrp->ai_addrlen));
+        if (bind(master, addrp->ai_addr, addrp->ai_addrlen) == 0)
+            break;
+        closesocket(master);
+    };
+    addrlen = addrp->ai_addrlen;
+    ta_freeaddrinfo(addresses);
+    if (addrp == NULL)
+    {
+        error("unable to bind the server socket: %s\n", sockerror());
+        exit(1);
+    }
+
+    if (listen(master, 1) < 0)
+    {
+        error("listen() failed: %s\n", sockerror());
+        exit(1);
+    }
+    set_status("ok: ready\n");
+
+    while (1)
+    {
+        SOCKET client;
+        debug("Waiting in accept()\n");
+        client = accept(master, NULL, NULL);
+        if (client >= 0)
+        {
+            if (is_host_allowed(client, opt_srchost, addrlen))
+                process_command(client);
+            closesocket(client);
+        }
+        else if (!sockeintr())
+        {
+            error("accept() failed: %s\n", sockerror());
+            exit(1);
+        }
+    }
+
+    cleanup_child();
+    return 0;
+}
-- 
1.7.10.4




More information about the wine-patches mailing list