E-MailRelay
gtask.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 gtask.cpp
19///
20
21#include "gdef.h"
22#include "gtask.h"
23#include "gfutureevent.h"
24#include "gnewprocess.h"
25#include "gcleanup.h"
26#include "gtimer.h"
27#include "gstr.h"
28#include "gtest.h"
29#include "gassert.h"
30#include "glog.h"
31
32//| \class GNet::TaskImp
33/// A private implementation class used by GNet::Task.
34///
35class GNet::TaskImp : private FutureEventHandler
36{
37public:
38 TaskImp( Task & , ExceptionSink es , bool sync ,
39 const G::ExecutableCommand & , const G::Environment & env ,
40 G::NewProcess::Fd fd_stdin , G::NewProcess::Fd fd_stdout , G::NewProcess::Fd fd_stderr ,
41 const G::Path & cd , const std::string & exec_error_format , const G::Identity & id ) ;
42 // Constructor. Spawns the child processes.
43 //
44 // The GNet::FutureEvent class is used to send the completion
45 // message from the waitpid(2) thread to the main thread via
46 // the event-loop.
47 //
48 // In a single-threaded build, or if multi-threading is broken,
49 // this constructor runs the task, waits for it to complete
50 // and posts the completion message to the event-loop
51 // before this constructor returns.
52
53 ~TaskImp() override ;
54 // Destructor.
55
56 void start() ;
57 // Starts a waitpid() thread asynchronously that emits a
58 // FutureEvent when done.
59
60 std::pair<int,std::string> wait() ;
61 // Runs waitpid() synchronously.
62 // Precondition: ctor 'sync'
63
64 bool zombify() ;
65 // Kills the task process and makes this object a zombie that lives
66 // only on the timer-list, or returns false.
67
68private: // overrides
69 void onFutureEvent() override ; // Override from GNet::FutureEventHandler.
70
71public:
72 TaskImp( const TaskImp & ) = delete ;
73 TaskImp( TaskImp && ) = delete ;
74 TaskImp & operator=( const TaskImp & ) = delete ;
75 TaskImp & operator=( TaskImp && ) = delete ;
76
77private:
78 void onTimeout() ;
79 static void waitThread( TaskImp * , HANDLE ) ; // thread function
80
81private:
82 Task * m_task ;
83 FutureEvent m_future_event ;
84 Timer<TaskImp> m_timer ;
85 bool m_logged ;
86 G::NewProcess m_process ;
87 G::threading::thread_type m_thread ;
88 static std::size_t m_zcount ;
89} ;
90
91std::size_t GNet::TaskImp::m_zcount = 0U ;
92
93// ==
94
95GNet::TaskImp::TaskImp( Task & task , ExceptionSink es , bool sync ,
96 const G::ExecutableCommand & commandline , const G::Environment & env ,
97 G::NewProcess::Fd fd_stdin , G::NewProcess::Fd fd_stdout , G::NewProcess::Fd fd_stderr ,
98 const G::Path & cd , const std::string & exec_error_format ,
99 const G::Identity & id ) :
100 m_task(&task) ,
101 m_future_event(*this,es) ,
102 m_timer(*this,&TaskImp::onTimeout,es) ,
103 m_logged(false) ,
104 m_process( commandline.exe() , commandline.args() ,
105 G::NewProcess::Config()
106 .set_env(env)
107 .set_stdin(fd_stdin)
108 .set_stdout(fd_stdout)
109 .set_stderr(fd_stderr)
110 .set_cd(cd)
111 .set_strict_exe(true)
112 .set_run_as(id)
113 .set_strict_id(true)
114 .set_exec_error_exit(127)
115 .set_exec_error_format(exec_error_format) )
116{
117 if( sync )
118 {
119 // no thread -- caller will call synchronous wait() method
120 }
121 else if( !G::threading::works() )
122 {
123 if( G::threading::using_std_thread )
124 G_WARNING_ONCE( "GNet::TaskImp::TaskImp: multi-threading disabled: running tasks synchronously" ) ;
125 waitThread( this , m_future_event.handle() ) ;
126 }
127 else
128 {
129 G_ASSERT( G::threading::using_std_thread ) ;
130 G::Cleanup::Block block_signals ;
131 m_thread = G::threading::thread_type( TaskImp::waitThread , this , m_future_event.handle() ) ;
132 }
133}
134
135GNet::TaskImp::~TaskImp()
136{
137 try
138 {
139 // (should be already join()ed)
140 if( m_thread.joinable() )
141 m_process.kill( true ) ;
142 if( m_thread.joinable() )
143 m_thread.join() ;
144 }
145 catch(...)
146 {
147 }
148}
149
150bool GNet::TaskImp::zombify()
151{
152 m_task = nullptr ;
153 if( m_thread.joinable() )
154 {
155 if( !G::Test::enabled("task-no-kill") )
156 m_process.kill( true ) ;
157
158 m_zcount++ ;
159 m_timer.startTimer( 1U ) ;
160
161 const std::size_t threshold = 30U ;
162 if( m_zcount == threshold )
163 G_WARNING_ONCE( "GNet::Task::dtor: large number of threads waiting for processes to finish" ) ;
164
165 return true ;
166 }
167 else
168 {
169 return false ;
170 }
171}
172
173void GNet::TaskImp::onTimeout()
174{
175 if( m_thread.joinable() )
176 {
177 if( !m_logged )
178 G_LOG( "TaskImp::dtor: waiting for killed process to terminate: pid " << m_process.id() ) ;
179 m_logged = true ;
180 m_timer.startTimer( 1U ) ;
181 }
182 else
183 {
184 if( m_logged )
185 G_LOG( "TaskImp::dtor: killed process has terminated: pid " << m_process.id() ) ;
186 delete this ;
187 m_zcount-- ;
188 }
189}
190
191std::pair<int,std::string> GNet::TaskImp::wait()
192{
193 m_process.waitable().wait() ;
194 int exit_code = m_process.waitable().get() ;
195 return { exit_code , m_process.waitable().output() } ;
196}
197
198void GNet::TaskImp::waitThread( TaskImp * This , HANDLE handle )
199{
200 // worker-thread -- keep it simple
201 try
202 {
203 This->m_process.waitable().wait() ;
204 FutureEvent::send( handle ) ;
205 }
206 catch(...) // worker thread outer function
207 {
208 FutureEvent::send( handle ) ; // noexcept
209 }
210}
211
212void GNet::TaskImp::onFutureEvent()
213{
214 G_DEBUG( "GNet::TaskImp::onFutureEvent: future event" ) ;
215 if( m_thread.joinable() )
216 m_thread.join() ;
217
218 int exit_code = m_process.waitable().get( std::nothrow ) ;
219 G_DEBUG( "GNet::TaskImp::onFutureEvent: exit code " << exit_code ) ;
220
221 std::string pipe_output = m_process.waitable().output() ;
222 G_DEBUG( "GNet::TaskImp::onFutureEvent: output: [" << G::Str::printable(pipe_output) << "]" ) ;
223
224 if( m_task )
225 m_task->done( exit_code , pipe_output ) ; // last
226}
227
228// ==
229
231 const std::string & exec_error_format , const G::Identity & id ) :
232 m_callback(callback) ,
233 m_es(es) ,
234 m_exec_error_format(exec_error_format) ,
235 m_id(id) ,
236 m_busy(false)
237{
238}
239
241{
242 try
243 {
244 stop() ;
245 }
246 catch(...) // dtor
247 {
248 }
249}
250
252{
253 // kill the process and release the imp to an independent life
254 // on the timer-list while the thread finishes up
255 if( m_imp && m_imp->zombify() )
256 GDEF_IGNORE_RETURN m_imp.release() ;
257 m_busy = false ;
258}
259
260#ifndef G_LIB_SMALL
261std::pair<int,std::string> GNet::Task::run( const G::ExecutableCommand & commandline ,
262 const G::Environment & env ,
263 G::NewProcess::Fd fd_stdin ,
264 G::NewProcess::Fd fd_stdout ,
265 G::NewProcess::Fd fd_stderr ,
266 const G::Path & cd )
267{
268 G_ASSERT( !m_busy ) ;
269 m_imp = std::make_unique<TaskImp>( *this , m_es , true , commandline ,
270 env , fd_stdin , fd_stdout , fd_stderr , cd ,
271 m_exec_error_format , m_id ) ;
272 return m_imp->wait() ;
273}
274#endif
275
276void GNet::Task::start( const G::ExecutableCommand & commandline )
277{
278 start( commandline , G::Environment::minimal() ,
279 G::NewProcess::Fd::devnull() ,
280 G::NewProcess::Fd::pipe() ,
281 G::NewProcess::Fd::devnull() ,
282 G::Path() ) ;
283}
284
285void GNet::Task::start( const G::ExecutableCommand & commandline ,
286 const G::Environment & env ,
287 G::NewProcess::Fd fd_stdin ,
288 G::NewProcess::Fd fd_stdout ,
289 G::NewProcess::Fd fd_stderr ,
290 const G::Path & cd )
291{
292 if( m_busy )
293 throw Busy() ;
294
295 m_busy = true ;
296 m_imp = std::make_unique<TaskImp>( *this , m_es , false , commandline ,
297 env , fd_stdin , fd_stdout , fd_stderr , cd ,
298 m_exec_error_format , m_id ) ;
299}
300
301void GNet::Task::done( int exit_code , const std::string & output )
302{
303 m_busy = false ;
304 m_callback.onTaskDone( exit_code , output ) ;
305}
306
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
An abstract interface for callbacks from GNet::Task.
Definition: gtask.h:114
Task(TaskCallback &, ExceptionSink es, const std::string &exec_error_format={}, const G::Identity &=G::Identity::invalid())
Constructor for an object that can be start()ed or run().
Definition: gtask.cpp:230
~Task()
Destructor.
Definition: gtask.cpp:240
void stop()
Attempts to kill the spawned process.
Definition: gtask.cpp:251
void start(const G::ExecutableCommand &commandline)
Starts the task by spawning a new process with the given command-line and also starting a thread to w...
Definition: gtask.cpp:276
std::pair< int, std::string > run(const G::ExecutableCommand &commandline, const G::Environment &env, G::NewProcess::Fd fd_stdin=G::NewProcess::Fd::devnull(), G::NewProcess::Fd fd_stdout=G::NewProcess::Fd::pipe(), G::NewProcess::Fd fd_stderr=G::NewProcess::Fd::devnull(), const G::Path &cd=G::Path())
Runs the task synchronously and returns the exit code and pipe output.
Definition: gtask.cpp:261
Holds a set of environment variables and also provides static methods to wrap getenv() and putenv().
Definition: genvironment.h:40
static Environment minimal(bool sbin=false)
Returns a minimal, safe set of environment variables.
A structure representing an external program, holding a path and a set of arguments.
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:44
A class for creating new processes.
Definition: gnewprocess.h:62
A Path object represents a file system path.
Definition: gpath.h:73
static std::string printable(const std::string &in, char escape='\\')
Returns a printable representation of the given input string, using chacter code ranges 0x20 to 0x7e ...
Definition: gstr.cpp:916
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
Low-level classes.
Definition: garg.h:30
A RAII class to temporarily block signal delivery.
Definition: gcleanup.h:44
Wraps up a file descriptor for passing to G::NewProcess.
Definition: gnewprocess.h:76