//-----------------------------------------------------------------------------------
// <copyright file="LqrSMACAsynchronousProcessExecutor.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
//   Implementation of AsynchronousProcessExecutor, which encapsulates asynchronous
//   execution of a process and fires events for process output and exit of the
//   process.
// </summary>
//
// <owner>Mike Cookson</owner>
//-----------------------------------------------------------------------------------

#include "LqrSMACAsynchronousProcessExecutor.h"
#include "LqrSMACCmdLineParser.h"
#include "LqrSMACGetErrorDescription.h"
#include "LqrSMACProcessTree.h"
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <unistd.h>
#include <boost/algorithm/string.hpp>
#include <boost/foreach.hpp>

#define IGNORE_RESULT(phrase) do { switch(phrase) { default: break; } } while(0)

using namespace std;

namespace LiquidRobotics {
namespace SMACFramework {

IMPLEMENT_INNER_EXCEPTION(AsynchronousProcessExecutor, ExecFailed, std::runtime_error);
IMPLEMENT_INNER_EXCEPTION(AsynchronousProcessExecutor, SystemError, std::runtime_error);

AsynchronousProcessExecutor::BackgroundThreadRunResult::BackgroundThreadRunResult()
: sem(0)
, systemErr(false)
{
}

void AsynchronousProcessExecutor::BackgroundThreadRunResult::AwaitResult()
{
	sem.Wait();

	if (exceptionText.length())
	{
		if (systemErr)
			throw SystemError(exceptionText);
		else
			throw ExecFailed(exceptionText);
	}
}

void AsynchronousProcessExecutor::BackgroundThreadRunResult::ReportExecFail(const string& msg)
{
	exceptionText = msg;
	sem.Post();
}

void AsynchronousProcessExecutor::BackgroundThreadRunResult::ReportSystemErr(const string& msg)
{
	exceptionText = msg;
	systemErr = true;
	sem.Post();
}

void AsynchronousProcessExecutor::BackgroundThreadRunResult::ReportSuccess()
{
	sem.Post();
}

AsynchronousProcessExecutor::AsynchronousProcessExecutor(Logger::Ptr _logger
							  , int _defaultKillSignal
							  , bool _killOnDelete
							  , int _deleteKillSignal
							  , int _pollingMilliseconds
							  , bool _routeStdErrToStdOut
							  , size_t _readSize)
: logger(_logger)
, loggerPtr(NULL)
, defaultKillSignal(_defaultKillSignal)
, killOnDelete(_killOnDelete)
, deleteKillSignal(_deleteKillSignal)
, pollingMilliseconds(_pollingMilliseconds)
, routeStdErrToStdOut(_routeStdErrToStdOut)
, readSize(_readSize)
, extremelyVerbose(false)
, runningCommand(false)
, resultCode(-1)
, pThread(NULL)
, pid((pid_t) -1)
, exited(false)
{
}

AsynchronousProcessExecutor::AsynchronousProcessExecutor(Logger& _logger
							  , int _defaultKillSignal
							  , bool _killOnDelete
							  , int _deleteKillSignal
							  , int _pollingMilliseconds
							  , bool _routeStdErrToStdOut
							  , size_t _readSize)
: loggerPtr(&_logger)
, defaultKillSignal(_defaultKillSignal)
, killOnDelete(_killOnDelete)
, deleteKillSignal(_deleteKillSignal)
, pollingMilliseconds(_pollingMilliseconds)
, routeStdErrToStdOut(_routeStdErrToStdOut)
, readSize(_readSize)
, extremelyVerbose(false)
, runningCommand(false)
, resultCode(-1)
, pThread(NULL)
, pid((pid_t) -1)
, exited(false)
{
}

AsynchronousProcessExecutor::~AsynchronousProcessExecutor()
{
	Lock lock(*this);

	if (killOnDelete && runningCommand)
		Kill(deleteKillSignal);

	Pipes::Ptr sdp = shutdownPipe;

	if (sdp && sdp->WritePipeOpen())
		IGNORE_RESULT( write(sdp->WritePipe(), "\r\n", 2) );

	if (pThread)
	{
		lock.unlock();
		pThread->join();
		lock.lock();
		delete pThread;
	}
}

int AsynchronousProcessExecutor::GetResultCode() const
{
	Lock lock(const_cast<AsynchronousProcessExecutor&>(*this));

	if (runningCommand)
		throw runtime_error("AsynchronousProcessExecutor cannot get process result code: process is running");

	return resultCode;
}

void AsynchronousProcessExecutor::Kill()
{
	Kill(defaultKillSignal);
}

void AsynchronousProcessExecutor::Kill(int sigNum)
{
	Lock lock(*this);

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Debug << "AsynchronousProcessExecutor killing child processes with signal " << sigNum << endl;
	}

	if (!runningCommand)
		throw runtime_error("AsynchronousProcessExecutor cannot kill running process since no process is running");

	ProcessTree processTree(true, false);
	ProcessTree::PIDList subProcs;

	processTree.GetPidListBelow(pid, subProcs, true);

#if 0
	BOOST_FOREACH(pid_t subProcPID, subProcs)
		try { CheckedKill(subProcPID, sigNum, false); } catch(...) { }

	try { CheckedKill(pid, sigNum, false); } catch(...) { }
#else
	stringstream cmd;
	cmd << "sudo kill -" << sigNum;

	BOOST_FOREACH(pid_t subProcPID, subProcs)
		cmd << " " << subProcPID;

	cmd << " " << pid;

	string cmdStr = cmd.str();

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Debug << "Issuing kill command '" << cmdStr << "'" << endl;
	}

	int retCode = system(cmdStr.c_str());

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Debug << "Kill command returned " << retCode << endl;
	}
#endif
}

bool AsynchronousProcessExecutor::ProcessExecuting() const
{
	Lock lock(const_cast<AsynchronousProcessExecutor&>(*this));
	return runningCommand;
}

bool AsynchronousProcessExecutor::ProcessExited() const
{
	Lock lock(const_cast<AsynchronousProcessExecutor&>(*this));
	return exited;
}

void AsynchronousProcessExecutor::RunAsynch(const string& commandLine, bool usePath, bool enableReadPipe)
{
	Lock lock(*this);

	if (runningCommand)
		throw runtime_error("AsynchronousProcessExecutor cannot run another command since there is a command running");

	string trimmed = commandLine;
	boost::trim(trimmed);

	if (trimmed.empty())
		throw invalid_argument("AsynchronousProcessExecutor cannot run empty command line");

	exited = false;

	if (pThread)
	{
		delete pThread;
		pThread = NULL;
	}

	string trimmedCommand = commandLine;
	boost::trim(trimmedCommand);

	BackgroundThreadRunResult::Ptr runResult;
	runResult.reset(new BackgroundThreadRunResult);
	shutdownPipe.reset(new Pipes);
	Pipes::MakeCloseOnExec(shutdownPipe->ReadPipe());
	Pipes::MakeCloseOnExec(shutdownPipe->WritePipe());
	pThread = new boost::thread(
				boost::bind(&AsynchronousProcessExecutor::BackgroundThread<string>, this, trimmedCommand, usePath, enableReadPipe, runResult));
	runningCommand = true;
	lock.unlock();

	try
	{
		runResult->AwaitResult();
	}
	catch(...)
	{
		pThread->join();
		lock.lock();
		runningCommand = false;
		pThread = NULL;
		throw;
	}
}

void AsynchronousProcessExecutor::RunAsynch(const StringArray& commandTokens, bool usePath, bool enableReadPipe)
{
	Lock lock(*this);

	if (runningCommand)
		throw runtime_error("AsynchronousProcessExecutor cannot run another command since there is a command running");

	exited = false;

	if (pThread)
	{
		delete pThread;
		pThread = NULL;
	}

	BackgroundThreadRunResult::Ptr runResult;
	runResult.reset(new BackgroundThreadRunResult);
	shutdownPipe.reset(new Pipes);
	Pipes::MakeCloseOnExec(shutdownPipe->ReadPipe());
	Pipes::MakeCloseOnExec(shutdownPipe->WritePipe());
	pThread = new boost::thread(
				boost::bind(&AsynchronousProcessExecutor::BackgroundThread<const StringArray>, this, commandTokens, usePath, enableReadPipe, runResult));
	runningCommand = true;
	lock.unlock();

	try
	{
		runResult->AwaitResult();
	}
	catch(...)
	{
		pThread->join();
		lock.lock();
		runningCommand = false;
		pThread = NULL;
		throw;
	}
}

inline void Set(int fd, int& nfds, fd_set& theSet)
{
	if (fd >= nfds)
		nfds = fd+1;

	FD_SET(fd, &theSet);
}

inline int SafeGetReadPipeFD(Pipes::Ptr pipe)
{
	if (pipe && pipe->ReadPipeOpen())
		return pipe->ReadPipe();
	else
		return -1;
}

template<typename CMDTYPE>
void AsynchronousProcessExecutor::DoExec(Pipes::Ptr& procPipe, Pipes::Ptr& procPipe2, Pipes::Ptr& readPipe, Pipes::Ptr& errPipe, CMDTYPE commandLine,
		bool usePath, bool enableReadPipe, BackgroundThreadRunResult::Ptr runResult)
{
	procPipe.reset(new Pipes);
	Pipes::MakeNonBlocking(procPipe->ReadPipe());		// reads return immediately, so an error byte will be returned without blocking
	procPipe2.reset(new Pipes);
	Pipes::MakeCloseOnExec(procPipe2->WritePipe());		// write handles closes on exec, so that running the new program will make reads fail without blocking

	if (enableReadPipe)
	{
		readPipe.reset(new Pipes);
		Pipes::MakeNonBlocking(readPipe->ReadPipe());

		if (!routeStdErrToStdOut)
		{
			errPipe.reset(new Pipes);
			Pipes::MakeNonBlocking(errPipe->ReadPipe());
		}
	}

	pid = fork();

	if (pid == 0)
		ExecChild(procPipe, procPipe2, readPipe, errPipe, commandLine, usePath, runResult);

	else if (pid == -1)
	{
		int errSave = errno;
		stringstream msg;
		msg << "AsynchronousProcessExecutor fork failed with error#" << errSave << " '" << GetErrorDescription(errSave) << "'";
		runResult->ReportSystemErr(msg.str());
		throw SystemError(msg.str());
	}

	else
		ExecParentStart(procPipe, procPipe2, readPipe, errPipe, commandLine, runResult);
}

template<typename CMDTYPE>
void AsynchronousProcessExecutor::ExecChild(Pipes::Ptr& procPipe, Pipes::Ptr& procPipe2, Pipes::Ptr& readPipe, Pipes::Ptr& errPipe,
		CMDTYPE& commandLine, bool usePath, BackgroundThreadRunResult::Ptr runResult)
{
	// no need to unlock the mutex

	try
	{
		shutdownPipe->Close();
		procPipe->CloseReadPipe();
		procPipe2->CloseReadPipe();

		if (readPipe) 		readPipe->CloseReadPipe();
		if (errPipe) 		errPipe->CloseReadPipe();

		close(0);
		close(1);
		close(2);

		if (readPipe)
		{
			dup2(readPipe->WritePipe(), 1);

			if (routeStdErrToStdOut)
				dup2(readPipe->WritePipe(), 2);
			else if (errPipe)
				dup2(errPipe->WritePipe(), 2);

			readPipe.reset();
			errPipe.reset();
		}
	}
	catch(exception& ex)
	{
		exit(6);
	}

	DoExec(commandLine, usePath);

	int errSave = errno;
	IGNORE_RESULT( write(procPipe->WritePipe(), &errSave, sizeof(errSave)) );
	exit(7);
}

void AsynchronousProcessExecutor::DoExec(const string& commandLine, bool usePath)
{
	if (usePath)
		execlp("/bin/sh", "/bin/sh", "-c", commandLine.c_str(), NULL);
	else
		execl("/bin/sh", "/bin/sh", "-c", commandLine.c_str(), NULL);
}

void AsynchronousProcessExecutor::DoExec(const StringArray& commandTokens, bool usePath)
{
	std::vector<const char*> tokens2;
	tokens2.reserve(commandTokens.size()+1);

	for (StringArray::const_iterator it = commandTokens.begin(); it != commandTokens.end(); ++it)
		tokens2.push_back(it->c_str());

	tokens2.push_back(NULL);

	if (usePath)
		execvp(tokens2[0], const_cast<char**>(&tokens2.front()));
	else
		execv(tokens2[0], const_cast<char**>(&tokens2.front()));
}

template<typename CMDTYPE>
void AsynchronousProcessExecutor::ExecParentStart(Pipes::Ptr& procPipe, Pipes::Ptr& procPipe2, Pipes::Ptr& readPipe, Pipes::Ptr& errPipe,
		CMDTYPE& commandLine, BackgroundThreadRunResult::Ptr runResult)
{
	// close the end of the pipe that we're not going to use
	procPipe->CloseWritePipe();
	procPipe2->CloseWritePipe();

	if (readPipe) 		readPipe->CloseWritePipe();
	if (errPipe) 		errPipe->CloseWritePipe();

	// waits for close on exit of procPipe2
	char temp[100];
	IGNORE_RESULT( read(procPipe2->ReadPipe(), temp, sizeof(temp)) );

	procPipe2.reset();

	int childErr = 0;

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Debug << "AsynchronousProcessExecutor waiting for child to start" << endl;
	}

	// read any result from procPipe and result in an error if an error code was read
	if (read(procPipe->ReadPipe(), &childErr, sizeof(childErr)) == sizeof(childErr))
	{
		stringstream msg;
		msg << "AsynchronousProcessExecutor exec of command line '" << GetCommandLineSummary(commandLine) << "'"
		    << " failed with error#" << childErr << " '" << GetErrorDescription(childErr) << "'";
		runResult->ReportExecFail(msg.str());
		throw ExecFailed(msg.str());
	}

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Debug << "AsynchronousProcessExecutor child started" << endl;
	}

	runResult->ReportSuccess();
}

AsynchronousProcessExecutor::string AsynchronousProcessExecutor::GetCommandLineSummary(const string& cmdLine)
{
	return cmdLine;
}

AsynchronousProcessExecutor::string AsynchronousProcessExecutor::GetCommandLineSummary(const StringArray& cmdLine)
{
	stringstream tmp;
	bool first = true;

	for (StringArray::const_iterator ci = cmdLine.begin(); ci != cmdLine.end(); ++ci)
	{
		if (first)
			first = false;
		else
			tmp << " ";

		tmp << " \"" << *ci << "\"";
	}

	string result = tmp.str();
	return result;
}

template<typename CMDTYPE>
void AsynchronousProcessExecutor::BackgroundThread(CMDTYPE commandLine, bool usePath, bool enableReadPipe,
		BackgroundThreadRunResult::Ptr runResult)
{
	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Info << "AsynchronousProcessExecutor background thread started" << endl;
	}

	Pipes::Ptr procPipe, procPipe2, readPipe, errPipe;

	try
	{
		DoExec(procPipe, procPipe2, readPipe, errPipe, commandLine, usePath, enableReadPipe, runResult);
		ExecParentComplete(procPipe, readPipe, errPipe);
	}
	catch(exception& exc)
	{
		if (HaveLogger())
		{
			Logger::Lock logLock(GetLogger());
			GetLogger().Error << "AsynchronousProcessExecutor background thread caught " << typeid(exc).name() << ": " << exc.what() << endl;
		}
	}

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Info << "AsynchronousProcessExecutor exiting" << endl;
	}
}

void AsynchronousProcessExecutor::ExecParentComplete(Pipes::Ptr& procPipe, Pipes::Ptr& readPipe, Pipes::Ptr& errPipe)
{
	Lock lock(*this);

	try
	{
		bool looping = true;
		bool procExit = false;
		Pipes::Ptr pptr;
		int fd0;

		while(!!(looping = looping && ((fd0 = SafeGetReadPipeFD(shutdownPipe)) != -1)))
		{
			fd_set readSet;
			fd_set exceptSet;
			int nfds = 0;

			FD_ZERO(&readSet);
			FD_ZERO(&exceptSet);

			// enable events from shutdown pipe
			Set(fd0, nfds, readSet);
			Set(fd0, nfds, exceptSet);

			// enable events from process pipe
			if (!procExit && ((fd0 = SafeGetReadPipeFD(procPipe)) != -1))
			{
				Set(fd0, nfds, readSet);
				Set(fd0, nfds, exceptSet);
			}

			// enable events from read pipe
			if ((fd0 = SafeGetReadPipeFD(readPipe)) != -1)
				Set(fd0, nfds, readSet);

			// enable events from error pipe
			if ((fd0 = SafeGetReadPipeFD(errPipe)) != -1)
				Set(fd0, nfds, readSet);

			lock.unlock();

			if (!nfds)
				break;

			//timeval tv;
			//tv.tv_sec = pollingMilliseconds/1000;;
			//tv.tv_usec = 1000*(pollingMilliseconds%1000);

			int status = select(nfds, &readSet, NULL, &exceptSet, NULL); //&tv);
			int errSave = errno;

			lock.lock();

			if (HaveLogger())
			{
				stringstream ss;

				ss << "AsynchronousProcessExecutor select() returned " << status;

				if (status == -1)
					ss << " error#" << errSave << " '" << GetErrorDescription(errSave) << "'";

				ss << endl;

				if ((status == -1) || extremelyVerbose)
				{
					Logger& logRef = GetLogger();
					Logger::Lock logLock(logRef);
					logRef.Debug << ss.str();
				}
			}

			looping = ((fd0 = SafeGetReadPipeFD(shutdownPipe)) != -1);

			if (looping)
				looping = !FD_ISSET(fd0, &readSet) && !FD_ISSET(fd0, &exceptSet);

			if (!looping)
				continue;

			bool readData = false;

			if (((fd0 = SafeGetReadPipeFD(readPipe)) != -1) && FD_ISSET(fd0, &readSet))
				if (ReportProcessOutput(fd0, false, lock))
					readData = true;

			if (((fd0 = SafeGetReadPipeFD(errPipe)) != -1) && FD_ISSET(fd0, &readSet))
				if (ReportProcessOutput(fd0, true, lock))
					readData = true;

			if (((fd0 = SafeGetReadPipeFD(procPipe)) != -1) && (FD_ISSET(fd0, &readSet) || FD_ISSET(fd0, &exceptSet)))
				procExit = true;

			looping = readData || !procExit;
		}

		if (procExit)
		{
			if (HaveLogger())
			{
				Logger::Lock logLock(GetLogger());
				GetLogger().Debug << "AsynchronousProcessExecutor process pipe closed, waiting for pid " << pid << " to return" << endl;
			}

			int waitResultCode = -1;
			pid_t result = waitpid(pid, &waitResultCode, 0);
			int errSave = errno;

			resultCode = waitResultCode;

			if (HaveLogger())
			{
				Logger& logRef = GetLogger();
				Logger::Lock logLock(logRef);
				logRef.Notice << "AsynchronousProcessExecutor waitpid(" << pid << ") returned " << result;

				if (result == -1)
					logRef.Notice << " error#" << errSave << " '" << GetErrorDescription(errSave) << "'";
				else
				{
					bool exited = WIFEXITED(waitResultCode);
					int exitStatus = exited ? WEXITSTATUS(waitResultCode) : 1;
					bool signaled = WIFSIGNALED(waitResultCode);
					logRef.Notice << " resultCode " << waitResultCode << " exited=" << exited << " exitStatus=" << exitStatus << " signaled=" << signaled;
				}

				logRef.Notice <<endl;
			}

			runningCommand = false;
			procPipe.reset();
			readPipe.reset();
			errPipe.reset();

			exited = true;

			lock.unlock();
			ExitSignal(*this, waitResultCode);
			lock.lock();
		}
	}
	catch(exception& ex)
	{
		if (HaveLogger())
		{
			Logger::Lock logLock(GetLogger());
			GetLogger().Error << "AsynchronousProcessExecutor background thread caught " << typeid(ex).name() << ": " << ex.what() << endl;
		}
	}

	if (HaveLogger())
	{
		Logger::Lock logLock(GetLogger());
		GetLogger().Info << "AsynchronousProcessExecutor background thread exiting" << endl;
	}
}

bool AsynchronousProcessExecutor::ReportProcessOutput(int fd, bool fromStdErr, Lock& theLock)
{
	int status = 1;
	bool readOnce = false;
	ByteArray readBuffer;

	readBuffer.reserve(readSize);
	readBuffer.resize(readSize);

	//while((status > 0) && pipe && pipe->ReadPipeOpen())
	if (fd != -1)
	{
		status = read(fd, &readBuffer.front(), readBuffer.size());

		if (status > 0)
		{
			readOnce = true;
			readBuffer.resize(status);

			if (extremelyVerbose && HaveLogger())
			{
				Logger::Lock logLock(GetLogger());
				GetLogger().Notice << "AsynchronousProcessExecutor read " << status
							       << " bytes from process output (stderr=" << fromStdErr << ")" << endl;
			}

			theLock.unlock();
			ReportSignal(*this, readBuffer, fromStdErr);
			theLock.lock();
		}
	}

	if (readOnce)
	{
		readBuffer.resize(0);
		theLock.unlock();
		ReportSignal(*this, readBuffer, fromStdErr);
		theLock.lock();
	}

	return readOnce;
}

bool AsynchronousProcessExecutor::CheckedKill(pid_t pid, int sigType, bool throwIfBadPID)
{
	int status = kill(pid, sigType);
	int errSave = errno;

	if ((status == -1) && (throwIfBadPID || (errSave != ESRCH)))
	{
		stringstream msg;
		msg << "AsynchronousProcessExecutor kill pid=" << pid << " signal=" << sigType << " failed with error#"
			<< errSave << ": '" << GetErrorDescription(errSave) << "'";

		throw SystemError(msg.str());
	}

	return status == 0;
}

AsynchronousProcessExecutor::ScopedConnectionPtr AsynchronousProcessExecutor::SubscribeExitSignal(ExitSignalType::slot_type slot)
{
	ScopedConnectionPtr result;

	result.reset(new ScopedConnection(ExitSignal.connect(slot)));
	return result;
}

AsynchronousProcessExecutor::ScopedConnectionPtr AsynchronousProcessExecutor::SubscribeReportSignal(ReportSignalType::slot_type slot)
{
	ScopedConnectionPtr result;

	result.reset(new ScopedConnection(ReportSignal.connect(slot)));
	return result;
}

bool AsynchronousProcessExecutor::HaveLogger() const
{
	return logger || loggerPtr;
}

Logger& AsynchronousProcessExecutor::GetLogger() const
{
	if (logger)
		return *logger;
	else if (loggerPtr)
		return *loggerPtr;
	else
		throw runtime_error("AsynchronousProcessExecutor does not have a logger configured");
}

bool AsynchronousProcessExecutor::GetExtremelyVerbose() const
{
	return extremelyVerbose;
}

void AsynchronousProcessExecutor::SetExtremelyVerbose(bool newValue)
{
	extremelyVerbose = newValue;
}

}
}
