diff options
author | Petr Mrázek <peterix@gmail.com> | 2015-07-10 01:11:06 +0200 |
---|---|---|
committer | Petr Mrázek <peterix@gmail.com> | 2015-07-10 01:11:06 +0200 |
commit | 8e7caf4e25dc3f56877a504c45d157860215496b (patch) | |
tree | 4ce3ea68190d4b693519ad6f76473b5b8f4da8be /logic/launch | |
parent | 5dd48e89f5a7facf8355641d0caf8deaec2a03ec (diff) | |
download | PrismLauncher-8e7caf4e25dc3f56877a504c45d157860215496b.tar.gz PrismLauncher-8e7caf4e25dc3f56877a504c45d157860215496b.tar.bz2 PrismLauncher-8e7caf4e25dc3f56877a504c45d157860215496b.zip |
GH-1053 move launch related things and rename them
Diffstat (limited to 'logic/launch')
-rw-r--r-- | logic/launch/LaunchTask.cpp | 641 | ||||
-rw-r--r-- | logic/launch/LaunchTask.h | 178 | ||||
-rw-r--r-- | logic/launch/LoggedProcess.cpp | 147 | ||||
-rw-r--r-- | logic/launch/LoggedProcess.h | 60 | ||||
-rw-r--r-- | logic/launch/MessageLevel.cpp | 36 | ||||
-rw-r--r-- | logic/launch/MessageLevel.h | 28 |
6 files changed, 1090 insertions, 0 deletions
diff --git a/logic/launch/LaunchTask.cpp b/logic/launch/LaunchTask.cpp new file mode 100644 index 00000000..83bbba97 --- /dev/null +++ b/logic/launch/LaunchTask.cpp @@ -0,0 +1,641 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "launch/LaunchTask.h" +#include "MessageLevel.h" +#include "MMCStrings.h" +#include "java/JavaChecker.h" +#include "tasks/Task.h" +#include <pathutils.h> +#include <QDebug> +#include <QDir> +#include <QEventLoop> +#include <QRegularExpression> +#include <QCoreApplication> +#include <QStandardPaths> + +#define IBUS "@im=ibus" + +void LaunchTask::initializeEnvironment() +{ + // prepare the process environment + QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); + + QStringList ignored = + { + "JAVA_ARGS", + "CLASSPATH", + "CONFIGPATH", + "JAVA_HOME", + "JRE_HOME", + "_JAVA_OPTIONS", + "JAVA_OPTIONS", + "JAVA_TOOL_OPTIONS" + }; + for(auto key: rawenv.keys()) + { + auto value = rawenv.value(key); + // filter out dangerous java crap + if(ignored.contains(key)) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // filter MultiMC-related things + if(key.startsWith("QT_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } +#ifdef Q_OS_LINUX + // Do not pass LD_* variables to java. They were intended for MultiMC + if(key.startsWith("LD_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // Strip IBus + // IBus is a Linux IME framework. For some reason, it breaks MC? + if (key == "XMODIFIERS" && value.contains(IBUS)) + { + QString save = value; + value.replace(IBUS, ""); + qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; + } + if(key == "GAME_PRELOAD") + { + m_env.insert("LD_PRELOAD", value); + continue; + } + if(key == "GAME_LIBRARY_PATH") + { + m_env.insert("LD_LIBRARY_PATH", value); + continue; + } +#endif + qDebug() << "Env: " << key << value; + m_env.insert(key, value); + } +#ifdef Q_OS_LINUX + // HACK: Workaround for QTBUG42500 + if(!m_env.contains("LD_LIBRARY_PATH")) + { + m_env.insert("LD_LIBRARY_PATH", ""); + } +#endif + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + m_env.insert(it.key(), it.value()); + } +} + +void LaunchTask::init() +{ + initializeEnvironment(); + + m_process.setProcessEnvironment(m_env); + connect(&m_process, &LoggedProcess::log, this, &LaunchTask::on_log); + connect(&m_process, &LoggedProcess::stateChanged, this, &LaunchTask::on_state); + + m_prelaunchprocess.setProcessEnvironment(m_env); + connect(&m_prelaunchprocess, &LoggedProcess::log, this, &LaunchTask::on_log); + connect(&m_prelaunchprocess, &LoggedProcess::stateChanged, this, &LaunchTask::on_pre_state); + + m_postlaunchprocess.setProcessEnvironment(m_env); + connect(&m_postlaunchprocess, &LoggedProcess::log, this, &LaunchTask::on_log); + connect(&m_postlaunchprocess, &LoggedProcess::stateChanged, this, &LaunchTask::on_post_state); + + m_instance->setRunning(true); +} + +std::shared_ptr<LaunchTask> LaunchTask::create(MinecraftInstancePtr inst) +{ + std::shared_ptr<LaunchTask> proc(new LaunchTask(inst)); + proc->init(); + return proc; +} + +LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance) +{ +} + +QString LaunchTask::censorPrivateInfo(QString in) +{ + if (!m_session) + return in; + + if (m_session->session != "-") + in.replace(m_session->session, "<SESSION ID>"); + in.replace(m_session->access_token, "<ACCESS TOKEN>"); + in.replace(m_session->client_token, "<CLIENT TOKEN>"); + in.replace(m_session->uuid, "<PROFILE ID>"); + in.replace(m_session->player_name, "<PROFILE NAME>"); + + auto i = m_session->u.properties.begin(); + while (i != m_session->u.properties.end()) + { + in.replace(i.value(), "<" + i.key().toUpper() + ">"); + ++i; + } + + return in; +} + +// console window +MessageLevel::Enum LaunchTask::guessLevel(const QString &line, MessageLevel::Enum level) +{ + QRegularExpression re("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]"); + auto match = re.match(line); + if(match.hasMatch()) + { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + if(levelStr == "INFO") + level = MessageLevel::Message; + if(levelStr == "WARN") + level = MessageLevel::Warning; + if(levelStr == "ERROR") + level = MessageLevel::Error; + if(levelStr == "FATAL") + level = MessageLevel::Fatal; + if(levelStr == "TRACE" || levelStr == "DEBUG") + level = MessageLevel::Debug; + } + else + { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || + line.contains("[FINER]") || line.contains("[FINEST]")) + level = MessageLevel::Message; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at "))) + return MessageLevel::Error; + return level; +} + +QMap<QString, QString> LaunchTask::getVariables() const +{ + auto mcInstance = std::dynamic_pointer_cast<MinecraftInstance>(m_instance); + QMap<QString, QString> out; + out.insert("INST_NAME", mcInstance->name()); + out.insert("INST_ID", mcInstance->id()); + out.insert("INST_DIR", QDir(mcInstance->instanceRoot()).absolutePath()); + out.insert("INST_MC_DIR", QDir(mcInstance->minecraftRoot()).absolutePath()); + out.insert("INST_JAVA", mcInstance->settings()->get("JavaPath").toString()); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + return out; +} + +QStringList LaunchTask::javaArguments() const +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(m_instance->extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(m_instance->windowTitle()); +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + args << QString("-Xms%1m").arg(m_instance->settings()->get("MinMemAlloc").toInt()); + args << QString("-Xmx%1m").arg(m_instance->settings()->get("MaxMemAlloc").toInt()); + + // No PermGen in newer java. + auto javaVersion = m_instance->settings()->get("JavaVersion"); + if(Strings::naturalCompare(javaVersion.toString(), "1.8.0", Qt::CaseInsensitive) < 0) + { + auto permgen = m_instance->settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + if (!m_nativeFolder.isEmpty()) + args << QString("-Djava.library.path=%1").arg(m_nativeFolder); + args << "-jar" << PathCombine(QCoreApplication::applicationDirPath(), "jars", "NewLaunch.jar"); + + return args; +} + +void LaunchTask::checkJava() +{ + m_javaPath = m_instance->settings()->get("JavaPath").toString(); + emit log("Java path is:\n" + m_javaPath + "\n\n"); + + auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); + if (realJavaPath.isEmpty()) + { + emit log(tr("The java binary \"%1\" couldn't be found. You may have to set up java " + "if Minecraft fails to launch.").arg(m_javaPath), + MessageLevel::Warning); + } + + QFileInfo javaInfo(realJavaPath); + qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedUnixTime = m_instance->settings()->get("JavaTimestamp").toLongLong(); + this->m_javaUnixTime = javaUnixTime; + // if they are not the same, check! + if(javaUnixTime != storedUnixTime) + { + m_JavaChecker = std::make_shared<JavaChecker>(); + bool successful = false; + QString errorLog; + QString version; + emit log(tr("Checking Java version..."), MessageLevel::MultiMC); + connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &LaunchTask::checkJavaFinished); + m_JavaChecker->m_path = realJavaPath; + m_JavaChecker->performCheck(); + } + preLaunch(); +} + +void LaunchTask::checkJavaFinished(JavaCheckResult result) +{ + if(!result.valid) + { + // Error message displayed if java can't start + emit log(tr("Could not start java:"), MessageLevel::Error); + auto lines = result.errorLog.split('\n'); + for(auto line: lines) + { + emit log(line, MessageLevel::Error); + } + emit log("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); + emitFailed(tr("Could not start java!")); + } + else + { + emit log(tr("Java version is %1!\n").arg(result.javaVersion), MessageLevel::MultiMC); + m_instance->settings()->set("JavaVersion", result.javaVersion); + m_instance->settings()->set("JavaTimestamp", m_javaUnixTime); + preLaunch(); + } +} + +void LaunchTask::executeTask() +{ + printHeader(); + emit log("Minecraft folder is:\n" + m_process.workingDirectory() + "\n\n"); + + checkJava(); +} + +void LaunchTask::launch() +{ + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); +} + +void LaunchTask::abort() +{ + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); +} + + +void LaunchTask::setWorkdir(QString path) +{ + QDir mcDir(path); + m_process.setWorkingDirectory(mcDir.absolutePath()); + m_prelaunchprocess.setWorkingDirectory(mcDir.absolutePath()); + m_postlaunchprocess.setWorkingDirectory(mcDir.absolutePath()); +} + +void LaunchTask::printHeader() +{ + emit log(m_header); +} + +void LaunchTask::on_log(QStringList lines, MessageLevel::Enum level) +{ + logOutput(lines, level); +} + +void LaunchTask::logOutput(const QStringList &lines, MessageLevel::Enum defaultLevel) +{ + for (auto & line: lines) + { + logOutput(line, defaultLevel); + } +} + +void LaunchTask::logOutput(QString line, MessageLevel::Enum level) +{ + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if(innerLevel != MessageLevel::Unknown) + { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) + { + level = this->guessLevel(line, level); + } + + // censor private user info + line = censorPrivateInfo(line); + + emit log(line, level); +} + +void LaunchTask::preLaunch() +{ + QString prelaunch_cmd = m_instance->settings()->get("PreLaunchCommand").toString(); + if (!prelaunch_cmd.isEmpty()) + { + prelaunch_cmd = substituteVariables(prelaunch_cmd); + // Launch + emit log(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd)); + m_prelaunchprocess.start(prelaunch_cmd); + } + else + { + on_pre_state(LoggedProcess::Skipped); + } +} + +void LaunchTask::on_pre_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + QString error = tr("Pre-Launch command failed with code %1.\n\n").arg(m_prelaunchprocess.exitCode()); + emit log(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + emit log(tr("Pre-Launch command ran successfully.\n\n")); + } + case LoggedProcess::Skipped: + { + m_instance->reload(); + updateInstance(); + } + default: + break; + } +} + +void LaunchTask::updateInstance() +{ + m_updateTask = m_instance->createUpdateTask(); + if(m_updateTask) + { + connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished())); + m_updateTask->start(); + return; + } + makeReady(); +} + +void LaunchTask::updateFinished() +{ + if(m_updateTask->successful()) + { + doJarModding(); + } + else + { + QString reason = tr("Instance update failed because: %1.\n\n").arg(m_updateTask->failReason()); + emit log(reason, MessageLevel::Fatal); + emitFailed(reason); + } +} + +void LaunchTask::doJarModding() +{ + m_jarModTask = m_instance->createJarModdingTask(); + if(!m_jarModTask) + { + jarModdingSucceeded(); + } + connect(m_jarModTask.get(), SIGNAL(succeeded()), this, SLOT(jarModdingSucceeded())); + connect(m_jarModTask.get(), SIGNAL(failed(QString)), this, SLOT(jarModdingFailed(QString))); + m_jarModTask->start(); +} + +void LaunchTask::jarModdingSucceeded() +{ + makeReady(); +} + +void LaunchTask::jarModdingFailed(QString reason) +{ + emitFailed(reason); +} + +void LaunchTask::makeReady() +{ + QStringList args = javaArguments(); + QString allArgs = args.join(", "); + emit log("Java Arguments:\n[" + censorPrivateInfo(allArgs) + "]\n\n"); + + QString wrapperCommand = m_instance->settings()->get("WrapperCommand").toString(); + if(!wrapperCommand.isEmpty()) + { + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + QString reason = tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand); + emit log(reason, MessageLevel::Fatal); + emitFailed(reason); + return; + } + emit log("Wrapper command is:\n" + wrapperCommand + "\n\n"); + args.prepend(m_javaPath); + m_process.start(wrapperCommand, args); + } + else + { + m_process.start(m_javaPath, args); + } + + // instantiate the launcher part + if (!m_process.waitForStarted()) + { + //: Error message displayed if instace can't start + QString reason = tr("Could not launch minecraft!"); + emit log(reason, MessageLevel::Fatal); + emitFailed(reason); + return; + } + + emit log(tr("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::MultiMC); + + // send the launch script to the launcher part + m_process.write(launchScript.toUtf8()); + + emit readyForLaunch(); +} + +void LaunchTask::on_state(LoggedProcess::State state) +{ + QProcess::ExitStatus estat = QProcess::NormalExit; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + estat = QProcess::CrashExit; + emitFailed("Game crashed."); + return; + } + case LoggedProcess::Finished: + { + auto exitCode = m_process.exitCode(); + m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); + // run post-exit + postLaunch(); + break; + } + case LoggedProcess::Skipped: + qWarning() << "Illegal game state: Skipped"; + break; + case LoggedProcess::Running: + m_instance->setLastLaunch(); + break; + default: + break; + } +} + +void LaunchTask::killProcess() +{ + killed = true; + if (m_prelaunchprocess.state() == LoggedProcess::Running) + { + m_prelaunchprocess.kill(); + } + else if(m_process.state() == LoggedProcess::Running) + { + m_process.kill(); + } + else if(m_postlaunchprocess.state() == LoggedProcess::Running) + { + m_postlaunchprocess.kill(); + } +} + +void LaunchTask::postLaunch() +{ + if(killed) + return; + QString postlaunch_cmd = m_instance->settings()->get("PostExitCommand").toString(); + if (!postlaunch_cmd.isEmpty()) + { + postlaunch_cmd = substituteVariables(postlaunch_cmd); + emit log(tr("Running Post-Launch command: %1").arg(postlaunch_cmd)); + m_postlaunchprocess.start(postlaunch_cmd); + return; + } + emitSucceeded(); +} + +void LaunchTask::on_post_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + QString error = tr("Post-Launch command failed with code %1.\n\n").arg(m_postlaunchprocess.exitCode()); + emit log(error, MessageLevel::Error); + emitFailed(error); + } + case LoggedProcess::Finished: + { + emit log(tr("Post-Launch command ran successfully.\n\n")); + } + case LoggedProcess::Skipped: + { + emitSucceeded(); + } + default: + break; + } +} + +void LaunchTask::emitSucceeded() +{ + m_instance->cleanupAfterRun(); + m_instance->setRunning(false); + Task::emitSucceeded(); +} + +void LaunchTask::emitFailed(QString reason) +{ + m_instance->cleanupAfterRun(); + m_instance->setRunning(false); + Task::emitFailed(reason); +} + +QString LaunchTask::substituteVariables(const QString &cmd) const +{ + QString out = cmd; + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + out.replace("$" + it.key(), it.value()); + } + auto env = QProcessEnvironment::systemEnvironment(); + for (auto var : env.keys()) + { + out.replace("$" + var, env.value(var)); + } + return out; +} + +qint64 LaunchTask::pid() +{ +#ifdef Q_OS_WIN + struct _PROCESS_INFORMATION *procinfo = m_process.pid(); + return procinfo->dwProcessId; +#else + return m_process.pid(); +#endif +} diff --git a/logic/launch/LaunchTask.h b/logic/launch/LaunchTask.h new file mode 100644 index 00000000..38142189 --- /dev/null +++ b/logic/launch/LaunchTask.h @@ -0,0 +1,178 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan <orochimarufan.x3@gmail.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include <QProcess> +#include "BaseInstance.h" +#include "MessageLevel.h" +#include "LoggedProcess.h" +/* HACK: MINECRAFT: split! */ +#include "minecraft/MinecraftInstance.h" +#include "java/JavaChecker.h" +#include "QObjectPtr.h" +#include "tasks/Task.h" + +class ProcessTask +{ + +}; + +class BaseProfilerFactory; +class LaunchTask: public Task +{ + Q_OBJECT +protected: + explicit LaunchTask(InstancePtr instance); + void init(); + +public: /* methods */ + static std::shared_ptr<LaunchTask> create(MinecraftInstancePtr inst); + virtual ~LaunchTask() {}; + + InstancePtr instance() + { + return m_instance; + } + + /// Set the text printed on top of the log + void setHeader(QString header) + { + m_header = header; + } + + void setWorkdir(QString path); + + void killProcess(); + + qint64 pid(); + + /** + * @brief prepare the process for launch (for multi-stage launch) + */ + virtual void executeTask() override; + + /** + * @brief launch the armed instance + */ + virtual void launch(); + + /** + * @brief abort launch + */ + virtual void abort(); + +public: /* HACK: MINECRAFT: split! */ + void setLaunchScript(QString script) + { + launchScript = script; + } + + void setNativeFolder(QString natives) + { + m_nativeFolder = natives; + } + + inline void setLogin(AuthSessionPtr session) + { + m_session = session; + } + + +protected: /* methods */ + void preLaunch(); + void updateInstance(); + void doJarModding(); + void makeReady(); + void postLaunch(); + virtual void emitFailed(QString reason); + virtual void emitSucceeded(); + QString substituteVariables(const QString &cmd) const; + void initializeEnvironment(); + + void printHeader(); + + virtual QMap<QString, QString> getVariables() const; + virtual QString censorPrivateInfo(QString in); + virtual MessageLevel::Enum guessLevel(const QString &message, MessageLevel::Enum defaultLevel); + +protected slots: + void jarModdingSucceeded(); + void jarModdingFailed(QString reason); + +signals: + /** + * @brief emitted when the launch preparations are done + */ + void readyForLaunch(); + + /** + * @brief emitted when we want to log something + * @param text the text to log + * @param level the level to log at + */ + void log(QString text, MessageLevel::Enum level = MessageLevel::MultiMC); + +protected slots: + void on_log(QStringList lines, MessageLevel::Enum level); + void logOutput(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::Message); + void logOutput(QString line, MessageLevel::Enum defaultLevel = MessageLevel::Message); + + void on_pre_state(LoggedProcess::State state); + void on_state(LoggedProcess::State state); + void on_post_state(LoggedProcess::State state); + + + +protected: + InstancePtr m_instance; + + LoggedProcess m_prelaunchprocess; + LoggedProcess m_postlaunchprocess; + LoggedProcess m_process; + QProcessEnvironment m_env; + BaseProfilerFactory * m_profiler = nullptr; + + bool killed = false; + QString m_header; + +/** + * java check step + */ +protected slots: + void checkJavaFinished(JavaCheckResult result); + +protected: + // for java checker and launch + QString m_javaPath; + qlonglong m_javaUnixTime; + std::shared_ptr<JavaChecker> m_JavaChecker; + +protected: /* HACK: MINECRAFT: split! */ + AuthSessionPtr m_session; + QString launchScript; + QString m_nativeFolder; + std::shared_ptr<Task> m_updateTask; + std::shared_ptr<Task> m_jarModTask; + +protected: /* HACK: MINECRAFT: split! */ + void checkJava(); + QStringList javaArguments() const; +private slots: + void updateFinished(); +}; + +class BaseProfilerFactory;
\ No newline at end of file diff --git a/logic/launch/LoggedProcess.cpp b/logic/launch/LoggedProcess.cpp new file mode 100644 index 00000000..53840621 --- /dev/null +++ b/logic/launch/LoggedProcess.cpp @@ -0,0 +1,147 @@ +#include "LoggedProcess.h" +#include "MessageLevel.h" +#include <QDebug> + +LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent) +{ + // QProcess has a strange interface... let's map a lot of those into a few. + connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); + connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); + connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus))); + connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError))); + connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); +} + +QStringList reprocess(const QByteArray & data, QString & leftover) +{ + QString str = leftover + QString::fromLocal8Bit(data); + + str.remove('\r'); + QStringList lines = str.split("\n"); + leftover = lines.takeLast(); + return lines; +} + +void LoggedProcess::on_stdErr() +{ + auto lines = reprocess(readAllStandardError(), m_err_leftover); + emit log(lines, MessageLevel::StdErr); +} + +void LoggedProcess::on_stdOut() +{ + auto lines = reprocess(readAllStandardOutput(), m_out_leftover); + emit log(lines, MessageLevel::StdOut); +} + +void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) +{ + // Flush console window + if (!m_err_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdErr); + m_err_leftover.clear(); + } + if (!m_out_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdOut); + m_out_leftover.clear(); + } + + // based on state, send signals + if (!m_is_aborting) + { + if (status == QProcess::NormalExit) + { + //: Message displayed on instance exit + emit log({tr("Process exited with code %1.").arg(exit_code)}, MessageLevel::MultiMC); + changeState(LoggedProcess::Finished); + } + else + { + //: Message displayed on instance crashed + if(exit_code == -1) + emit log({tr("Process crashed.").arg(exit_code)}, MessageLevel::MultiMC); + else + emit log({tr("Process crashed with exitcode %1.").arg(exit_code)}, MessageLevel::MultiMC); + changeState(LoggedProcess::Crashed); + } + } + else + { + //: Message displayed after the instance exits due to kill request + emit log({tr("Process was killed by user.")}, MessageLevel::Error); + changeState(LoggedProcess::Aborted); + } +} + +void LoggedProcess::on_error(QProcess::ProcessError error) +{ + switch(error) + { + case QProcess::FailedToStart: + { + emit log({tr("The process failed to start.")}, MessageLevel::Fatal); + changeState(LoggedProcess::FailedToStart); + break; + } + // we'll just ignore those... never needed them + case QProcess::Crashed: + case QProcess::ReadError: + case QProcess::Timedout: + case QProcess::UnknownError: + case QProcess::WriteError: + break; + } +} + +void LoggedProcess::kill() +{ + m_is_aborting = true; + QProcess::kill(); +} + +int LoggedProcess::exitCode() const +{ + return m_exit_code; +} + +void LoggedProcess::changeState(LoggedProcess::State state) +{ + if(state == m_state) + return; + m_state = state; + emit stateChanged(m_state); +} + +LoggedProcess::State LoggedProcess::state() const +{ + return m_state; +} + +void LoggedProcess::on_stateChange(QProcess::ProcessState state) +{ + switch(state) + { + case QProcess::NotRunning: + break; // let's not - there are too many that handle this already. + case QProcess::Starting: + { + if(m_state != LoggedProcess::NotRunning) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Starting; + } + changeState(LoggedProcess::Starting); + return; + } + case QProcess::Running: + { + if(m_state != LoggedProcess::Starting) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Running; + } + changeState(LoggedProcess::Running); + return; + } + } +} diff --git a/logic/launch/LoggedProcess.h b/logic/launch/LoggedProcess.h new file mode 100644 index 00000000..253be2c1 --- /dev/null +++ b/logic/launch/LoggedProcess.h @@ -0,0 +1,60 @@ +#pragma once +#include <QProcess> +#include "MessageLevel.h" + +/* + * This is a basic process. + * It has line-based logging support and hides some of the nasty bits. + */ +class LoggedProcess : public QProcess +{ +Q_OBJECT +public: + enum State + { + NotRunning, + Starting, + FailedToStart, + Running, + Finished, + Crashed, + Aborted, + Skipped + }; + +public: + explicit LoggedProcess(QObject* parent = 0); + virtual ~LoggedProcess() {}; + + State state() const; + int exitCode() const; + +signals: + void log(QStringList lines, MessageLevel::Enum level); + void stateChanged(LoggedProcess::State state); + +public slots: + /** + * @brief kill the process - equivalent to kill -9 + */ + void kill(); + + +private slots: + void on_stdErr(); + void on_stdOut(); + void on_exit(int exit_code, QProcess::ExitStatus status); + void on_error(QProcess::ProcessError error); + void on_stateChange(QProcess::ProcessState); + +private: + void changeState(LoggedProcess::State state); + +private: + QString m_err_leftover; + QString m_out_leftover; + bool m_killed = false; + State m_state = NotRunning; + int m_exit_code = 0; + bool m_is_aborting = false; +}; diff --git a/logic/launch/MessageLevel.cpp b/logic/launch/MessageLevel.cpp new file mode 100644 index 00000000..a5191290 --- /dev/null +++ b/logic/launch/MessageLevel.cpp @@ -0,0 +1,36 @@ +#include "MessageLevel.h" + +MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) +{ + if (levelName == "MultiMC") + return MessageLevel::MultiMC; + else if (levelName == "Debug") + return MessageLevel::Debug; + else if (levelName == "Info") + return MessageLevel::Info; + else if (levelName == "Message") + return MessageLevel::Message; + else if (levelName == "Warning") + return MessageLevel::Warning; + else if (levelName == "Error") + return MessageLevel::Error; + else if (levelName == "Fatal") + return MessageLevel::Fatal; + // Skip PrePost, it's not exposed to !![]! + // Also skip StdErr and StdOut + else + return MessageLevel::Unknown; +} + +MessageLevel::Enum MessageLevel::fromLine(QString &line) +{ + // Level prefix + int endmark = line.indexOf("]!"); + if (line.startsWith("!![") && endmark != -1) + { + auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/logic/launch/MessageLevel.h b/logic/launch/MessageLevel.h new file mode 100644 index 00000000..0128148d --- /dev/null +++ b/logic/launch/MessageLevel.h @@ -0,0 +1,28 @@ +#pragma once + +#include <QString> + +/** + * @brief the MessageLevel Enum + * defines what level a log message is + */ +namespace MessageLevel +{ +enum Enum +{ + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + MultiMC, /**< MultiMC Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ +}; +MessageLevel::Enum getLevel(const QString &levelName); + +/* Get message level from a line. Line is modified if it was successful. */ +MessageLevel::Enum fromLine(QString &line); +} |