E-MailRelay
gtask.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 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 , private ExceptionHandler
36{
37public:
38 TaskImp( Task & , EventState , 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. If the waiting thread does not
66 // finish immediately then this TaskImp object is given
67 // an independent life and true is returned.
68
69private: // overrides
70 void onFutureEvent() override ; // GNet::FutureEventHandler
71 void onException( ExceptionSource * , std::exception & , bool ) override ; // GNet::ExceptionHandler
72
73public:
74 TaskImp( const TaskImp & ) = delete ;
75 TaskImp( TaskImp && ) = delete ;
76 TaskImp & operator=( const TaskImp & ) = delete ;
77 TaskImp & operator=( TaskImp && ) = delete ;
78
79private:
80 void onTimeout() ;
81 static void waitThread( TaskImp * , HANDLE ) ; // thread function
82
83private:
84 Task * m_task ;
85 GNet::EventState m_es ;
86 FutureEvent m_future_event ;
87 Timer<TaskImp> m_timer ;
88 bool m_logged {false} ;
89 G::NewProcess m_process ;
90 G::threading::thread_type m_thread ;
91 static std::size_t m_zcount ;
92} ;
93
94std::size_t GNet::TaskImp::m_zcount = 0U ;
95
96// ==
97
98GNet::TaskImp::TaskImp( Task & task , EventState , bool sync ,
99 const G::ExecutableCommand & commandline , const G::Environment & env ,
100 G::NewProcess::Fd fd_stdin , G::NewProcess::Fd fd_stdout , G::NewProcess::Fd fd_stderr ,
101 const G::Path & cd , const std::string & exec_error_format ,
102 const G::Identity & id ) :
103 m_task(&task) ,
104 m_es(EventState::create(std::nothrow)) ,
105 m_future_event(*this,m_es) ,
106 m_timer(*this,&TaskImp::onTimeout,m_es) ,
107 m_process( commandline.exe() , commandline.args() ,
108 G::NewProcess::Config()
109 .set_env(env)
110 .set_stdin(fd_stdin)
111 .set_stdout(fd_stdout)
112 .set_stderr(fd_stderr)
113 .set_cd(cd)
114 .set_strict_exe(true)
115 .set_run_as(id)
116 .set_strict_id(true)
117 .set_exec_error_exit(127)
118 .set_exec_error_format(exec_error_format) )
119{
120 if( sync )
121 {
122 // no thread -- caller will call synchronous wait() method
123 }
124 else if( !G::threading::works() )
125 {
126 if( G::threading::using_std_thread )
127 G_WARNING_ONCE( "GNet::TaskImp::TaskImp: multi-threading disabled: running tasks synchronously" ) ;
128 waitThread( this , m_future_event.handle() ) ;
129 }
130 else
131 {
132 G_ASSERT( G::threading::using_std_thread ) ;
133 G::Cleanup::Block block_signals ;
134 m_thread = G::threading::thread_type( TaskImp::waitThread , this , m_future_event.handle() ) ;
135 }
136}
137
138GNet::TaskImp::~TaskImp()
139{
140 try
141 {
142 // (should be already join()ed)
143 if( m_thread.joinable() )
144 m_process.kill( true ) ;
145 if( m_thread.joinable() )
146 m_thread.join() ;
147 }
148 catch(...)
149 {
150 }
151}
152
153void GNet::TaskImp::onException( ExceptionSource * , std::exception & e , bool done )
154{
155 // we cannot use the exception handler inherited from the Task because we
156 // may be detached, with the Task already deleted
157 if( !done )
158 G_LOG( "GNet::TaskImp: exception: " << e.what() ) ;
159}
160
161bool GNet::TaskImp::zombify()
162{
163 G_ASSERT( m_es.esrc() == nullptr ) ;
164 G_ASSERT( m_es.logging() == nullptr ) ;
165
166 // detach the TaskImp from the Task
167 m_task = nullptr ;
168
169 // if necessary kill the process and start a timer to wait for the worker thread to finish
170 if( m_thread.joinable() )
171 {
172 if( !G::Test::enabled("task-no-kill") )
173 m_process.kill( true ) ;
174
175 m_zcount++ ;
176 m_timer.startTimer( 1U ) ; // periodic, until it finishes
177
178 static constexpr std::size_t warning_threshold = 30U ;
179 if( m_zcount == warning_threshold )
180 G_WARNING_ONCE( "GNet::Task::dtor: large number of threads waiting for processes to finish" ) ;
181
182 return true ; // waiting
183 }
184 else
185 {
186 return false ;
187 }
188}
189
190void GNet::TaskImp::onTimeout()
191{
192 G_ASSERT( m_task == nullptr ) ;
193 if( m_thread.joinable() )
194 {
195 if( !m_logged )
196 G_LOG( "TaskImp::dtor: waiting for killed process to terminate: pid " << m_process.id() ) ;
197 m_logged = true ;
198 m_timer.startTimer( 1U ) ;
199 }
200 else
201 {
202 if( m_logged )
203 G_LOG( "TaskImp::dtor: killed process has terminated: pid " << m_process.id() ) ;
204 delete this ;
205 m_zcount-- ;
206 }
207}
208
209std::pair<int,std::string> GNet::TaskImp::wait()
210{
211 m_process.waitable().wait() ;
212 int exit_code = m_process.waitable().get() ;
213 return { exit_code , m_process.waitable().output() } ;
214}
215
216void GNet::TaskImp::waitThread( TaskImp * This , HANDLE handle )
217{
218 // worker-thread -- keep it simple
219 try
220 {
221 This->m_process.waitable().wait() ;
222 FutureEvent::send( handle ) ;
223 }
224 catch(...) // worker thread outer function
225 {
226 static_assert( noexcept(FutureEvent::send(handle)) , "" ) ;
227 FutureEvent::send( handle ) ;
228 }
229}
230
231void GNet::TaskImp::onFutureEvent()
232{
233 G_DEBUG( "GNet::TaskImp::onFutureEvent: future event" ) ;
234 if( m_thread.joinable() )
235 m_thread.join() ;
236
237 int exit_code = m_process.waitable().get( std::nothrow ) ;
238 G_DEBUG( "GNet::TaskImp::onFutureEvent: exit code " << exit_code ) ;
239
240 std::string pipe_output = m_process.waitable().output() ;
241 G_LOG_MORE( "GNet::TaskImp::onFutureEvent: executable output: [" << G::Str::printable(pipe_output) << "]" ) ;
242
243 if( m_task )
244 m_task->done( exit_code , pipe_output ) ; // last
245}
246
247// ==
248
250 const std::string & exec_error_format , const G::Identity & id ) :
251 m_callback(callback) ,
252 m_es(es) ,
253 m_exec_error_format(exec_error_format) ,
254 m_id(id)
255{
256}
257
259{
260 try
261 {
262 stop() ;
263 }
264 catch(...) // dtor
265 {
266 }
267}
268
270{
271 // kill the process and release the imp to an independent life
272 // on the timer-list while the thread finishes up
273 if( m_imp && m_imp->zombify() )
274 {
275 // release the pointer so TaskImp::onTimeout() can do 'delete this'
276 GDEF_IGNORE_RETURN m_imp.release() ;
277 }
278 m_busy = false ;
279}
280
281#ifndef G_LIB_SMALL
282std::pair<int,std::string> GNet::Task::run( const G::ExecutableCommand & commandline ,
283 const G::Environment & env ,
284 G::NewProcess::Fd fd_stdin ,
285 G::NewProcess::Fd fd_stdout ,
286 G::NewProcess::Fd fd_stderr ,
287 const G::Path & cd )
288{
289 G_ASSERT( !m_busy ) ;
290 m_imp = std::make_unique<TaskImp>( *this ,
291 m_es , true , commandline , env ,
292 fd_stdin , fd_stdout , fd_stderr , cd ,
293 m_exec_error_format , m_id ) ;
294 return m_imp->wait() ;
295}
296#endif
297
298void GNet::Task::start( const G::ExecutableCommand & commandline )
299{
300 start( commandline , G::Environment::minimal() ,
301 G::NewProcess::Fd::devnull() ,
302 G::NewProcess::Fd::pipe() ,
303 G::NewProcess::Fd::devnull() ,
304 G::Path() ) ;
305}
306
307void GNet::Task::start( const G::ExecutableCommand & commandline ,
308 const G::Environment & env ,
309 G::NewProcess::Fd fd_stdin ,
310 G::NewProcess::Fd fd_stdout ,
311 G::NewProcess::Fd fd_stderr ,
312 const G::Path & cd )
313{
314 if( m_busy )
315 throw Busy() ;
316
317 m_busy = true ;
318 m_imp = std::make_unique<TaskImp>( *this , m_es , false , commandline ,
319 env , fd_stdin , fd_stdout , fd_stderr , cd ,
320 m_exec_error_format , m_id ) ;
321}
322
323void GNet::Task::done( int exit_code , const std::string & output )
324{
325 m_busy = false ;
326 m_callback.onTaskDone( exit_code , output ) ;
327}
328
A lightweight object containing an ExceptionHandler pointer, optional ExceptionSource pointer and opt...
Definition: geventstate.h:131
An abstract interface for callbacks from GNet::Task.
Definition: gtask.h:114
Task(TaskCallback &, EventState 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:249
~Task()
Destructor.
Definition: gtask.cpp:258
void stop()
Attempts to kill the spawned process.
Definition: gtask.cpp:269
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:298
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:282
Holds a set of environment variables and also provides static methods to wrap getenv() and putenv().
Definition: genvironment.h:42
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:45
A class for creating new processes.
Definition: gnewprocess.h:62
A Path object represents a file system path.
Definition: gpath.h:82
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:913
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
Low-level classes.
Definition: garg.h:36
STL namespace.
A RAII class to temporarily block signal delivery.
Definition: gcleanup.h:45
Wraps up a file descriptor for passing to G::NewProcess.
Definition: gnewprocess.h:76