c++ – How to fork Qt6 program


I have already exhaustively googled this topic, and the answer is “don’t do it”.

However I am kind of stuck between a rock and a hard place, so I need to thoroughly explore this topic before giving up on the concept of “forking a Qt application”. Also, I think it can be useful as an exercise to fully understand the nature of Qt initialization.

The premise is that I need to create a new running instance of my current app on the fly. The new instance will run simultaneously with the parent instance.

I would like to create a function that is run to fork my running application. It should be robust enough to handle whatever existing state is present;

  1. My app is instantiated in different ways, two of the most common ways are; the main function of the app is instantiated in a QTest::testCase() or it is instantiated in a int main(int argc, char *argv[]) entrypoint. I don’t know which way the app was instantiated.
  2. I don’t know if a Qt eventloop has already been started (via QApplcation/QTest::qExec etc) or not.
  3. I don’t know what OS I am on, or what graphical subsystem is in use.
  4. I don’t know what dbus, file handles, OpenGL contexts, signals or other resources the app has interacted with this far.
  5. The parent app should continue running uninterrupted after the fork occurs.
  6. The child should safely reset state and start fresh, by instanciating QApplication and building an UI etc.

So my naive idea is basically the following;

  1. Parent Qt6 app is already running happily with non-trivial state already set up.
  2. We fork the app.
    • In parent all is as before.
    • In child side we have now a stale copy of everything from parent.
  3. We close all the stale state down in child.
  4. We start a new QApplication and set up the state we need in child.

Please see the implementation of my naive attempt below.

The problems are plentiful with this approach, but they mostly boil down to the following; Chasing down resources and closing them successfully is an infinite game of whack-a-mole, and the chance of succeeding with this is slim to none, especially across platforms and Qt versions. This is perfectly exemplified by my naive implementation that simply hangs forever when I try to instantiate a push button in the child after cleaning out the old state.

My next idea is to somehow replicate or call the code of the exit() system call and let it run to the point where the OS terminates the process. It seems there are a myriad of exec*(), fork*() and posix_*() calls that perform variations of this. This is dandy if we don’t care about platform independence. Worst case, we will go this route and implement our method in platform dependent way for all platforms we care about.

Please note that using QProcess is not feasible because the code we want to run does not live in separate executable from the currently running program, and since we don’t control how the parent is wrapped and/or instantiated, we can’t simply start argv[0] (for example the code may be started in a QTest that takes full control of command line arguments).

My naive attempt looks like this;

testFork.hpp:

#ifndef TESTFORK_HPP
#define TESTFORK_HPP

#include "test/Common.hpp"

class TestFork:public QObject
{
    Q_OBJECT
private slots:
    void test();
};

#endif
// TESTFORK_HPP

testFork.cpp:

#include "TestFork.hpp"

#include <QDebug>
#include <QApplication>
#include <QPushButton>
#include <QTimer>
#include <QAbstractEventDispatcher>
#include <QOpenGLContext>
#include <QtDBus/QDBusConnection>

#include <csignal>
#include <unistd.h> // For fork
#include <sys/wait.h> // For waitpid


void TestFork::test()
{
    qDebug() << "TEST FORK";
    
    pid_t pid = fork();
    
    if (pid < 0)
    {
        qCritical() << "Fork failed!";
        return;
    }
    else if (pid == 0)
    {
        qDebug() << "In child process A1a";
        
        QDBusConnection::disconnectFromBus("session");
        qDebug() << "In child process A1b";
        QDBusConnection::disconnectFromBus("system");

        // Reset OpenGL contexts if they exist
        if (QOpenGLContext::currentContext()) {
            qDebug() << "In child process A6a";
            QOpenGLContext::currentContext()->doneCurrent();
            qDebug() << "In child process A6b";
            QOpenGLContext::currentContext()->deleteLater();
        }
        
        
        qDebug() << "In child process A7a";
        // Reset signal handlers to default
        signal(SIGPIPE, SIG_DFL);
        qDebug() << "In child process A7b";
        signal(SIGCHLD, SIG_DFL);
        qDebug() << "In child process A8";
        
        /* Commented out so we can see log output but does not seem to make a difference.
        close(0); // Close standard input
        close(1); // Close standard output
        close(2); // Close standard error
*/
        qDebug() << "In child process A9";
        for (int fd = 3; fd < 1024; ++fd) {
            close(fd);
        }
        
        if (QAbstractEventDispatcher::instance() != nullptr)
        {
            qDebug() << "An existing event loop is running, exiting it.";
            QCoreApplication::exit();  // End the existing event loop if running
        }
        
        
        int argc = 0;
        char **argv = nullptr;
        qDebug() << "In child process B1";
        QApplication childApp(argc, argv);
        qDebug() << "In child process B2";
        QPushButton *button = new QPushButton("Terminate Child");
        qDebug() << "In child process C";
        
        
        QObject::connect(button, &QPushButton::clicked, &childApp, &QApplication::quit);
        qDebug() << "In child process D";
        
        QTimer::singleShot(100, [button]() {
            qDebug() << "In child process E";
            button->show();
            qDebug() << "In child process F";
        });
        
        qDebug() << "In child process G";
        childApp.exec();
        qDebug() << "In child process H";
        
        _exit(0);  // Ensure clean exit of the child process
    }
    else
    {
        qDebug() << "In parent process";
        int status;
        waitpid(pid, &status, 0);
    }
}



Source link

Leave a Comment