Porting a Linux application to Mac: DialogWindow::LaunchOptions::launchAsync makes trouble


#1

Hi,

I’m developing an application on a Linux system. It runs fine on the Linux development machine, but while porting it to Mac I get some errors. Possibly originating from some bad code design. Let me explain what I do:

I have a bunch of buttons, launching some more or less time consuming tasks. To not lock up the GUI when such a task is in progress, the Component owning those buttons also owns a ThreadPool and a nested ThreadPoolJob for all button actions. The buttonClicked callback then just calls ThreadPool::addJob and dispatches the acual work to the Thread Pool. This works quite well on my Linux system.
Now there is one Button that invokes a child process which needs to display the console output of that process after it has finished its work to the user, so I acquire a MessageManagerLock (which in my understanding allows me to safely do some GUI calls from a background thread?), create a Label, put some text in it, create a DialogWindow, let it own the Label with the text and then call dialogWindow.launchAsync(). This works as expected on my Linux system, but locks up the whole GUI on my Mac while not even displaying that dialog window. I can see that some Main Thread Checker: UI API called on a background thread errors appear on the console. However, I thought GUI calls from a background thread are okay as long as the MessageManagerLock is acquired - am I wrong in this point?

So, what’s the wrong step here and how should I change my code-design to make it better?

For your information, here are the important parts of the code:

JobStatus runJob() override {
        ChildProcess networkSettingsProcess;
        
        // Let the child process do its work and check if it did everything right...

        MessageManagerLock mml;

        Label *dialogMessage = new Label;

        dialogMessage->setColour(Label::ColourIds::backgroundColourId, Colours::darkgrey);
    
        dialogMessage->setText("Network adapter settings were successfully changed. Log messages:\n\n" + networkSettingsProcess.readAllProcessOutput(), NotificationType::dontSendNotification);
        dialogMessage->setBounds(0, 0, 500, 500); }
        
        DialogWindow::LaunchOptions dialogWindow;
        dialogWindow.dialogTitle = "Network Setup Done";
        dialogWindow.content.setOwned(dialogMessage);
        dialogWindow.launchAsync();

        return jobHasFinished;
    };

#2

It’s very dangerous to just grab a MessageManagerLock without giving it a thread to check - read the docs:

    If you pass nullptr for the thread object, it will wait indefinitely for the lock - be
    careful when doing this, because it's very easy to deadlock if your message thread
    attempts to call stopThread() on a thread just as that thread attempts to get the
    message lock.

#3

Thank you for the quick reply. However it seems to me, that I don’t really understand the problem, so it‘s quite hard to understand the solution. I‘m not sure under what circumstances the message thread should call stopThread() on the thread that runs the ThreadPoolJob? I imagine that will just happen in the ThreadPool destructor, which will indeed be called from the message thread, as my threadPool is owned by a component, but that won‘t be the problem here, will it?

So could you describe a scenario in which this would happen to let me understand how passing a pointer to some (which?) thread to the MessageManagerLock would solve the problem?


#4

Well, that may not be your problem here, but it’s definitely something that will cause a deadlock! Think about it: the component gets deleted by the message thread, it deletes the pool, the pool tries to stop its threads, and then this thread hits the messagemanagerlock, which it can never possibly acquire because the message thread is already busy waiting for it!


#5

Okay, of course, I think I got it. So to avoid this possible deadlock, I should just change MessageManagerLock mml to MessageManagerLock mml (this) as the lock is acquired inside a ThreadPoolThread::runJob block. Now if the situation described above will occur, then this would lead the MessageManagerLock to not acquiring the lock, so I should check mml.lockWasGained() afterwards before accessing any component, right?

Now if this is clear - let’s come back to the original question: Does anyone have an idea what actually goes wrong in the code posted above? Is this MainThreadChecker thing somehow blocking? I just interpreted it as a warning rather than a “real” error.


#6

Some additional information:
When the application is blocked as described above, hitting the close button of the application window triggers the assert in juce_Desktop.cpp, line 51 (jassert (desktopComponents.size() == 0);). Setting a breakpoint right above that assert and then looking into the desktopComponents array, shows that there is a DefaultDialogWindow instance in that array, which will most likely be the one that was launched before. So even if I don’t see any dialog window on the desktop, at least it seems to be created. Not sure if that helps?


#7

Surely you can see exactly what’s blocked, by just looking at the stacks when it’s in this state?


#8

Sorry Jules, I don’t understand what you’re meaning by this sentence - most probably because I’m no native English speaker. So what exactly do you refer to?


#9

I mean use your debugger!

A deadlock isn’t something you should need to ask about, because it’s something where the debugger will show you exactly what’s going on, and which threads are blocked.


#10

Okay, to be honest I’m not that experienced - maybe I’ll start a new forum thread about spotting deadlocks with the debugger for beginners to collect some more knowledge on this topic. However, I used my debugger, hit the pause button when my UI was in this blocked state and then took a look at the Message Thread Stack, which looked like this:
msgtrheadstack
Correct me if I’m wrong, but that doesn’t look like a deadlock to me, more like totally normal behaviour. Turning on my sound lets me hear this beep when clicking anywhere in my application window, which normally tells the user, that there is a modal window in front of the application, so the UI ist temporarily blocked as the modal window needs be closed first. So I come to the conclusion that my assumption regarding the deadlock was wrong.

Right-clicking on the application icon in the dock and choosing “Show all Windows” gives me this view:


If you look close, you see “Network Setup Done” on the upper left area, which is the title of the Dialog Window launched. So it is there on the desktop, seems to be in a modal state in which it’s blocking my main Window but it is somehow completely invisible and not accessible for that reason. What did I do wrong here?


#11

Now after a lot try & error work I finally got it working by not calling launchAsync. Instead my ThreadPoolJob now also is an AsyncUpdater and calls triggerAsyncUpdate at the point where launchAsync was called before. In handleAsyncUpdate I now call runModal on my DialogWindow::LaunchOptions instance, which is now heap allocated and managed by a ScopedPointer held by the class. So my code now looks like this

class ConfigureNetwork : public ThreadPoolJob, public AsyncUpdater {

public:

    ConfigureNetwork() : ThreadPoolJob ("Configure Network"){};
    ~ConfigureNetwork() {};

    JobStatus runJob() override {
        ChildProcess networkSettingsProcess;
    
        // Let the child process do its work and check if it did everything right...

        MessageManagerLock mml (this);
        
        // if the message manager lock failed to acquire the lock, it's most likely because the thread pool is going out of scope right now
        if (mml.lockWasGained() == false) {
            // Some error message                
            return jobHasFinished;
        }

        Label *dialogMessage = new Label;

        dialogMessage->setColour(Label::ColourIds::backgroundColourId, Colours::darkgrey);

        dialogMessage->setText("Network adapter settings were successfully changed. Log messages:\n\n" + networkSettingsProcess.readAllProcessOutput(), NotificationType::dontSendNotification);
        dialogMessage->setBounds(0, 0, 500, 500); }
    
        dialogWindow = new DialogWindow::LaunchOptions;
        dialogWindow->dialogTitle = "Network Setup Done";
        dialogWindow->content.setOwned(dialogMessage);

        triggerAsyncUpdate();

        return jobHasFinished;
    };
        
    void handleAsyncUpdate() override {
        dialogWindow->runModal();
    }

private:
    ScopedPointer<DialogWindow::LaunchOptions> dialogWindow;
};

I’m happy I got this working now, but can anyone explain to me why this works on mac while the old solution obviously only worked on Linux? I prefer to actually understand a solution to gain some knowledge for the future.