povray/source/frontend/shelloutprocessing.cpp
2013-11-06 13:07:19 -05:00

633 lines
19 KiB
C++

/*******************************************************************************
* shelloutprocessing.cpp
*
* ---------------------------------------------------------------------------
* Persistence of Vision Ray Tracer ('POV-Ray') version 3.7.
* Copyright 1991-2013 Persistence of Vision Raytracer Pty. Ltd.
*
* POV-Ray is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* POV-Ray is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------------
* POV-Ray is based on the popular DKB raytracer version 2.12.
* DKBTrace was originally written by David K. Buck.
* DKBTrace Ver 2.0-2.12 were written by David K. Buck & Aaron A. Collins.
* ---------------------------------------------------------------------------
* $File: //depot/public/povray/3.x/source/frontend/shelloutprocessing.cpp $
* $Revision: #1 $
* $Change: 6069 $
* $DateTime: 2013/11/06 11:59:40 $
* $Author: chrisc $
*******************************************************************************/
#include <string>
#include <cctype>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
// configbase.h must always be the first POV file included within base *.cpp files
#include "base/configbase.h"
#include "base/types.h"
#include "base/povmscpp.h"
#include "base/povmsgid.h"
#include "frontend/configfrontend.h"
#include "frontend/shelloutprocessing.h"
// this must be the last file included
#include "base/povdebug.h"
namespace pov_frontend
{
using namespace pov_base;
ShelloutAction::ShelloutAction(const ShelloutProcessing *sp, unsigned int attribID, POVMS_Object& opts): returnAction(ignore), returnNegate(false), isSet(false)
{
if (opts.Exist(attribID))
{
POVMS_Object attr;
opts.Get(attribID, attr);
if (attr.Exist(kPOVAttrib_CommandString))
{
rawCommand = attr.GetString(kPOVAttrib_CommandString);
if (sp->ExtractCommand(rawCommand, command, rawParameters))
{
int action = attr.TryGetInt(kPOVAttrib_ReturnAction, (int) ignore);
switch (tolower(action))
{
case ignore:
case skipOne:
case skipAll:
case quit:
case abort:
case fatal:
returnAction = (Action) tolower(action);
returnNegate = isupper(action);
isSet = true;
break;
default:
break;
}
}
}
}
}
// expand the parameters in the rawParameters member into the parameters member, applying the escapes
// documented below. scene is the scene name, ofn the output file name, w/h the width/height, clock the
// current animation clock value, and frame the current frame number.
void ShelloutAction::ExpandParameters(const string& scene, const string& ofn, unsigned int w, unsigned int h, float clock, unsigned int frame)
{
// %o: output file name in full
// %s: scene name without path or extension
// %n: frame number
// %k: clock value
// %h: height
// %w: width
// %%: percent character
parameters.clear();
for (const char *cmd = rawParameters.c_str(); *cmd; cmd++)
{
if (*cmd != '%')
{
parameters.push_back(*cmd);
continue;
}
switch (*++cmd)
{
case '%':
parameters.append("%%");
break;
case 'o':
parameters.append(ofn);
break;
case 's':
parameters.append(scene);
break;
case 'n':
parameters.append(str(boost::format("%u") % frame));
break;
case 'k':
parameters.append(str(boost::format("%#.6f") % clock));
break;
case 'h':
parameters.append(str(boost::format("%u") % h));
break;
case 'w':
parameters.append(str(boost::format("%u") % w));
break;
default:
parameters.push_back('%');
parameters.push_back(*cmd);
break;
}
}
}
// ------------------------------------------------------------------
ShelloutProcessing::ShelloutProcessing(POVMS_Object& opts, const string& scene, unsigned int width, unsigned int height): sceneName(scene), imageWidth(width), imageHeight(height)
{
skipReturn = cancelReturn = exitCode = 0;
clockVal = 0.0;
frameNo = 0;
hadPreScene = hadPostScene = killRequested = skipCallouts = cancelRender = skipNextFrame = skipAllFrames = false;
commandProhibited = processStartRequested = hadUserAbort = hadFatalError = false;
postProcessEvent = lastShelloutEvent;
cancelFormat.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
skipFormat.exceptions(boost::io::all_error_bits ^ boost::io::too_many_args_bit);
shellouts[preFrame].reset(new ShelloutAction(this, kPOVAttrib_PreFrameCommand, opts));
shellouts[postFrame].reset(new ShelloutAction(this, kPOVAttrib_PostFrameCommand, opts));
shellouts[preScene].reset(new ShelloutAction(this, kPOVAttrib_PreSceneCommand, opts));
shellouts[postScene].reset(new ShelloutAction(this, kPOVAttrib_PostSceneCommand, opts));
shellouts[userAbort].reset(new ShelloutAction(this, kPOVAttrib_UserAbortCommand, opts));
shellouts[fatalError].reset(new ShelloutAction(this, kPOVAttrib_FatalErrorCommand, opts));
}
ShelloutProcessing::~ShelloutProcessing()
{
}
// called by the internal parser during construction to separate commands from parameters.
// given a raw string in the form returned from the POV INI file, extract the command and any parameters.
// the default version of this method should suffice for most implementations, however some platforms
// may need different treatment of quotes or escapes. for example, the windows platform may wish to
// provide a special-case for strings that look like windows paths, and exempt them from escaping of
// the backslash.
//
// the default method will trim the source and then search it for the first whitespace character
// that is both outside of a quoted string and not escaped. this forms the boundary between the
// command and the parameters (if not found, the entire source is considered the command). the code
// accepts single and double quotes as acceptable delimiters. if quotes are found around the command,
// they are removed. no other quotes (including any within the parameters) will be altered.
//
// when parsing the command portion of the string, any backslashes found are considered to be escapes
// which remove the special meaning of the following character. the escape is removed and the next
// character will not be subject to special interpretation (this affects single quote, double quote,
// backslashes, and any whitespace characters. any escapes in the text after the point where the
// parameters start will not be removed.
//
// this method should return true if the command is non-empty upon completion.
bool ShelloutProcessing::ExtractCommand(const string& src, string& command, string& parameters) const
{
bool inSQ = false;
bool inDQ = false;
bool hadEscape = false;
const char *s;
command.clear();
parameters.clear();
string str = boost::trim_copy(src);
for (s = str.c_str(); *s; *s++)
{
if (hadEscape)
{
hadEscape = false;
command.push_back(*s);
}
else if (*s == '\\')
{
hadEscape = true;
}
else
{
if (*s == '"')
inDQ = !inDQ;
else if (*s == '\'')
inSQ = !inSQ;
else if (isspace(*s) && !inSQ && !inDQ)
break;
command.push_back(*s);
}
}
boost::trim(command);
if (command.empty())
return false;
if (command.size() > 1)
{
char ch1 = command[0];
char ch2 = command[command.size() - 1];
if ((ch1 == '\'' || ch1 == '"') && ch1 == ch2)
{
command = boost::trim_copy(command.substr(1, command.size() - 2));
if (command.empty())
return false;
}
}
parameters = boost::trim_copy(string(s));
return true;
}
string ShelloutProcessing::GetPhaseName(shelloutEvent event)
{
switch (event)
{
case preScene:
return "pre-scene";
case postScene:
return "post-scene";
case preFrame:
return "pre-frame";
case postFrame:
return "post-frame";
case userAbort:
return "user abort";
case fatalError:
return "fatal error";
default:
return "";
}
}
bool ShelloutProcessing::HandleProcessEvent(shelloutEvent event, bool internalCall)
{
if (hadPostScene)
if (event < postScene)
return cancelRender;
if (!hadPreScene)
event = preScene;
bool skipWasSet(skipCallouts);
string phaseName(GetPhaseName(event));
ShelloutPtr sh(shellouts[event]);
switch (event)
{
case preScene:
if (hadPreScene)
return cancelRender;
hadPreScene = true;
phaseName = "pre-scene";
break;
case postScene:
if (hadPostScene)
return cancelRender;
hadPostScene = true;
phaseName = "post-scene";
break;
case preFrame:
if (skipNextFrame || skipAllFrames)
return cancelRender;
phaseName = "pre-frame";
break;
case postFrame:
if (skipNextFrame || skipAllFrames)
return cancelRender;
phaseName = "post-frame";
break;
case userAbort:
if (hadUserAbort)
return cancelRender;
hadUserAbort = true;
phaseName = "user abort";
break;
case fatalError:
// if cancel render is already set, we return unless this is an internal call
if (cancelRender && !internalCall)
return true;
cancelRender = true;
skipCallouts = true;
exitCode = 1;
phaseName = "fatal error";
break;
default:
return cancelRender;
}
// skipCallouts is set if an event return indicated we should exit with no further shellouts.
// skipWasSet is initialized to its value before the above switch
if (skipWasSet)
return cancelRender;
// if there's no command set or shellouts are not supported, just return
if (!sh->IsSet() || !ShelloutsSupported())
return cancelRender;
processStartRequested = true;
runningProcessName = sh->Command();
postProcessEvent = event;
commandProhibited = false;
// perform the substitutions on the parameter list
sh->ExpandParameters(sceneName, outputFile, imageWidth, imageHeight, clockVal, frameNo);
// make sure the command is allowed to run
if (!CommandPermitted(sh->Command(), sh->Parameters()))
{
commandProhibited = true;
throw POV_EXCEPTION(kCannotOpenFileErr, str(boost::format("Execution of shellout '%1%' prohibited") % sh->Command()));
}
try
{
ExecuteCommand(sh->Command(), sh->Parameters());
}
catch(...)
{
skipCallouts = true;
throw;
}
return cancelRender;
}
bool ShelloutProcessing::PostProcessEvent(void)
{
int ret;
string output;
shelloutEvent event = postProcessEvent;
string phaseName(GetPhaseName(event));
processStartRequested = false;
runningProcessName.clear();
postProcessEvent = lastShelloutEvent;
if (event == lastShelloutEvent)
return cancelRender; // nothing to do
// we always call collect, even if commandProhibited is true
// if collect returns 0 and the command was a prohibited one, we set it to 1
if ((ret = CollectCommand(output)) == 0)
if (commandProhibited)
ret = 1;
// no need to do anything more if a kill was requested
if (killRequested)
return true;
// if the return action had the '!' or '-' prefix, we toggle the meaning of the result.
bool execResult = ret == 0;
ShelloutPtr sh(shellouts[event]);
if (sh->ReturnNegate())
execResult = !execResult;
if (execResult == false)
{
ShelloutAction::Action action = sh->ReturnAction();
if (action != ShelloutAction::ignore)
{
if (event < userAbort)
{
// common case for preScene, postScene, preFrame and postFrame
switch (action)
{
case ShelloutAction::quit:
// stop render immediately, exit code is 0
skipCallouts = cancelRender = true;
cancelPhase = phaseName;
cancelReason = "quit rendering";
cancelReturn = ret;
cancelCommand = sh->Command();
cancelParameters = sh->Parameters();
cancelOutput = LastShelloutResult();
exitCode = 0;
return true;
case ShelloutAction::abort:
// call abort, exit code is 2
cancelRender = true;
exitCode = 2;
HandleProcessEvent(userAbort, true);
cancelPhase = phaseName;
cancelReason = "generate a user abort";
cancelReturn = ret;
cancelCommand = sh->Command();
cancelParameters = sh->Parameters();
cancelOutput = LastShelloutResult();
return true;
case ShelloutAction::fatal:
// call fatal error, exit code is 1
cancelRender = true;
exitCode = 1;
HandleProcessEvent(fatalError, true);
cancelPhase = phaseName;
cancelReason = "generate a fatal error";
cancelReturn = ret;
cancelCommand = sh->Command();
cancelParameters = sh->Parameters();
cancelOutput = LastShelloutResult();
return true;
}
}
switch (event)
{
case preScene:
switch (action)
{
case ShelloutAction::skipOne:
case ShelloutAction::skipAll:
// stop render immediately, exit code is 0
cancelRender = true;
exitCode = 0;
skipPhase = cancelPhase = phaseName;
skipReason = cancelReason = "skip all frames";
skipReturn = cancelReturn = ret;
skipCommand = cancelCommand = sh->Command();
skipParameters = cancelParameters = sh->Parameters();
skipOutput = cancelOutput = LastShelloutResult();
// if result was skipAll, don't call post-scene
if (action == ShelloutAction::skipAll)
skipCallouts = true;
return true;
}
break;
case preFrame:
switch (action)
{
case ShelloutAction::skipOne:
case ShelloutAction::skipAll:
skipPhase = phaseName;
skipReturn = ret;
skipCommand = sh->Command();
skipParameters = sh->Parameters();
skipOutput = LastShelloutResult();
if (action == ShelloutAction::skipAll)
{
cancelRender = skipAllFrames = true;
skipReason = "skip all remaining frames";
exitCode = 0;
cancelPhase = phaseName;
cancelReason = "skip all frames";
cancelReturn = ret;
cancelCommand = sh->Command();
cancelParameters = sh->Parameters();
cancelOutput = LastShelloutResult();
}
else
{
skipNextFrame = true;
skipReason = str(boost::format("skip frame %1%") % (frameNo + 1));
}
break;
}
break;
case postFrame:
switch (action)
{
case ShelloutAction::skipOne:
case ShelloutAction::skipAll:
cancelRender = skipAllFrames = true;
cancelPhase = skipPhase = phaseName;
cancelReturn = skipReturn = ret;
cancelCommand = skipCommand = sh->Command();
cancelParameters = skipParameters = sh->Parameters();
cancelOutput = skipOutput = LastShelloutResult();
cancelReason = skipReason = "skip all remaining frames";
break;
}
break;
case userAbort:
cancelRender = true;
exitCode = 2;
cancelPhase = phaseName;
cancelReason = "generate a user abort";
cancelCommand = sh->Command();
cancelParameters = sh->Parameters();
cancelOutput = LastShelloutResult();
if (action == ShelloutAction::fatal)
{
// call fatal error
exitCode = 1;
HandleProcessEvent(fatalError, true);
cancelReason = "generate a fatal error";
}
cancelReturn = ret;
return true;
}
}
}
return cancelRender;
}
// returns true if a shellout is currently running. if this method is being called in an
// event loop to wait until a shellout has completed, it is the responsibility of the event
// loop to perform appropriate sleeps to avoid wasting CPU time.
bool ShelloutProcessing::ShelloutRunning(void)
{
bool result = CommandRunning();
if (processStartRequested == true && result == false)
PostProcessEvent();
return result;
}
// the message is constructed as per the documentation for the boost::format class.
// the positional parameters are as follows:
// 1: the event causing the cancel (as a string), e.g. "pre-scene"
// 2: the POV-Ray return code (as an integer)
// 3: the return code (as an upper-case string), e.g. "USER ABORT"
// 4: the return code (as a lower-case string).
// 5: the reason for the cancel (as a string), e.g. "generate a user abort"
// 6: the command name that generated the cancel
// 7: the command parameters (CAUTION: may contain escape codes)
// 8: the command return code (as an integer)
// 9: output text from the command, as returned by LastShelloutResult()
string ShelloutProcessing::GetCancelMessage(void)
{
return str(
cancelFormat % cancelPhase
% exitCode
% ExitDesc()
% boost::to_lower_copy(ExitDesc())
% cancelReason
% cancelCommand
% cancelParameters
% cancelReturn
% cancelOutput);
}
// the message is constructed as per the documentation for the boost::format class.
// the positional parameters are as follows:
// 1: the event causing the skip (as a string), e.g. "pre-scene"
// 2: the type of the skip (as a string); e.g. "skip frame 11" or "skip all frames"
// 3: the command name that generated the skip
// 4: the command parameters (CAUTION: may contain escape codes)
// 5: the command return code (as an integer)
// 6: output text from the command, as returned by LastShelloutResult()
string ShelloutProcessing::GetSkipMessage(void)
{
return str(
skipFormat % skipPhase
% skipReason
% skipCommand
% skipParameters
% skipReturn
% skipOutput);
}
// shutdown any currently-running shellouts. if force is true, force them to exit.
// in either case, don't wait more than timeout seconds. return true if there are
// no more processes running afterwards.
bool ShelloutProcessing::KillShellouts(int timeout, bool force)
{
killRequested = true;
return KillCommand(timeout, force);
}
// execute the given command with the supplied parameters, which have already
// been expanded as per the docs, and immediately return true without waiting
// for completion of the process. if the command can't be run other than for
// one of the reasons documented below, return false (in which case CollectCommand
// should return -2 if called later on).
//
// if shellouts are not supported, or access to the executable is prohibited by
// POV-Ray internal rules (not the OS), throw a kCannotOpenFile exception with an
// appropriate message. you should also throw a (different) exception if a process
// is still running. any exception thrown will cancel a render (including remaining
// frames of an animation) and the fatal error shellout (if defined) will not be
// called.
//
// you should reap any processes in your destructor in case CollectCommand doesn't
// get called.
//
// if the platform implemeting a subclass of this method has the equivalent of a
// system log (e.g. syslog on unix, event log on windows), the implementation should
// consider providing a user-controllable option to log any commands using such.
bool ShelloutProcessing::ExecuteCommand(const string& cmd, const string& params)
{
throw POV_EXCEPTION(kCannotOpenFileErr, "Shellouts not implemented on this platform");
}
}