LCOV - code coverage report
Current view: top level - tools - Subprocess.cpp (source / functions) Hit Total Coverage
Test: plumed test coverage Lines: 79 116 68.1 %
Date: 2026-11-05 10:19:04 Functions: 15 17 88.2 %

          Line data    Source code
       1             : /* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
       2             :    Copyright (c) 2018-2023 The plumed team
       3             :    (see the PEOPLE file at the root of the distribution for a list of names)
       4             : 
       5             :    See http://www.plumed.org for more information.
       6             : 
       7             :    This file is part of plumed, version 2.
       8             : 
       9             :    plumed is free software: you can redistribute it and/or modify
      10             :    it under the terms of the GNU Lesser General Public License as published by
      11             :    the Free Software Foundation, either version 3 of the License, or
      12             :    (at your option) any later version.
      13             : 
      14             :    plumed is distributed in the hope that it will be useful,
      15             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
      16             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17             :    GNU Lesser General Public License for more details.
      18             : 
      19             :    You should have received a copy of the GNU Lesser General Public License
      20             :    along with plumed.  If not, see <http://www.gnu.org/licenses/>.
      21             : +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
      22             : #include "Subprocess.h"
      23             : #include "Exception.h"
      24             : #include "Tools.h"
      25             : #ifdef __PLUMED_HAS_SUBPROCESS
      26             : #include <unistd.h>
      27             : #include <csignal>
      28             : #include <cerrno>
      29             : #include <chrono>
      30             : #include <thread>
      31             : #include <sys/wait.h>
      32             : #endif
      33             : 
      34             : namespace PLMD {
      35             : 
      36             : /// Retrieve PLUMED_ENABLE_SIGNALS.
      37             : /// Inline static so that it can store a static variable (for quicker access)
      38             : /// without adding a unique global symbol to a library including this header file.
      39          38 : inline static bool SubprocessPidGetenvSignals() noexcept {
      40          38 :   static const bool res=std::getenv("PLUMED_ENABLE_SIGNALS");
      41          38 :   return res;
      42             : }
      43             : 
      44             : /// Small utility class, used to avoid inclusion of unistd.h> in a header file.
      45             : ///
      46             : /// The subprocess is launched as `/bin/sh -c cmd` and is made leader of a new
      47             : /// process group (with pgid equal to its pid). All signals are sent to the whole
      48             : /// process group (`kill(-pid,...)`), so that they also reach the processes
      49             : /// spawned by the shell (e.g. the python interpreter launched by a script).
      50             : /// Sending them only to the shell would leave the actual worker untouched.
      51             : class SubprocessPid {
      52             : #ifdef __PLUMED_HAS_SUBPROCESS
      53             :   /// Time given to the process group to exit after SIGTERM before sending SIGKILL.
      54             :   static constexpr std::chrono::milliseconds gracePeriod{1000};
      55             :   /// Returns true if the shell process has exited.
      56             :   /// The process is NOT reaped (WNOWAIT), so that its pid (and thus the process group id)
      57             :   /// cannot be recycled by the system before we are done signaling the group.
      58       34890 :   bool exited() const noexcept {
      59             :     while(true) {
      60             :       siginfo_t info;
      61       34890 :       info.si_pid=0;
      62       34890 :       if(waitid(P_PID,pid,&info,WEXITED|WNOHANG|WNOWAIT)==0) {
      63       34890 :         return info.si_pid==pid;
      64             :       }
      65           0 :       if(errno!=EINTR) {
      66             :         // ECHILD or other errors: nothing to wait for
      67             :         return true;
      68             :       }
      69           0 :     }
      70             :   }
      71             : public:
      72             :   const pid_t pid;
      73        5033 :   explicit SubprocessPid(pid_t pid_):
      74        5033 :     pid(pid_) {
      75        5033 :     plumed_assert(pid!=0 && pid!=-1);
      76        5033 :   }
      77          21 :   void stop() noexcept {
      78             :     // Signals give problems with MPI on Travis.
      79             :     // I disable them for now.
      80          21 :     if(SubprocessPidGetenvSignals()) {
      81           3 :       kill(-pid,SIGSTOP);
      82             :     }
      83          21 :   }
      84          17 :   void cont() noexcept {
      85             :     // Signals give problems with MPI on Travis.
      86             :     // I disable them for now.
      87          17 :     if(SubprocessPidGetenvSignals()) {
      88           1 :       kill(-pid,SIGCONT);
      89             :     }
      90          17 :   }
      91        5033 :   ~SubprocessPid() {
      92             :     // The destructor implies we do not need the subprocess anymore.
      93             :     // At this point its stdin has already been closed.
      94             :     // We first ask the whole process group to terminate with SIGTERM, which
      95             :     // allows a clean exit (e.g., shell EXIT traps removing temporary files).
      96             :     // SIGCONT is needed because the group might have been stopped with SIGSTOP
      97             :     // (see stop()), in which case SIGTERM would remain pending forever.
      98        5033 :     kill(-pid,SIGTERM);
      99        5033 :     kill(-pid,SIGCONT);
     100             :     // Poll with an increasing interval, so that the common case
     101             :     // (immediate exit) is fast.
     102        5033 :     const auto start=std::chrono::steady_clock::now();
     103        5033 :     std::chrono::microseconds interval{50};
     104       34890 :     while(!exited() && std::chrono::steady_clock::now()-start<gracePeriod) {
     105       24824 :       std::this_thread::sleep_for(interval);
     106       24824 :       if(interval<std::chrono::milliseconds(20)) {
     107             :         interval*=2;
     108             :       }
     109             :     }
     110             :     // If the shell did not exit within the grace period (e.g. because it ignores SIGTERM),
     111             :     // kill the whole process group.
     112             :     // This is safe since the shell has not been reaped yet, so the pgid cannot
     113             :     // have been recycled.
     114             :     // Notice that if the shell has exited we do not send SIGKILL to the rest of the group:
     115             :     // processes started by the shell received SIGTERM as well, and they might still be
     116             :     // running their cleanup (e.g. a bash EXIT trap in a script launched by a
     117             :     // shell that does not exec it).
     118        5033 :     if(!exited()) {
     119           1 :       kill(-pid,SIGKILL);
     120             :     }
     121             :     // Reap the shell. This is anyway required to avoid leaks
     122             :     int status;
     123       10066 :     while(waitpid(pid, &status, 0)<0 && errno==EINTR) {}
     124        5033 :   }
     125             : #endif
     126             : };
     127             : 
     128        5033 : Subprocess::Subprocess(const std::string & cmd) {
     129             : #ifdef __PLUMED_HAS_SUBPROCESS
     130        5033 :   char* arr [] = {
     131             :     // const_cast are necessary here due to the declaration of execv
     132             :     const_cast<char*>("/bin/sh"),
     133             :     const_cast<char*>("-c"),
     134             :     const_cast<char*>(cmd.c_str()),
     135             :     nullptr
     136        5033 :   };
     137             :   int cp[2];
     138             :   int pc[2];
     139        5033 :   if(pipe(pc)<0) {
     140           0 :     plumed_error()<<"error creating parent to child pipe";
     141             :   }
     142        5033 :   if(pipe(cp)<0) {
     143           0 :     plumed_error()<<"error creating child to parent pipe";
     144             :   }
     145        5033 :   pid_t forkedPid=fork();
     146        5033 :   switch(forkedPid) {
     147           0 :   case -1:
     148           0 :     plumed_error()<<"error forking";
     149             :     break;
     150             : // CHILD:
     151           0 :   case 0: {
     152           0 :     if(close(1)<0) {
     153           0 :       plumed_error()<<"error closing file";
     154             :     }
     155           0 :     if(dup(cp[1])<0) {
     156           0 :       plumed_error()<<"error duplicating file";
     157             :     }
     158           0 :     if(close(0)<0) {
     159           0 :       plumed_error()<<"error closing file";
     160             :     }
     161           0 :     if(dup(pc[0])<0) {
     162           0 :       plumed_error()<<"error duplicating file";
     163             :     }
     164           0 :     if(close(pc[1])<0) {
     165           0 :       plumed_error()<<"error closing file";
     166             :     }
     167           0 :     if(close(cp[0])<0) {
     168           0 :       plumed_error()<<"error closing file";
     169             :     }
     170             :     // Make the child the leader of a new process group, so that signals
     171             :     // can be sent to it and to all the processes it spawns.
     172           0 :     if(setpgid(0,0)<0) {
     173           0 :       plumed_error()<<"error setting process group";
     174             :     }
     175           0 :     auto err=execv(arr[0],arr);
     176           0 :     plumed_error()<<"error in script file " << cmd << ", execv returned "<<err;
     177             :   }
     178             : // PARENT::
     179        5033 :   default:
     180             :     // setpgid is also called here to avoid a race condition:
     181             :     // the parent might send a signal to the group before the child has created it.
     182             :     // Errors are ignored: the call fails with EACCES if the child has already called execv,
     183             :     // but in that case the child has already set its process group.
     184        5033 :     setpgid(forkedPid,forkedPid);
     185        5033 :     pid=Tools::make_unique<SubprocessPid>(forkedPid);
     186        5033 :     if(close(pc[0])<0) {
     187           0 :       plumed_error()<<"error closing file";
     188             :     }
     189        5033 :     if(close(cp[1])<0) {
     190           0 :       plumed_error()<<"error closing file";
     191             :     }
     192        5033 :     fpc=pc[1];
     193        5033 :     fcp=cp[0];
     194        5033 :     fppc=fdopen(fpc,"w");
     195        5033 :     parent_to_child.link(fppc);
     196        5033 :     fpcp=fdopen(fcp,"r");
     197        5033 :     child_to_parent.link(fpcp);
     198             :   }
     199             : #else
     200             :   plumed_error()<<"Subprocess not supported";
     201             : #endif
     202        5033 : }
     203             : 
     204        5033 : Subprocess::~Subprocess() {
     205             : #ifdef __PLUMED_HAS_SUBPROCESS
     206             : // close files:
     207        5033 :   fclose(fppc);
     208        5033 :   fclose(fpcp);
     209             : // fclose also closes the underlying descriptors,
     210             : // so this is not needed:
     211        5033 :   close(fpc);
     212        5033 :   close(fcp);
     213             : // after closing the communication, the subprocess is killed
     214             : // in pid's destructor
     215             : #endif
     216        5033 : }
     217             : 
     218         190 : bool Subprocess::available() noexcept {
     219             : #ifdef __PLUMED_HAS_SUBPROCESS
     220         190 :   return true;
     221             : #else
     222             :   return false;
     223             : #endif
     224             : }
     225             : 
     226          21 : void Subprocess::stop() noexcept {
     227             : #ifdef __PLUMED_HAS_SUBPROCESS
     228          21 :   pid->stop();
     229             : #endif
     230          21 : }
     231             : 
     232          17 : void Subprocess::cont() noexcept {
     233             : #ifdef __PLUMED_HAS_SUBPROCESS
     234          17 :   pid->cont();
     235             : #endif
     236          17 : }
     237             : 
     238          16 : void Subprocess::flush() {
     239          16 :   parent_to_child.flush();
     240          16 : }
     241             : 
     242          47 : Subprocess & Subprocess::getline(std::string & line) {
     243          47 :   child_to_parent.getline(line);
     244          47 :   if(!child_to_parent) {
     245           0 :     plumed_error() <<"error reading subprocess";
     246             :   }
     247          47 :   return (*this);
     248             : }
     249             : 
     250          17 : Subprocess::Handler::Handler(Subprocess *subp) noexcept:
     251          17 :   sp(subp) {
     252          17 :   subp->cont();
     253          17 : }
     254             : 
     255          17 : Subprocess::Handler::~Handler() {
     256          17 :   if(sp) {
     257          17 :     sp->stop();
     258             :   }
     259          17 : }
     260             : 
     261           0 : Subprocess::Handler::Handler(Handler && handler) noexcept :
     262           0 :   sp(handler.sp) {
     263           0 :   handler.sp=nullptr;
     264           0 : }
     265             : 
     266           0 : Subprocess::Handler & Subprocess::Handler::operator=(Handler && handler) noexcept {
     267           0 :   if(this!=&handler) {
     268           0 :     if(sp) {
     269           0 :       sp->stop();
     270             :     }
     271           0 :     sp=handler.sp;
     272           0 :     handler.sp=nullptr;
     273             :   }
     274           0 :   return *this;
     275             : }
     276             : 
     277             : 
     278             : }

Generated by: LCOV version 1.16