/* 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 "launch/LaunchTask.h" #include "MessageLevel.h" #include "MMCStrings.h" #include "java/JavaChecker.h" #include "tasks/Task.h" #include #include #include #include #include #include #include #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::create(MinecraftInstancePtr inst) { std::shared_ptr 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, ""); 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 LaunchTask::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 LaunchTask::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 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(); 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 }