E-MailRelay
gnewprocess_unix.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 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
48namespace G
49{
50 namespace NewProcessUnixImp
51 {
52 class Pipe ;
53 }
54}
55
56class G::NewProcessUnixImp::Pipe
57{
58 public:
59 Pipe() ;
60 ~Pipe() ;
61 void inChild() ; // writer
62 void inParent() ; // reader
63 int fd() const ;
64 void dupTo( int fd_std ) ; // onto stdout/stderr
65 void write( const std::string & ) ;
66
67public:
68 Pipe( const Pipe & ) = delete ;
69 Pipe( Pipe && ) = delete ;
70 Pipe & operator=( const Pipe & ) = delete ;
71 Pipe & operator=( Pipe && ) = delete ;
72
73private:
74 std::array<int,2U> m_fds{{-1,-1}} ;
75 int m_fd{-1} ;
76} ;
77
78class G::NewProcessImp
79{
80public:
81 using Fd = NewProcess::Fd ;
82 using Pipe = NewProcessUnixImp::Pipe ;
83 NewProcessImp( const Path & , const StringArray & , const NewProcess::Config & ) ;
84 int id() const noexcept ;
85 static std::pair<bool,pid_t> fork() ;
86 NewProcessWaitable & waitable() noexcept ;
87 int run( const Path & , const StringArray & , const Environment & , bool strict_exe ) ;
88 void kill() noexcept ;
89 static void printError( int , const std::string & s ) ;
90 std::string execErrorFormat( const std::string & format , int errno_ ) ;
91 static bool duplicate( Fd , int ) ;
92
93public:
94 ~NewProcessImp() = default ;
95 NewProcessImp( const NewProcessImp & ) = delete ;
96 NewProcessImp( NewProcessImp && ) = delete ;
97 NewProcessImp & operator=( const NewProcessImp & ) = delete ;
98 NewProcessImp & operator=( NewProcessImp && ) = delete ;
99
100private:
101 Pipe m_pipe ;
102 NewProcessWaitable m_waitable ;
103 pid_t m_child_pid {-1} ;
104 bool m_killed {false} ;
105} ;
106
107// ==
108
109G::NewProcess::NewProcess( const Path & exe , const StringArray & args , const Config & config ) :
110 m_imp(std::make_unique<NewProcessImp>(exe,args,config))
111{
112}
113
115= default;
116
118{
119 return m_imp->waitable() ;
120}
121
122std::pair<bool,pid_t> G::NewProcess::fork()
123{
124 return NewProcessImp::fork() ;
125}
126
127int G::NewProcess::id() const noexcept
128{
129 return m_imp->id() ;
130}
131
132void G::NewProcess::kill( bool yield ) noexcept
133{
134 m_imp->kill() ;
135 if( yield )
136 {
137 G::threading::yield() ;
138 ::close( ::open( "/dev/null" , O_RDONLY ) ) ; // hmm // NOLINT
139 G::threading::yield() ;
140 }
141}
142
143// ==
144
145G::NewProcessImp::NewProcessImp( const Path & exe , const StringArray & args , const NewProcess::Config & config )
146{
147 // sanity checks
148 if( 1 != (config.stdout==Fd::pipe()?1:0) + (config.stderr==Fd::pipe()?1:0) || config.stdin==Fd::pipe() )
149 throw NewProcess::InvalidParameter() ;
150 if( exe.empty() )
151 throw NewProcess::InvalidParameter() ;
152
153 // safety checks
154 if( config.strict_exe && exe.isRelative() )
155 throw NewProcess::InvalidPath( exe.str() ) ;
156 if( config.strict_id && config.run_as != Identity::invalid() &&
157 ( Identity::effective().isRoot() || config.run_as.isRoot() ) )
158 throw NewProcess::Insecure() ;
159
160 // fork
161 bool in_child {} ;
162 std::tie(in_child,m_child_pid) = fork() ;
163 if( in_child )
164 {
165 try
166 {
167 // change directory
168 if( !config.cd.empty() )
169 Process::cd( config.cd ) ; // throws on error
170
171 // set real id
172 if( config.run_as != Identity::invalid() )
173 Process::beOrdinaryForExec( config.run_as ) ;
174
175 // set up standard streams
176 m_pipe.inChild() ;
177 if( config.stdout == Fd::pipe() )
178 {
179 m_pipe.dupTo( STDOUT_FILENO ) ;
180 duplicate( config.stderr , STDERR_FILENO ) ;
181 }
182 else
183 {
184 duplicate( config.stdout , STDOUT_FILENO ) ;
185 m_pipe.dupTo( STDERR_FILENO ) ;
186 }
187 duplicate( config.stdin , STDIN_FILENO ) ;
189
190 // restore SIGPIPE handling so that writing to
191 // the closed pipe should terminate the child
192 ::signal( SIGPIPE , SIG_DFL ) ;
193
194 // start a new process group
195 ::setpgrp() ; // feature-tested -- see gdef.h
196
197 // set the execvp() path
198 if( !config.strict_exe && !config.exec_search_path.empty() )
199 G::Environment::put( "PATH" , config.exec_search_path ) ;
200
201 // exec -- doesnt normally return from run()
202 int e = run( exe , args , config.env , config.strict_exe ) ;
203
204 // execve() failed -- write an error message to the pipe
205 int fd_pipe = config.stdout == Fd::pipe() ? STDOUT_FILENO : STDERR_FILENO ;
206 if( config.exec_error_format_fn != nullptr )
207 printError( fd_pipe , (config.exec_error_format_fn)(config.exec_error_format,e) ) ;
208 else if( !config.exec_error_format.empty() )
209 printError( fd_pipe , execErrorFormat(config.exec_error_format,e) ) ;
210 }
211 catch(...)
212 {
213 }
214 std::_Exit( config.exec_error_exit ) ;
215 }
216 else
217 {
218 m_pipe.inParent() ;
219 m_waitable.assign( m_child_pid , m_pipe.fd() ) ;
220 }
221}
222
223std::pair<bool,pid_t> G::NewProcessImp::fork()
224{
225 std::cout << std::flush ;
226 std::cerr << std::flush ;
227 pid_t rc = ::fork() ;
228 const bool ok = rc != -1 ;
229 if( !ok ) throw NewProcess::CannotFork() ;
230 bool in_child = rc == 0 ;
231 auto child_pid = static_cast<pid_t>(rc) ;
232 return { in_child , child_pid } ;
233}
234
235void G::NewProcessImp::printError( int stdxxx , const std::string & s )
236{
237 // write an exec-failure message back down the pipe
238 if( stdxxx <= 0 ) return ;
239 GDEF_IGNORE_RETURN ::write( stdxxx , s.c_str() , s.length() ) ;
240}
241
242int G::NewProcessImp::run( const G::Path & exe , const StringArray & args ,
243 const Environment & env , bool strict_exe )
244{
245 char ** argv = new char* [ args.size() + 2U ] ;
246 argv[0U] = const_cast<char*>( exe.cstr() ) ;
247 unsigned int argc = 1U ;
248 for( auto arg_p = args.begin() ; arg_p != args.end() ; ++arg_p , argc++ )
249 argv[argc] = const_cast<char*>(arg_p->c_str()) ;
250 argv[argc] = nullptr ;
251
252 int e = 0 ;
253 if( env.empty() )
254 {
255 if( strict_exe )
256 {
257 ::execv( exe.cstr() , argv ) ;
258 e = Process::errno_() ;
259 }
260 else
261 {
262 ::execvp( exe.cstr() , argv ) ;
263 e = Process::errno_() ;
264 }
265 }
266 else
267 {
268 if( strict_exe )
269 {
270 ::execve( exe.cstr() , argv , env.v() ) ;
271 e = Process::errno_() ;
272 }
273 else
274 {
275 ::execvpe( exe.cstr() , argv , env.v() ) ;
276 e = Process::errno_() ;
277 }
278 }
279
280 delete [] argv ;
281 G_DEBUG( "G::NewProcess::run: execve() returned: errno=" << e << ": " << exe ) ;
282 return e ;
283}
284
285int G::NewProcessImp::id() const noexcept
286{
287 return static_cast<int>(m_child_pid) ;
288}
289
290G::NewProcessWaitable & G::NewProcessImp::waitable() noexcept
291{
292 return m_waitable ;
293}
294
295void G::NewProcessImp::kill() noexcept
296{
297 if( !m_killed && m_child_pid != -1 )
298 {
299 // kill the group so the pipe is closed in all processes and the read returns zero
300 ::kill( -m_child_pid , SIGTERM ) ;
301 m_killed = true ;
302 }
303}
304
305std::string G::NewProcessImp::execErrorFormat( const std::string & format , int errno_ )
306{
307 std::string result = format ;
308 Str::replaceAll( result , "__""errno""__" , Str::fromInt(errno_) ) ;
309 Str::replaceAll( result , "__""strerror""__" , Process::strerror(errno_) ) ;
310 return result ;
311}
312
313bool G::NewProcessImp::duplicate( Fd fd , int fd_std )
314{
315 G_ASSERT( !(fd==Fd::pipe()) ) ;
316 if( fd == Fd::devnull() )
317 {
318 int fd_null = ::open( G::Path::nullDevice().cstr() , fd_std == STDIN_FILENO ? O_RDONLY : O_WRONLY ) ; // NOLINT
319 if( fd_null < 0 ) throw NewProcess::Error( "failed to open /dev/null" ) ;
320 ::dup2( fd_null , fd_std ) ;
321 return true ;
322 }
323 else if( fd.m_fd != fd_std )
324 {
325 if( ::dup2(fd.m_fd,fd_std) != fd_std )
326 throw NewProcess::Error( "dup failed" ) ;
327 ::close( fd.m_fd ) ;
328 return true ;
329 }
330 else
331 {
332 return false ;
333 }
334}
335
336// ==
337
338G::NewProcessUnixImp::Pipe::Pipe()
339{
340 if( ::socketpair( AF_UNIX , SOCK_STREAM , 0 , &m_fds[0] ) < 0 ) // must be a stream to dup() onto stdout
341 throw NewProcess::PipeError() ;
342 G_DEBUG( "G::Pipe::ctor: " << m_fds[0] << " " << m_fds[1] ) ;
343}
344
345G::NewProcessUnixImp::Pipe::~Pipe()
346{
347 if( m_fds[0] >= 0 ) ::close( m_fds[0] ) ;
348 if( m_fds[1] >= 0 ) ::close( m_fds[1] ) ;
349}
350
351void G::NewProcessUnixImp::Pipe::inChild()
352{
353 ::close( m_fds[0] ) ; // close read end
354 m_fds[0] = -1 ;
355 m_fd = m_fds[1] ; // writer
356}
357
358void G::NewProcessUnixImp::Pipe::inParent()
359{
360 ::close( m_fds[1] ) ; // close write end
361 m_fds[1] = -1 ;
362 m_fd = m_fds[0] ; // reader
363}
364
365int G::NewProcessUnixImp::Pipe::fd() const
366{
367 return m_fd ;
368}
369
370void G::NewProcessUnixImp::Pipe::dupTo( int fd_std )
371{
372 if( NewProcessImp::duplicate( NewProcess::Fd::fd(m_fd) , fd_std ) )
373 {
374 m_fd = -1 ;
375 m_fds[1] = -1 ;
376 }
377}
378
379// ==
380
382 m_test_mode(G::Test::enabled("waitpid-slow"))
383{
384}
385
386#ifndef G_LIB_SMALL
388 m_buffer(1024U) ,
389 m_pid(pid) ,
390 m_fd(fd) ,
391 m_test_mode(G::Test::enabled("waitpid-slow"))
392{
393}
394#endif
395
396void G::NewProcessWaitable::assign( pid_t pid , int fd )
397{
398 m_buffer.resize( 1024U ) ;
399 m_hprocess = 0 ;
400 m_hpipe = 0 ;
401 m_pid = pid ;
402 m_fd = fd ;
403 m_rc = 0 ;
404 m_status = 0 ;
405 m_error = 0 ;
406 m_read_error = 0 ;
407}
408
409#ifndef G_LIB_SMALL
410void G::NewProcessWaitable::waitp( std::promise<std::pair<int,std::string>> p ) noexcept
411{
412 try
413 {
414 wait() ;
415 int rc = get() ;
416 p.set_value( std::make_pair(rc,output()) ) ;
417 }
418 catch(...)
419 {
420 try { p.set_exception( std::current_exception() ) ; } catch(...) {}
421 }
422}
423#endif
424
426{
427 // (worker thread - keep it simple - never throws - does read then waitpid)
428 {
429 std::array<char,64U> discard {} ;
430 char * p = &m_buffer[0] ;
431 std::size_t space = m_buffer.size() ;
432 std::size_t size = 0U ;
433 while( m_fd >= 0 )
434 {
435 ssize_t n = ::read( m_fd , p?p:&discard[0] , p?space:discard.size() ) ;
436 m_read_error = errno ;
437 if( n < 0 && m_read_error == EINTR )
438 {
439 ; // keep reading
440 }
441 else if( n < 0 )
442 {
443 m_buffer.clear() ;
444 break ;
445 }
446 else if( n == 0 )
447 {
448 m_buffer.resize( std::min(size,m_buffer.size()) ) ; // shrink, so no-throw
449 m_read_error = 0 ;
450 break ;
451 }
452 else if( p )
453 {
454 std::size_t nn = static_cast<std::size_t>(n) ; // n>0
455 nn = std::min( nn , space ) ; // just in case
456 p += nn ;
457 size += nn ;
458 space -= nn ;
459 if( space == 0U )
460 p = nullptr ;
461 }
462 }
463 }
464 while( m_pid != 0 )
465 {
466 errno = 0 ;
467 m_rc = ::waitpid( m_pid , &m_status , 0 ) ;
468 m_error = errno ;
469 if( m_rc >= 0 && ( WIFSTOPPED(m_status) || WIFCONTINUED(m_status) ) ) // NOLINT
470 ; // keep waiting
471 else if( m_rc == -1 && m_error == EINTR )
472 ; // keep waiting
473 else
474 break ;
475 }
476 if( m_test_mode )
477 sleep( 10 ) ;
478 return *this ;
479}
480
482{
483 int result = 0 ;
484 if( m_pid != 0 )
485 {
486 if( m_error == ECHILD )
487 {
488 // only here if SIGCHLD is explicitly ignored, but in that case
489 // we get no zombie process and cannot recover the exit code
490 result = 126 ;
491 }
492 else if( m_error || m_read_error )
493 {
494 std::ostringstream ss ;
495 ss << "errno=" << (m_read_error?m_read_error:m_error) ;
496 throw NewProcess::WaitError( ss.str() ) ;
497 }
498 else if( !WIFEXITED(m_status) ) // NOLINT
499 {
500 // uncaught signal
501 std::ostringstream ss ;
502 ss << "pid=" << m_pid ;
503 if( WIFSIGNALED(m_status) ) // NOLINT
504 ss << " signal=" << WTERMSIG(m_status) ; // NOLINT
505 throw NewProcess::ChildError( ss.str() ) ;
506 }
507 else
508 {
509 result = WEXITSTATUS( m_status ) ; // NOLINT
510 }
511 }
512 return result ;
513}
514
515int G::NewProcessWaitable::get( std::nothrow_t , int ec ) const noexcept
516{
517 int result = 0 ;
518 if( m_pid != 0 )
519 {
520 if( m_error || m_read_error )
521 result = ec ;
522 else if( !WIFEXITED(m_status) ) // NOLINT
523 result = 128 + WTERMSIG(m_status) ; // NOLINT
524 else
525 result = WEXITSTATUS( m_status ) ; // NOLINT
526 }
527 return result ;
528}
529
531{
532 if( m_fd < 0 || m_read_error != 0 )
533 return {} ;
534 else
535 return { &m_buffer[0] , m_buffer.size() } ;
536}
537
static void put(const std::string &name, const std::string &value)
Sets the environment variable value.
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.
Holds the parameters and future results of a waitpid() system call.
Definition: gnewprocess.h:198
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.
NewProcessWaitable & waitable() noexcept
Returns a reference to the Waitable sub-object so that the caller can wait for the child process to e...
NewProcess(const Path &exe, const G::StringArray &args, const Config &)
Constructor.
A Path object represents a file system path.
Definition: gpath.h:73
const char * cstr() const noexcept
Returns the path string.
Definition: gpath.h:230
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:326
bool isRelative() const noexcept
Returns true if the path is a relative path or empty().
Definition: gpath.cpp:341
std::string str() const
Returns the path string.
Definition: gpath.h:224
bool empty() const noexcept
Returns true if size() is zero.
Definition: gpath.h:212
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-id 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 unsigned int replaceAll(std::string &s, string_view from, string_view to)
Does a global replace on string 's', replacing all occurrences of sub-string 'from' with 'to'.
Definition: gstr.cpp:247
static std::string fromInt(int i)
Converts int 'i' to a string.
Definition: gstr.h:598
A static interface for enabling test features at run-time.
Definition: gtest.h:50
bool enabled() noexcept
Returns true if pop code is built in.
Low-level classes.
Definition: garg.h:30
std::vector< std::string > StringArray
A std::vector of std::strings.
Definition: gstringarray.h:30
STL namespace.
Configuration structure for G::NewProcess.
Definition: gnewprocess.h:89