QTfrontend/net/tcpBase.cpp
author sheepluva
Sat, 07 Dec 2019 16:18:20 +0100
branchhedgeroid
changeset 15517 45d6806548e9
parent 15263 1ce9b717bb41
permissions -rw-r--r--
hedgeroid: add debug logging to library loading

/*
 * Hedgewars, a free turn based strategy game
 * Copyright (c) 2006-2007 Igor Ulyanov <iulyanov@gmail.com>
 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <QList>
#include <QImage>
#include <QThread>
#include <QApplication>
#include <QProcessEnvironment>

#include "tcpBase.h"
#include "hwconsts.h"
#include "MessageDialog.h"
#include "gameuiconfig.h"

#ifdef HWLIBRARY
extern "C" {
    void RunEngine(int argc, char ** argv);

    int operatingsystem_parameter_argc;
    char ** operatingsystem_parameter_argv;
}


EngineInstance::EngineInstance(QObject *parent)
    : QObject(parent)
{

}

EngineInstance::~EngineInstance()
{
    qDebug() << "EngineInstance delete" << QThread::currentThreadId();
}

void EngineInstance::setArguments(const QStringList & arguments)
{
    m_arguments.clear();
    m_arguments << qApp->arguments().at(0).toUtf8();

    m_argv.resize(arguments.size() + 1);
    m_argv[0] = m_arguments.last().data();

    int i = 1;
    foreach(const QString & s, arguments)
    {
        m_arguments << s.toUtf8();
        m_argv[i] = m_arguments.last().data();
        ++i;
    }
}

void EngineInstance::start()
{
    qDebug() << "EngineInstance start" << QThread::currentThreadId();

    RunEngine(m_argv.size(), m_argv.data());

    emit finished();
}

#endif

QList<TCPBase*> srvsList;
QPointer<QTcpServer> TCPBase::IPCServer(0);

TCPBase::~TCPBase()
{
    if(m_hasStarted)
    {
        if(IPCSocket)
            IPCSocket->close();

        if(m_connected)
        {
#ifdef HWLIBRARY
            if(!thread)
                qDebug("WTF");
            thread->quit();
            thread->wait();
#else
            process->waitForFinished(1000);
#endif
        }
    }
    // make sure this object is not in the server list anymore
    srvsList.removeOne(this);

    if (IPCSocket)
        IPCSocket->deleteLater();

}

TCPBase::TCPBase(bool demoMode, bool usesCustomLanguage, QObject *parent) :
    QObject(parent),
    m_hasStarted(false),
    m_isDemoMode(demoMode),
    m_connected(false),
    m_usesCustomLanguage(usesCustomLanguage),
    IPCSocket(0)
{
    process = 0;

    if(!IPCServer)
    {
        IPCServer = new QTcpServer(0);
        IPCServer->setMaxPendingConnections(1);
        if (!IPCServer->listen(QHostAddress::LocalHost))
        {
            MessageDialog::ShowFatalMessage(tr("Unable to start server at %1.").arg(IPCServer->errorString()));
            exit(0); // FIXME - should be graceful exit here (lower Critical -> Warning above when implemented)
        }
    }

    ipc_port=IPCServer->serverPort();
}

void TCPBase::NewConnection()
{
    if(IPCSocket)
    {
        // connection should be already finished
        return;
    }

    disconnect(IPCServer, SIGNAL(newConnection()), this, SLOT(NewConnection()));
    IPCSocket = IPCServer->nextPendingConnection();

    if(!IPCSocket) return;

    m_connected = true;

    connect(IPCSocket, SIGNAL(disconnected()), this, SLOT(ClientDisconnect()));
    connect(IPCSocket, SIGNAL(readyRead()), this, SLOT(ClientRead()));
    SendToClientFirst();

    if(simultaneousRun())
    {
        srvsList.removeOne(this);
        emit isReadyNow();
    }
}

void TCPBase::RealStart()
{
    connect(IPCServer, SIGNAL(newConnection()), this, SLOT(NewConnection()));
    IPCSocket = 0;

#ifdef HWLIBRARY
    thread = new QThread(this);
    EngineInstance *instance = new EngineInstance();
    instance->setArguments(getArguments());

    instance->moveToThread(thread);

    connect(thread, SIGNAL(started()), instance, SLOT(start(void)));
    connect(instance, SIGNAL(finished()), thread, SLOT(quit()));
    connect(instance, SIGNAL(finished()), instance, SLOT(deleteLater()));
    connect(instance, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
#else
    process = new QProcess(this);
    connect(process, SIGNAL(error(QProcess::ProcessError)),
        this, SLOT(StartProcessError(QProcess::ProcessError)));
    connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
        this, SLOT(onEngineDeath(int, QProcess::ExitStatus)));
    QStringList arguments = getArguments();

#ifdef QT_DEBUG
    // redirect everything written on stdout/stderr
    process->setProcessChannelMode(QProcess::ForwardedChannels);
#endif

    // If game config uses non-system locale, we set the environment
    // of the engine first
    if(m_usesCustomLanguage)
    {
        QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
        QString hwengineLang = QLocale().name() + ".UTF8";
        qDebug("Setting hwengine environment: LANG=%s", qPrintable(hwengineLang));
        // TODO: Check if this is correct and works on all systems
        env.insert("LANG", QLocale().name() + ".UTF8");
        process->setProcessEnvironment(env);
    }
    qDebug("Starting hwengine ...");
    process->start(bindir->absolutePath() + "/hwengine", arguments);
#endif
    m_hasStarted = true;
}

void TCPBase::ClientDisconnect()
{
    onClientDisconnect();

    if(!simultaneousRun())
    {
#ifdef HWLIBRARY
        thread->quit();
        thread->wait();
#endif
        emit isReadyNow();
    }

    if(IPCSocket) {
      disconnect(IPCSocket, SIGNAL(readyRead()), this, SLOT(ClientRead()));
      IPCSocket->deleteLater();
      IPCSocket = NULL;
    }

    deleteLater();
}

void TCPBase::ClientRead()
{
    QByteArray read = IPCSocket->readAll();
    if(read.isEmpty()) return;
    readbuffer.append(read);
    onClientRead();
}

void TCPBase::StartProcessError(QProcess::ProcessError error)
{
    MessageDialog::ShowFatalMessage(tr("Unable to run engine at %1\nError code: %2").arg(bindir->absolutePath() + "/hwengine").arg(error));
    ClientDisconnect();
}

void TCPBase::onEngineDeath(int exitCode, QProcess::ExitStatus exitStatus)
{
    Q_UNUSED(exitStatus);

    if(!m_connected) { // yes, it is intended to be like this
      ClientDisconnect(); // need to do cleanup in case no connection occured,
      //if m_connected is true, it is done automatically in socket disconnect handler
    }

    // show error message if there was an error that was not an engine's
    // fatal error - because that one already sent a info via IPC
    if ((exitCode != HWENGINE_EXITCODE_OK) && (exitCode != HWENGINE_EXITCODE_FATAL))
    {
        // inform user that something bad happened
        MessageDialog::ShowFatalMessage(
            tr("The game engine died unexpectedly!\n"
            "(exit code %1)\n\n"
            "We are very sorry for the inconvenience :(\n\n"
            "If this keeps happening, please click the '%2' button in the main menu!")
            .arg(exitCode)
            .arg(QCoreApplication::translate("PageMain", "Feedback")));

    }
}

void TCPBase::tcpServerReady()
{
    if (!srvsList.isEmpty())
    {
        disconnect(srvsList.first(), SIGNAL(isReadyNow()), this, SLOT(tcpServerReady()));
        RealStart();
    }
    else
    {
        qDebug("tcpServerReady() called while srvsList was empty. Not starting TCP server");
    }
}

void TCPBase::Start(bool couldCancelPreviousRequest)
{
    if(srvsList.isEmpty())
    {
        srvsList.push_back(this);
        RealStart();
    }
    else
    {
        TCPBase * last = srvsList.last();
        if(couldCancelPreviousRequest
            && last->couldBeRemoved()
            && (last->isConnected() || !last->hasStarted())
            && (last->parent() == parent()))
        {
            srvsList.removeLast();
            delete last;
            Start(couldCancelPreviousRequest);
        } else
        {
            connect(last, SIGNAL(isReadyNow()), this, SLOT(tcpServerReady()));
            srvsList.push_back(this);
        }
    }
}

void TCPBase::onClientRead()
{
}

void TCPBase::onClientDisconnect()
{
}

void TCPBase::SendToClientFirst()
{
}

void TCPBase::SendIPC(const QByteArray & buf)
{
    if (buf.size() > MAXMSGCHARS) return;
    quint8 len = buf.size();
    RawSendIPC(QByteArray::fromRawData((char *)&len, 1) + buf);
}

void TCPBase::RawSendIPC(const QByteArray & buf)
{
    if (!IPCSocket)
    {
        toSendBuf += buf;
    }
    else
    {
        if (toSendBuf.size() > 0)
        {
            IPCSocket->write(toSendBuf);
            if(m_isDemoMode) demo.append(toSendBuf);
            toSendBuf.clear();
        }
        if(!buf.isEmpty())
        {
            IPCSocket->write(buf);
            if(m_isDemoMode) demo.append(buf);
        }
    }
}

bool TCPBase::couldBeRemoved()
{
    return false;
}

bool TCPBase::isConnected()
{
    return m_connected;
}

bool TCPBase::simultaneousRun()
{
    return false;
}

bool TCPBase::hasStarted()
{
    return m_hasStarted;
}