-
-
Notifications
You must be signed in to change notification settings - Fork 10.1k
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 |
Note that I've been experimenting with a more general solution to this in the testing framework (as by default most checks can trigger an early out from a test function, leaving things un-ended/un-popup) so hopefully once it is matured we can provide more tools to handle that. |
…hild() on a child window. (#1651) Internals: Moved some error handling code.
Running into the same problems here, a script error stopping a script in the middle of valid ImGui calls, causing ImGui to throw a failed assertion. Was looking for a way to "unroll" or "end all" or "pop all" the BeginX calls and found this thread. Good to know I'm not the only one.. :D |
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.
|
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: