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
47namespace G
48{
49 namespace NewProcessUnixImp
50 {
51 class Pipe ;
52 }
53}
54
55class 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
66public:
67 Pipe( const Pipe & ) = delete ;
68 Pipe( Pipe && ) = delete ;
69 Pipe & operator=( const Pipe & ) = delete ;
70 Pipe & operator=( Pipe && ) = delete ;
71
72private:
73 std::array<int,2U> m_fds{{-1,-1}} ;
74 int m_fd{-1} ;
75} ;
76
77class G::NewProcessImp
78{
79public:
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
108G::NewProcess::NewProcess( const Path & exe , const StringArray & args , const Config & config ) :
109 m_imp(std::make_unique<NewProcessImp>(exe,args,config))
110{
111}
112
114= default;
115
117{
118 return m_imp->waitable() ;
119}
120
121std::pair<bool,pid_t> G::NewProcess::fork()
122{
123 return NewProcessImp::fork() ;
124}
125
126int G::NewProcess::id() const noexcept
127{
128 return m_imp->id() ;
129}
130
131void 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 ) ;
188
189 // close other file descriptors
190 if( config.close_other_fds )
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
238void 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
245int 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
289int G::NewProcessImp::id() const noexcept
290{
291 return static_cast<int>(m_child_pid) ;
292}
293
294G::NewProcessWaitable & G::NewProcessImp::waitable() noexcept
295{
296 return m_waitable ;
297}
298
299void 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
317bool 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
356void 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
363void 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
370int G::NewProcessUnixImp::Pipe::fd() const
371{
372 return m_fd ;
373}
374
375void 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
387 m_test_mode(G::Test::enabled("waitpid-slow"))
388{
389}
390
391#ifndef G_LIB_SMALL
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
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
414#ifndef G_LIB_SMALL
415void 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
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
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
520int 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
536{
537 if( m_fd < 0 || m_read_error != 0 )
538 return {} ;
539 else
540 return { m_buffer.data() , m_buffer.size() } ;
541}
542
static void put(const std::string &name, const std::string &value)
Sets the environment variable value.
static std::vector< char * > array(const std::string &block)
Returns a pointer array pointing into the given block(), with const-casts.
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
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:204
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:82
const value_type * cstr() const noexcept
Returns the path's c-string.
Definition: gpath.h:249
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:314
bool isRelative() const noexcept
Returns true if the path is a relative path or empty().
Definition: gpath.cpp:334
std::string str() const
Returns the path string.
Definition: gpath.h:243
bool empty() const noexcept
Returns true if the path is empty.
Definition: gpath.h:237
static void closeOtherFiles(int fd_keep=-1)
Closes all open file descriptors except the three standard ones and optionally 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 void inheritStandardFiles()
Makes sure that the standard file descriptors are inherited.
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:594
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
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:36
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