AGS: Test for hanging script using timeout instead of while loops

From upstream c34f0c96596f1a7814c15e7a06c2c498cb95ebfc
This commit is contained in:
Thierry Crozat
2022-06-23 23:57:27 +01:00
parent aa9983c28d
commit 6f665dfe68
6 changed files with 53 additions and 20 deletions
+1 -1
View File
@@ -417,7 +417,7 @@ HGameInitError InitGameState(const LoadedGameEntities &ents, GameDataVersion dat
// NOTE: we must do this before plugin start, because some plugins may
// require access to script API at initialization time.
//
ccSetScriptAliveTimer(150000);
ccSetScriptAliveTimer(10u, 1000u);
ccSetStringClassImpl(&_GP(myScriptStringImpl));
setup_script_exports(base_api, compat_api);
+33 -10
View File
@@ -39,6 +39,7 @@
#include "ags/engine/ac/dynobj/script_user_object.h"
#include "ags/engine/ac/statobj/ags_static_object.h"
#include "ags/engine/ac/statobj/static_array.h"
#include "ags/engine/ac/sys_events.h"
#include "ags/engine/ac/dynobj/cc_dynamic_object_addr_and_manager.h"
#include "ags/shared/util/memory.h"
#include "ags/shared/util/string_utils.h" // linux strnicmp definition
@@ -206,7 +207,6 @@ struct FunctionCallStack {
int Count;
};
ccInstance *ccInstance::GetCurrentInstance() {
return _GP(InstThreads).size() > 0 ? _GP(InstThreads).back() : nullptr;
}
@@ -226,6 +226,11 @@ ccInstance *ccInstance::CreateEx(PScript scri, ccInstance *joined) {
return cinst;
}
void ccInstance::SetExecTimeout(unsigned sys_poll_ms, unsigned abort_ms) {
_G(timeoutCheckMs) = sys_poll_ms;
_G(timeoutAbortMs) = abort_ms;
}
ccInstance::ccInstance() {
flags = 0;
globaldata = nullptr;
@@ -431,7 +436,6 @@ int ccInstance::Run(int32_t curpc) {
int32_t thisbase[MAXNEST], funcstart[MAXNEST];
int was_just_callas = -1;
int curnest = 0;
int loopIterations = 0;
int num_args_to_func = -1;
int next_call_needs_object = 0;
int loopIterationCheckDisabled = 0;
@@ -441,9 +445,13 @@ int ccInstance::Run(int32_t curpc) {
bool write_debug_dump = ccGetOption(SCOPT_DEBUGRUN) ||
(gDebugLevel > 0 && DebugMan.isDebugChannelEnabled(::AGS::kDebugScript));
ScriptOperation codeOp;
FunctionCallStack func_callstack;
const auto timeout = std::chrono::milliseconds(_G(timeoutCheckMs));
const auto timeout_abort = std::chrono::milliseconds(_G(timeoutAbortMs));
_lastAliveTs = AGS_Clock::now();
bool timeout_warn = false;
while ((flags & INSTF_ABORTED) == 0) {
if (_G(abort_engine))
return -1;
@@ -774,15 +782,30 @@ int ccInstance::Run(int32_t curpc) {
case SCMD_JMP:
pc += arg1.IValue;
if ((arg1.IValue < 0) && (_G(maxWhileLoops) > 0) && (loopIterationCheckDisabled == 0)) {
// Make sure it's not stuck in a While loop
loopIterations ++;
// Make sure it's not stuck in a While loop
if (arg1.IValue < 0) {
auto now = AGS_Clock::now();
auto test_dur = std::chrono::duration_cast<std::chrono::milliseconds>(now - _lastAliveTs);
if (flags & INSTF_RUNNING) {
loopIterations = 0;
// was notified still running, don't do anything
flags &= ~INSTF_RUNNING;
} else if (loopIterations > _G(maxWhileLoops)) {
cc_error("!Script appears to be hung (a while loop ran %d times). The problem may be in a calling function; check the call stack.", loopIterations);
return -1;
_lastAliveTs = now;
timeout_warn = false;
} else if (test_dur > timeout) {
// minimal timeout occured
if (test_dur.count() > timeout_abort.count()) {
// critical timeout occured
if (loopIterationCheckDisabled == 0) {
cc_error("!Script appears to be hung (no game update for %lld ms). The problem may be in a calling function; check the call stack.", test_dur.count());
return -1;
}
if (!timeout_warn) {
debug_script_warn("WARNING: script execution hung? (%lld ms)", test_dur.count());
timeout_warn = true;
}
}
// at least let user to manipulate the game window
sys_evt_process_pending();
}
}
break;
+6 -1
View File
@@ -24,6 +24,7 @@
#include "ags/lib/std/memory.h"
#include "ags/lib/std/map.h"
#include "ags/engine/ac/timer.h"
#include "ags/shared/script/cc_internal.h"
#include "ags/shared/script/cc_script.h" // ccScript
#include "ags/engine/script/non_blocking_script_function.h"
@@ -144,6 +145,7 @@ public:
// create a runnable instance of the supplied script
static ccInstance *CreateFromScript(PScript script);
static ccInstance *CreateEx(PScript scri, ccInstance *joined);
static void SetExecTimeout(unsigned sys_poll_ms, unsigned abort_ms);
ccInstance();
~ccInstance();
@@ -176,7 +178,7 @@ public:
// Also change CALLEXT op-codes to CALLAS when they pertain to a script instance
bool ResolveImportFixups(const ccScript *scri);
protected:
private:
bool _Create(PScript scri, ccInstance *joined);
// free the memory associated with the instance
void Free();
@@ -213,6 +215,9 @@ protected:
// Function call stack processing
void PushToFuncCallStack(FunctionCallStack &func_callstack, const RuntimeScriptValue &rval);
void PopFromFuncCallStack(FunctionCallStack &func_callstack, int32_t num_entries);
// Last time the script was noted of being "alive"
AGS_Clock::time_point _lastAliveTs;
};
extern void script_commands_init();
+3 -5
View File
@@ -42,8 +42,6 @@
namespace AGS3 {
// static const char ccRunnerCopyright[] = "ScriptExecuter32 v" SCOM_VERSIONSTR " (c) 2001 Chris Jones";
bool ccAddExternalStaticFunction(const String &name, ScriptAPIFunction *pfn) {
return _GP(simp).add(name, RuntimeScriptValue().SetStaticFunction(pfn), nullptr) != UINT32_MAX;
}
@@ -115,9 +113,9 @@ Plugins::PluginMethod ccGetSymbolAddressForPlugin(const String &name) {
// If a while loop does this many iterations without the
// NofityScriptAlive function getting called, the script
// aborts. Set to 0 to disable.
void ccSetScriptAliveTimer(int numloop) {
_G(maxWhileLoops) = numloop;
}
void ccSetScriptAliveTimer(unsigned sys_poll_timeout, unsigned abort_timeout) {
ccInstance::SetExecTimeout(sys_poll_timeout, abort_timeout);
}
void ccNotifyScriptStillAlive() {
ccInstance *cur_inst = ccInstance::GetCurrentInstance();
+4 -2
View File
@@ -77,8 +77,10 @@ extern Plugins::PluginMethod ccGetSymbolAddressForPlugin(const String &name);
typedef void (*new_line_hook_type)(ccInstance *, int);
extern void ccSetDebugHook(new_line_hook_type jibble);
// Set the number of while loop iterations that aborts the script
extern void ccSetScriptAliveTimer(int);
// Set the script interpreter timeout values, in milliseconds:
// * sys_poll_timeout - defines the timeout at which the interpreter will run system events poll;
// * abort_timeout - defines the timeout at which the interpreter will cancel with error.
extern void ccSetScriptAliveTimer(unsigned sys_poll_timeout, unsigned abort_timeout);
// reset the current while loop counter
extern void ccNotifyScriptStillAlive();
// for calling exported plugin functions old-style
+6 -1
View File
@@ -1279,7 +1279,12 @@ public:
*/
new_line_hook_type _new_line_hook = nullptr;
int _maxWhileLoops = 0;
// Minimal timeout: how much time may pass without any engine update
// before we want to check on the situation and do system poll
unsigned _timeoutCheckMs = 60u;
// Critical timeout: how much time may pass without any engine update
// before we abort or post a warning
unsigned _timeoutAbortMs = 60u * 10;
ccInstance *_loadedInstances[MAX_LOADED_INSTANCES];
/**@}*/