633 lines
19 KiB
C++
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");
|
|
}
|
|
|
|
}
|