/******************************************************************************* * 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 . * --------------------------------------------------------------------------- * 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 #include #include #include // 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"); } }