Imported Upstream version 14.45.0
[platform/upstream/libzypp.git] / zypp / PluginScript.cc
index bbbc4f7..4024bc7 100644 (file)
@@ -9,11 +9,17 @@
 /** \file      zypp/PluginScript.cc
  *
 */
+#include <sys/types.h>
+#include <signal.h>
+
 #include <iostream>
 #include <sstream>
 
 #include "zypp/base/LogTools.h"
+#include "zypp/base/DefaultIntegral.h"
 #include "zypp/base/String.h"
+#include "zypp/base/Signal.h"
+#include "zypp/base/IOStream.h"
 
 #include "zypp/PluginScript.h"
 #include "zypp/ExternalProgram.h"
 
 using std::endl;
 
+#undef  ZYPP_BASE_LOGGER_LOGGROUP
+#define ZYPP_BASE_LOGGER_LOGGROUP "zypp::plugin"
+
 ///////////////////////////////////////////////////////////////////
 namespace zypp
 { /////////////////////////////////////////////////////////////////
 
+  namespace
+  {
+    const char * PLUGIN_DEBUG = getenv( "ZYPP_PLUGIN_DEBUG" );
+
+    /** Dump buffer string if PLUGIN_DEBUG is on.
+     * \ingroup g_RAII
+     */
+    struct PluginDebugBuffer
+    {
+      PluginDebugBuffer( const std::string & buffer_r ) : _buffer( buffer_r ) {}
+      ~PluginDebugBuffer()
+      {
+       if ( PLUGIN_DEBUG )
+       {
+         if ( _buffer.empty() )
+         {
+           _DBG("PLUGIN") << "< (empty)" << endl;
+         }
+         else
+         {
+           std::istringstream datas( _buffer );
+           iostr::copyIndent( datas, _DBG("PLUGIN"), "< "  ) << endl;
+         }
+       }
+      }
+      const std::string & _buffer;
+    };
+
+    /** Dump programms stderr.
+     * \ingroup g_RAII
+     */
+    struct PluginDumpStderr
+    {
+      PluginDumpStderr( ExternalProgramWithStderr & prog_r ) : _prog( prog_r ) {}
+      ~PluginDumpStderr()
+      {
+       std::string line;
+       while ( _prog.stderrGetline( line ) )
+         _WAR("PLUGIN") << "! " << line << endl;
+      }
+      ExternalProgramWithStderr & _prog;
+    };
+
+    inline void setBlocking( FILE * file_r, bool yesno_r = true )
+    {
+      if ( ! file_r )
+       ZYPP_THROW( PluginScriptException( "setNonBlocking" ) );
+
+      int fd = ::fileno( file_r );
+      if ( fd == -1 )
+       ZYPP_THROW( PluginScriptException( "setNonBlocking" ) );
+
+      int flags = ::fcntl( fd, F_GETFL );
+      if ( flags == -1 )
+       ZYPP_THROW( PluginScriptException( "setNonBlocking" ) );
+
+      if ( ! yesno_r )
+       flags |= O_NONBLOCK;
+      else if ( flags & O_NONBLOCK )
+       flags ^= O_NONBLOCK;
+
+      flags = ::fcntl( fd, F_SETFL, flags );
+      if ( flags == -1 )
+       ZYPP_THROW( PluginScriptException( "setNonBlocking" ) );
+    }
+
+    inline void setNonBlocking( FILE * file_r, bool yesno_r = true )
+    { setBlocking( file_r, !yesno_r ); }
+  }
+
   ///////////////////////////////////////////////////////////////////
   //
   //   CLASS NAME : PluginScript::Impl
@@ -34,7 +113,9 @@ namespace zypp
   {
     public:
       Impl( const Pathname & script_r = Pathname(), const Arguments & args_r = Arguments() )
-        : _script( script_r )
+        : _sendTimeout( _defaultSendTimeout )
+        , _receiveTimeout( _defaultReceiveTimeout )
+        , _script( script_r )
         , _args( args_r )
       {}
 
@@ -42,6 +123,13 @@ namespace zypp
       { try { close(); } catch(...) {} }
 
     public:
+      static long _defaultSendTimeout;
+      static long _defaultReceiveTimeout;
+
+      long _sendTimeout;
+      long _receiveTimeout;
+
+   public:
       const Pathname & script() const
       { return _script; }
 
@@ -52,12 +140,18 @@ namespace zypp
       { return _cmd ? _cmd->getpid() : NotConnected; }
 
       bool isOpen() const
-      { return _cmd; }
+      { return _cmd != nullptr; }
+
+      int lastReturn() const
+      { return _lastReturn; }
+
+      const std::string & lastExecError() const
+      { return _lastExecError; }
 
     public:
       void open( const Pathname & script_r = Pathname(), const Arguments & args_r = Arguments() );
 
-      void close();
+      int close();
 
       void send( const PluginFrame & frame_r ) const;
 
@@ -66,17 +160,32 @@ namespace zypp
     private:
       Pathname _script;
       Arguments _args;
-      scoped_ptr<ExternalProgram> _cmd;
+      scoped_ptr<ExternalProgramWithStderr> _cmd;
+      DefaultIntegral<int,0> _lastReturn;
+      std::string _lastExecError;
   };
   ///////////////////////////////////////////////////////////////////
 
   /** \relates PluginScrip::Impl Stream output */
   inline std::ostream & operator<<( std::ostream & str, const PluginScript::Impl & obj )
   {
-    return dumpRangeLine( str << "PluginScript[" << obj.getPid() << "] " << obj.script(),
-                         obj.args().begin(), obj.args().end() );
+    return str << "PluginScript[" << obj.getPid() << "] " << obj.script();
+  }
+
+  ///////////////////////////////////////////////////////////////////
+
+  namespace
+  {
+    const long PLUGIN_TIMEOUT =        str::strtonum<long>( getenv( "ZYPP_PLUGIN_TIMEOUT" ) );
+    const long PLUGIN_SEND_TIMEOUT =   str::strtonum<long>( getenv( "ZYPP_PLUGIN_SEND_TIMEOUT" ) );
+    const long PLUGIN_RECEIVE_TIMEOUT =        str::strtonum<long>( getenv( "ZYPP_PLUGIN_RECEIVE_TIMEOUT" ) );
   }
 
+  long PluginScript::Impl::_defaultSendTimeout =    ( PLUGIN_SEND_TIMEOUT > 0    ? PLUGIN_SEND_TIMEOUT
+                                                                                : ( PLUGIN_TIMEOUT > 0 ? PLUGIN_TIMEOUT : 30 ) );
+  long PluginScript::Impl::_defaultReceiveTimeout = ( PLUGIN_RECEIVE_TIMEOUT > 0 ? PLUGIN_RECEIVE_TIMEOUT
+                                                                                : ( PLUGIN_TIMEOUT > 0 ? PLUGIN_TIMEOUT : 30 ) );
+
   ///////////////////////////////////////////////////////////////////
 
   void PluginScript::Impl::open( const Pathname & script_r, const Arguments & args_r )
@@ -96,64 +205,225 @@ namespace zypp
     // TODO: ExternalProgram::maybe use Stderr_To_FileDesc for script loging
     Arguments args;
     args.reserve( args_r.size()+1 );
-    //args.push_back( "<"+script_r.asString() );
     args.push_back( script_r.asString() );
     args.insert( args.end(), args_r.begin(), args_r.end() );
-    _cmd.reset( new ExternalProgram( args, ExternalProgram::Discard_Stderr ) );
+    _cmd.reset( new ExternalProgramWithStderr( args ) );
+
+    // Be protected against full pipe, etc.
+    setNonBlocking( _cmd->outputFile() );
+    setNonBlocking( _cmd->inputFile() );
 
     // store running scripts data
     _script = script_r;
     _args = args_r;
+    _lastReturn.reset();
+    _lastExecError.clear();
 
-    DBG << *this << endl;
+    dumpRangeLine( DBG << *this, _args.begin(), _args.end() ) << endl;
   }
 
-  void PluginScript::Impl::close()
+  int PluginScript::Impl::close()
   {
     if ( _cmd )
     {
       DBG << "Close:" << *this << endl;
-      _cmd->kill();
+      bool doKill = true;
+      try {
+       // do not kill script if _DISCONNECT is ACKed.
+       send( PluginFrame( "_DISCONNECT" ) );
+       PluginFrame ret( receive() );
+       if ( ret.isAckCommand() )
+       {
+         doKill = false;
+         str::strtonum( ret.getHeaderNT( "exit" ), _lastReturn.get() );
+         _lastExecError = ret.body();
+       }
+      }
+      catch (...)
+      { /* NOP */ }
+
+      if ( doKill )
+      {
+       _cmd->kill();
+       _lastReturn = _cmd->close();
+       _lastExecError = _cmd->execError();
+      }
+      DBG << *this << " -> [" << _lastReturn << "] " << _lastExecError << endl;
       _cmd.reset();
     }
-    DBG << *this << endl;
+    return _lastReturn;
   }
 
   void PluginScript::Impl::send( const PluginFrame & frame_r ) const
   {
     if ( !_cmd )
-      ZYPP_THROW( PluginScriptException( "Not connected", str::Str() << *this ) );
+      ZYPP_THROW( PluginScriptNotConnected( "Not connected", str::Str() << *this ) );
 
     if ( frame_r.command().empty() )
-      ZYPP_THROW( PluginScriptException( "Send: No command in frame", str::Str() << frame_r ) );
+      WAR << "Send: No command in frame" << frame_r << endl;
 
-    // TODO: dumb writer does not care about error or deadlocks
+    // prepare frame data to write
     std::string data;
     {
       std::ostringstream datas;
       frame_r.writeTo( datas );
       datas.str().swap( data );
     }
-    DBG << frame_r << endl;
-    DBG << " ->write " << data.size() << endl;
+    DBG << *this << " ->send " << frame_r << endl;
+
+    if ( PLUGIN_DEBUG )
+    {
+      std::istringstream datas( data );
+      iostr::copyIndent( datas, _DBG("PLUGIN") ) << endl;
+    }
+
+    // try writing the pipe....
+    FILE * filep = _cmd->outputFile();
+    if ( ! filep )
+      ZYPP_THROW( PluginScriptException( "Bad file pointer." ) );
 
-    FILE * outputfile = _cmd->outputFile();
-    if ( ::fwrite( data.c_str(), data.size(), 1, outputfile ) != 1 )
-      ZYPP_THROW( PluginScriptException( "Send: send error" ) );
-    ::fflush( outputfile );
+    int fd = ::fileno( filep );
+    if ( fd == -1 )
+      ZYPP_THROW( PluginScriptException( "Bad file descriptor" ) );
+
+    //DBG << " ->[" << fd << " " << (::feof(filep)?'e':'_') << (::ferror(filep)?'F':'_') << "]" << endl;
+    {
+      PluginDumpStderr _dump( *_cmd ); // dump scripts stderr before leaving
+      SignalSaver sigsav( SIGPIPE, SIG_IGN );
+      const char * buffer = data.c_str();
+      ssize_t buffsize = data.size();
+      do {
+       fd_set wfds;
+       FD_ZERO( &wfds );
+       FD_SET( fd, &wfds );
+
+       struct timeval tv;
+       tv.tv_sec = _sendTimeout;
+       tv.tv_usec = 0;
+
+       int retval = select( fd+1, NULL, &wfds, NULL, &tv );
+       if ( retval > 0 )       // FD_ISSET( fd, &wfds ) will be true.
+       {
+         //DBG << "Ready to write..." << endl;
+         ssize_t ret = ::write( fd, buffer, buffsize );
+         if ( ret == buffsize )
+         {
+           //DBG << "::write(" << buffsize << ") -> " << ret << endl;
+           ::fflush( filep );
+           break;              // -> done
+         }
+         else if ( ret > 0 )
+         {
+           //WAR << "::write(" << buffsize << ") -> " << ret << " INCOMPLETE..." << endl;
+           ::fflush( filep );
+           buffsize -= ret;
+           buffer += ret;      // -> continue
+         }
+         else // ( retval == -1 )
+         {
+           if ( errno != EINTR )
+           {
+             ERR << "write(): " << Errno() << endl;
+             if ( errno == EPIPE )
+               ZYPP_THROW( PluginScriptDiedUnexpectedly( "Send: script died unexpectedly", str::Str() << Errno() ) );
+             else
+               ZYPP_THROW( PluginScriptException( "Send: send error", str::Str() << Errno() ) );
+           }
+         }
+       }
+       else if ( retval == 0 )
+       {
+         WAR << "Not ready to write within timeout." << endl;
+         ZYPP_THROW( PluginScriptSendTimeout( "Not ready to write within timeout." ) );
+       }
+       else // ( retval == -1 )
+       {
+         if ( errno != EINTR )
+         {
+           ERR << "select(): " << Errno() << endl;
+           ZYPP_THROW( PluginScriptException( "Error waiting on file descriptor", str::Str() << Errno() ) );
+         }
+       }
+      } while( true );
+    }
   }
 
   PluginFrame PluginScript::Impl::receive() const
   {
     if ( !_cmd )
-      ZYPP_THROW( PluginScriptException( "Not connected", str::Str() << *this ) );
+      ZYPP_THROW( PluginScriptNotConnected( "Not connected", str::Str() << *this ) );
 
-    // TODO: dumb writer does not care about error or deadlocks
-    std::string data( _cmd->receiveUpto( '\0' ) );
-    DBG << " <-read " << data.size() << endl;
+    // try reading the pipe....
+    FILE * filep = _cmd->inputFile();
+    if ( ! filep )
+      ZYPP_THROW( PluginScriptException( "Bad file pointer." ) );
+
+    int fd = ::fileno( filep );
+    if ( fd == -1 )
+      ZYPP_THROW( PluginScriptException( "Bad file descriptor" ) );
+
+    ::clearerr( filep );
+    std::string data;
+    {
+      PluginDebugBuffer _debug( data ); // dump receive buffer if PLUGIN_DEBUG
+      PluginDumpStderr _dump( *_cmd ); // dump scripts stderr before leaving
+      do {
+       int ch = fgetc( filep );
+       if ( ch != EOF )
+       {
+         data.push_back( ch );
+         if ( ch == '\0' )
+           break;
+       }
+       else if ( ::feof( filep ) )
+       {
+         WAR << "Unexpected EOF" << endl;
+         ZYPP_THROW( PluginScriptDiedUnexpectedly( "Receive: script died unexpectedly", str::Str() << Errno() ) );
+       }
+       else if ( errno != EINTR )
+       {
+         if ( errno == EWOULDBLOCK )
+         {
+           // wait a while for fd to become ready for reading...
+           fd_set rfds;
+           FD_ZERO( &rfds );
+           FD_SET( fd, &rfds );
+
+           struct timeval tv;
+           tv.tv_sec = _receiveTimeout;
+           tv.tv_usec = 0;
+
+           int retval = select( fd+1, &rfds, NULL, NULL, &tv );
+           if ( retval > 0 )   // FD_ISSET( fd, &rfds ) will be true.
+           {
+             ::clearerr( filep );
+           }
+           else if ( retval == 0 )
+           {
+             WAR << "Not ready to read within timeout." << endl;
+             ZYPP_THROW( PluginScriptReceiveTimeout( "Not ready to read within timeout." ) );
+           }
+           else // ( retval == -1 )
+           {
+             if ( errno != EINTR )
+             {
+               ERR << "select(): " << Errno() << endl;
+               ZYPP_THROW( PluginScriptException( "Error waiting on file descriptor", str::Str() << Errno() ) );
+             }
+           }
+         }
+         else
+         {
+           ERR << "read(): " << Errno() << endl;
+           ZYPP_THROW( PluginScriptException( "Receive: receive error", str::Str() << Errno() ) );
+         }
+       }
+      } while ( true );
+    }
+    // DBG << " <-read " << data.size() << endl;
     std::istringstream datas( data );
     PluginFrame ret( datas );
-    DBG << ret << endl;
+    DBG << *this << " <-" << ret << endl;
     return ret;
   }
 
@@ -165,6 +435,30 @@ namespace zypp
 
   const pid_t PluginScript::NotConnected( -1 );
 
+  long PluginScript::defaultSendTimeout()
+  { return Impl::_defaultSendTimeout; }
+
+  long PluginScript::defaultReceiveTimeout()
+  { return Impl::_defaultReceiveTimeout; }
+
+  void PluginScript::defaultSendTimeout( long newval_r )
+  { Impl::_defaultSendTimeout = newval_r > 0 ? newval_r : 0; }
+
+  void PluginScript::defaultReceiveTimeout( long newval_r )
+  { Impl::_defaultReceiveTimeout = newval_r > 0 ? newval_r : 0; }
+
+  long PluginScript::sendTimeout() const
+  { return _pimpl->_sendTimeout; }
+
+  long PluginScript::receiveTimeout() const
+  { return _pimpl->_receiveTimeout; }
+
+  void PluginScript::sendTimeout( long newval_r )
+  { _pimpl->_sendTimeout = newval_r > 0 ? newval_r : 0; }
+
+  void PluginScript::receiveTimeout( long newval_r )
+  { _pimpl->_receiveTimeout = newval_r > 0 ? newval_r : 0; }
+
   PluginScript::PluginScript()
     : _pimpl( new Impl )
   {}
@@ -189,6 +483,12 @@ namespace zypp
   pid_t PluginScript::getPid() const
   { return _pimpl->getPid(); }
 
+  int PluginScript::lastReturn() const
+  { return _pimpl->lastReturn(); }
+
+  const std::string & PluginScript::lastExecError() const
+  { return _pimpl->lastExecError(); }
+
   void PluginScript::open()
   { _pimpl->open( _pimpl->script(), _pimpl->args() ); }
 
@@ -198,8 +498,8 @@ namespace zypp
   void PluginScript::open( const Pathname & script_r, const Arguments & args_r )
   { _pimpl->open( script_r, args_r ); }
 
-  void PluginScript::close()
-  { _pimpl->close(); }
+  int PluginScript::close()
+  { return _pimpl->close(); }
 
   void PluginScript::send( const PluginFrame & frame_r ) const
   { _pimpl->send( frame_r ); }