From 34ddfc7ecc2b0450b3d501e65cb4203ac747ed42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Mon, 11 May 2015 22:21:37 +0200 Subject: GH-1053 base process and launch refactor, part 1 --- logic/BaseInstance.h | 4 +- logic/BaseLauncher.cpp | 311 +++++++++++++++++++++++++ logic/BaseLauncher.h | 125 ++++++++++ logic/BaseProcess.cpp | 420 ---------------------------------- logic/BaseProcess.h | 142 ------------ logic/CMakeLists.txt | 12 +- logic/LoggedProcess.cpp | 147 ++++++++++++ logic/LoggedProcess.h | 60 +++++ logic/MessageLevel.cpp | 36 +++ logic/MessageLevel.h | 28 +++ logic/NullInstance.h | 2 +- logic/minecraft/LegacyInstance.cpp | 6 +- logic/minecraft/LegacyInstance.h | 2 +- logic/minecraft/MinecraftLauncher.cpp | 296 ++++++++++++++++++++++++ logic/minecraft/MinecraftLauncher.h | 78 +++++++ logic/minecraft/MinecraftProcess.cpp | 290 ----------------------- logic/minecraft/MinecraftProcess.h | 77 ------- logic/minecraft/OneSixInstance.cpp | 6 +- logic/minecraft/OneSixInstance.h | 2 +- logic/tools/BaseExternalTool.cpp | 10 - logic/tools/BaseExternalTool.h | 1 - logic/tools/BaseProfiler.cpp | 2 +- logic/tools/BaseProfiler.h | 6 +- logic/tools/JProfiler.cpp | 8 +- logic/tools/JVisualVM.cpp | 8 +- 25 files changed, 1112 insertions(+), 967 deletions(-) create mode 100644 logic/BaseLauncher.cpp create mode 100644 logic/BaseLauncher.h delete mode 100644 logic/BaseProcess.cpp delete mode 100644 logic/BaseProcess.h create mode 100644 logic/LoggedProcess.cpp create mode 100644 logic/LoggedProcess.h create mode 100644 logic/MessageLevel.cpp create mode 100644 logic/MessageLevel.h create mode 100644 logic/minecraft/MinecraftLauncher.cpp create mode 100644 logic/minecraft/MinecraftLauncher.h delete mode 100644 logic/minecraft/MinecraftProcess.cpp delete mode 100644 logic/minecraft/MinecraftProcess.h (limited to 'logic') diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 4c547a77..91d46df1 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -27,7 +27,7 @@ class QDir; class Task; -class BaseProcess; +class BaseLauncher; class BaseInstance; // pointer for lazy people @@ -138,7 +138,7 @@ public: virtual std::shared_ptr doUpdate() = 0; /// returns a valid process, ready for launch with the given account. - virtual BaseProcess *prepareForLaunch(AuthSessionPtr account) = 0; + virtual BaseLauncher *prepareForLaunch(AuthSessionPtr account) = 0; /// do any necessary cleanups after the instance finishes. also runs before /// 'prepareForLaunch' diff --git a/logic/BaseLauncher.cpp b/logic/BaseLauncher.cpp new file mode 100644 index 00000000..96cb2b93 --- /dev/null +++ b/logic/BaseLauncher.cpp @@ -0,0 +1,311 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "BaseLauncher.h" +#include "MessageLevel.h" +#include +#include +#include + +BaseLauncher::BaseLauncher(InstancePtr instance): m_instance(instance) +{ +} + +void BaseLauncher::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 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; + } +#endif + qDebug() << "Env: " << key << value; + m_env.insert(key, value); + } +#ifdef LINUX + // HACK: Workaround for QTBUG-42500 + 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 BaseLauncher::init() +{ + initializeEnvironment(); + + m_process.setProcessEnvironment(m_env); + connect(&m_process, &LoggedProcess::log, this, &BaseLauncher::on_log); + connect(&m_process, &LoggedProcess::stateChanged, this, &BaseLauncher::on_state); + + m_prelaunchprocess.setProcessEnvironment(m_env); + connect(&m_prelaunchprocess, &LoggedProcess::log, this, &BaseLauncher::on_log); + connect(&m_prelaunchprocess, &LoggedProcess::stateChanged, this, &BaseLauncher::on_pre_state); + + m_postlaunchprocess.setProcessEnvironment(m_env); + connect(&m_postlaunchprocess, &LoggedProcess::log, this, &BaseLauncher::on_log); + connect(&m_postlaunchprocess, &LoggedProcess::stateChanged, this, &BaseLauncher::on_post_state); + + // a process has been constructed for the instance. It is running from MultiMC POV + m_instance->setRunning(true); +} + + +void BaseLauncher::setWorkdir(QString path) +{ + QDir mcDir(path); + m_process.setWorkingDirectory(mcDir.absolutePath()); + m_prelaunchprocess.setWorkingDirectory(mcDir.absolutePath()); + m_postlaunchprocess.setWorkingDirectory(mcDir.absolutePath()); +} + +void BaseLauncher::printHeader() +{ + emit log(m_header); +} + +void BaseLauncher::on_log(QStringList lines, MessageLevel::Enum level) +{ + logOutput(lines, level); +} + +void BaseLauncher::logOutput(const QStringList &lines, MessageLevel::Enum defaultLevel) +{ + for (auto & line: lines) + { + logOutput(line, defaultLevel); + } +} + +void BaseLauncher::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 BaseLauncher::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 BaseLauncher::on_pre_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + emit log(tr("Pre-Launch command failed with code %1.\n\n") + .arg(m_prelaunchprocess.exitCode()), + MessageLevel::Fatal); + m_instance->cleanupAfterRun(); + emit prelaunch_failed(m_instance, m_prelaunchprocess.exitCode(), m_prelaunchprocess.exitStatus()); + // not running, failed + m_instance->setRunning(false); + } + case LoggedProcess::Finished: + { + emit log(tr("Pre-Launch command ran successfully.\n\n")); + m_instance->reload(); + } + case LoggedProcess::Skipped: + default: + break; + } +} + +void BaseLauncher::on_state(LoggedProcess::State state) +{ + QProcess::ExitStatus estat = QProcess::NormalExit; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + estat = QProcess::CrashExit; + case LoggedProcess::Finished: + { + auto exitCode = m_process.exitCode(); + m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); + + // run post-exit + postLaunch(); + m_instance->cleanupAfterRun(); + // no longer running... + m_instance->setRunning(false); + emit ended(m_instance, exitCode, estat); + } + case LoggedProcess::Skipped: + qWarning() << "Illegal game state: Skipped"; + default: + break; + } +} + +void BaseLauncher::on_post_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + case LoggedProcess::Finished: + case LoggedProcess::Skipped: + { + + } + default: + break; + } +} + +void BaseLauncher::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 BaseLauncher::postLaunch() +{ + 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); + if (m_postlaunchprocess.exitStatus() != QProcess::NormalExit) + { + emit log(tr("Post-Launch command failed with code %1.\n\n") + .arg(m_postlaunchprocess.exitCode()), + MessageLevel::Error); + emit postlaunch_failed(m_instance, m_postlaunchprocess.exitCode(), + m_postlaunchprocess.exitStatus()); + // not running, failed + m_instance->setRunning(false); + } + else + emit log(tr("Post-Launch command ran successfully.\n\n")); + } +} + +QString BaseLauncher::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 BaseLauncher::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/BaseLauncher.h b/logic/BaseLauncher.h new file mode 100644 index 00000000..f47d3351 --- /dev/null +++ b/logic/BaseLauncher.h @@ -0,0 +1,125 @@ +/* Copyright 2013-2015 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 +#include "BaseInstance.h" +#include "MessageLevel.h" +#include "LoggedProcess.h" + +class BaseLauncher: public QObject +{ + Q_OBJECT +protected: + explicit BaseLauncher(InstancePtr instance); + void init(); + +public: /* methods */ + virtual ~BaseLauncher() {}; + + 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 arm() = 0; + + /** + * @brief launch the armed instance + */ + virtual void launch() = 0; + + /** + * @brief abort launch + */ + virtual void abort() = 0; + +protected: /* methods */ + void preLaunch(); + void postLaunch(); + QString substituteVariables(const QString &cmd) const; + void initializeEnvironment(); + + void printHeader(); + + virtual QMap getVariables() const = 0; + virtual QString censorPrivateInfo(QString in) = 0; + virtual MessageLevel::Enum guessLevel(const QString &message, MessageLevel::Enum defaultLevel) = 0; + +signals: + /** + * @brief emitted when the Process immediately fails to run + */ + void launch_failed(InstancePtr); + + /** + * @brief emitted when the PreLaunchCommand fails + */ + void prelaunch_failed(InstancePtr, int code, QProcess::ExitStatus status); + + /** + * @brief emitted when the PostLaunchCommand fails + */ + void postlaunch_failed(InstancePtr, int code, QProcess::ExitStatus status); + + /** + * @brief emitted when the process has finished and the PostLaunchCommand was run + */ + void ended(InstancePtr, int code, QProcess::ExitStatus status); + + /** + * @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; + + bool killed = false; + QString m_header; +}; diff --git a/logic/BaseProcess.cpp b/logic/BaseProcess.cpp deleted file mode 100644 index 852bec4c..00000000 --- a/logic/BaseProcess.cpp +++ /dev/null @@ -1,420 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 "BaseProcess.h" -#include -#include -#include - -#define IBUS "@im=ibus" - -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 !![]! - else - return MessageLevel::Message; -} - -BaseProcess::BaseProcess(InstancePtr instance): QProcess(), m_instance(instance) -{ -} - -void BaseProcess::init() -{ - connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), - SLOT(finish(int, QProcess::ExitStatus))); - - // prepare the process environment - QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); - - QProcessEnvironment env; - - QStringList ignored = - { - "JAVA_ARGS", - "CLASSPATH", - "CONFIGPATH", - "JAVA_HOME", - "JRE_HOME", - "_JAVA_OPTIONS", - "JAVA_OPTIONS", - "JAVA_TOOL_OPTIONS", - "CDPATH" - }; - 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; - } -#endif - if(key == "GAME_PRELOAD") - { - env.insert("LD_PRELOAD", value); - continue; - } - if(key == "GAME_LIBRARY_PATH") - { - env.insert("LD_LIBRARY_PATH", value); - continue; - } - qDebug() << "Env: " << key << value; - env.insert(key, value); - } -#ifdef Q_OS_LINUX - // HACK: Workaround for QTBUG-42500 - if(!env.contains("LD_LIBRARY_PATH")) - { - env.insert("LD_LIBRARY_PATH", ""); - } -#endif - - // export some infos - auto variables = getVariables(); - for (auto it = variables.begin(); it != variables.end(); ++it) - { - env.insert(it.key(), it.value()); - } - - this->setProcessEnvironment(env); - m_prepostlaunchprocess.setProcessEnvironment(env); - - // std channels - connect(this, SIGNAL(readyReadStandardError()), SLOT(on_stdErr())); - connect(this, SIGNAL(readyReadStandardOutput()), SLOT(on_stdOut())); - - // Log prepost launch command output (can be disabled.) - if (m_instance->settings()->get("LogPrePostOutput").toBool()) - { - connect(&m_prepostlaunchprocess, &QProcess::readyReadStandardError, this, - &BaseProcess::on_prepost_stdErr); - connect(&m_prepostlaunchprocess, &QProcess::readyReadStandardOutput, this, - &BaseProcess::on_prepost_stdOut); - } - - // a process has been constructed for the instance. It is running from MultiMC POV - m_instance->setRunning(true); -} - - -void BaseProcess::setWorkdir(QString path) -{ - QDir mcDir(path); - this->setWorkingDirectory(mcDir.absolutePath()); - m_prepostlaunchprocess.setWorkingDirectory(mcDir.absolutePath()); -} - -void BaseProcess::printHeader() -{ - emit log(m_header); -} - - -void BaseProcess::logOutput(const QStringList &lines, MessageLevel::Enum defaultLevel, - bool guessLevel, bool censor) -{ - for (int i = 0; i < lines.size(); ++i) - logOutput(lines[i], defaultLevel, guessLevel, censor); -} - -void BaseProcess::logOutput(QString line, MessageLevel::Enum defaultLevel, bool guessLevel, - bool censor) -{ - MessageLevel::Enum level = defaultLevel; - - // Level prefix - int endmark = line.indexOf("]!"); - if (line.startsWith("!![") && endmark != -1) - { - level = MessageLevel::getLevel(line.left(endmark).mid(3)); - line = line.mid(endmark + 2); - } - // Guess level - else if (guessLevel) - level = this->guessLevel(line, defaultLevel); - - if (censor) - line = censorPrivateInfo(line); - - emit log(line, level); -} - -void BaseProcess::on_stdErr() -{ - QByteArray data = readAllStandardError(); - QString str = m_err_leftover + QString::fromLocal8Bit(data); - - str.remove('\r'); - QStringList lines = str.split("\n"); - m_err_leftover = lines.takeLast(); - - logOutput(lines, MessageLevel::Error); -} - -void BaseProcess::on_stdOut() -{ - QByteArray data = readAllStandardOutput(); - QString str = m_out_leftover + QString::fromLocal8Bit(data); - - str.remove('\r'); - QStringList lines = str.split("\n"); - m_out_leftover = lines.takeLast(); - - logOutput(lines); -} - -void BaseProcess::on_prepost_stdErr() -{ - QByteArray data = m_prepostlaunchprocess.readAllStandardError(); - QString str = m_err_leftover + QString::fromLocal8Bit(data); - - str.remove('\r'); - QStringList lines = str.split("\n"); - m_err_leftover = lines.takeLast(); - - logOutput(lines, MessageLevel::PrePost, false, false); -} - -void BaseProcess::on_prepost_stdOut() -{ - QByteArray data = m_prepostlaunchprocess.readAllStandardOutput(); - QString str = m_out_leftover + QString::fromLocal8Bit(data); - - str.remove('\r'); - QStringList lines = str.split("\n"); - m_out_leftover = lines.takeLast(); - - logOutput(lines, MessageLevel::PrePost, false, false); -} - -// exit handler -void BaseProcess::finish(int code, ExitStatus status) -{ - // Flush console window - if (!m_err_leftover.isEmpty()) - { - logOutput(m_err_leftover, MessageLevel::Error); - m_err_leftover.clear(); - } - if (!m_out_leftover.isEmpty()) - { - logOutput(m_out_leftover); - m_out_leftover.clear(); - } - - if (!killed) - { - if (status == NormalExit) - { - //: Message displayed on instance exit - emit log(tr("Game exited with exitcode %1.").arg(code)); - } - else - { - //: Message displayed on instance crashed - if(code == -1) - emit log(tr("Game crashed.").arg(code)); - else - emit log(tr("Game crashed with exitcode %1.").arg(code)); - } - } - else - { - //: Message displayed after the instance exits due to kill request - emit log(tr("Game was killed by user."), MessageLevel::Error); - } - - m_prepostlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(code)); - - // run post-exit - postLaunch(); - m_instance->cleanupAfterRun(); - // no longer running... - m_instance->setRunning(false); - emit ended(m_instance, code, status); -} - -void BaseProcess::killProcess() -{ - killed = true; - if (m_prepostlaunchprocess.state() == QProcess::Running) - { - m_prepostlaunchprocess.kill(); - } - else - { - kill(); - } -} - -bool BaseProcess::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_prepostlaunchprocess.start(prelaunch_cmd); - if (!waitForPrePost()) - { - emit log(tr("The command failed to start"), MessageLevel::Fatal); - return false; - } - // Flush console window - if (!m_err_leftover.isEmpty()) - { - logOutput(m_err_leftover, MessageLevel::PrePost); - m_err_leftover.clear(); - } - if (!m_out_leftover.isEmpty()) - { - logOutput(m_out_leftover, MessageLevel::PrePost); - m_out_leftover.clear(); - } - // Process return values - if (m_prepostlaunchprocess.exitStatus() != NormalExit) - { - emit log(tr("Pre-Launch command failed with code %1.\n\n") - .arg(m_prepostlaunchprocess.exitCode()), - MessageLevel::Fatal); - m_instance->cleanupAfterRun(); - emit prelaunch_failed(m_instance, m_prepostlaunchprocess.exitCode(), - m_prepostlaunchprocess.exitStatus()); - // not running, failed - m_instance->setRunning(false); - return false; - } - else - emit log(tr("Pre-Launch command ran successfully.\n\n")); - - return m_instance->reload(); - } - return true; -} -bool BaseProcess::postLaunch() -{ - 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_prepostlaunchprocess.start(postlaunch_cmd); - if (!waitForPrePost()) - { - return false; - } - // Flush console window - if (!m_err_leftover.isEmpty()) - { - logOutput(m_err_leftover, MessageLevel::PrePost); - m_err_leftover.clear(); - } - if (!m_out_leftover.isEmpty()) - { - logOutput(m_out_leftover, MessageLevel::PrePost); - m_out_leftover.clear(); - } - if (m_prepostlaunchprocess.exitStatus() != NormalExit) - { - emit log(tr("Post-Launch command failed with code %1.\n\n") - .arg(m_prepostlaunchprocess.exitCode()), - MessageLevel::Error); - emit postlaunch_failed(m_instance, m_prepostlaunchprocess.exitCode(), - m_prepostlaunchprocess.exitStatus()); - // not running, failed - m_instance->setRunning(false); - } - else - emit log(tr("Post-Launch command ran successfully.\n\n")); - - return m_instance->reload(); - } - return true; -} - -bool BaseProcess::waitForPrePost() -{ - if (!m_prepostlaunchprocess.waitForStarted()) - return false; - QEventLoop eventLoop; - auto finisher = [this, &eventLoop](QProcess::ProcessState state) - { - if (state == QProcess::NotRunning) - { - eventLoop.quit(); - } - }; - auto connection = connect(&m_prepostlaunchprocess, &QProcess::stateChanged, finisher); - int ret = eventLoop.exec(); - disconnect(connection); - return ret == 0; -} - -QString BaseProcess::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; -} diff --git a/logic/BaseProcess.h b/logic/BaseProcess.h deleted file mode 100644 index 75bf0217..00000000 --- a/logic/BaseProcess.h +++ /dev/null @@ -1,142 +0,0 @@ -/* Copyright 2013-2015 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 -#include "BaseInstance.h" - -/** - * @brief the MessageLevel Enum - * defines what level a message is - */ -namespace MessageLevel -{ -enum Enum -{ - MultiMC, /**< MultiMC Messages */ - Debug, /**< Debug Messages */ - Info, /**< Info Messages */ - Message, /**< Standard Messages */ - Warning, /**< Warnings */ - Error, /**< Errors */ - Fatal, /**< Fatal Errors */ - PrePost, /**< Pre/Post Launch command output */ -}; -MessageLevel::Enum getLevel(const QString &levelName); -} - -class BaseProcess: public QProcess -{ - Q_OBJECT -protected: - explicit BaseProcess(InstancePtr instance); - void init(); - -public: /* methods */ - virtual ~BaseProcess() {}; - - 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(); - - /** - * @brief prepare the process for launch (for multi-stage launch) - */ - virtual void arm() = 0; - - /** - * @brief launch the armed instance - */ - virtual void launch() = 0; - - /** - * @brief abort launch - */ - virtual void abort() = 0; - -protected: /* methods */ - bool preLaunch(); - bool postLaunch(); - bool waitForPrePost(); - QString substituteVariables(const QString &cmd) const; - - void printHeader(); - - virtual QMap getVariables() const = 0; - virtual QString censorPrivateInfo(QString in) = 0; - virtual MessageLevel::Enum guessLevel(const QString &message, MessageLevel::Enum defaultLevel) = 0; - -signals: - /** - * @brief emitted when the Process immediately fails to run - */ - void launch_failed(InstancePtr); - - /** - * @brief emitted when the PreLaunchCommand fails - */ - void prelaunch_failed(InstancePtr, int code, QProcess::ExitStatus status); - - /** - * @brief emitted when the PostLaunchCommand fails - */ - void postlaunch_failed(InstancePtr, int code, QProcess::ExitStatus status); - - /** - * @brief emitted when the process has finished and the PostLaunchCommand was run - */ - void ended(InstancePtr, int code, QProcess::ExitStatus status); - - /** - * @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 finish(int, QProcess::ExitStatus status); - void on_stdErr(); - void on_stdOut(); - void on_prepost_stdOut(); - void on_prepost_stdErr(); - void logOutput(const QStringList &lines, - MessageLevel::Enum defaultLevel = MessageLevel::Message, - bool guessLevel = true, bool censor = true); - void logOutput(QString line, - MessageLevel::Enum defaultLevel = MessageLevel::Message, - bool guessLevel = true, bool censor = true); - -protected: - InstancePtr m_instance; - QString m_err_leftover; - QString m_out_leftover; - QProcess m_prepostlaunchprocess; - bool killed = false; - QString m_header; -}; diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt index e6e36225..19686aa0 100644 --- a/logic/CMakeLists.txt +++ b/logic/CMakeLists.txt @@ -8,9 +8,13 @@ set(LOGIC_SOURCES BaseVersionList.cpp InstanceList.h InstanceList.cpp + LoggedProcess.h + LoggedProcess.cpp + MessageLevel.cpp + MessageLevel.h BaseVersion.h - BaseProcess.h - BaseProcess.cpp + BaseLauncher.h + BaseLauncher.cpp BaseInstance.h BaseInstance.cpp NullInstance.h @@ -136,8 +140,8 @@ set(LOGIC_SOURCES minecraft/JarMod.h minecraft/MinecraftInstance.cpp minecraft/MinecraftInstance.h - minecraft/MinecraftProcess.cpp - minecraft/MinecraftProcess.h + minecraft/MinecraftLauncher.cpp + minecraft/MinecraftLauncher.h minecraft/MinecraftVersion.cpp minecraft/MinecraftVersion.h minecraft/MinecraftVersionList.cpp diff --git a/logic/LoggedProcess.cpp b/logic/LoggedProcess.cpp new file mode 100644 index 00000000..53840621 --- /dev/null +++ b/logic/LoggedProcess.cpp @@ -0,0 +1,147 @@ +#include "LoggedProcess.h" +#include "MessageLevel.h" +#include + +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/LoggedProcess.h b/logic/LoggedProcess.h new file mode 100644 index 00000000..253be2c1 --- /dev/null +++ b/logic/LoggedProcess.h @@ -0,0 +1,60 @@ +#pragma once +#include +#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/MessageLevel.cpp b/logic/MessageLevel.cpp new file mode 100644 index 00000000..a5191290 --- /dev/null +++ b/logic/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/MessageLevel.h b/logic/MessageLevel.h new file mode 100644 index 00000000..0128148d --- /dev/null +++ b/logic/MessageLevel.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +/** + * @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); +} diff --git a/logic/NullInstance.h b/logic/NullInstance.h index cf8c7fce..5e81b2e4 100644 --- a/logic/NullInstance.h +++ b/logic/NullInstance.h @@ -43,7 +43,7 @@ public: { return instanceRoot(); }; - virtual BaseProcess* prepareForLaunch(AuthSessionPtr) + virtual BaseLauncher* prepareForLaunch(AuthSessionPtr) { return nullptr; } diff --git a/logic/minecraft/LegacyInstance.cpp b/logic/minecraft/LegacyInstance.cpp index 387975fa..a2b813e2 100644 --- a/logic/minecraft/LegacyInstance.cpp +++ b/logic/minecraft/LegacyInstance.cpp @@ -24,7 +24,7 @@ #include "minecraft/LegacyUpdate.h" #include "icons/IconList.h" -#include "minecraft/MinecraftProcess.h" +#include "minecraft/MinecraftLauncher.h" #include "minecraft/ModList.h" LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) @@ -95,7 +95,7 @@ std::shared_ptr LegacyInstance::doUpdate() return std::shared_ptr(new LegacyUpdate(this, this)); } -BaseProcess *LegacyInstance::prepareForLaunch(AuthSessionPtr account) +BaseLauncher *LegacyInstance::prepareForLaunch(AuthSessionPtr account) { QString launchScript; QIcon icon = ENV.icons()->getIcon(iconKey()); @@ -122,7 +122,7 @@ BaseProcess *LegacyInstance::prepareForLaunch(AuthSessionPtr account) launchScript += "lwjgl " + lwjgl + "\n"; launchScript += "launcher legacy\n"; } - auto process = MinecraftProcess::create(std::dynamic_pointer_cast(getSharedPtr())); + auto process = MinecraftLauncher::create(std::dynamic_pointer_cast(getSharedPtr())); process->setLaunchScript(launchScript); process->setWorkdir(minecraftRoot()); process->setLogin(account); diff --git a/logic/minecraft/LegacyInstance.h b/logic/minecraft/LegacyInstance.h index 64bcb08b..236771f4 100644 --- a/logic/minecraft/LegacyInstance.h +++ b/logic/minecraft/LegacyInstance.h @@ -111,7 +111,7 @@ public: virtual void setShouldUpdate(bool val) override; virtual std::shared_ptr doUpdate() override; - virtual BaseProcess *prepareForLaunch(AuthSessionPtr account) override; + virtual BaseLauncher *prepareForLaunch(AuthSessionPtr account) override; virtual void cleanupAfterRun() override; virtual QString getStatusbarDescription() override; diff --git a/logic/minecraft/MinecraftLauncher.cpp b/logic/minecraft/MinecraftLauncher.cpp new file mode 100644 index 00000000..60fc935f --- /dev/null +++ b/logic/minecraft/MinecraftLauncher.cpp @@ -0,0 +1,296 @@ +/* Copyright 2013-2014 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "minecraft/MinecraftLauncher.h" +#include "BaseInstance.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "osutils.h" +#include "pathutils.h" +#include "cmdutils.h" + +// constructor +MinecraftLauncher::MinecraftLauncher(MinecraftInstancePtr inst) : BaseLauncher(inst) +{ +} + +MinecraftLauncher* MinecraftLauncher::create(MinecraftInstancePtr inst) +{ + auto proc = new MinecraftLauncher(inst); + proc->init(); + return proc; +} + + +QString MinecraftLauncher::censorPrivateInfo(QString in) +{ + if (!m_session) + return in; + + if (m_session->session != "-") + in.replace(m_session->session, ""); + in.replace(m_session->access_token, ""); + in.replace(m_session->client_token, ""); + in.replace(m_session->uuid, ""); + in.replace(m_session->player_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 MinecraftLauncher::guessLevel(const QString &line, MessageLevel::Enum level) +{ + QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + 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 MinecraftLauncher::getVariables() const +{ + auto mcInstance = std::dynamic_pointer_cast(m_instance); + QMap 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 MinecraftLauncher::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 OSX + 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; +} + +bool MinecraftLauncher::checkJava(QString JavaPath) +{ + auto realJavaPath = QStandardPaths::findExecutable(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(JavaPath), + MessageLevel::Warning); + } + + QFileInfo javaInfo(realJavaPath); + qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedUnixTime = m_instance->settings()->get("JavaTimestamp").toLongLong(); + // if they are not the same, check! + if(javaUnixTime != storedUnixTime) + { + QEventLoop ev; + auto checker = std::make_shared(); + bool successful = false; + QString errorLog; + QString version; + emit log(tr("Checking Java version..."), MessageLevel::MultiMC); + connect(checker.get(), &JavaChecker::checkFinished, + [&](JavaCheckResult result) + { + successful = result.valid; + errorLog = result.errorLog; + version = result.javaVersion; + ev.exit(); + }); + checker->m_path = realJavaPath; + checker->performCheck(); + ev.exec(); + if(!successful) + { + // Error message displayed if java can't start + emit log(tr("Could not start java:"), MessageLevel::Error); + auto lines = errorLog.split('\n'); + for(auto line: lines) + { + emit log(line, MessageLevel::Error); + } + emit log("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); + m_instance->cleanupAfterRun(); + emit launch_failed(m_instance); + // not running, failed + m_instance->setRunning(false); + return false; + } + emit log(tr("Java version is %1!\n").arg(version), MessageLevel::MultiMC); + m_instance->settings()->set("JavaVersion", version); + m_instance->settings()->set("JavaTimestamp", javaUnixTime); + } + return true; +} + +void MinecraftLauncher::arm() +{ + printHeader(); + emit log("Minecraft folder is:\n" + m_process.workingDirectory() + "\n\n"); + + /* + if (!preLaunch()) + { + emit ended(m_instance, 1, QProcess::CrashExit); + return; + } + */ + + m_instance->setLastLaunch(); + + QString JavaPath = m_instance->settings()->get("JavaPath").toString(); + emit log("Java path is:\n" + JavaPath + "\n\n"); + + if(!checkJava(JavaPath)) + { + return; + } + + 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()) + { + emit log(tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand), MessageLevel::Warning); + m_instance->cleanupAfterRun(); + emit launch_failed(m_instance); + m_instance->setRunning(false); + return; + } + emit log("Wrapper command is:\n" + wrapperCommand + "\n\n"); + args.prepend(JavaPath); + m_process.start(wrapperCommand, args); + } + else + { + m_process.start(JavaPath, args); + } + + // instantiate the launcher part + if (!m_process.waitForStarted()) + { + //: Error message displayed if instace can't start + emit log(tr("Could not launch minecraft!"), MessageLevel::Error); + m_instance->cleanupAfterRun(); + emit launch_failed(m_instance); + // not running, failed + m_instance->setRunning(false); + 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()); +} + +void MinecraftLauncher::launch() +{ + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); +} + +void MinecraftLauncher::abort() +{ + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); +} diff --git a/logic/minecraft/MinecraftLauncher.h b/logic/minecraft/MinecraftLauncher.h new file mode 100644 index 00000000..1c870e84 --- /dev/null +++ b/logic/minecraft/MinecraftLauncher.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2014 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 +#include "minecraft/MinecraftInstance.h" +#include "BaseLauncher.h" + +/** + * The MinecraftLauncher class + */ +class MinecraftLauncher : public BaseLauncher +{ + Q_OBJECT +protected: + MinecraftLauncher(MinecraftInstancePtr inst); +public: + static MinecraftLauncher *create(MinecraftInstancePtr inst); + + virtual ~MinecraftLauncher(){}; + + /** + * @brief start the launcher part with the provided launch script + */ + void arm() override; + + /** + * @brief launch the armed instance! + */ + void launch() override; + + /** + * @brief abort launch! + */ + void abort() override; + + void setLaunchScript(QString script) + { + launchScript = script; + } + + void setNativeFolder(QString natives) + { + m_nativeFolder = natives; + } + + inline void setLogin(AuthSessionPtr session) + { + m_session = session; + } + +protected: + AuthSessionPtr m_session; + QString launchScript; + QString m_nativeFolder; + +protected: + bool checkJava(QString path); + virtual QMap getVariables() const override; + QStringList javaArguments() const; + virtual QString censorPrivateInfo(QString in) override; + virtual MessageLevel::Enum guessLevel(const QString &message, MessageLevel::Enum defaultLevel) override; +}; diff --git a/logic/minecraft/MinecraftProcess.cpp b/logic/minecraft/MinecraftProcess.cpp deleted file mode 100644 index d0f2a6cc..00000000 --- a/logic/minecraft/MinecraftProcess.cpp +++ /dev/null @@ -1,290 +0,0 @@ -/* Copyright 2013-2014 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 "minecraft/MinecraftProcess.h" -#include "BaseInstance.h" -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "osutils.h" -#include "pathutils.h" -#include "cmdutils.h" - -// constructor -MinecraftProcess::MinecraftProcess(MinecraftInstancePtr inst) : BaseProcess(inst) -{ -} - -MinecraftProcess* MinecraftProcess::create(MinecraftInstancePtr inst) -{ - auto proc = new MinecraftProcess(inst); - proc->init(); - return proc; -} - - -QString MinecraftProcess::censorPrivateInfo(QString in) -{ - if (!m_session) - return in; - - if (m_session->session != "-") - in.replace(m_session->session, ""); - in.replace(m_session->access_token, ""); - in.replace(m_session->client_token, ""); - in.replace(m_session->uuid, ""); - in.replace(m_session->player_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 MinecraftProcess::guessLevel(const QString &line, MessageLevel::Enum level) -{ - QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - 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 MinecraftProcess::getVariables() const -{ - auto mcInstance = std::dynamic_pointer_cast(m_instance); - QMap 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 MinecraftProcess::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 OSX - 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 MinecraftProcess::arm() -{ - printHeader(); - emit log("Minecraft folder is:\n" + workingDirectory() + "\n\n"); - - if (!preLaunch()) - { - emit ended(m_instance, 1, QProcess::CrashExit); - return; - } - - m_instance->setLastLaunch(); - - QString JavaPath = m_instance->settings()->get("JavaPath").toString(); - emit log("Java path is:\n" + JavaPath + "\n\n"); - - auto realJavaPath = QStandardPaths::findExecutable(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(JavaPath), - MessageLevel::Warning); - } - - // check java version here. - { - QFileInfo javaInfo(realJavaPath); - qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); - auto storedUnixTime = m_instance->settings()->get("JavaTimestamp").toLongLong(); - // if they are not the same, check! - if(javaUnixTime != storedUnixTime) - { - QEventLoop ev; - auto checker = std::make_shared(); - bool successful = false; - QString errorLog; - QString version; - emit log(tr("Checking Java version..."), MessageLevel::MultiMC); - connect(checker.get(), &JavaChecker::checkFinished, - [&](JavaCheckResult result) - { - successful = result.valid; - errorLog = result.errorLog; - version = result.javaVersion; - ev.exit(); - }); - checker->m_path = realJavaPath; - checker->performCheck(); - ev.exec(); - if(!successful) - { - // Error message displayed if java can't start - emit log(tr("Could not start java:"), MessageLevel::Error); - auto lines = errorLog.split('\n'); - for(auto line: lines) - { - emit log(line, MessageLevel::Error); - } - emit log("\nCheck your MultiMC Java settings.", MessageLevel::MultiMC); - m_instance->cleanupAfterRun(); - emit launch_failed(m_instance); - // not running, failed - m_instance->setRunning(false); - return; - } - emit log(tr("Java version is %1!\n").arg(version), MessageLevel::MultiMC); - m_instance->settings()->set("JavaVersion", version); - m_instance->settings()->set("JavaTimestamp", javaUnixTime); - } - } - - 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()) - { - emit log(tr("The wrapper command \"%1\" couldn't be found.").arg(wrapperCommand), MessageLevel::Warning); - m_instance->cleanupAfterRun(); - emit launch_failed(m_instance); - m_instance->setRunning(false); - return; - } - emit log("Wrapper command is:\n" + wrapperCommand + "\n\n"); - args.prepend(JavaPath); - start(wrapperCommand, args); - } - else - { - start(JavaPath, args); - } - - if (!waitForStarted()) - { - //: Error message displayed if instace can't start - emit log(tr("Could not launch minecraft!"), MessageLevel::Error); - m_instance->cleanupAfterRun(); - emit launch_failed(m_instance); - // not running, failed - m_instance->setRunning(false); - return; - } - - emit log(tr("Minecraft process ID: %1\n\n").arg(processId()), MessageLevel::MultiMC); - - // send the launch script to the launcher part - QByteArray bytes = launchScript.toUtf8(); - writeData(bytes.constData(), bytes.length()); -} - -void MinecraftProcess::launch() -{ - QString launchString("launch\n"); - QByteArray bytes = launchString.toUtf8(); - writeData(bytes.constData(), bytes.length()); -} - -void MinecraftProcess::abort() -{ - QString launchString("abort\n"); - QByteArray bytes = launchString.toUtf8(); - writeData(bytes.constData(), bytes.length()); -} diff --git a/logic/minecraft/MinecraftProcess.h b/logic/minecraft/MinecraftProcess.h deleted file mode 100644 index 34c02b77..00000000 --- a/logic/minecraft/MinecraftProcess.h +++ /dev/null @@ -1,77 +0,0 @@ -/* Copyright 2013-2014 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 -#include "minecraft/MinecraftInstance.h" -#include "BaseProcess.h" - -/** - * The MinecraftProcess class - */ -class MinecraftProcess : public BaseProcess -{ - Q_OBJECT -protected: - MinecraftProcess(MinecraftInstancePtr inst); -public: - static MinecraftProcess *create(MinecraftInstancePtr inst); - - virtual ~MinecraftProcess(){}; - - /** - * @brief start the launcher part with the provided launch script - */ - void arm() override; - - /** - * @brief launch the armed instance! - */ - void launch() override; - - /** - * @brief abort launch! - */ - void abort() override; - - void setLaunchScript(QString script) - { - launchScript = script; - } - - void setNativeFolder(QString natives) - { - m_nativeFolder = natives; - } - - inline void setLogin(AuthSessionPtr session) - { - m_session = session; - } - -protected: - AuthSessionPtr m_session; - QString launchScript; - QString m_nativeFolder; - - virtual QMap getVariables() const override; - - QStringList javaArguments() const; - virtual QString censorPrivateInfo(QString in) override; - virtual MessageLevel::Enum guessLevel(const QString &message, MessageLevel::Enum defaultLevel) override; -}; diff --git a/logic/minecraft/OneSixInstance.cpp b/logic/minecraft/OneSixInstance.cpp index b7937e31..ed757fd5 100644 --- a/logic/minecraft/OneSixInstance.cpp +++ b/logic/minecraft/OneSixInstance.cpp @@ -22,7 +22,7 @@ #include "minecraft/OneSixUpdate.h" #include "minecraft/MinecraftProfile.h" #include "minecraft/VersionBuildError.h" -#include "minecraft/MinecraftProcess.h" +#include "minecraft/MinecraftLauncher.h" #include "minecraft/OneSixProfileStrategy.h" #include "MMCZip.h" @@ -123,7 +123,7 @@ QStringList OneSixInstance::processMinecraftArgs(AuthSessionPtr session) return parts; } -BaseProcess *OneSixInstance::prepareForLaunch(AuthSessionPtr session) +BaseLauncher *OneSixInstance::prepareForLaunch(AuthSessionPtr session) { QString launchScript; QIcon icon = ENV.icons()->getIcon(iconKey()); @@ -230,7 +230,7 @@ BaseProcess *OneSixInstance::prepareForLaunch(AuthSessionPtr session) } launchScript += "launcher onesix\n"; - auto process = MinecraftProcess::create(std::dynamic_pointer_cast(getSharedPtr())); + auto process = MinecraftLauncher::create(std::dynamic_pointer_cast(getSharedPtr())); process->setLaunchScript(launchScript); process->setWorkdir(minecraftRoot()); process->setLogin(session); diff --git a/logic/minecraft/OneSixInstance.h b/logic/minecraft/OneSixInstance.h index dd5ddf5e..5c71687d 100644 --- a/logic/minecraft/OneSixInstance.h +++ b/logic/minecraft/OneSixInstance.h @@ -49,7 +49,7 @@ public: virtual QString instanceConfigFolder() const override; virtual std::shared_ptr doUpdate() override; - virtual BaseProcess *prepareForLaunch(AuthSessionPtr account) override; + virtual BaseLauncher *prepareForLaunch(AuthSessionPtr account) override; virtual void cleanupAfterRun() override; diff --git a/logic/tools/BaseExternalTool.cpp b/logic/tools/BaseExternalTool.cpp index d2931db7..af7f9a3f 100644 --- a/logic/tools/BaseExternalTool.cpp +++ b/logic/tools/BaseExternalTool.cpp @@ -19,16 +19,6 @@ BaseExternalTool::~BaseExternalTool() { } -qint64 BaseExternalTool::pid(QProcess *process) -{ -#ifdef Q_OS_WIN - struct _PROCESS_INFORMATION *procinfo = process->pid(); - return procinfo->dwProcessId; -#else - return process->pid(); -#endif -} - BaseDetachedTool::BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent) : BaseExternalTool(settings, instance, parent) { diff --git a/logic/tools/BaseExternalTool.h b/logic/tools/BaseExternalTool.h index 3c0b23ed..be23036d 100644 --- a/logic/tools/BaseExternalTool.h +++ b/logic/tools/BaseExternalTool.h @@ -17,7 +17,6 @@ public: protected: InstancePtr m_instance; SettingsObjectPtr globalSettings; - qint64 pid(QProcess *process); }; class BaseDetachedTool : public BaseExternalTool diff --git a/logic/tools/BaseProfiler.cpp b/logic/tools/BaseProfiler.cpp index 2c9fed9b..cfac47d4 100644 --- a/logic/tools/BaseProfiler.cpp +++ b/logic/tools/BaseProfiler.cpp @@ -7,7 +7,7 @@ BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QOb { } -void BaseProfiler::beginProfiling(BaseProcess *process) +void BaseProfiler::beginProfiling(BaseLauncher *process) { beginProfilingImpl(process); } diff --git a/logic/tools/BaseProfiler.h b/logic/tools/BaseProfiler.h index 56397489..54ba89ce 100644 --- a/logic/tools/BaseProfiler.h +++ b/logic/tools/BaseProfiler.h @@ -4,7 +4,7 @@ class BaseInstance; class SettingsObject; -class BaseProcess; +class BaseLauncher; class QProcess; class BaseProfiler : public BaseExternalTool @@ -15,13 +15,13 @@ public: public slots: - void beginProfiling(BaseProcess *process); + void beginProfiling(BaseLauncher *process); void abortProfiling(); protected: QProcess *m_profilerProcess; - virtual void beginProfilingImpl(BaseProcess *process) = 0; + virtual void beginProfilingImpl(BaseLauncher *process) = 0; virtual void abortProfilingImpl(); signals: diff --git a/logic/tools/JProfiler.cpp b/logic/tools/JProfiler.cpp index 2522f2b3..73121239 100644 --- a/logic/tools/JProfiler.cpp +++ b/logic/tools/JProfiler.cpp @@ -4,7 +4,7 @@ #include #include "settings/SettingsObject.h" -#include "BaseProcess.h" +#include "BaseLauncher.h" #include "BaseInstance.h" class JProfiler : public BaseProfiler @@ -18,7 +18,7 @@ private slots: void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(BaseProcess *process); + void beginProfilingImpl(BaseLauncher *process); private: int listeningPort = 0; @@ -48,13 +48,13 @@ void JProfiler::profilerFinished(int exit, QProcess::ExitStatus status) } } -void JProfiler::beginProfilingImpl(BaseProcess *process) +void JProfiler::beginProfilingImpl(BaseLauncher *process) { listeningPort = globalSettings->get("JProfilerPort").toInt(); QProcess *profiler = new QProcess(this); QStringList profilerArgs = { - "-d", QString::number(pid(process)), + "-d", QString::number(process->pid()), "--gui", "-p", QString::number(listeningPort) }; diff --git a/logic/tools/JVisualVM.cpp b/logic/tools/JVisualVM.cpp index e148e7dc..fbfd858a 100644 --- a/logic/tools/JVisualVM.cpp +++ b/logic/tools/JVisualVM.cpp @@ -4,7 +4,7 @@ #include #include "settings/SettingsObject.h" -#include "BaseProcess.h" +#include "BaseLauncher.h" #include "BaseInstance.h" class JVisualVM : public BaseProfiler @@ -18,7 +18,7 @@ private slots: void profilerFinished(int exit, QProcess::ExitStatus status); protected: - void beginProfilingImpl(BaseProcess *process); + void beginProfilingImpl(BaseLauncher *process); }; @@ -45,12 +45,12 @@ void JVisualVM::profilerFinished(int exit, QProcess::ExitStatus status) } } -void JVisualVM::beginProfilingImpl(BaseProcess *process) +void JVisualVM::beginProfilingImpl(BaseLauncher *process) { QProcess *profiler = new QProcess(this); QStringList profilerArgs = { - "--openpid", QString::number(pid(process)) + "--openpid", QString::number(process->pid()) }; auto programPath = globalSettings->get("JVisualVMPath").toString(); -- cgit v1.2.3