E-MailRelay

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