E-MailRelay
gnewprocess_unix.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gnewprocess_unix.cpp
19///
20
21#include "gdef.h"
22#include "gnewprocess.h"
23#include "gprocess.h"
24#include "genvironment.h"
25#include "gfile.h"
26#include "gidentity.h"
27#include "gassert.h"
28#include "gtest.h"
29#include "gstr.h"
30#include "glog.h"
31#include <cerrno>
32#include <array>
33#include <algorithm> // std::swap()
34#include <utility> // std::swap()
35#include <tuple> // std::tie()
36#include <iostream>
37#include <csignal> // ::kill()
38#include <sys/types.h>
39#include <sys/wait.h>
40#include <sys/stat.h>
41#include <fcntl.h> // open()
42#include <unistd.h> // setuid() etc
43
44#ifndef WIFCONTINUED
45#define WIFCONTINUED(wstatus) 0
46#endif
47
48//| \class G::Pipe
49/// A private implementation class used by G::NewProcess that wraps
50/// a unix pipe.
51///
53{
54public:
55 Pipe() ;
56 ~Pipe() ;
57 void inChild() ; // writer
58 void inParent() ; // reader
59 int fd() const ;
60 void dupTo( int fd_std ) ; // onto stdout/stderr
61 void write( const std::string & ) ;
62
63public:
64 Pipe( const Pipe & ) = delete ;
65 Pipe( Pipe && ) = delete ;
66 void operator=( const Pipe & ) = delete ;
67 void operator=( Pipe && ) = delete ;
68
69private:
70 std::array<int,2U> m_fds{{-1,-1}} ;
71 int m_fd{-1} ;
72} ;
73
74//| \class G::NewProcessImp
75/// A pimple-pattern implementation class used by G::NewProcess.
76///
78{
79public:
80 using Fd = NewProcess::Fd ;
81 NewProcessImp( const Path & exe , const StringArray & args , const Environment & env ,
82 Fd fd_stdin , Fd fd_stdout , Fd fd_stderr , const G::Path & cd ,
83 bool strict_path , Identity run_as_id , bool strict_id ,
84 int exec_error_exit , const std::string & exec_error_format ,
85 std::string (*exec_error_format_fn)(std::string,int) ) ;
86 int id() const noexcept ;
87 static std::pair<bool,pid_t> fork() ;
88 NewProcessWaitable & waitable() noexcept ;
89 int run( const Path & , const StringArray & , const Environment & , bool strict_path ) ;
90 void kill() noexcept ;
91 static void printError( int , const std::string & s ) ;
92 std::string execErrorFormat( const std::string & format , int errno_ ) ;
93 static bool duplicate( Fd , int ) ;
94
95public:
96 ~NewProcessImp() = default ;
97 NewProcessImp( const NewProcessImp & ) = delete ;
98 NewProcessImp( NewProcessImp && ) = delete ;
99 void operator=( const NewProcessImp & ) = delete ;
100 void operator=( NewProcessImp && ) = delete ;
101
102private:
103 Pipe m_pipe ;
104 NewProcessWaitable m_waitable ;
105 pid_t m_child_pid{-1} ;
106 bool m_killed{false} ;
107} ;
108
109// ==
110
111G::NewProcess::NewProcess( const Path & exe , const StringArray & args , const Environment & env ,
112 Fd fd_stdin , Fd fd_stdout , Fd fd_stderr , const G::Path & cd ,
113 bool strict_path , Identity run_as_id , bool strict_id ,
114 int exec_error_exit , const std::string & exec_error_format ,
115 std::string (*exec_error_format_fn)(std::string,int) ) :
116 m_imp(std::make_unique<NewProcessImp>(exe,args,env,fd_stdin,fd_stdout,fd_stderr,cd,strict_path,
117 run_as_id,strict_id,exec_error_exit,exec_error_format,exec_error_format_fn))
118{
119}
120
122= default;
123
125{
126 return m_imp->waitable() ;
127}
128
129std::pair<bool,pid_t> G::NewProcess::fork()
130{
131 return NewProcessImp::fork() ;
132}
133
134int G::NewProcess::id() const noexcept
135{
136 return m_imp->id() ;
137}
138
139void G::NewProcess::kill( bool yield ) noexcept
140{
141 m_imp->kill() ;
142 if( yield )
143 {
144 G::threading::yield() ;
145 ::close( ::open( "/dev/null" , O_RDONLY ) ) ; // hmm // NOLINT
146 G::threading::yield() ;
147 }
148}
149
150// ==
151
152G::NewProcessImp::NewProcessImp( const Path & exe , const StringArray & args , const Environment & env ,
153 Fd fd_stdin , Fd fd_stdout , Fd fd_stderr , const G::Path & cd ,
154 bool strict_path , Identity run_as_id , bool strict_id ,
155 int exec_error_exit , const std::string & exec_error_format ,
156 std::string (*exec_error_format_fn)(std::string,int) )
157{
158 // sanity checks
159 if( 1 != (fd_stdout==Fd::pipe()?1:0) + (fd_stderr==Fd::pipe()?1:0) || fd_stdin==Fd::pipe() )
160 throw NewProcess::InvalidParameter() ;
161 if( exe.empty() )
162 throw NewProcess::InvalidParameter() ;
163
164 // safety checks
165 if( strict_path && exe.isRelative() )
166 throw NewProcess::InvalidPath( exe.str() ) ;
167 if( strict_id && run_as_id != Identity::invalid() &&
168 ( Identity::effective().isRoot() || run_as_id.isRoot() ) )
169 throw NewProcess::Insecure() ;
170
171 // fork
172 bool in_child {} ;
173 std::tie(in_child,m_child_pid) = fork() ;
174 if( in_child )
175 {
176 try
177 {
178 // change directory
179 if( !cd.empty() )
180 Process::cd( cd ) ;
181
182 // set real id
183 if( run_as_id != Identity::invalid() )
184 Process::beOrdinaryForExec( run_as_id ) ;
185
186 // set up standard streams
187 m_pipe.inChild() ;
188 if( fd_stdout == Fd::pipe() )
189 {
190 m_pipe.dupTo( STDOUT_FILENO ) ;
191 duplicate( fd_stderr , STDERR_FILENO ) ;
192 }
193 else
194 {
195 duplicate( fd_stdout , STDOUT_FILENO ) ;
196 m_pipe.dupTo( STDERR_FILENO ) ;
197 }
198 duplicate( fd_stdin , STDIN_FILENO ) ;
200
201 // restore SIGPIPE handling so that writing to
202 // the closed pipe should terminate the child
203 ::signal( SIGPIPE , SIG_DFL ) ;
204
205 // start a new process group
206 ::setpgrp() ; // feature-tested -- see gdef.h
207
208 // exec -- doesnt normally return from run()
209 int e = run( exe , args , env , strict_path ) ;
210
211 // execve() failed -- write an error message to the pipe
212 int fd_pipe = fd_stdout == Fd::pipe() ? STDOUT_FILENO : STDERR_FILENO ;
213 if( exec_error_format_fn != nullptr )
214 printError( fd_pipe , (*exec_error_format_fn)(exec_error_format,e) ) ;
215 else if( !exec_error_format.empty() )
216 printError( fd_pipe , execErrorFormat(exec_error_format,e) ) ;
217 }
218 catch(...)
219 {
220 }
221 std::_Exit( exec_error_exit ) ;
222 }
223 else
224 {
225 m_pipe.inParent() ;
226 m_waitable.assign( m_child_pid , m_pipe.fd() ) ;
227 }
228}
229
230std::pair<bool,pid_t> G::NewProcessImp::fork()
231{
232 std::cout << std::flush ;
233 std::cerr << std::flush ;
234 pid_t rc = ::fork() ;
235 const bool ok = rc != -1 ;
236 if( !ok ) throw NewProcess::CannotFork() ;
237 bool in_child = rc == 0 ;
238 auto child_pid = static_cast<pid_t>(rc) ;
239 return std::make_pair( in_child , child_pid ) ;
240}
241
242void G::NewProcessImp::printError( int stdxxx , const std::string & s )
243{
244 // write an exec-failure message back down the pipe
245 if( stdxxx <= 0 ) return ;
246 GDEF_IGNORE_RETURN ::write( stdxxx , s.c_str() , s.length() ) ;
247}
248
249int G::NewProcessImp::run( const G::Path & exe , const StringArray & args ,
250 const Environment & env , bool strict_path )
251{
252 char ** argv = new char* [ args.size() + 2U ] ;
253 argv[0U] = const_cast<char*>( exe.cstr() ) ;
254 unsigned int argc = 1U ;
255 for( auto arg_p = args.begin() ; arg_p != args.end() ; ++arg_p , argc++ )
256 argv[argc] = const_cast<char*>(arg_p->c_str()) ;
257 argv[argc] = nullptr ;
258
259 int e = 0 ;
260 if( env.empty() )
261 {
262 if( strict_path )
263 {
264 ::execv( exe.cstr() , argv ) ;
265 e = Process::errno_() ;
266 }
267 else
268 {
269 ::execvp( exe.cstr() , argv ) ;
270 e = Process::errno_() ;
271 }
272 }
273 else
274 {
275 if( strict_path )
276 {
277 ::execve( exe.cstr() , argv , env.v() ) ;
278 e = Process::errno_() ;
279 }
280 else
281 {
282 ::execvpe( exe.cstr() , argv , env.v() ) ;
283 e = Process::errno_() ;
284 }
285 }
286
287 delete [] argv ;
288 G_DEBUG( "G::NewProcess::run: execve() returned: errno=" << e << ": " << exe ) ;
289 return e ;
290}
291
292int G::NewProcessImp::id() const noexcept
293{
294 return static_cast<int>(m_child_pid) ;
295}
296
297G::NewProcessWaitable & G::NewProcessImp::waitable() noexcept
298{
299 return m_waitable ;
300}
301
302void G::NewProcessImp::kill() noexcept
303{
304 if( !m_killed && m_child_pid != -1 )
305 {
306 // kill the group so the pipe is closed in all processes and the read returns zero
307 ::kill( -m_child_pid , SIGTERM ) ;
308 m_killed = true ;
309 }
310}
311
312std::string G::NewProcessImp::execErrorFormat( const std::string & format , int errno_ )
313{
314 std::string result = format ;
315 Str::replaceAll( result , "__errno__" , Str::fromInt(errno_) ) ;
316 Str::replaceAll( result , "__strerror__" , Process::strerror(errno_) ) ;
317 return result ;
318}
319
320bool G::NewProcessImp::duplicate( Fd fd , int fd_std )
321{
322 G_ASSERT( !(fd==Fd::pipe()) ) ;
323 if( fd == Fd::devnull() )
324 {
325 int fd_null = ::open( G::Path::nullDevice().cstr() , fd_std == STDIN_FILENO ? O_RDONLY : O_WRONLY ) ; // NOLINT
326 if( fd_null < 0 ) throw NewProcess::Error( "failed to open /dev/null" ) ;
327 ::dup2( fd_null , fd_std ) ;
328 return true ;
329 }
330 else if( fd.m_fd != fd_std )
331 {
332 if( ::dup2(fd.m_fd,fd_std) != fd_std )
333 throw NewProcess::Error( "dup failed" ) ;
334 ::close( fd.m_fd ) ;
335 return true ;
336 }
337 else
338 {
339 return false ;
340 }
341}
342
343// ==
344
345G::Pipe::Pipe()
346{
347 if( ::socketpair( AF_UNIX , SOCK_STREAM , 0 , &m_fds[0] ) < 0 ) // must be a stream to dup() onto stdout
348 throw NewProcess::PipeError() ;
349 G_DEBUG( "G::Pipe::ctor: " << m_fds[0] << " " << m_fds[1] ) ;
350}
351
352G::Pipe::~Pipe()
353{
354 if( m_fds[0] >= 0 ) ::close( m_fds[0] ) ;
355 if( m_fds[1] >= 0 ) ::close( m_fds[1] ) ;
356}
357
358void G::Pipe::inChild()
359{
360 ::close( m_fds[0] ) ; // close read end
361 m_fds[0] = -1 ;
362 m_fd = m_fds[1] ; // writer
363}
364
365void G::Pipe::inParent()
366{
367 ::close( m_fds[1] ) ; // close write end
368 m_fds[1] = -1 ;
369 m_fd = m_fds[0] ; // reader
370}
371
372int G::Pipe::fd() const
373{
374 return m_fd ;
375}
376
377void G::Pipe::dupTo( int fd_std )
378{
379 if( NewProcessImp::duplicate( NewProcess::Fd::fd(m_fd) , fd_std ) )
380 {
381 m_fd = -1 ;
382 m_fds[1] = -1 ;
383 }
384}
385
386// ==
387
389 m_test_mode(G::Test::enabled("waitpid-slow"))
390{
391}
392
394 m_buffer(1024U) ,
395 m_pid(pid) ,
396 m_fd(fd) ,
397 m_test_mode(G::Test::enabled("waitpid-slow"))
398{
399}
400
401void G::NewProcessWaitable::assign( pid_t pid , int fd )
402{
403 m_buffer.resize( 1024U ) ;
404 m_hprocess = 0 ;
405 m_hpipe = 0 ;
406 m_pid = pid ;
407 m_fd = fd ;
408 m_rc = 0 ;
409 m_status = 0 ;
410 m_error = 0 ;
411 m_read_error = 0 ;
412}
413
414void G::NewProcessWaitable::waitp( std::promise<std::pair<int,std::string>> p ) noexcept
415{
416 try
417 {
418 wait() ;
419 int rc = get() ;
420 p.set_value( std::make_pair(rc,output()) ) ;
421 }
422 catch(...)
423 {
424 try { p.set_exception( std::current_exception() ) ; } catch(...) {}
425 }
426}
427
429{
430 // (worker thread - keep it simple - never throws - does read then waitpid)
431 {
432 std::array<char,64U> discard {} ;
433 char * p = &m_buffer[0] ;
434 std::size_t space = m_buffer.size() ;
435 std::size_t size = 0U ;
436 while( m_fd >= 0 )
437 {
438 ssize_t n = ::read( m_fd , p?p:&discard[0] , p?space:discard.size() ) ;
439 m_read_error = errno ;
440 if( n < 0 && m_read_error == EINTR )
441 {
442 ; // keep reading
443 }
444 else if( n < 0 )
445 {
446 m_buffer.clear() ;
447 break ;
448 }
449 else if( n == 0 )
450 {
451 m_buffer.resize( std::min(size,m_buffer.size()) ) ; // shrink, so no-throw
452 m_read_error = 0 ;
453 break ;
454 }
455 else if( p )
456 {
457 std::size_t nn = static_cast<std::size_t>(n) ; // n>0
458 nn = std::min( nn , space ) ; // just in case
459 p += nn ;
460 size += nn ;
461 space -= nn ;
462 if( space == 0U )
463 p = nullptr ;
464 }
465 }
466 }
467 while( m_pid != 0 )
468 {
469 errno = 0 ;
470 m_rc = ::waitpid( m_pid , &m_status , 0 ) ;
471 m_error = errno ;
472 if( m_rc >= 0 && ( WIFSTOPPED(m_status) || WIFCONTINUED(m_status) ) ) // NOLINT
473 ; // keep waiting
474 else if( m_rc == -1 && m_error == EINTR )
475 ; // keep waiting
476 else
477 break ;
478 }
479 if( m_test_mode )
480 sleep( 10 ) ;
481 return *this ;
482}
483
485{
486 int result = 0 ;
487 if( m_pid != 0 )
488 {
489 if( m_error == ECHILD )
490 {
491 // only here if SIGCHLD is explicitly ignored, but in that case
492 // we get no zombie process and cannot recover the exit code
493 result = 126 ;
494 }
495 else if( m_error || m_read_error )
496 {
497 std::ostringstream ss ;
498 ss << "errno=" << (m_read_error?m_read_error:m_error) ;
499 throw NewProcess::WaitError( ss.str() ) ;
500 }
501 else if( !WIFEXITED(m_status) ) // NOLINT
502 {
503 // uncaught signal
504 std::ostringstream ss ;
505 ss << "pid=" << m_pid ;
506 if( WIFSIGNALED(m_status) ) // NOLINT
507 ss << " signal=" << WTERMSIG(m_status) ; // NOLINT
508 throw NewProcess::ChildError( ss.str() ) ;
509 }
510 else
511 {
512 result = WEXITSTATUS( m_status ) ; // NOLINT
513 }
514 }
515 return result ;
516}
517
518int G::NewProcessWaitable::get( std::nothrow_t , int ec ) const
519{
520 int result = 0 ;
521 if( m_pid != 0 )
522 {
523 if( m_error || m_read_error )
524 result = ec ;
525 else if( !WIFEXITED(m_status) )
526 result = 128 + WTERMSIG(m_status) ;
527 else
528 result = WEXITSTATUS( m_status ) ;
529 }
530 return result ;
531}
532
534{
535 if( m_fd < 0 || m_read_error != 0 )
536 return std::string() ;
537 else
538 return std::string( &m_buffer[0] , m_buffer.size() ) ;
539}
540
Holds a set of environment variables and also provides static methods to wrap getenv() and putenv().
Definition: genvironment.h:39
A combination of user-id and group-id, with a very low-level interface to the get/set/e/uid/gid funct...
Definition: gidentity.h:43
bool isRoot() const noexcept
Returns true if the userid is zero.
static Identity invalid() noexcept
Returns an invalid identity.
static Identity effective() noexcept
Returns the current effective identity.
A pimple-pattern implementation class used by G::NewProcess.
Holds the parameters and future results of a waitpid() system call, as performed by the wait() method...
Definition: gnewprocess.h:178
int get() const
Returns the result of the wait() as either the process exit code or as a thrown exception.
void waitp(std::promise< std::pair< int, std::string > >) noexcept
Calls wait() and then sets the given promise with the get() and output() values or an exception:
NewProcessWaitable()
Default constructor for an object where wait() does nothing and get() returns zero.
std::string output() const
Returns the first bit of child-process output.
NewProcessWaitable & wait()
Waits for the process identified by the constructor parameter to exit.
void assign(pid_t pid, int fd)
Reinitialises as if constructed with the given proces-id and file descriptor.
int id() const noexcept
Returns the process id.
void kill(bool yield=false) noexcept
Tries to kill the spawned process and optionally yield to a thread that might be waiting on it.
static std::pair< bool, pid_t > fork()
A utility function that forks the calling process and returns twice; once in the parent and once in t...
~NewProcess()
Destructor.
NewProcess(const Path &exe, const StringArray &args, const Environment &env=Environment::minimal(), Fd fd_stdin=Fd::devnull(), Fd fd_stdout=Fd::pipe(), Fd fd_stderr=Fd::devnull(), const G::Path &cd=G::Path(), bool strict_path=true, Identity run_as_id=Identity::invalid(), bool strict_id=true, int exec_error_exit=127, const std::string &exec_error_format=std::string(), std::string(*exec_error_format_fn)(std::string, int)=nullptr)
Constructor.
NewProcessWaitable & waitable() noexcept
Returns a reference to the Waitable sub-object so that the caller can wait for the child process to e...
A Path object represents a file system path.
Definition: gpath.h:72
bool isRelative() const
Returns true if the path is a relative path or empty().
Definition: gpath.cpp:323
const char * cstr() const noexcept
Returns the path string.
Definition: gpath.h:221
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:308
std::string str() const
Returns the path string.
Definition: gpath.h:215
bool empty() const noexcept
Returns true if size() is zero.
Definition: gpath.h:203
A private implementation class used by G::NewProcess that wraps a unix pipe.
static void closeOtherFiles(int fd_keep=-1)
Closes all open file descriptors except the three standard ones and possibly one other.
static std::string strerror(int errno_)
Translates an 'errno' value into a meaningful diagnostic string.
static void beOrdinaryForExec(Identity run_as_id) noexcept
Sets the real and effective user and group ids to those given, on a best-effort basis.
static int errno_(const SignalSafe &=G::SignalSafe()) noexcept
Returns the process's current 'errno' value.
static void cd(const Path &dir)
Changes directory.
static std::string fromInt(int i)
Converts int 'i' to a string.
Definition: gstr.h:561
static unsigned int replaceAll(std::string &s, const std::string &from, const std::string &to)
Does a global replace on string 's', replacing all occurrences of sub-string 'from' with 'to'.
Definition: gstr.cpp:287
A static interface for enabling test features at run-time.
Definition: gtest.h:50
A simple version of boost::format for formatting strings in an i18n-friendly way.
Definition: gformat.h:46
Low-level classes.
Definition: galign.h:28
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstrings.h:31
Wraps up a file descriptor for passing to G::NewProcess.
Definition: gnewprocess.h:77