Blocking UI elements cause infinite loop after certain C64 SubSys actions (API, Telnet)
Due to the way the UI loop is handled using a main as well as local loops, there are certain cases that are currently not handled correctly. Reproduction:
-
Press the Menu button and go into any of the blocking UI elements (popup, string_box, string_edit, choice, run_editor). Fastest is pressing F4 to get the "System Information" run_editor popup.
-
Trigger an action through the C64 subsystem that will cause the C64 to become active again (reset, reboot, load cart/prg/etc). For example go to the HTTP server and click Reboot.
-
Try pressing the Menu button (nothing will happen). At this point the main thread will never recover and a power cycle is needed to recover. The C64 still works, as does most subsystems, but the main UI thread is hung.
The problem is that the C64 SubSystem will mostly do something like this
case MENU_C64_RESET:
if (c64->client) { // we can't execute this yet
c64->client->release_host(); // disconnect from user interface
c64->client = 0;
}
c64->unfreeze();
c64->reset();
break;
This is done on a thread other than the main UI thread (Telnet, API). The release_host() call will do
void UserInterface :: release_host(void)
{
for(int i=focus;i>=0;i--) { // tear down
ui_objects[i]->deinit();
}
doBreak = true;
}
which will pull the rug out from under the main UI thread (the UI objects are deinitialized).
Meanwhile the main UI thread is stuck in run_editor() inside a polling loop.
void UserInterface :: run_editor(const char *text_buf, int max_len)
{
Editor *edit = new Editor(this, text_buf, max_len);
edit->init(screen, keyboard);
int ret;
do {
ret = edit->poll(0);
} while(!ret);
edit->deinit();
delete edit;
}
poll() will never return and the UI thread will be stuck there forever. In particular, it never comes back out to the top level poll-loop that checks doBreak but also checks for the Menu button being pressed. This is why the main UI thread can never recover.
I tried playing around with breaking out of the local poll loop (i.e cancelling the blocking UI element) when doBreak is set, but it soon gets very messy since release_host() calls deinit() and then run_editor() will do the same causing double free, etc. Checking for doBreak in the entire call tree from the main UI polling loop down to the local poll loop to "do the right thing" quickly becomes unwieldy, error prone and generally ugly.
I spent quite some time trying to come up with an easy solution, but failed. Maybe some kind of additional abstraction for entering/leaving the UI (and C64 mode) is needed? Maybe the "polling-loop" also needs too be abstracted in the same way to allow the top loop as well as the "UI element local" loop to run the same code handling menu buttons, disk swap, exit/enter C64, etc, etc.
When the U64E2 launch is over, maybe @GideonZ can give some input here.