// Copyright (C) 2025 Jarek Kobus
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default


#include <QtTaskTree/qprocesstask.h>

#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QElapsedTimer>
#include <QtCore/QMutex>
#include <QtCore/QThread>
#include <QtCore/QTimer>
#include <QtCore/QWaitCondition>

QT_BEGIN_NAMESPACE

#if QT_CONFIG(process)

namespace QtTaskTree {

class ProcessReaperPrivate;

class ProcessReaper final
{
public:
    static void reap(QProcess *process, int timeoutMs = 500);
    ProcessReaper();
    ~ProcessReaper();

private:
    static ProcessReaper *instance();

    QThread m_thread;
    ProcessReaperPrivate *m_private;
};

static const int s_timeoutThreshold = 10000; // 10 seconds

static QString execWithArguments(QProcess *process)
{
    QStringList commandLine;
    commandLine.append(process->program());
    commandLine.append(process->arguments());
    return commandLine.join(QChar::Space);
}

struct ReaperSetup
{
    QProcess *m_process = nullptr;
    int m_timeoutMs;
};

class Reaper : public QObject
{
    Q_OBJECT

public:
    Reaper(const ReaperSetup &reaperSetup) : m_reaperSetup(reaperSetup) {}

    void reap()
    {
        m_timer.start();
        connect(m_reaperSetup.m_process, &QProcess::finished, this, &Reaper::handleFinished);
        if (emitFinished())
            return;
        terminate();
    }

Q_SIGNALS:
    void finished();

private:
    void terminate()
    {
        m_reaperSetup.m_process->terminate();
        QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleTerminateTimeout);
    }

    void kill() { m_reaperSetup.m_process->kill(); }

    bool emitFinished()
    {
        if (m_reaperSetup.m_process->state() != QProcess::NotRunning)
            return false;

        if (!m_finished) {
            const int timeout = m_timer.elapsed();
            if (timeout > s_timeoutThreshold) {
                qWarning() << "Finished parallel reaping of" << execWithArguments(m_reaperSetup.m_process)
                           << "in" << (timeout / 1000.0) << "seconds.";
            }

            m_finished = true;
            Q_EMIT finished();
        }
        return true;
    }

    void handleFinished()
    {
        if (emitFinished())
            return;
        qWarning() << "Finished process still running...";
        // In case the process is still running - wait until it has finished
        QTimer::singleShot(m_reaperSetup.m_timeoutMs, this, &Reaper::handleFinished);
    }

    void handleTerminateTimeout()
    {
        if (emitFinished())
            return;
        kill();
    }

    bool m_finished = false;
    QElapsedTimer m_timer;
    const ReaperSetup m_reaperSetup;
};

class ProcessReaperPrivate : public QObject
{
    Q_OBJECT

public:
    // Called from non-reaper's thread
    void scheduleReap(const ReaperSetup &reaperSetup)
    {
        Q_ASSERT_X(QThread::currentThread() != thread(), "ProcessReaperPrivate",
                   "Can't schedule reap from the reaper internal thread.");

        QMutexLocker locker(&m_mutex);
        m_reaperSetupList.append(reaperSetup);
        QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush);
    }

    // Called from non-reaper's thread
    void waitForFinished()
    {
        Q_ASSERT_X(QThread::currentThread() != thread(), "ProcessReaperPrivate",
                   "Can't wait for finished from the reaper internal thread.");

        QMetaObject::invokeMethod(this, &ProcessReaperPrivate::flush,
                                  Qt::BlockingQueuedConnection);
        QMutexLocker locker(&m_mutex);
        if (m_reaperList.isEmpty())
            return;

        m_waitCondition.wait(&m_mutex);
    }

private:
    // All the private methods are called from the reaper's thread
    QList<ReaperSetup> takeReaperSetupList()
    {
        QMutexLocker locker(&m_mutex);
        return std::exchange(m_reaperSetupList, {});
    }

    void flush()
    {
        while (true) {
            const QList<ReaperSetup> reaperSetupList = takeReaperSetupList();
            if (reaperSetupList.isEmpty())
                return;
            for (const ReaperSetup &reaperSetup : reaperSetupList)
                reap(reaperSetup);
        }
    }

    void reap(const ReaperSetup &reaperSetup)
    {
        Reaper *reaper = new Reaper(reaperSetup);
        connect(reaper, &Reaper::finished, this, [this, reaper, process = reaperSetup.m_process] {
            QMutexLocker locker(&m_mutex);
            const bool isRemoved = m_reaperList.removeOne(reaper);
            if (!isRemoved)
                qWarning() << "Reaper list doesn't contain the finished process.";

            delete reaper;
            delete process;
            if (m_reaperList.isEmpty())
                m_waitCondition.wakeOne();
        }, Qt::QueuedConnection);

        {
            QMutexLocker locker(&m_mutex);
            m_reaperList.append(reaper);
        }

        reaper->reap();
    }

    QMutex m_mutex;
    QWaitCondition m_waitCondition;
    QList<ReaperSetup> m_reaperSetupList;
    QList<Reaper *> m_reaperList;
};

static ProcessReaper *s_instance = nullptr;
static QMutex s_instanceMutex;

// Call me with s_instanceMutex locked.
ProcessReaper *ProcessReaper::instance()
{
    if (!s_instance)
        s_instance = new ProcessReaper;
    return s_instance;
}

ProcessReaper::ProcessReaper()
    : m_private(new ProcessReaperPrivate)
{
    m_private->moveToThread(&m_thread);
    QObject::connect(&m_thread, &QThread::finished, m_private, &QObject::deleteLater);
    m_thread.start();
    m_thread.moveToThread(qApp->thread());
}

ProcessReaper::~ProcessReaper()
{
    Q_ASSERT_X(QThread::isMainThread(), "ProcessReaper",
               "Destructing process reaper from non-main thread.");

    instance()->m_private->waitForFinished();
    m_thread.quit();
    m_thread.wait();
}

void ProcessReaper::reap(QProcess *process, int timeoutMs)
{
    if (!process)
        return;

    if (QThread::currentThread() != process->thread()) {
        qWarning() << "Process can only be reaped from its own thread.";
        return;
    }

    process->disconnect();
    if (process->state() == QProcess::NotRunning) {
        delete process;
        return;
    }

    // Neither can move object with a parent into a different thread
    // nor reaping the process with a parent makes any sense.
    process->setParent(nullptr);

    QMutexLocker locker(&s_instanceMutex);
    ProcessReaperPrivate *priv = instance()->m_private;

    process->moveToThread(priv->thread());
    const ReaperSetup reaperSetup {process, timeoutMs};
    priv->scheduleReap(reaperSetup);
}

/*!
    \class QtTaskTree::QProcessTaskDeleter
    \inheaderfile qprocesstask.h
    \inmodule QtTaskTree
    \brief A custom deleter for QProcess, used by QProcessTask.
    \reentrant

    QProcessTaskDeleter is used to enable fast destruction of running
    QProcess instances via QProcessTask in QTaskTree. Instead of deleting
    the running QProcess, which may block the caller's thread execution
    for a long time, the QProcessTaskDeleter moves the running QProcess into
    a separate thread and tries to finish it in a most gentle way.
    This consist of calling QProcess::terminate() with \c {500 ms} timeout,
    and if the process is still running after this timeout passed,
    the additional call to QProcess::kill() is performed.

    Finally, on application quit, QProcessTaskDeleter::syncAll() should be called
    in order to synchronize all processes being still potentially finalized
    in separate thread. The call to QProcessTaskDeleter::syncAll() is blocking
    in case some processes are still being finalized.

    This strategy seems sensible, since when deleting the running QProcess via
    QProcessTaskDeleter we don't block immediately, but postpone the possible
    (but not certain) block until the end of an application.

    It is used by QProcessTask.
*/

/*!
    This method should be called on application quit to synchronize the
    finalization of all still possibly running QProcess instances that were
    deleted before using QProcessTaskDeleter. The call should be executed from
    the main thread.
*/
void QProcessTaskDeleter::syncAll()
{
    if (!QThread::isMainThread()) {
        qWarning("The QProcessTaskDeleter::syncAll() should be called from the main thread. "
                 "The current call is ignored.");
        return;
    }
    QMutexLocker locker(&s_instanceMutex);
    delete s_instance;
    s_instance = nullptr;
}

/*!
    Deletes the passed \a process in a most safe way.
    If the passed \a process is not running, it is deleted directly.
    Otherwise, it moves the \a process into a separate thread and
    tries to finish it in a most safe way by calling QProcess::terminate()
    with \c {500 ms} timeout, and if the process is still running after
    this timeout passed, the additional call to QProcess::kill() is performed.
*/
void QProcessTaskDeleter::operator()(QProcess *process) const
{
    ProcessReaper::reap(process);
}

void QProcessTaskAdapter::operator()(QProcess *task, QTaskInterface *iface) const
{
    QObject::connect(task, &QProcess::finished, iface, [iface, task] {
        const bool success = task->exitStatus() == QProcess::NormalExit
                             && task->error() == QProcess::UnknownError
                             && task->exitCode() == 0;
        iface->reportDone(toDoneResult(success));
    });
    QObject::connect(task, &QProcess::errorOccurred, iface, [iface](QProcess::ProcessError error) {
        if (error != QProcess::FailedToStart)
            return;
        iface->reportDone(DoneResult::Error);
    });
    task->start();
}

/*!
    \typedef QtTaskTree::QProcessTask
    \relates QtTaskTree::QCustomTask

    Type alias for the QCustomTask<QProcess>, using QProcessTaskDeleter,
    to be used inside recipes.
*/

} // namespace QtTaskTree

#endif // QT_CONFIG(process)

QT_END_NAMESPACE

#if QT_CONFIG(process)

#include "qprocesstask.moc"

#endif // QT_CONFIG(process)

