E-MailRelay

gtask.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 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///
35 class GNet::TaskImp : private FutureEventHandler
36{
37 public:
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 contructor 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 process and makes this object a zombie that lives
66 // only on the timer-list, or returns false.
67
68 private: // overrides
69 void onFutureEvent() override ; // Override from GNet::FutureEventHandler.
70
71 public:
72 TaskImp( const TaskImp & ) = delete ;
73 TaskImp( TaskImp && ) = delete ;
74 void operator=( const TaskImp & ) = delete ;
75 void operator=( TaskImp && ) = delete ;
76
77 private:
78 void onTimeout() ;
79 static void waitThread( TaskImp * , FutureEvent::handle_type ) ; // thread function
80
81 private:
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 env,
106 fd_stdin ,
107 fd_stdout ,
108 fd_stderr ,
109 cd ,
110 true , // strict path
111 id ,
112 true, // strict id
113 127 , // exec error exit
114 exec_error_format )
115{
116 if( sync )
117 {
118 // no thread -- caller will call synchronous wait() method
119 }
120 else if( !G::threading::works() )
121 {
122 if( G::threading::using_std_thread )
123 G_WARNING_ONCE( "GNet::TaskImp::TaskImp: multi-threading disabled: running tasks synchronously" ) ;
124 waitThread( this , m_future_event.handle() ) ;
125 }
126 else
127 {
128 G_ASSERT( G::threading::using_std_thread ) ;
129 G::Cleanup::Block block_signals ;
130 m_thread = G::threading::thread_type( TaskImp::waitThread , this , m_future_event.handle() ) ;
131 }
132}
133
134GNet::TaskImp::~TaskImp()
135{
136 try
137 {
138 // (should be already join()ed)
139 if( m_thread.joinable() )
140 m_process.kill( true ) ;
141 if( m_thread.joinable() )
142 m_thread.join() ;
143 }
144 catch(...)
145 {
146 }
147}
148
149 bool GNet::TaskImp::zombify()
150{
151 m_task = nullptr ;
152 if( m_thread.joinable() )
153 {
154 if( !G::Test::enabled("task-no-kill") )
155 m_process.kill( true ) ;
156
157 m_zcount++ ;
158 m_timer.startTimer( 1U ) ;
159
160 std::size_t threshold = G::Test::enabled("task-warning") ? 3U : 100U ;
161 if( m_zcount == threshold )
162 G_WARNING_ONCE( "GNet::Task::dtor: large number of threads waiting for processes to finish" ) ;
163
164 return true ;
165 }
166 else
167 {
168 return false ;
169 }
170}
171
172 void GNet::TaskImp::onTimeout()
173{
174 if( m_thread.joinable() )
175 {
176 if( !m_logged )
177 G_LOG( "TaskImp::dtor: waiting for killed process to terminate: pid " << m_process.id() ) ;
178 m_logged = true ;
179 m_timer.startTimer( 1U ) ;
180 }
181 else
182 {
183 if( m_logged )
184 G_LOG( "TaskImp::dtor: killed process has terminated: pid " << m_process.id() ) ;
185 delete this ;
186 m_zcount-- ;
187 }
188}
189
190std::pair<int,std::string> GNet::TaskImp::wait()
191{
192 m_process.waitable().wait() ;
193 int exit_code = m_process.waitable().get() ;
194 return std::make_pair( exit_code , m_process.waitable().output() ) ;
195}
196
197 void GNet::TaskImp::waitThread( TaskImp * This , FutureEvent::handle_type handle )
198{
199 // worker-thread -- keep it simple
200 try
201 {
202 This->m_process.waitable().wait() ;
203 FutureEvent::send( handle ) ;
204 }
205 catch(...) // worker thread outer function
206 {
207 FutureEvent::send( handle ) ; // noexcept
208 }
209}
210
211 void GNet::TaskImp::onFutureEvent()
212{
213 G_DEBUG( "GNet::TaskImp::onFutureEvent: future event" ) ;
214 if( m_thread.joinable() )
215 m_thread.join() ;
216
217 int exit_code = m_process.waitable().get( std::nothrow ) ;
218 G_DEBUG( "GNet::TaskImp::onFutureEvent: exit code " << exit_code ) ;
219
220 std::string pipe_output = m_process.waitable().output() ;
221 G_DEBUG( "GNet::TaskImp::onFutureEvent: output: [" << G::Str::printable(pipe_output) << "]" ) ;
222
223 if( m_task )
224 m_task->done( exit_code , pipe_output ) ; // last
225}
226
227// ==
228
229 GNet::Task::Task( TaskCallback & callback , ExceptionSink es ,
230 const std::string & exec_error_format , const G::Identity & id ) :
231 m_callback(callback) ,
232 m_es(es) ,
233 m_exec_error_format(exec_error_format) ,
234 m_id(id) ,
235 m_busy(false)
236{
237}
238
239 GNet::Task::~Task()
240{
241 try
242 {
243 stop() ;
244 }
245 catch(...) // dtor
246 {
247 }
248}
249
250 void GNet::Task::stop()
251{
252 // kill the process and release the imp to an independent life
253 // on the timer-list while the thread finishes up
254 if( m_imp && m_imp->zombify() )
255 GDEF_IGNORE_RETURN m_imp.release() ;
256 m_busy = false ;
257}
258
259std::pair<int,std::string> GNet::Task::run( const G::ExecutableCommand & commandline ,
260 const G::Environment & env ,
261 G::NewProcess::Fd fd_stdin ,
262 G::NewProcess::Fd fd_stdout ,
263 G::NewProcess::Fd fd_stderr ,
264 const G::Path & cd )
265{
266 G_ASSERT( !m_busy ) ;
267 m_imp = std::make_unique<TaskImp>( *this , m_es , true , commandline ,
268 env , fd_stdin , fd_stdout , fd_stderr , cd ,
269 m_exec_error_format , m_id ) ;
270 return m_imp->wait() ;
271}
272
273 void GNet::Task::start( const G::ExecutableCommand & commandline )
274{
275 start( commandline , G::Environment::minimal() ,
276 G::NewProcess::Fd::devnull() ,
277 G::NewProcess::Fd::pipe() ,
278 G::NewProcess::Fd::devnull() ,
279 G::Path() ) ;
280}
281
282 void GNet::Task::start( 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 if( m_busy )
290 throw Busy() ;
291
292 m_busy = true ;
293 m_imp = std::make_unique<TaskImp>( *this , m_es , false , commandline ,
294 env , fd_stdin , fd_stdout , fd_stderr , cd ,
295 m_exec_error_format , m_id ) ;
296}
297
298 void GNet::Task::done( int exit_code , const std::string & output )
299{
300 m_busy = false ;
301 m_callback.onTaskDone( exit_code , output ) ;
302}
303
GNet::ExceptionSink
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
Definition: gexceptionsink.h:100
GNet::FutureEventHandler
A callback interface for GNet::FutureEvent.
Definition: gfutureevent.h:113
GNet::FutureEvent
A FutureEvent object can be used to send a one-shot event via the event loop to the relevant event ha...
Definition: gfutureevent.h:72
GNet::FutureEvent::handle
handle_type handle() noexcept
Extracts a handle that can be passed between threads and used in send().
Definition: gfutureevent_unix.cpp:162
GNet::FutureEvent::send
static bool send(handle_type handle, bool close=true) noexcept
Pokes an event into the main event loop so that the FutureEventHandler callback is called asynchronou...
Definition: gfutureevent_unix.cpp:157
GNet::TaskCallback
An abstract interface for callbacks from GNet::Task.
Definition: gtask.h:113
GNet::TaskImp
A private implementation class used by GNet::Task.
Definition: gtask.cpp:36
GNet::Task
A class for running an exectuable in a separate process with an asychronous completion callback.
Definition: gtask.h:45
GNet::Task::Task
Task(TaskCallback &, ExceptionSink es, const std::string &exec_error_format=std::string(), const G::Identity &=G::Identity::invalid())
Constructor for an object that can be start()ed or run().
Definition: gtask.cpp:229
GNet::Task::~Task
~Task()
Destructor.
Definition: gtask.cpp:239
GNet::Task::stop
void stop()
Attempts to kill the spawned process.
Definition: gtask.cpp:250
GNet::Task::start
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:273
GNet::Task::run
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:259
GNet::Timer
A timer class template in which the timeout is delivered to the specified method.
Definition: gtimer.h:129
G::Environment
Holds a set of environment variables and also provides static methods to wrap getenv() and putenv().
Definition: genvironment.h:39
G::Environment::minimal
static Environment minimal()
Returns a minimal, safe set of environment variables.
Definition: genvironment_unix.cpp:46
G::ExecutableCommand
A structure representing an external program, holding a path and a set of arguments.
Definition: gexecutablecommand.h:44
G::Identity
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:43
G::NewProcess
A class for creating new processes.
Definition: gnewprocess.h:64
G::Path
A Path object represents a file system path.
Definition: gpath.h:72
G::Str::printable
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:885
G::Test::enabled
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
gassert.h
gcleanup.h
gfutureevent.h
glog.h
gnewprocess.h
gstr.h
gtask.h
gtest.h
gtimer.h
G::Cleanup::Block
A RAII class to temporarily block signal delivery.
Definition: gcleanup.h:44
G::NewProcess::Fd
Wraps up a file descriptor for passing to G::NewProcess.
Definition: gnewprocess.h:77