-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Context recovery / graceful handling of bad state #1651
Comments
I agree this is desirable, I'll have to think about how we can do it but there's definitively improvements to make. One of the problem is that several of those issues ideally wants some reporting, and for more local push/pop it is nice to be able to notify the programmer locally. I'm also not really sure we want imgui to be one of those library that spams you with warnings and log (whether they are printf or a specific window for that purpose!). Even if that's hidden behind some sort of flag, it typically end up being either enabled on all machines and spammy, or disabled and unnecessary, so it's a little tricky to find the right balance. If you have any suggestion.. |
I'm not a fan of having the library spam output anywhere - how about having End() take a custom user function like this: using handler = HandlerResponse (*)(Context* context, /* args for error type and description */, void* userData);
ImGui::End(handler CustomHandler = ImGui::DefaultHandler, void* userData /*passed as-is to handler */ = nullptr); This way, code is backwards-compatible. If you don't specify a custom handler, the default handler can call IM_ASSERT(). The code also won't have to implement writing anything anywhere - that's the handler's job. And HandlerResponse can communicate info back to imgui (maybe what to do on failure - drop entire frame vs attempt to recover as much UI as you can?). What do you think? |
I'll have to experiment with Begin/End, Push/Pop variants and first see how we can reliably detect and recover. Another kind of imgui issue that may be interesting to detect are ID collisions, which can happen anywhere. For those if we want a lightweight check we'll have to do it when either widget is active. This might end up getting similar to just rewiring IM_ASSERT() when you are in the context of your scripting language, assuming imgui can recover after said assert. |
Would it be possible to save / load the context's state to a chunk of memory? I'm thinking of something like void* buffer = malloc();
ImGui::SaveState(buffer, bufferSize);
bool success = runScriptUICode();
if(!success)
{
ImGui::LoadState(buffer, bufferSize); //Back where we started
}
ImGui::EndFrame(); |
Not really, but just destroying and recreating a context generally yield good result because the windows position/size are saved. You lose things like tree node open/close state but otherwise it's not really visible. |
That's workable. Ideally though, being able to save the state once the UI code gets "borked" then returning to it once it's unborked would be great. Some of the plugin code operates on a DCC-like app's object hierarchy and tree node state specifically is valuable to me. Not to mention, this might also help with the case of restoring UI state across launches which is another issue I was planning to bring up at one point... |
Depend what you mean by UI state: Tree nodes/Collapsing header open/close state I don't think you really want to be preserving most of tree node state but it may be useful to certain certain nodes for saving. If we end up reorganizing the state storage field we could add enough info. Currently the struct is:
We could stick in a few things, e.g.
Haven't really thought about it more than that. Z-order What else? I hope in the upcoming few months I can tackle more graceful error handling tho. |
Man I don't even know what all state involved in a restore is, but I'm setting out to find out now. |
@ocornut -- Did you get a chance to think about / tackle this more? I'm exposing ImGui in a live scripting context so it's common that there are mismatches when the code a user is writing isn't 'complete' yet due to try/catch style behavior in the scripting language. I have a workaround for now by having my own utilities that take and call a closure and make sure to do the imgui.inWindow(..., function()
imgui.selectable(...)
-- Can do stuff throwing errors here, will still call `.end()` after
error('oops!')
end) Again, sorry for using a non-C++ language to show this example but the point was to show the utility allowing safe calls that I wrote in the scripting language. :) Because I have these utilities it's not super urgent for me, but wondering if you've thought further. |
That's exactly the case I'm running into. I have a live reload system in place and sometimes a script is written to disk with a logic error somewhere, and on the next live reload my UI state goes FUBAR. I've resorted to some state tracking similar to what you have, but it's fragile, doesn't cover all cases, and feels hacky af. |
Sorry I haven't had time to look at that yet. It looks sufficiently simple and useful that I may. I think it possibly might make more sense as an explicit helper to be called optionally (and generally by user of scripting languages) rather than being hard-wired by default. The reason being that we can't just silently ignore those errors. The expectation is that the code running script would poll an error flag and turn this into end-user feedback, however at the library/C++ level we have no way of providing this feedback to the user. Making this explicit also solve the issue that scope are not easy to define (e.g. the issue with PushStyleCol/Begin/PopStyleCol/End patterns, see #1767). Even though they are less common, we would nice to also protect from excessive calls to PopXXX/EndXXX functions. Please understand that this never will be a 100% sandboxing, are we in agreement that this would be designed to minimize the side-effect and cost of programmer errors, but not to protect from malicious users? |
@ocornut — I’m in full agreement with all of this! Yeah, not meant to be a security feature at all, just convenience at most (in my case). One little question: Would the error polling just show most recent error or collect errors since the last poll? For GL calls for example it only collects last one which makes one need to call |
Yes, this shouldn't be intended to protect from malice, just misuse. If you're dealing with a malicious user you'll need to have bindings that apply enough mitigation to have some sort of measurable impact, and that doesn't belong in imgui - people who want such mitigations can implement them on top of the public imgui API just fine. |
Hello, Context: Our editor can be extended using plugins. Plugin hot-reload+ImGui is extremely fast to iterate with. Problem: Unfortunately API misuses are really ruining our party. These come in two flavors:
Both usually end with a C++ assertion being thrown which is less than ideal. Solution?: Both scenarios can be adressed in the same manner but solving the first one might be more work. Handling scenario 2. could look something like: ImGui::EnterUntrustedSection();
bool success = call_plugin_issuing_ImGui_calls());
if (!success)
ImGui::RollbackUntrustedSection();
ImGui::ExitUntrustedSection();
if (!success)
ImGui::Text("So sad"); Script execution failure was reported by the client code but that doesn't cover API misuse. The ideal version would validate the untrusted section on exit and roll it back if it did not pass. ImGui::EnterUntrustedSection();
call_plugin_issuing_ImGui_calls(); // alea jacta est
if (!ImGui::ExitUntrustedSection())
ImGui::Text("So sad"); Obviously logic already executed in the client code can not be rolled back (eg. While not a critical feature to have I wonder how hard it would be to implement on top of the current design? |
Seeing that #2096 is cross-referenced here might I add that I would be much more interested in completely rolling back a broken declaration rather than having a partial declaration automatically rendered valid. I'd rather present the user a known-to-be-working UI of my choosing in case of a failure. |
There's no magic "rolling back" we have to address specific issues one by one, clarify the fail cases and what we want to do out of them. It probably boils down to clearing most stacks. E.g. We could merely aim at making The problem is that genuine error needs to be reported somehow, we can't have a fully silent form of error handling. |
Precise error reporting would be nice to have and overriding IM_ASSERT to report errors seems good. This together with a safe However, I don't really understand your objection to the magic "roll back", is it on a technical standpoint? It looks like the most foolproof approach to me. |
I don't know what "rollback" means other than taking a snapshot of the entire memory used by dear imgui and restoring that, complete with pointers provided by the user-allocator (so yeah, it is a technical problem to implement). |
…ght by an assert in the End() function itself at the call site (instead of being reported in EndFrame). Past the assert, they don't lead to crashes any more. Missing calls to End(), pass the assert, should not lead to crashes any more, nor to the fallback/debug window appearing on screen. (#1651).
I made some changes so that mismatched Begin/End calls should not lead to crashes or other side effects any more. However note that they will still trigger IM_ASSERT() ( and I have improved the formulation and location of those asserts). For scripting language it is expected that you teach your assert handler to play nice with the app (e.g. error/log/abort script?). While this doesn't cover everything I believe this should cover MOST errors, and would be interested to know if it works better for you. @sherief , @ejulien , etc. |
…en showing the CTRL+Tab list and or fallback "...." tooltip.
…ssert when showing the CTRL+Tab list and or fallback "...." tooltip." This reverts commit 1b0e38d.
… mis-usage don't lead to hard crashes any more, facilitating integration with scripting languages. (#1651)
Note for the future: just found out it does not cover |
FYI this is what I've been using in the TestEngine, with quite some success: void ImGuiTestContext::RecoverFromUiContextErrors(bool verbose)
{
ImGuiContext& g = *UiContext;
while (g.CurrentWindowStack.Size > 0)
{
#ifdef IMGUI_HAS_TABLE
while (g.CurrentTable && (g.CurrentTable->OuterWindow == g.CurrentWindow || g.CurrentTable->InnerWindow == g.CurrentWindow))
{
if (verbose) LogWarning("Recovered from missing EndTable() call.");
ImGui::EndTable();
}
#endif
while (g.CurrentTabBar != NULL)
{
if (verbose) LogWarning("Recovered from missing EndTabBar() call.");
ImGui::EndTabBar();
}
while (g.CurrentWindow->DC.TreeDepth > 0)
{
if (verbose) LogWarning("Recovered from missing TreePop() call.");
ImGui::TreePop();
}
while (g.GroupStack.Size > g.CurrentWindow->DC.StackSizesOnBegin.SizeOfGroupStack)
{
if (verbose) LogWarning("Recovered from missing EndGroup() call.");
ImGui::EndGroup();
}
while (g.CurrentWindow->IDStack.Size > 1)
{
if (verbose) LogWarning("Recovered from missing PopID() call.");
ImGui::PopID();
}
while (g.ColorStack.Size > g.CurrentWindow->DC.StackSizesOnBegin.SizeOfColorStack)
{
if (verbose) LogWarning("Recovered from missing PopStyleColor() for '%s'", ImGui::GetStyleColorName(g.ColorStack.back().Col));
ImGui::PopStyleColor();
}
while (g.StyleVarStack.Size > g.CurrentWindow->DC.StackSizesOnBegin.SizeOfStyleVarStack)
{
if (verbose) LogWarning("Recovered from missing PopStyleVar().");
ImGui::PopStyleVar();
}
while (g.FocusScopeStack.Size > g.CurrentWindow->DC.StackSizesOnBegin.SizeOfFocusScopeStack)
{
if (verbose) LogWarning("Recovered from missing PopFocusScope().");
ImGui::PopFocusScope();
}
if (g.CurrentWindowStack.Size == 1)
{
IM_ASSERT(g.CurrentWindow->IsFallbackWindow);
break;
}
if (g.CurrentWindow->Flags & ImGuiWindowFlags_ChildWindow)
{
if (verbose) LogWarning("Recovered from missing EndChild() call.");
ImGui::EndChild();
}
else
{
if (verbose) LogWarning("Recovered from missing End() call.");
ImGui::End();
}
}
} It ends to save the day lots of the time, but technically speaking lots of that is done incorrectly as multiple stacks won't be unpopped in the right order. I could try moving this function to becoming an internal inside ImGui:: and provide a callback or something to report on errors. |
That would be neat. I'll use this snippet for now, and report any problems, if any, that occurs here. Thanks! |
Pushed this in master as 9712bff |
It might be a good idea to add the |
The way we've been handling stack unrolling on error in Pioneer has been to save a copy of the stack sizes, including the CurrentWindowStackSize, when entering a protected call ( Would you be interested in a pull request that allows unrolling to a "known state" (implemented with |
Could we get some more insights on how to expose Dear ImGui to "unsafe" environments. One could For instance see the function IM_ASSERT(tab_bar->LastTabItemIdx >= 0);
ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx];
if (!(tab->Flags & ImGuiTabItemFlags_NoPushId))
PopID(); You can easily crash the application by calling |
Your bindings can't just be automated C++<-->scripting (Lua or otherwise) bridges, you need to have validation in your bindings that are exposed to untrusted code. |
That means you would have to go through every single ImGui function and check whether it does sanity checks or not? And that every ImGui update because stuff could change. Sounds insane. Sanity checks could be done by default being opt-in with macros to maintain performance for who don't need it. In the case of Edit: |
I went through this "insanity" myself, I ended up making bindings for Angelscript this way. Every function that pushes/pops to the stack has a secondary stack with some information attached to it as to where in the scripts the stack was pushed from. This allows me to have any kind of "broken" ImGui stack unrolled correctly as well as making sure you can't pop something off the stack in the wrong order (like calling My bindings end up looking something like this: static bool ScriptUI_BeginTabItem(const string& label, bool& open, ImGuiTabItemFlags flags)
{
bool opened = true;
bool ret = ImGui::BeginTabItem(label, &opened, flags);
open = opened;
if (ret) {
PushToPlugin(ScriptPlugin::uist_TabItem);
}
return ret;
}
static void ScriptUI_EndTabItem()
{
if (PopFromPlugin(ScriptPlugin::uist_TabItem)) {
ImGui::EndTabItem();
}
} Most of the "magic" happens in I then have a loop to unroll through my secondary stack: switch (item.m_type) {
case ScriptPlugin::uist_Window: ImGui::End(); break;
case ScriptPlugin::uist_Group: ImGui::EndGroup(); break;
case ScriptPlugin::uist_StyleColor: ImGui::PopStyleColor(); break;
case ScriptPlugin::uist_StyleVar: ImGui::PopStyleVar(); break;
case ScriptPlugin::uist_ID: ImGui::PopID(); break;
case ScriptPlugin::uist_Font: ImGui::PopFont(); break;
case ScriptPlugin::uist_FontNull: break; // Do nothing because we didn't *really* push anything
case ScriptPlugin::uist_MenuBar: ImGui::EndMenuBar(); break;
case ScriptPlugin::uist_Menu: ImGui::EndMenu(); break;
case ScriptPlugin::uist_Tree: ImGui::TreePop(); break;
case ScriptPlugin::uist_Tooltip: ImGui::EndTooltip(); break;
case ScriptPlugin::uist_Popup: ImGui::EndPopup(); break;
case ScriptPlugin::uist_Combo: ImGui::EndCombo(); break;
case ScriptPlugin::uist_Child: ImGui::EndChild(); break;
case ScriptPlugin::uist_TabBar: ImGui::EndTabBar(); break;
case ScriptPlugin::uist_TabItem: ImGui::EndTabItem(); break;
case ScriptPlugin::uist_Table: ImGui::EndTable(); break;
case ScriptPlugin::uist_ListBox: ImGui::EndListBox(); break;
case ScriptPlugin::uist_TextWrap: ImGui::PopTextWrapPos(); break;
case ScriptPlugin::uist_ItemWidth: ImGui::PopItemWidth(); break;
case ScriptPlugin::uist_Disabled: ImGui::EndDisabled(); break;
} It's a bit more work, but for my case it was well worth it, and I already have quite a large coverage of the API manually done like this. Perhaps something like that can be done in a more automatic code generation way, too.. 🤷♀️ |
Sanity checks ARE done by default except our priority are native users, notifying of usage mistakes instead of silently failing, and maintaining high performances. I am open to increase coverage of “failing without not crash” which is the purpose of IM_ASSERT_USER_ERROR() but I am not expecting this coverage will ever get anywhere 100%, rather we can aim to cover the most common cases. And I would correct them on a case by case basic, eg ok to make EndTabItem() non-crashing. |
@ocornut Sounds good, it would be great to have one of the assert types to cover as many cases as possible. And wouldn't you agree that any library should do assert sanity checks in their public API functions? For maximum performance you disable the assert macros anyway. @codecat Thank you. Seems like a good additional way to safeguard without too much effort. Do you also call |
No, I am not calling that function. Judging by the comments above it, the function doesn't sound super reliable, but I haven't played around with it. |
We’ve been using that function for a long time in the test engine. It’s not perfect but doesn’t make things worse, and recovers many cases.
|
… Added helper default callback. Comments. (#1651) (doesn't affect test engine hook for it as trailing \n are trimmed anyhow)
…ting in a child inside a tab bar. (#1651)
The great bit is that if you use ImGuiErrorFlags_NoAssert you can get away with a tooltip: #if 0
if (ImGui::Begin("Error 1"))
{
ImGui::Text("This window calls End() inside the if (Begin()) block!");
ImGui::End();
}
else
{
printf("");
}
#endif
#if 1
if (ImGui::Begin("Error 2"))
{
ImGui::Text("This window calls PushID() without popping!");
ImGui::PushID("Hello");
}
ImGui::End(); // Macros used by Recoverable Error handling
// - Only dispatch error if _EXPR: evaluate as assert (similar to an assert macro).
// - The message will always be a string literal, in order to increase likelihood of being display by an assert handler.
// - The intent is that you may rewire this macro to dispatch dynamically:
// - On programmers machines, when debugger is attached, on direct imgui API usage error: always assert!
// - On exception recovery and script language recovery: you may decide to error log.
#ifndef IM_ASSERT_USER_ERROR
#define IM_ASSERT_USER_ERROR(_EXPR,_MSG) do { if (!(_EXPR) && ImGui::ErrorLog(_MSG)) { IM_ASSERT((_EXPR) && _MSG); } } while (0) // Recoverable User Error
#endif
// - Error recovery is provided as a way to facilitate recovery from errors in e.g. scripting languages, or after specific exceptions handlers.
// - Error recovery is not perfect nor guaranteed! You are not supposed to rely on it in the course of a normal application run.
// - Always ensure that on programmers seat you have at minimum Asserts or Tooltips enabled when making direct imgui API call,
// and especially in native code. Otherwise it would severely hinder your ability to catch and correct mistakes!
// - (1) Programmer seats: default path is:
// - Recoverable errors will call IM_ASSERT_USER_ERROR(), leading to IM_ASSERT() being executed.
// - Recovery will generally be performed, assuming you can resume from your assert.
// - (2) Programmer seats: if you disable Asserts (either via ImGuiErrorFlags_NoAssert either via a disabled IM_ASSERT macros) but keep Tooltips:
// - Recoverable errors will be visible in a tooltip.
// - Programmer is prompted to press F1 to enable asserts.
// - THIS IMPLICITLY RELY ON RECOVERY BEING 100% FUNCTIONAL WHICH IS NOT GUARANTEED. A FEW CASES MIGHT PROBABLY STILL CRASH/ASSERTS.
// - THIS IMPLICITLY RELY ON 'F1' KEY BEING AVAILABLE WITHOUT CAUSING MUCH HARM IN YOUR APP. // FIXME-ERRORHANDLING: Need to find a solution for this.
// - (3) What you might want to setup on non-programmers seats:
// - Use ImGuiErrorFlags_NoAssert, but make sure log entries are well visible or reported somewhere.
// - If errors are not well resurfaced to programmers, it may hinder your ability to prevent errors from staying/spreading in the codebase. Use with caution!
// - (4) If you offer the ability to write Dear ImGui code via e.g. scripting language, you may want to:
// - Use ErrorRecoveryStoreState() to record stack sizes before running the script interpreter.
// - Use ImGuiErrorFlags_NoAssert but only while in the context of the script interpreter.
// - Maybe trigger a script break from your ErrorCallback if you can.
// - Ensure that e.g ImGuiErrorFlags_NoTooltip is NOT set, and that log entries are well surfaced and visible somewhere.
// - (5) To handle resuming code execution after an exception:
// - Use ErrorRecoveryStoreState() to record stack sizes before your try {} block.
// - Temporary adjust settings (e.g. disable assert, set a log callback).
// - Call ErrorRecoveryTryToRecoverState()
// - Restore settings.
// FIXME-ERRORHANDLING: recover may log many times and assert once?
enum ImGuiErrorFlags_
{
ImGuiErrorFlags_None = 0,
ImGuiErrorFlags_EnableRecovery = 1 << 0, // Enable recovery attempts. Some errors won't be detected and lead to direct crashes if recovery is disabled.
ImGuiErrorFlags_NoDebugLog = 1 << 1, // Disable debug log on error.
ImGuiErrorFlags_NoAssert = 1 << 2, // Disable asserts on error. Otherwise: we call IM_ASSERT() when returning from a failing IM_ASSERT_USER_ERROR()
ImGuiErrorFlags_NoTooltip = 1 << 3, // Disable tooltip on error. The tooltip will display a keyboard shortcut to enable asserts if they were disabled!
};
typedef void (*ImGuiErrorCallback)(ImGuiContext* ctx, void* user_data, const char* msg); // Error Handling [EXPERIMENTAL]
// (this will eventually be moved to public ImGuiIO when we are happy with the feature)
ImGuiErrorFlags ErrorFlags; // = Assert+DebugLog+Tooltip. See ImGuiErrorHandlingFlags_
ImGuiErrorCallback ErrorCallback; // = NULL
void* ErrorCallbackUserData; // = NULL Branch https://github.com/ocornut/imgui/compare/features/error_recovery?expand=1 I need to stress that it is unlikely that recovery is going to be 100% perfect all the time but I believe if it handles 99% of realistic error cases then we may be good. This is not merged yet. Curious if you have feedback on whether this can be useful to your case. The only feature "regression" is that if you are to disable _EnableRecovery it doesn't get you exactly the same asserts as before. I believe if the feature works it should be a non-issue, but I would need to see how it behave in real world vs my few tests. (I think I may simultaneously start working on official scoped/raii helpers so people have them in a a separate header, even if I don't fancy them) |
One additional concern for your consideration is that the recovery feature may also be usable / extremely useful in a "graceful exit" scenario in the presence of external error handling. We're currently using a slightly patched and integrated version of ImGui 1.89.8's Of note, we've had to patch The proposed |
This is one of the scenario that I my changes are aimed to support, see block (4) in my comments.
This was fixed by bc77041 a few days ago (before I made my post).
Right, this was a mistake.
Added too. I will soon push my work on this. When done I would appreciate if you can update your copy and try using the new functions. |
I've now given the linked branch a more full read-through and I think the proposed API would neatly replace our adaptation in its entirety - I'm very glad to see this feature be proposed as a native/upstream functionality.
I would be very happy to! It might take me a little bit - upgrading the ImGui version used by Pioneer is not entirely trivial, but once we've migrated to v1.9.2 it should be easy to integrate the proposed branch and test against our usecase. |
I'm using vanilla Dear Imgui v1.60 WIP.
I have an app that exposes imgui API to plugins using a scripting language. Now, in my own code I can be sure that I am "well-behaved" - for example, I match calls to ImGui::Begin() with calls to ImGui::End(). But I can't be sure that plugins will be the same. I also have live reloading of plugin scripts, and sometimes I end up trying to load plugin code that won't compile (I can keep using the last-known-good version in this case) or worse: plugin code that isn't well-behaved with respect to imgui API. Consider a plugin that consists entirely of [the scrip equivalent of] the following:
I end up hitting this assertion:
As a feature request, is it possible to consider supporting more graceful failures? I don't have a solid idea in mind, but you can see my use case above. It could be something like D3D12's command lists, where the CloseCommandList() function returns a status code indicating whether this was a valid or malformed command list - maybe ImGui::EndFrame() could return a similar value and just drop the frame altogether, or render as much of it as it can [and I realize how this is a fuzzy concept - I'm just thinking out loud here].
Is there some other way to do this that I'm missing? I wouldn't mind running the plugin code in its own imgui context (multiple context support rocks!), but currently that doesn't seem to solve the problem I have.
The text was updated successfully, but these errors were encountered: