/* 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 "MultiMC.h" #include "BuildConfig.h" #include "MinecraftProcess.h" #include <QDataStream> #include <QFile> #include <QDir> #include <QProcessEnvironment> #include <QRegularExpression> #include <QStandardPaths> #include "BaseInstance.h" #include "osutils.h" #include "pathutils.h" #include "cmdutils.h" #define IBUS "@im=ibus" // constructor MinecraftProcess::MinecraftProcess(InstancePtr inst) : m_instance(inst) { 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" }; for(auto key: rawenv.keys()) { auto value = rawenv.value(key); // filter out dangerous java crap if(ignored.contains(key)) { QLOG_INFO() << "Env: ignoring" << key << value; continue; } // filter MultiMC-related things if(key.startsWith("QT_")) { QLOG_INFO() << "Env: ignoring" << key << value; continue; } #ifdef LINUX // Do not pass LD_* variables to java. They were intended for MultiMC if(key.startsWith("LD_")) { QLOG_INFO() << "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, ""); QLOG_INFO() << "Env: stripped" << IBUS << "from" << save << ":" << value; } #endif QLOG_INFO() << "Env: " << key << value; env.insert(key, value); } #ifdef LINUX // HACK: Workaround for QTBUG-42500 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, &MinecraftProcess::on_prepost_stdErr); connect(&m_prepostlaunchprocess, &QProcess::readyReadStandardOutput, this, &MinecraftProcess::on_prepost_stdOut); } // a process has been constructed for the instance. It is running from MultiMC POV m_instance->setRunning(true); } void MinecraftProcess::setWorkdir(QString path) { QDir mcDir(path); this->setWorkingDirectory(mcDir.absolutePath()); m_prepostlaunchprocess.setWorkingDirectory(mcDir.absolutePath()); } QString MinecraftProcess::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 MinecraftProcess::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; } MessageLevel::Enum MinecraftProcess::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; } void MinecraftProcess::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 MinecraftProcess::logOutput(QString line, MessageLevel::Enum defaultLevel, bool guessLevel, bool censor) { MessageLevel::Enum level = defaultLevel; //FIXME: make more flexible in the future if(line.contains("ignoring option PermSize")) { return; } // Level prefix int endmark = line.indexOf("]!"); if (line.startsWith("! { if (state == QProcess::NotRunning) { eventLoop.quit(); } }; auto connection = connect(&m_prepostlaunchprocess, &QProcess::stateChanged, finisher); int ret = eventLoop.exec(); disconnect(connection); return ret == 0; } QMap<QString, QString> MinecraftProcess::getVariables() const { QMap<QString, QString> out; out.insert("INST_NAME", m_instance->name()); out.insert("INST_ID", m_instance->id()); out.insert("INST_DIR", QDir(m_instance->instanceRoot()).absolutePath()); out.insert("INST_MC_DIR", QDir(m_instance->minecraftRoot()).absolutePath()); out.insert("INST_JAVA", m_instance->settings().get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); return out; } QString MinecraftProcess::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; } 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()); 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(MMC->bin(), "jars", "NewLaunch.jar"); return args; } void MinecraftProcess::arm() { emit log("MultiMC version: " + BuildConfig.printableVersionString() + "\n\n"); emit log("Minecraft folder is:\n" + workingDirectory() + "\n\n"); if (!preLaunch()) { emit ended(m_instance, 1, QProcess::CrashExit); return; } m_instance->setLastLaunch(); QStringList args = javaArguments(); QString JavaPath = m_instance->settings().get("JavaPath").toString(); emit log("Java path is:\n" + JavaPath + "\n\n"); QString allArgs = args.join(", "); emit log("Java Arguments:\n[" + censorPrivateInfo(allArgs) + "]\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); } // instantiate the launcher part 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; } // 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()); }