/*
    KWin - the KDE window manager
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com>
    SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
    SPDX-FileCopyrightText: 2022 David Edmundson <davidedmundson@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "xwaylandlauncher.h"

#include "config-kwin.h"

#include "xwayland_logging.h"
#include "xwaylandsocket.h"

#include "options.h"
#include "wayland_server.h"

#if KWIN_BUILD_NOTIFICATIONS
#include <KLocalizedString>
#include <KNotification>
#endif

#include <QAbstractEventDispatcher>
#include <QDataStream>
#include <QFile>
#include <QRandomGenerator>
#include <QScopeGuard>
#include <QSocketNotifier>
#include <QTimer>

// system
#include <cerrno>
#include <cstring>
#include <sys/socket.h>
#include <unistd.h>

namespace KWin
{
namespace Xwl
{

XwaylandLauncher::XwaylandLauncher(QObject *parent)
    : QObject(parent)
{
    m_resetCrashCountTimer = new QTimer(this);
    m_resetCrashCountTimer->setSingleShot(true);
    connect(m_resetCrashCountTimer, &QTimer::timeout, this, &XwaylandLauncher::resetCrashCount);
}

XwaylandLauncher::~XwaylandLauncher()
{
}

void XwaylandLauncher::setListenFDs(const QList<int> &listenFds)
{
    m_listenFds = listenFds;
}

void XwaylandLauncher::setDisplayName(const QString &displayName)
{
    m_displayName = displayName;
}

void XwaylandLauncher::setXauthority(const QString &xauthority)
{
    m_xAuthority = xauthority;
}

void XwaylandLauncher::enable()
{
    if (m_enabled) {
        return;
    }

    if (!m_listenFds.isEmpty()) {
        Q_ASSERT(!m_displayName.isEmpty());
    } else {
        auto socket = std::make_unique<XwaylandSocket>(XwaylandSocket::OperationMode::CloseFdsOnExec);
        if (!socket->isValid()) {
            qCWarning(KWIN_XWL) << "Failed to establish X11 socket";
            return;
        }
        m_socket = std::move(socket);
        m_displayName = m_socket->name();
        m_listenFds = m_socket->fileDescriptors();
    }

    for (int socket : std::as_const(m_listenFds)) {
        QSocketNotifier *notifier = new QSocketNotifier(socket, QSocketNotifier::Read, this);
        connect(notifier, &QSocketNotifier::activated, this, [this]() {
            if (!m_xwaylandProcess) {
                start();
            }
        });
        connect(this, &XwaylandLauncher::started, notifier, [notifier]() {
            notifier->setEnabled(false);
        });
        connect(this, &XwaylandLauncher::finished, notifier, [this, notifier]() {
            // only reactivate if we've not shut down due to the crash count
            notifier->setEnabled(m_enabled);
        });
    }

    m_enabled = true;
}

void XwaylandLauncher::disable()
{
    m_enabled = false;
    stop();
}

bool XwaylandLauncher::start()
{
    Q_ASSERT(m_enabled);
    if (m_xwaylandProcess) {
        return false;
    }
    QList<int> fdsToClose;
    auto cleanup = qScopeGuard([&fdsToClose] {
        for (const int fd : std::as_const(fdsToClose)) {
            close(fd);
        }
    });

    int pipeFds[2];
    if (pipe(pipeFds) != 0) {
        qCWarning(KWIN_XWL, "Failed to create pipe to start Xwayland: %s", strerror(errno));
        Q_EMIT errorOccurred();
        return false;
    }
    fdsToClose << pipeFds[1];

    int sx[2];
    if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) {
        qCWarning(KWIN_XWL, "Failed to open socket for XCB connection: %s", strerror(errno));
        Q_EMIT errorOccurred();
        return false;
    }
    int fd = dup(sx[1]);
    if (fd < 0) {
        qCWarning(KWIN_XWL, "Failed to open socket for XCB connection: %s", strerror(errno));
        Q_EMIT errorOccurred();
        return false;
    }

    const int waylandSocket = waylandServer()->createXWaylandConnection();
    if (waylandSocket == -1) {
        qCWarning(KWIN_XWL, "Failed to open socket for Xwayland server: %s", strerror(errno));
        Q_EMIT errorOccurred();
        return false;
    }
    const int wlfd = dup(waylandSocket);
    if (wlfd < 0) {
        qCWarning(KWIN_XWL, "Failed to open socket for Xwayland server: %s", strerror(errno));
        Q_EMIT errorOccurred();
        return false;
    }

    m_xcbConnectionFd = sx[0];

    QStringList arguments;

    arguments << m_displayName;

    if (!m_listenFds.isEmpty()) {
        // xauthority externally set and managed
        if (!m_xAuthority.isEmpty()) {
            arguments << QStringLiteral("-auth") << m_xAuthority;
        }

        for (int socket : std::as_const(m_listenFds)) {
            int dupSocket = dup(socket);
            fdsToClose << dupSocket;
#if HAVE_XWAYLAND_LISTENFD
            arguments << QStringLiteral("-listenfd") << QString::number(dupSocket);
#else
            arguments << QStringLiteral("-listen") << QString::number(dupSocket);
#endif
        }
    }

    arguments << QStringLiteral("-displayfd") << QString::number(pipeFds[1]);
    arguments << QStringLiteral("-rootless");
    arguments << QStringLiteral("-wm") << QString::number(fd);
#if HAVE_XWAYLAND_ENABLE_EI_PORTAL
    arguments << QStringLiteral("-enable-ei-portal");
#endif

    m_xwaylandProcess = new QProcess(this);
    m_xwaylandProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel);
    m_xwaylandProcess->setProgram(QStringLiteral("Xwayland"));
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    env.insert("WAYLAND_SOCKET", QByteArray::number(wlfd));
    if (qEnvironmentVariableIntValue("KWIN_XWAYLAND_DEBUG") == 1) {
        env.insert("WAYLAND_DEBUG", QByteArrayLiteral("1"));
    }
    m_xwaylandProcess->setProcessEnvironment(env);
    m_xwaylandProcess->setArguments(arguments);
    connect(m_xwaylandProcess, &QProcess::errorOccurred, this, &XwaylandLauncher::handleXwaylandError);
    connect(m_xwaylandProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
            this, &XwaylandLauncher::handleXwaylandFinished);

    // When Xwayland starts writing the display name to displayfd, it is ready. Alternatively,
    // the Xwayland can send us the SIGUSR1 signal, but it's already reserved for VT hand-off.
    m_readyNotifier = new QSocketNotifier(pipeFds[0], QSocketNotifier::Read, this);
    connect(m_readyNotifier, &QSocketNotifier::activated, this, [this]() {
        maybeDestroyReadyNotifier();
        Q_EMIT started();
    });

    m_xwaylandProcess->start();

    return true;
}

QString XwaylandLauncher::displayName() const
{
    return m_displayName;
}

QString XwaylandLauncher::xauthority() const
{
    return m_xAuthority;
}

int XwaylandLauncher::xcbConnectionFd() const
{
    return m_xcbConnectionFd;
}

QProcess *XwaylandLauncher::process() const
{
    return m_xwaylandProcess;
}

void XwaylandLauncher::stop()
{
    if (!m_xwaylandProcess) {
        return;
    }
    Q_EMIT finished();

    maybeDestroyReadyNotifier();
    waylandServer()->destroyXWaylandConnection();

    // When the Xwayland process is finally terminated, the finished() signal will be emitted,
    // however we don't actually want to process it anymore. Furthermore, we also don't really
    // want to handle any errors that may occur during the teardown.
    if (m_xwaylandProcess->state() != QProcess::NotRunning) {
        disconnect(m_xwaylandProcess, nullptr, this, nullptr);
        m_xwaylandProcess->terminate();
        m_xwaylandProcess->waitForFinished(5000);
    }
    delete m_xwaylandProcess;
    m_xwaylandProcess = nullptr;
}

void XwaylandLauncher::maybeDestroyReadyNotifier()
{
    if (m_readyNotifier) {
        close(m_readyNotifier->socket());

        delete m_readyNotifier;
        m_readyNotifier = nullptr;
    }
}

void XwaylandLauncher::handleXwaylandFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    qCDebug(KWIN_XWL) << "Xwayland process has quit with exit status:" << exitStatus << "exit code:" << exitCode;

#if KWIN_BUILD_NOTIFICATIONS
    KNotification::event(QStringLiteral("xwaylandcrash"), i18n("Xwayland has crashed"));
#endif
    m_resetCrashCountTimer->stop();

    switch (options->xwaylandCrashPolicy()) {
    case XwaylandCrashPolicy::Restart:
        if (++m_crashCount <= options->xwaylandMaxCrashCount()) {
            stop();
            m_resetCrashCountTimer->start(std::chrono::minutes(10));
        } else {
            qCWarning(KWIN_XWL, "Stopping Xwayland server because it has crashed %d times "
                                "over the past 10 minutes",
                      m_crashCount);
            disable();
        }
        break;
    case XwaylandCrashPolicy::Stop:
        disable();
        break;
    }
}

void XwaylandLauncher::resetCrashCount()
{
    qCDebug(KWIN_XWL) << "Resetting the crash counter, its current value is" << m_crashCount;
    m_crashCount = 0;
}

void XwaylandLauncher::handleXwaylandError(QProcess::ProcessError error)
{
    switch (error) {
    case QProcess::FailedToStart:
        qCWarning(KWIN_XWL) << "Xwayland process failed to start";
        return;
    case QProcess::Crashed:
        qCWarning(KWIN_XWL) << "Xwayland process crashed";
        break;
    case QProcess::Timedout:
        qCWarning(KWIN_XWL) << "Xwayland operation timed out";
        break;
    case QProcess::WriteError:
    case QProcess::ReadError:
        qCWarning(KWIN_XWL) << "An error occurred while communicating with Xwayland";
        break;
    case QProcess::UnknownError:
        qCWarning(KWIN_XWL) << "An unknown error has occurred in Xwayland";
        break;
    }
    Q_EMIT errorOccurred();
}

}
}

#include "moc_xwaylandlauncher.cpp"
